Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,6 @@ Do not read source files and assert on their contents (`.toContain('pattern')`).

### External Dependencies

- Vendored modules in `src/external/` (e.g., ink-table)
- Dependencies bundled into `dist/cli.js` via esbuild
- Uses Socket registry overrides for security
- Custom patches applied to dependencies in `patches/`
Expand Down
11 changes: 3 additions & 8 deletions docs/build-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ socket-cli/
│ ├── cli/ # Main CLI package
│ │ ├── src/ # TypeScript source
│ │ ├── build/ # Intermediate build files
│ │ │ ├── cli.js # Bundled CLI (esbuild output)
│ │ │ └── yoga-sync.mjs # Downloaded WASM module
│ │ │ └── cli.js # Bundled CLI (esbuild output)
│ │ └── dist/ # Distribution files
│ │ ├── index.js # Entry point loader
│ │ ├── cli.js # CLI bundle (copied from build/)
Expand All @@ -69,7 +68,6 @@ socket-cli/
│ │ └── downloaded/ # Cached downloads
│ │ ├── node-smol/ # Node.js binaries
│ │ ├── binject/ # Binary injection tool
│ │ ├── yoga-layout/ # Yoga WASM
│ │ └── models/ # AI models
│ └── package-builder/ # Package generation templates
└── scripts/ # Monorepo build scripts
Expand All @@ -86,7 +84,6 @@ Phase 1: Clean (optional, with --force)
Phase 2: Prepare (parallel)
├── Generate CLI packages from templates
└── Download assets from socket-btm releases
├── yoga-layout (WASM for terminal rendering)
├── node-smol (minimal Node.js binaries)
├── binject (binary injection tool)
└── models (AI models for analysis)
Expand Down Expand Up @@ -191,9 +188,8 @@ pnpm build:watch

**What it does**:

1. Downloads yoga WASM (first time only)
2. Starts esbuild in watch mode
3. Rebuilds `build/cli.js` on changes
1. Starts esbuild in watch mode
2. Rebuilds `build/cli.js` on changes

**Note**: Watch mode only rebuilds the CLI bundle, not SEA binaries.

Expand Down Expand Up @@ -250,7 +246,6 @@ Assets are downloaded from [socket-btm](https://github.com/SocketDev/socket-btm)
| ------------- | ----------------------- | ------------------------------------ |
| `node-smol` | Minimal Node.js for SEA | `node-smol/<platform>-<arch>/node` |
| `binject` | Binary injection tool | `binject/<platform>-<arch>/binject` |
| `yoga-layout` | Terminal layout WASM | `yoga-layout/assets/yoga-sync-*.mjs` |
| `models` | AI models for analysis | `models/` |

### Cache Management
Expand Down
13 changes: 6 additions & 7 deletions packages/build-infra/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Shared build infrastructure utilities for Socket CLI. Provides esbuild plugins,
This package centralizes build-time utilities that are shared across multiple Socket CLI build configurations. It provides:

1. **esbuild plugins** for code transformations required by SEA (Single Executable Application) binaries
2. **GitHub release utilities** for downloading node-smol, yoga-wasm, and other build dependencies
2. **GitHub release utilities** for downloading node-smol and other build dependencies
3. **Extraction caching** to avoid regenerating files when source hasn't changed

## Modules
Expand Down Expand Up @@ -97,7 +97,7 @@ const __importMetaUrl = require('node:url').pathToFileURL(__filename).href

### GitHub Releases

Downloads assets from SocketDev/socket-btm releases with retry logic and caching. Used for node-smol binaries, yoga-wasm, AI models, and build tools.
Downloads assets from SocketDev/socket-btm releases with retry logic and caching. Used for node-smol binaries, AI models, and build tools.

#### `getLatestRelease(tool, options)`

Expand All @@ -112,7 +112,7 @@ const tag = await getLatestRelease('node-smol')

**Parameters:**

- `tool` (string) - Tool name prefix (e.g., 'node-smol', 'yoga-layout', 'binject')
- `tool` (string) - Tool name prefix (e.g., 'node-smol', 'binject')
- `options.quiet` (boolean) - Suppress log messages

**Returns:** Latest tag string or `null` if not found
Expand Down Expand Up @@ -154,9 +154,9 @@ Downloads a release asset with automatic redirect following.
import { downloadReleaseAsset } from 'build-infra/lib/github-releases'

await downloadReleaseAsset(
'yoga-layout-20250120-def5678',
'yoga-sync-20250120.mjs',
'/path/to/output.mjs',
'node-smol-20250120-abc1234',
'node-smol-linux-x64',
'/path/to/output',
)
```

Expand Down Expand Up @@ -403,7 +403,6 @@ The `build/downloaded/` directory stores cached GitHub release assets:
build/downloaded/
├── binject-{tag}-{platform}-{arch}
├── node-smol-{tag}-{platform}-{arch}
├── yoga-layout-{tag}.mjs
└── models-{tag}.tar.gz
```

Expand Down
12 changes: 0 additions & 12 deletions packages/cli/.config/esbuild.cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -145,18 +145,6 @@ const config = {
},
},

{
name: 'yoga-wasm-alias',
setup(build) {
// Redirect yoga-layout to our custom synchronous implementation.
build.onResolve({ filter: /^yoga-layout$/ }, () => {
return {
path: path.join(rootPath, 'build/yoga-sync.mjs'),
}
})
},
},

{
name: 'stub-problematic-packages',
setup(build) {
Expand Down
21 changes: 1 addition & 20 deletions packages/cli/scripts/build-js.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,7 @@ const logger = getDefaultLogger()

async function main() {
try {
// Step 1: Download yoga WASM.
logger.step('Downloading yoga WASM')
const extractResult = await spawn(
'node',
['--max-old-space-size=8192', 'scripts/download-assets.mjs', 'yoga'],
{ stdio: 'inherit' },
)

if (!extractResult) {
logger.error('Failed to start asset download')
process.exitCode = 1
return
}

if (extractResult.code !== 0) {
process.exitCode = extractResult.code
return
}

// Step 2: Build with esbuild.
// Step 1: Build with esbuild.
logger.step('Building CLI bundle')
const buildResult = await spawn(
'node',
Expand Down
29 changes: 1 addition & 28 deletions packages/cli/scripts/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -104,34 +104,7 @@ async function main() {
logger.info('Starting watch mode...')
}

// First download yoga WASM (only needed asset for CLI bundle).
const extractResult = await spawn(
'node',
[...NODE_MEMORY_FLAGS, 'scripts/download-assets.mjs', 'yoga'],
{
shell: WIN32,
stdio: 'inherit',
},
)

if (!extractResult) {
const error = new Error('Failed to start asset download process')
logger.error(error.message)
process.exitCode = 1
throw error
}

if (extractResult.code !== 0) {
const exitCode = extractResult.code ?? 1
const error = new Error(
`Asset download failed with exit code ${extractResult.code ?? 'unknown'}`,
)
logger.error(error.message)
process.exitCode = exitCode
throw error
}

// Then start esbuild in watch mode.
// Start esbuild in watch mode.
const watchResult = await spawn(
'node',
[...NODE_MEMORY_FLAGS, '.config/esbuild.cli.mjs', '--watch'],
Expand Down
135 changes: 2 additions & 133 deletions packages/cli/scripts/download-assets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@
* Usage:
* node scripts/download-assets.mjs [asset-names...] [options]
* node scripts/download-assets.mjs # Download all assets (parallel)
* node scripts/download-assets.mjs yoga models # Download specific assets (parallel)
* node scripts/download-assets.mjs models # Download specific assets (parallel)
* node scripts/download-assets.mjs --no-parallel # Download all assets (sequential)
*
* Assets:
* binject - Binary injection tool
* iocraft - iocraft native bindings (.node files)
* models - AI models tar.gz (MiniLM, CodeT5)
* node-smol - Minimal Node.js binaries
* yoga - Yoga layout WASM (yoga-sync.mjs)
*/

import { existsSync, promises as fs } from 'node:fs'
Expand All @@ -26,11 +25,6 @@ import { getDefaultLogger } from '@socketsecurity/lib/logger'
import { downloadSocketBtmRelease } from '@socketsecurity/lib/releases/socket-btm'
import { spawn } from '@socketsecurity/lib/spawn'

import {
computeFileHash,
generateHeader,
} from './utils/socket-btm-releases.mjs'

const __dirname = path.dirname(fileURLToPath(import.meta.url))
const rootPath = path.join(__dirname, '..')
const logger = getDefaultLogger()
Expand Down Expand Up @@ -94,36 +88,13 @@ const ASSETS = {
name: 'node-smol',
type: 'binary',
},
yoga: {
description: 'Yoga layout WASM',
download: {
asset: 'yoga-sync-*.mjs',
cwd: rootPath,
downloadDir: '../../packages/build-infra/build/downloaded',
quiet: false,
tool: 'yoga-layout',
},
name: 'yoga',
process: {
format: 'javascript',
outputPath: path.join(rootPath, 'build/yoga-sync.mjs'),
},
type: 'processed',
},
}

/**
* Download a single asset.
*/
async function downloadAsset(config) {
const {
description,
download,
extract,
name,
process: processConfig,
type,
} = config
const { description, download, extract, name, type } = config

try {
logger.group(`Extracting ${name} from socket-btm releases...`)
Expand All @@ -149,8 +120,6 @@ async function downloadAsset(config) {
// Process based on asset type.
if (type === 'archive' && extract) {
await extractArchive(assetPath, extract, name)
} else if (type === 'processed' && processConfig) {
await processAsset(assetPath, processConfig, name)
}

logger.groupEnd()
Expand Down Expand Up @@ -221,106 +190,6 @@ async function extractArchive(tarGzPath, extractConfig, assetName) {
await fs.writeFile(versionPath, tag, 'utf-8')
}

/**
* Transform yoga-sync.mjs to remove top-level await for CJS compatibility.
*
* The newer yoga-sync builds incorrectly use top-level await which isn't
* compatible with esbuild's CJS output format. Despite the name, yogaPromise
* is synchronous (-sWASM_ASYNC_COMPILATION=0), so we can call it directly.
*/
function transformYogaSync(content) {
// Pattern: const Yoga = wrapAssembly(await yogaPromise);
// Transform to: const Yoga = wrapAssembly(yogaPromise);
// (yogaPromise is synchronous despite its name)
const hasTopLevelAwait = content.includes('wrapAssembly(await yogaPromise)')
if (!hasTopLevelAwait) {
return content
}

// Replace the top-level await pattern with synchronous call.
return content.replace(
/const Yoga = wrapAssembly\(await yogaPromise\);/,
'const Yoga = wrapAssembly(yogaPromise);',
)
}

/**
* Process and transform asset (e.g., add header to JS file).
*/
async function processAsset(assetPath, processConfig, assetName) {
const { outputPath } = processConfig

// Check if extraction needed by comparing version.
const assetDir = path.dirname(assetPath)
const sourceVersionPath = path.join(assetDir, '.version')
const outputVersionPath = path.join(
path.dirname(outputPath),
`${path.basename(outputPath, path.extname(outputPath))}.version`,
)

if (
existsSync(outputVersionPath) &&
existsSync(outputPath) &&
existsSync(sourceVersionPath)
) {
const cachedVersion = (await fs.readFile(outputVersionPath, 'utf8')).trim()
const sourceVersion = (await fs.readFile(sourceVersionPath, 'utf8')).trim()
if (cachedVersion === sourceVersion) {
logger.info(`${assetName} already up to date`)
return
}

logger.info(`${assetName} version changed, re-extracting...`)
}

// Read the downloaded asset.
let content = await fs.readFile(assetPath, 'utf-8')

// Transform yoga-sync to remove top-level await for CJS compatibility.
if (assetName === 'yoga') {
content = transformYogaSync(content)
}

// Compute source hash for cache validation.
const sourceHash = await computeFileHash(assetPath)

// Get tag from source version file.
if (!existsSync(sourceVersionPath)) {
throw new Error(
`Source version file not found: ${sourceVersionPath}. ` +
'Please download assets first using the build system.',
)
}

const tag = (await fs.readFile(sourceVersionPath, 'utf8')).trim()
if (!tag || tag.length === 0) {
throw new Error(
`Invalid version file content at ${sourceVersionPath}. ` +
'Please re-download assets.',
)
}

// Generate output file with header.
const header = generateHeader({
assetName: path.basename(assetPath),
scriptName: 'scripts/download-assets.mjs',
sourceHash,
tag,
})

const output = `${header}

${content}
`

// Ensure build directory exists before writing.
await fs.mkdir(path.dirname(outputPath), { recursive: true })
await fs.writeFile(outputPath, output, 'utf-8')

// Write version file.
await fs.writeFile(outputVersionPath, tag, 'utf-8')
}

/**
* Download multiple assets (parallel by default, sequential opt-in).
*
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/json/output-cmd-json.mts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { existsSync } from 'node:fs'
import fs from 'node:fs'
import path from 'node:path'

import { safeReadFileSync, safeStatsSync } from '@socketsecurity/lib/fs'
Expand All @@ -16,7 +16,7 @@ export async function outputCmdJson(cwd: string) {
const sockJsonPath = path.join(cwd, SOCKET_JSON)
const tildeSockJsonPath = VITEST ? REDACTED : tildify(sockJsonPath)

if (!existsSync(sockJsonPath)) {
if (!fs.existsSync(sockJsonPath)) {
logger.fail(`Not found: ${tildeSockJsonPath}`)
process.exitCode = 1
return
Expand Down
Loading