diff --git a/.claude/commands/audit-gha-settings.md b/.claude/commands/audit-gha-settings.md index 7ce9368..6ce21eb 100644 --- a/.claude/commands/audit-gha-settings.md +++ b/.claude/commands/audit-gha-settings.md @@ -15,9 +15,10 @@ If no arguments given, audit the canonical fleet repo list: - `SocketDev/socket-sdk-js` - `SocketDev/socket-sdxgen` - `SocketDev/socket-stuie` +- `SocketDev/socket-vscode` +- `SocketDev/socket-webext` - `SocketDev/socket-wheelhouse` - `SocketDev/ultrathink` -- `SocketDev/vscode-socket-security` ## Process diff --git a/.claude/hooks/actionlint-on-workflow-edit/index.mts b/.claude/hooks/actionlint-on-workflow-edit/index.mts index 7046a30..5e21b15 100644 --- a/.claude/hooks/actionlint-on-workflow-edit/index.mts +++ b/.claude/hooks/actionlint-on-workflow-edit/index.mts @@ -13,7 +13,7 @@ // No-op when actionlint isn't on PATH — most fleet machines have it via // brew, CI runners have it preinstalled, but downstreams may not. -import { spawnSync } from '@socketsecurity/lib-stable/spawn' +import { spawnSync } from '@socketsecurity/lib-stable/spawn/spawn' import process from 'node:process' import { readStdin } from '../_shared/transcript.mts' diff --git a/.claude/hooks/actionlint-on-workflow-edit/test/index.test.mts b/.claude/hooks/actionlint-on-workflow-edit/test/index.test.mts index 167087d..bc748e7 100644 --- a/.claude/hooks/actionlint-on-workflow-edit/test/index.test.mts +++ b/.claude/hooks/actionlint-on-workflow-edit/test/index.test.mts @@ -1,6 +1,6 @@ // node --test specs for the actionlint-on-workflow-edit hook. -import { spawn, spawnSync } from '@socketsecurity/lib-stable/spawn' +import { spawn, spawnSync } from '@socketsecurity/lib-stable/spawn/spawn' import path from 'node:path' import { fileURLToPath } from 'node:url' import test from 'node:test' @@ -13,6 +13,11 @@ type Result = { code: number; stderr: string } async function runHook(payload: Record): Promise { const child = spawn(process.execPath, [HOOK], { stdio: 'pipe' }) + // v6 lib-stable spawn returns an enriched Promise that rejects on + // non-zero exit; this test reads stderr + exit via manual listeners + // instead. Swallow the Promise rejection so it doesn't race the + // listener-based resolve and trigger "async activity after test ended". + void child.catch(() => undefined) child.stdin!.end(JSON.stringify(payload)) let stderr = '' child.process.stderr!.on('data', chunk => { diff --git a/.claude/hooks/ask-suppression-reminder/test/index.test.mts b/.claude/hooks/ask-suppression-reminder/test/index.test.mts index 9e49bf1..f39fee1 100644 --- a/.claude/hooks/ask-suppression-reminder/test/index.test.mts +++ b/.claude/hooks/ask-suppression-reminder/test/index.test.mts @@ -3,7 +3,7 @@ // prefer-async-spawn: streaming-stdio-required — test spawns child // subprocess and pipes stdin/stdout/stderr; Node spawn returns the // ChildProcess streaming surface the lib promise wrapper does not. -import { spawn } from '@socketsecurity/lib-stable/spawn' +import { spawn } from '@socketsecurity/lib-stable/spawn/spawn' import { mkdtempSync, writeFileSync } from 'node:fs' import os from 'node:os' import path from 'node:path' @@ -28,6 +28,11 @@ function writeTranscript(userTurns: string[]): string { async function runHook(payload: Record): Promise { const child = spawn(process.execPath, [HOOK], { stdio: 'pipe' }) + // v6 lib-stable spawn returns an enriched Promise that rejects on + // non-zero exit; this test reads stderr + exit via manual listeners + // instead. Swallow the Promise rejection so it doesn't race the + // listener-based resolve and trigger "async activity after test ended". + void child.catch(() => undefined) child.stdin!.end(JSON.stringify(payload)) let stderr = '' child.process.stderr!.on('data', chunk => { diff --git a/.claude/hooks/auth-rotation-reminder/index.mts b/.claude/hooks/auth-rotation-reminder/index.mts index 5da5864..a408abf 100644 --- a/.claude/hooks/auth-rotation-reminder/index.mts +++ b/.claude/hooks/auth-rotation-reminder/index.mts @@ -40,7 +40,7 @@ // SOCKET_AUTH_ROTATION_DISABLED default: unset // If set to a truthy value, skip the hook entirely. -import { spawnSync } from '@socketsecurity/lib-stable/spawn' +import { spawnSync } from '@socketsecurity/lib-stable/spawn/spawn' import { existsSync, mkdirSync, @@ -54,7 +54,7 @@ import path from 'node:path' import process from 'node:process' import { errorMessage } from '@socketsecurity/lib-stable/errors' -import { safeDelete } from '@socketsecurity/lib-stable/fs' +import { safeDelete } from '@socketsecurity/lib-stable/fs/safe' import { getDefaultLogger } from '@socketsecurity/lib-stable/logger' import { diff --git a/.claude/hooks/auth-rotation-reminder/test/auth-rotation-reminder.test.mts b/.claude/hooks/auth-rotation-reminder/test/auth-rotation-reminder.test.mts index 2f25192..54f457e 100644 --- a/.claude/hooks/auth-rotation-reminder/test/auth-rotation-reminder.test.mts +++ b/.claude/hooks/auth-rotation-reminder/test/auth-rotation-reminder.test.mts @@ -1,7 +1,7 @@ // prefer-async-spawn: streaming-stdio-required — test spawns child // subprocess and pipes stdin/stdout/stderr; Node spawn returns the // ChildProcess streaming surface the lib promise wrapper does not. -import { spawn } from '@socketsecurity/lib-stable/spawn' +import { spawn } from '@socketsecurity/lib-stable/spawn/spawn' import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from 'node:fs' import os from 'node:os' import path from 'node:path' @@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url' import { test } from 'node:test' import assert from 'node:assert/strict' -import { safeDelete } from '@socketsecurity/lib-stable/fs' +import { safeDelete } from '@socketsecurity/lib-stable/fs/safe' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const HOOK = path.resolve(__dirname, '..', 'index.mts') @@ -36,6 +36,11 @@ function runHook( ...opts.env, }, }) + // v6 lib-stable spawn returns an enriched Promise that rejects on + // non-zero exit; this test reads stderr + exit via manual listeners + // instead. Swallow the Promise rejection so it doesn't race the + // listener-based resolve and trigger "async activity after test ended". + void child.catch(() => undefined) let stderr = '' child.process.stderr!.on('data', d => { stderr += d.toString() diff --git a/.claude/hooks/check-new-deps/audit.mts b/.claude/hooks/check-new-deps/audit.mts index fa78ab7..be3f3fb 100644 --- a/.claude/hooks/check-new-deps/audit.mts +++ b/.claude/hooks/check-new-deps/audit.mts @@ -26,8 +26,8 @@ import path from 'node:path' import { stringify } from '@socketregistry/packageurl-js-stable' import type { PackageURL } from '@socketregistry/packageurl-js-stable' -import { createTtlCache } from '@socketsecurity/lib-stable/cache-with-ttl' -import type { TtlCache } from '@socketsecurity/lib-stable/cache-with-ttl' +import { createTtlCache } from '@socketsecurity/lib-stable/ttl-cache/cache' +import type { TtlCache } from '@socketsecurity/lib-stable/ttl-cache/types' import { errorMessage } from '@socketsecurity/lib-stable/errors' import type { diff --git a/.claude/hooks/claude-md-section-size-guard/README.md b/.claude/hooks/claude-md-section-size-guard/README.md index 444027e..54c4b97 100644 --- a/.claude/hooks/claude-md-section-size-guard/README.md +++ b/.claude/hooks/claude-md-section-size-guard/README.md @@ -4,7 +4,7 @@ PreToolUse hook that caps the body length of individual `### ` sections inside t ## What it does -Complements `claude-md-size-guard` (40KB byte cap on the whole block) by enforcing a per-section line cap inside the block. Each `### Section heading` inside the `` markers gets at most **8 body lines** (configurable via `CLAUDE_MD_FLEET_SECTION_MAX_LINES`). +Complements `claude-md-size-guard` (48KB byte cap on the whole block) by enforcing a per-section line cap inside the block. Each `### Section heading` inside the `` markers gets at most **8 body lines** (configurable via `CLAUDE_MD_FLEET_SECTION_MAX_LINES`). Sections that exceed 8 lines should have a long-form companion at `docs/claude.md/fleet/.md` and the inline body should shrink to 1-2 sentences plus a link. The cap was 20 initially (during the bootstrap when several fleet sections were 12-19 lines); it tightened to 8 once those sections were outsourced. @@ -24,7 +24,7 @@ When a section exceeds the cap, the hook prints: ## Why a per-section cap, not just the byte cap -The failure mode this hook addresses: an operator can grow a single rule from 2 lines to 60 lines of detailed prose without ever tripping the 40KB byte cap — until enough other sections accrete that an unrelated 1-line addition breaks the build. The per-section cap catches this directly, at the moment the long content is written, when the operator has the long-form text in hand and can immediately drop it into a `docs/claude.md/fleet/.md` companion. +The failure mode this hook addresses: an operator can grow a single rule from 2 lines to 60 lines of detailed prose without ever tripping the 48KB byte cap — until enough other sections accrete that an unrelated 1-line addition breaks the build. The per-section cap catches this directly, at the moment the long content is written, when the operator has the long-form text in hand and can immediately drop it into a `docs/claude.md/fleet/.md` companion. ## Override diff --git a/.claude/hooks/claude-md-section-size-guard/test/index.test.mts b/.claude/hooks/claude-md-section-size-guard/test/index.test.mts index 5910406..3a6fc1e 100644 --- a/.claude/hooks/claude-md-section-size-guard/test/index.test.mts +++ b/.claude/hooks/claude-md-section-size-guard/test/index.test.mts @@ -5,7 +5,7 @@ import assert from 'node:assert/strict' // prefer-async-spawn: streaming-stdio-required — test spawns child // subprocess and pipes stdin/stdout/stderr; Node spawn returns the // ChildProcess streaming surface the lib promise wrapper does not. -import { spawn } from '@socketsecurity/lib-stable/spawn' +import { spawn } from '@socketsecurity/lib-stable/spawn/spawn' import path from 'node:path' import { fileURLToPath } from 'node:url' @@ -22,6 +22,11 @@ async function runHook( stdio: 'pipe', env: { ...process.env, ...env }, }) + // v6 lib-stable spawn returns an enriched Promise that rejects on + // non-zero exit; this test reads stderr + exit via manual listeners + // instead. Swallow the Promise rejection so it doesn't race the + // listener-based resolve and trigger "async activity after test ended". + void child.catch(() => undefined) child.stdin!.end(JSON.stringify(payload)) let stderr = '' child.process.stderr!.on('data', chunk => { diff --git a/.claude/hooks/claude-md-size-guard/README.md b/.claude/hooks/claude-md-size-guard/README.md index ead7e36..910c722 100644 --- a/.claude/hooks/claude-md-size-guard/README.md +++ b/.claude/hooks/claude-md-size-guard/README.md @@ -1,10 +1,10 @@ # claude-md-size-guard -PreToolUse Edit/Write hook that blocks CLAUDE.md edits which would push the **fleet-canonical block** (between `` / `` markers) above 40 KB. +PreToolUse Edit/Write hook that blocks CLAUDE.md edits which would push the **fleet-canonical block** (between `` / `` markers) above 48 KB. ## Why -The fleet block is byte-identical across every `socket-*` repo. Every byte added there costs N copies of in-context tokens fleet-wide. Per-repo content outside the markers is paid once. Capping the fleet block at 40 KB: +The fleet block is byte-identical across every `socket-*` repo. Every byte added there costs N copies of in-context tokens fleet-wide. Per-repo content outside the markers is paid once. Capping the fleet block at 48 KB: - Forces new fleet rules to be **terse + reference-deferred** (link to `docs/references/.md`). - Leaves headroom for per-repo content. Per-repo CLAUDE.md additions are NOT capped here. @@ -16,7 +16,7 @@ The hook fires on Edit/Write tool calls. For Write, it inspects `content`. For E ## Cap -- **Default:** 40 KB (40 960 bytes). +- **Default:** 48 KB (49 152 bytes). Sized to leave per-repo CLAUDE.md additions ample room outside the fleet block. - **Override:** set `CLAUDE_MD_FLEET_BLOCK_BYTES=` in env (rarely needed; bumping the cap should be a deliberate fleet-wide decision). ## Failing open diff --git a/.claude/hooks/claude-md-size-guard/index.mts b/.claude/hooks/claude-md-size-guard/index.mts index 06338ef..efd5011 100644 --- a/.claude/hooks/claude-md-size-guard/index.mts +++ b/.claude/hooks/claude-md-size-guard/index.mts @@ -2,7 +2,7 @@ // Claude Code PreToolUse hook — claude-md-size-guard. // // Blocks Edit/Write tool calls that would push the CLAUDE.md -// fleet-canonical block above the 40KB size cap. The fleet block lives +// fleet-canonical block above the 48KB size cap. The fleet block lives // between `` and `` // markers; everything outside is per-repo content owned by the host // repo (different cap, evaluated separately). @@ -24,8 +24,9 @@ // remediation (move detail into `docs/references/.md`). // // Cap policy: -// - Default: 40 KB (40_960 bytes). Override per-repo by setting -// `CLAUDE_MD_FLEET_BLOCK_BYTES` in the env (rarely needed). +// - Default: 48 KB (49_152 bytes). Sized to leave room for per-repo +// CLAUDE.md additions outside the fleet block. Override per-repo by +// setting `CLAUDE_MD_FLEET_BLOCK_BYTES` in the env (rarely needed). // - Whole-file cap: NOT enforced here. Per-repo content can grow // freely; this hook only protects the fleet block. // @@ -44,7 +45,7 @@ import process from 'node:process' import { readStdin } from '../_shared/transcript.mts' -const DEFAULT_CAP_BYTES = 40 * 1024 +const DEFAULT_CAP_BYTES = 48 * 1024 const FLEET_BEGIN_MARKER = '', + '```sh', + 'npm install lodash', + '```', + ].join('\n') + assert.strictEqual(scanDocsPnpmFirst(md).length, 0) +}) + +test('scanDocsPnpmFirst: handles multiple fences independently', () => { + const md = [ + '```sh', + 'pnpm add foo', + '```', + '', + 'And here is a fallback:', + '', + '```sh', + 'npm install foo', + '```', + ].join('\n') + // First fence has pnpm form → ok. Second fence is bare npm with no + // pnpm leader → one warning. + assert.strictEqual(scanDocsPnpmFirst(md).length, 1) +}) + +test('scanDocsPnpmFirst: handles $-prefixed prompt lines', () => { + const md = ['```sh', '$ npm install foo', '```'].join('\n') + assert.strictEqual(scanDocsPnpmFirst(md).length, 1) +}) + +test('scanDocsPnpmFirst: ignores non-install npm commands', () => { + // `npm run build` is not an install — out of scope for the leader + // rule (which is about how users *get* a package). + const md = ['```sh', 'npm run build', '```'].join('\n') + assert.strictEqual(scanDocsPnpmFirst(md).length, 0) +}) diff --git a/docs/claude.md/fleet/sorting.md b/docs/claude.md/fleet/sorting.md index 2f7b5cc..9ea6ea6 100644 --- a/docs/claude.md/fleet/sorting.md +++ b/docs/claude.md/fleet/sorting.md @@ -12,7 +12,7 @@ Sort lists alphanumerically (literal byte order, ASCII before letters). - **`Set` constructor arguments** — `new Set([...])` and `new SafeSet([...])` literals. The runtime is order-insensitive, so source order is alphanumeric. Same rationale as Array literals: predictable diffs, no merge conflicts on insertions. - **Regex alternation groups** — `(foo|bar|baz)` reads as `(bar|baz|foo)`. Capturing, non-capturing, and named-capture groups all follow the rule. Auto-fixable when every alternative is a simple literal. The exception is order-bearing alternations where the regex engine MUST try one alternative before another (rare; the canonical example is markup parsers where `` would silently mismatch if reordered) — append `// socket-hook: allow regex-alternation-order` on those lines. - **String-equality disjunctions** — `x === 'a' || x === 'b' || x === 'c'` reads with the comparand strings in alpha order. The De Morgan dual `x !== 'a' && x !== 'b'` (negative-membership check) follows the same rule. The `||` chain short-circuits regardless of operand order; sorting reduces diff churn when adding new comparands and makes "is X in this set?" checks visually consistent. Auto-fixable when every clause has the same left operand and uses string-literal comparands. Mixed shape (different left, different operator, non-string right) is skipped — those are usually genuine ordering-sensitive predicates and the autofix would change semantics. -- **Boolean identifier chains** — `agentshieldOk && zizmorOk && sfwOk` reads with the names in alpha order: `agentshieldOk && sfwOk && zizmorOk`. Same rule for `||` chains. The lint rule fires only when every leaf is a bare `Identifier` (no calls, no member access, no literals, no negations) — those have side-effect or short-circuit semantics where order can be observable. Duplicate identifiers and chains with interior comments are skipped (the autofix would lose information). Enforced by `socket/sort-boolean-chains`. +- **Boolean identifier chains** — `agentshieldOk && zizmorOk && sfwOk` reads with the names in alpha order: `agentshieldOk && sfwOk && zizmorOk`. Same rule for `||` chains. The lint rule fires only when (1) every leaf is a bare `Identifier` (no calls, no member access, no literals, no negations) — those have side-effect or short-circuit semantics where order can be observable — AND (2) the chain has **3 or more operands**. Two-operand chains like `useHttp && oauthEnabled` are guard patterns where order carries narrative ("in HTTP mode, did OAuth get enabled?") that alpha-sort would destroy; only length-3+ chains are unambiguously flag lists. Duplicate identifiers and chains with interior comments are skipped (the autofix would lose information). Enforced by `socket/sort-boolean-chains`. - **TypeScript union of string literals** — `type Source = 'download' | 'path' | 'vfs'` (not `'vfs' | 'path' | 'download'`). Members are interchangeable at the type level; alpha order makes "which values can this take?" answerable without scanning. Applies to type aliases, inline parameter unions, and template-literal type alternatives. Position-bearing unions (rare — e.g. a discriminator where order encodes priority) keep their meaningful order; append `// socket-hook: allow union-order` on those lines. ## Default diff --git a/packages/build-infra/lib/release-checksums/consumer.mts b/packages/build-infra/lib/release-checksums/consumer.mts index 33222be..e7b34d8 100644 --- a/packages/build-infra/lib/release-checksums/consumer.mts +++ b/packages/build-infra/lib/release-checksums/consumer.mts @@ -17,9 +17,9 @@ import path from 'node:path' import process from 'node:process' import { errorMessage } from '@socketsecurity/lib/errors' -import { safeMkdir } from '@socketsecurity/lib/fs' +import { safeMkdir } from '@socketsecurity/lib/fs/safe' import { getDefaultLogger } from '@socketsecurity/lib/logger' -import { getLatestRelease } from '@socketsecurity/lib/releases/github-api' +import { getLatestRelease } from '@socketsecurity/lib/releases/github-listing' import { downloadReleaseAsset } from '@socketsecurity/lib/releases/github-downloads' import type { RepoConfig } from '@socketsecurity/lib/releases/github-types' @@ -35,34 +35,41 @@ export interface ChecksumsResult { const checksumCache = new Map() +/** + * Clear the checksum cache. Useful for testing or forcing re-download. + */ +export function clearChecksumCache(): void { + checksumCache.clear() +} + interface GetChecksumsOptions { /** * The producing repo whose releases we're verifying against. */ repoConfig: RepoConfig /** - * Tool name prefix used in the producing repo's release tag (e.g. `iocraft`). + * Tool name prefix used in the producing repo's release tag (e.g. `lief`). */ tool: string /** * Specific tag to fetch. If omitted, uses the embedded tag, then `latest`. */ - releaseTag?: string + releaseTag?: string | undefined /** * Where to cache the downloaded `checksums.txt`. Defaults to * `/build/temp`. */ - tempDir?: string + tempDir?: string | undefined /** * Suppress info/warn logging (errors still log). */ - quiet?: boolean + quiet?: boolean | undefined /** * If true (default), use embedded checksums when available even if a network * fetch could find newer ones. Set false to force a network fetch — useful * when bumping checksums. */ - preferEmbedded?: boolean + preferEmbedded?: boolean | undefined } /** @@ -208,10 +215,3 @@ export async function getReleaseChecksums( return { checksums: {}, source: 'network', tag } } } - -/** - * Clear the checksum cache. Useful for testing or forcing re-download. - */ -export function clearChecksumCache(): void { - checksumCache.clear() -} diff --git a/packages/build-infra/lib/release-checksums/core.mts b/packages/build-infra/lib/release-checksums/core.mts index 7be5e63..aaa1ae5 100644 --- a/packages/build-infra/lib/release-checksums/core.mts +++ b/packages/build-infra/lib/release-checksums/core.mts @@ -29,7 +29,7 @@ const __dirname = path.dirname(__filename) // --------------------------------------------------------------------------- export interface ToolConfig { - description?: string + description?: string | undefined tag: string checksums: Record } @@ -37,10 +37,10 @@ export interface ToolConfig { export type EmbeddedChecksums = Record export interface VerifyResult { - actual?: string - expected?: string - source?: string - skipped?: boolean + actual?: string | undefined + expected?: string | undefined + source?: string | undefined + skipped?: boolean | undefined valid: boolean } @@ -55,27 +55,16 @@ export interface VerifyResult { let embeddedChecksums: EmbeddedChecksums | undefined | null -export function getEmbeddedChecksums(): EmbeddedChecksums | undefined { - if (embeddedChecksums === null) { - return undefined - } - if (embeddedChecksums === undefined) { - try { - const checksumPath = path.join( - __dirname, - '..', - '..', - 'release-assets.json', - ) - embeddedChecksums = JSON.parse( - readFileSync(checksumPath, 'utf8'), - ) as EmbeddedChecksums - } catch { - embeddedChecksums = null - return undefined - } +/** + * Compute SHA256 hash of a file as lowercase hex. + */ +export async function computeFileHash(filePath: string): Promise { + const hash = crypto.createHash('sha256') + const stream = createReadStream(filePath) + for await (const chunk of stream) { + hash.update(chunk) } - return embeddedChecksums + return hash.digest('hex') } export function getEmbeddedChecksum( @@ -97,20 +86,27 @@ export function getEmbeddedChecksum( return { checksum, tag: toolConfig.tag } } -// --------------------------------------------------------------------------- -// Format primitives. -// --------------------------------------------------------------------------- - -/** - * Compute SHA256 hash of a file as lowercase hex. - */ -export async function computeFileHash(filePath: string): Promise { - const hash = crypto.createHash('sha256') - const stream = createReadStream(filePath) - for await (const chunk of stream) { - hash.update(chunk) +export function getEmbeddedChecksums(): EmbeddedChecksums | undefined { + if (embeddedChecksums === null) { + return undefined } - return hash.digest('hex') + if (embeddedChecksums === undefined) { + try { + const checksumPath = path.join( + __dirname, + '..', + '..', + 'release-assets.json', + ) + embeddedChecksums = JSON.parse( + readFileSync(checksumPath, 'utf8'), + ) as EmbeddedChecksums + } catch { + embeddedChecksums = undefined + return undefined + } + } + return embeddedChecksums } /** @@ -136,15 +132,11 @@ export function parseChecksums(content: string): Record { return checksums } -// --------------------------------------------------------------------------- -// Verify. -// --------------------------------------------------------------------------- - interface VerifyOptions { filePath: string assetName: string tool: string - quiet?: boolean + quiet?: boolean | undefined } /** diff --git a/packages/build-infra/release-assets.schema.json b/packages/build-infra/release-assets.schema.json index d049c86..4044659 100644 --- a/packages/build-infra/release-assets.schema.json +++ b/packages/build-infra/release-assets.schema.json @@ -29,7 +29,7 @@ }, "tag": { "type": "string", - "description": "Full release tag, including the tool prefix (e.g. `iocraft-20260424-18f0f46`).", + "description": "Full release tag, including the tool prefix (e.g. `lief-20260507-76c1796`).", "minLength": 1 }, "checksums": { diff --git a/scripts/ai-lint-fix/cli.mts b/scripts/ai-lint-fix/cli.mts index 51df308..bdcea6e 100644 --- a/scripts/ai-lint-fix/cli.mts +++ b/scripts/ai-lint-fix/cli.mts @@ -36,7 +36,8 @@ import path from 'node:path' import process from 'node:process' import { getDefaultLogger } from '@socketsecurity/lib-stable/logger' -import { isSpawnError, spawn } from '@socketsecurity/lib-stable/spawn' +import { isSpawnError } from '@socketsecurity/lib-stable/spawn/errors' +import { spawn } from '@socketsecurity/lib-stable/spawn/spawn' import { AI_HANDLED_RULES, RULE_GUIDANCE } from './rule-guidance.mts' diff --git a/scripts/ai-lint-fix/rule-guidance.mts b/scripts/ai-lint-fix/rule-guidance.mts index 2847584..12dd301 100644 --- a/scripts/ai-lint-fix/rule-guidance.mts +++ b/scripts/ai-lint-fix/rule-guidance.mts @@ -54,18 +54,18 @@ export const RULE_GUIDANCE: Readonly> = { 'Rewrite `fs.access` / `fs.stat` existence-checks to `existsSync(p)` from `node:fs`. Common shapes: `try { await fs.access(p); return true } catch { return false }` → `return existsSync(p)`. `await fs.access(p).then(() => true).catch(() => false)` → `existsSync(p)`. `if (await fs.stat(p))` → `if (existsSync(p))`. When the stat result is destructured for metadata (`s.size`, `s.mtime`, `s.isDirectory()`), KEEP the stat call and add a one-line comment stating intent — that is not an existence check. Trace back through callers: if the caller awaited a Promise, the rewrite collapses to a sync boolean and the await becomes a no-op (safe).', 'socket/prefer-node-builtin-imports': "Rewrite `import fs from 'node:fs'` / `import * as fs from 'node:fs'` to `import { … } from 'node:fs'` with the names actually used in the file. Change every `fs.X` reference to bare `X`. If `fs` is passed as a value (e.g. `someApi(fs)`), keep the namespace import and add a `// prefer-node-builtin-imports: passed-as-value` comment.", - 'socket/prefer-async-spawn': `Replace \`node:child_process\` spawn calls with their \`@socketsecurity/lib-stable/spawn\` equivalents. The lib re-exports BOTH names so a sync caller keeps using \`spawnSync\` and only the import source changes; only convert sync → async when the enclosing function is already async (or can be safely made async) AND every caller of that function is async-ready. + 'socket/prefer-async-spawn': `Replace \`node:child_process\` spawn calls with their \`@socketsecurity/lib-stable/spawn/spawn\` equivalents. The lib re-exports BOTH names so a sync caller keeps using \`spawnSync\` and only the import source changes; only convert sync → async when the enclosing function is already async (or can be safely made async) AND every caller of that function is async-ready. 1. List every spawn-family callsite in the file: \`spawnSync(\`, \`spawn(\`, \`child_process.spawnSync(\`, \`cp.spawnSync(\`. Note which names are actually used. 2. For each callsite, decide: (a) keep sync semantics — use \`spawnSync\` from the lib (drop-in, same args, same return shape \`{ status, stdout, stderr }\`); or (b) convert to async — use \`spawn\` from the lib (returns a Promise of \`{ code, stdout, stderr }\`, requires \`await\`, requires async enclosing context, return shape uses \`.code\` not \`.status\`). Default to (a) unless you can verify (b) is safe — sync → async is a contract change. - 3. Update the import line. If every callsite stays sync: \`import { spawnSync } from '@socketsecurity/lib-stable/spawn'\`. If every callsite becomes async: \`import { spawn } from '@socketsecurity/lib-stable/spawn'\`. If mixed: \`import { spawn, spawnSync } from '@socketsecurity/lib-stable/spawn'\`. - 4. Self-verify before stopping: re-read the file. Confirm EVERY \`spawnSync(\` callsite is satisfied by the new import (either the name is in the import list OR you converted that callsite to \`await spawn(\`). A file with \`import { spawn } from '@socketsecurity/lib-stable/spawn'\` and a body containing \`spawnSync(\` is broken — fix it before you declare done. + 3. Update the import line. If every callsite stays sync: \`import { spawnSync } from '@socketsecurity/lib-stable/spawn/spawn'\`. If every callsite becomes async: \`import { spawn } from '@socketsecurity/lib-stable/spawn/spawn'\`. If mixed: \`import { spawn, spawnSync } from '@socketsecurity/lib-stable/spawn/spawn'\`. + 4. Self-verify before stopping: re-read the file. Confirm EVERY \`spawnSync(\` callsite is satisfied by the new import (either the name is in the import list OR you converted that callsite to \`await spawn(\`). A file with \`import { spawn } from '@socketsecurity/lib-stable/spawn/spawn'\` and a body containing \`spawnSync(\` is broken — fix it before you declare done. - import { spawnSync } from 'node:child_process' -+ import { spawnSync } from '@socketsecurity/lib-stable/spawn' ++ import { spawnSync } from '@socketsecurity/lib-stable/spawn/spawn' function run(cmd) { const r = spawnSync(cmd, [], { encoding: 'utf8' }) @@ -75,7 +75,7 @@ export const RULE_GUIDANCE: Readonly> = { - import { spawnSync } from 'node:child_process' -+ import { spawn } from '@socketsecurity/lib-stable/spawn' ++ import { spawn } from '@socketsecurity/lib-stable/spawn/spawn' function run(cmd) { const r = spawnSync(cmd, [], { encoding: 'utf8' }) // ❌ spawnSync is no longer imported — runtime ReferenceError @@ -85,7 +85,7 @@ export const RULE_GUIDANCE: Readonly> = { - import { spawnSync } from 'node:child_process' -+ import { spawn } from '@socketsecurity/lib-stable/spawn' ++ import { spawn } from '@socketsecurity/lib-stable/spawn/spawn' async function run(cmd) { const r = await spawn(cmd, [], { stdio: 'pipe' }) diff --git a/scripts/check-prompt-less-setup.mts b/scripts/check-prompt-less-setup.mts index 0edd986..7cecaa4 100644 --- a/scripts/check-prompt-less-setup.mts +++ b/scripts/check-prompt-less-setup.mts @@ -26,7 +26,7 @@ * signing/keychain prompt surprises you. */ -import { spawnSync } from '@socketsecurity/lib-stable/spawn' +import { spawnSync } from '@socketsecurity/lib-stable/spawn/spawn' import { existsSync, readFileSync } from 'node:fs' import os from 'node:os' import path from 'node:path' diff --git a/scripts/fix.mts b/scripts/fix.mts index c57d75c..d79bc8e 100644 --- a/scripts/fix.mts +++ b/scripts/fix.mts @@ -20,7 +20,7 @@ import { existsSync } from 'node:fs' import process from 'node:process' import { getDefaultLogger } from '@socketsecurity/lib-stable/logger' -import { spawn } from '@socketsecurity/lib-stable/spawn' +import { spawn } from '@socketsecurity/lib-stable/spawn/spawn' const WIN32 = process.platform === 'win32' const logger = getDefaultLogger() diff --git a/scripts/install-claude-plugins.mts b/scripts/install-claude-plugins.mts index 2563205..47ba52a 100644 --- a/scripts/install-claude-plugins.mts +++ b/scripts/install-claude-plugins.mts @@ -26,7 +26,7 @@ * matching version + sha + ISO date. */ -import { spawnSync } from '@socketsecurity/lib-stable/spawn' +import { spawnSync } from '@socketsecurity/lib-stable/spawn/spawn' import { existsSync, readFileSync } from 'node:fs' import path from 'node:path' import process from 'node:process' diff --git a/scripts/install-git-hooks.mts b/scripts/install-git-hooks.mts index 24420ea..bae3fef 100644 --- a/scripts/install-git-hooks.mts +++ b/scripts/install-git-hooks.mts @@ -10,7 +10,7 @@ * into this repo yet). */ -import { spawnSync } from '@socketsecurity/lib-stable/spawn' +import { spawnSync } from '@socketsecurity/lib-stable/spawn/spawn' import { existsSync } from 'node:fs' import path from 'node:path' import process from 'node:process' diff --git a/scripts/install-sfw.mts b/scripts/install-sfw.mts index 8689c23..552ed9c 100644 --- a/scripts/install-sfw.mts +++ b/scripts/install-sfw.mts @@ -37,7 +37,7 @@ import { parseArgs } from 'node:util' import { WIN32, getArch } from '@socketsecurity/lib-stable/constants/platform' import { downloadBinary } from '@socketsecurity/lib-stable/dlx/binary' import { errorMessage } from '@socketsecurity/lib-stable/errors' -import { safeDelete, safeMkdirSync } from '@socketsecurity/lib-stable/fs' +import { safeDelete, safeMkdirSync } from '@socketsecurity/lib-stable/fs/safe' import { getDefaultLogger } from '@socketsecurity/lib-stable/logger' import { getSocketAppDir, diff --git a/scripts/install-token-minifier.mts b/scripts/install-token-minifier.mts index 2430a2e..f5a9f57 100644 --- a/scripts/install-token-minifier.mts +++ b/scripts/install-token-minifier.mts @@ -34,10 +34,10 @@ import { fileURLToPath } from 'node:url' import { parseArgs } from 'node:util' import { errorMessage } from '@socketsecurity/lib-stable/errors' -import { safeMkdirSync } from '@socketsecurity/lib-stable/fs' +import { safeMkdirSync } from '@socketsecurity/lib-stable/fs/safe' import { getDefaultLogger } from '@socketsecurity/lib-stable/logger' import { getSocketAppDir } from '@socketsecurity/lib-stable/paths/socket' -import { spawnSync } from '@socketsecurity/lib-stable/spawn' +import { spawnSync } from '@socketsecurity/lib-stable/spawn/spawn' const logger = getDefaultLogger() @@ -94,7 +94,7 @@ interface CatalogYamlMap { * line-shaped (key: value) and we only need the @socketsecurity/* entries the * proxy actually references. */ -function readNeededCatalogEntries(): CatalogYamlMap { +export function readNeededCatalogEntries(): CatalogYamlMap { const yamlPath = path.join(WHEELHOUSE_ROOT, 'pnpm-workspace.yaml') const text = readFileSync(yamlPath, 'utf8') const lines = text.split('\n') @@ -140,7 +140,7 @@ function readNeededCatalogEntries(): CatalogYamlMap { * catalog aliases the package source declares. Keeps imports of * `@socketsecurity/lib-stable/...` resolvable from inside the install. */ -function writeInstallWorkspaceYaml(catalog: CatalogYamlMap): void { +export function writeInstallWorkspaceYaml(catalog: CatalogYamlMap): void { const lines = ['catalog:'] for (const [k, v] of Object.entries(catalog)) { // Quote values that aren't bare versions (e.g. `npm:foo@1.0.0`). @@ -161,7 +161,7 @@ function writeInstallWorkspaceYaml(catalog: CatalogYamlMap): void { * Stripping `bin`/`exports` keeps pnpm from trying to wire global binaries at * install time — we drop our own shim explicitly. */ -function writeInstallPackageJson(sourceVersion: string): void { +export function writeInstallPackageJson(sourceVersion: string): void { const sourcePkg = JSON.parse( readFileSync(path.join(PKG_SOURCE_DIR, 'package.json'), 'utf8'), ) @@ -190,7 +190,7 @@ function writeInstallPackageJson(sourceVersion: string): void { * `fs.cp` with recursive + force is the cross-platform equivalent of `cp -r`. * Force overwrites stale files on reinstall. */ -function copySource(): void { +export function copySource(): void { // Use sync fs API for consistency with the rest of the script — this // is a one-shot install, not a hot path. `cpSync` exists since // Node 20; the recursive option is required for directories. @@ -207,14 +207,14 @@ function copySource(): void { * when the recorded x-source-version in the dest's package.json differs from * the source. */ -function readSourceVersion(): string { +export function readSourceVersion(): string { const pkg = JSON.parse( readFileSync(path.join(PKG_SOURCE_DIR, 'package.json'), 'utf8'), ) return pkg.version ?? '0.0.0' } -function readInstalledVersion(): string | undefined { +export function readInstalledVersion(): string | undefined { const installedPkgPath = path.join(INSTALL_DIR, 'package.json') if (!existsSync(installedPkgPath)) { return undefined @@ -227,7 +227,7 @@ function readInstalledVersion(): string | undefined { } } -function pnpmInstallAtDest(quiet: boolean): void { +export function pnpmInstallAtDest(quiet: boolean): void { const result = spawnSync( 'pnpm', [ @@ -248,7 +248,7 @@ function pnpmInstallAtDest(quiet: boolean): void { } } -function writeBinShim(): void { +export function writeBinShim(): void { // Shim execs the proxy's top-level bin/ entry. Source lives at // INSTALL_DIR/bin/, NOT under node_modules/ — so Node 22+ can strip // types from the .mts file at runtime. `node` is on PATH on every diff --git a/scripts/janus.mts b/scripts/janus.mts index 1e369e5..539eb7e 100644 --- a/scripts/janus.mts +++ b/scripts/janus.mts @@ -27,7 +27,7 @@ import { fileURLToPath } from 'node:url' import { WIN32 } from '@socketsecurity/lib-stable/constants/platform' import { getDefaultLogger } from '@socketsecurity/lib-stable/logger' import { getSocketHomePath } from '@socketsecurity/lib-stable/paths/socket' -import { spawn } from '@socketsecurity/lib-stable/spawn' +import { spawn } from '@socketsecurity/lib-stable/spawn/spawn' const logger = getDefaultLogger() diff --git a/scripts/lint-github-settings.mts b/scripts/lint-github-settings.mts index ab504c0..ff28721 100644 --- a/scripts/lint-github-settings.mts +++ b/scripts/lint-github-settings.mts @@ -22,7 +22,7 @@ * scripts/lint-github-settings.mts --json # machine-readable. */ -import { spawnSync } from '@socketsecurity/lib-stable/spawn' +import { spawnSync } from '@socketsecurity/lib-stable/spawn/spawn' import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' import path from 'node:path' import process from 'node:process' diff --git a/scripts/lockstep/emit-schema.mts b/scripts/lockstep/emit-schema.mts index bec79ef..84c65da 100644 --- a/scripts/lockstep/emit-schema.mts +++ b/scripts/lockstep/emit-schema.mts @@ -10,7 +10,7 @@ import { writeFileSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' -import { spawn } from '@socketsecurity/lib-stable/spawn' +import { spawn } from '@socketsecurity/lib-stable/spawn/spawn' import { getDefaultLogger } from '@socketsecurity/lib-stable/logger' import { LockstepManifestSchema } from './schema.mts' diff --git a/scripts/lockstep/git.mts b/scripts/lockstep/git.mts index 1a9f8e8..42d785a 100644 --- a/scripts/lockstep/git.mts +++ b/scripts/lockstep/git.mts @@ -10,7 +10,7 @@ * helpers write to. */ -import { spawnSync } from '@socketsecurity/lib-stable/spawn' +import { spawnSync } from '@socketsecurity/lib-stable/spawn/spawn' import type { Upstream } from './schema.mts' import type { DriftCommit, Manifest } from './types.mts' diff --git a/scripts/power-state.mts b/scripts/power-state.mts index 2e75287..9418340 100644 --- a/scripts/power-state.mts +++ b/scripts/power-state.mts @@ -29,7 +29,7 @@ import { isBuiltin } from 'node:module' import path from 'node:path' import process from 'node:process' -import { spawn } from '@socketsecurity/lib-stable/spawn' +import { spawn } from '@socketsecurity/lib-stable/spawn/spawn' // Probe for node:smol-power. Lives in socket-btm's node-smol binary // — `isBuiltin()` returns true on those builds and false on system diff --git a/scripts/security.mts b/scripts/security.mts index 426f51a..d04ae92 100644 --- a/scripts/security.mts +++ b/scripts/security.mts @@ -20,10 +20,10 @@ import process from 'node:process' -import { which } from '@socketsecurity/lib-stable/bin' +import { which } from '@socketsecurity/lib-stable/bin/which' import { WIN32 } from '@socketsecurity/lib-stable/constants/platform' import { getDefaultLogger } from '@socketsecurity/lib-stable/logger' -import { spawn } from '@socketsecurity/lib-stable/spawn' +import { spawn } from '@socketsecurity/lib-stable/spawn/spawn' const logger = getDefaultLogger() diff --git a/scripts/socket-wheelhouse-emit-schema.mts b/scripts/socket-wheelhouse-emit-schema.mts index adc84b5..5a60c30 100644 --- a/scripts/socket-wheelhouse-emit-schema.mts +++ b/scripts/socket-wheelhouse-emit-schema.mts @@ -8,7 +8,7 @@ import { writeFileSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' -import { spawn } from '@socketsecurity/lib-stable/spawn' +import { spawn } from '@socketsecurity/lib-stable/spawn/spawn' import { getDefaultLogger } from '@socketsecurity/lib-stable/logger' import { SocketWheelhouseConfigSchema } from './socket-wheelhouse-schema.mts' diff --git a/scripts/test/check-lock-step-header.test.mts b/scripts/test/check-lock-step-header.test.mts index 605763f..ea783bb 100644 --- a/scripts/test/check-lock-step-header.test.mts +++ b/scripts/test/check-lock-step-header.test.mts @@ -9,7 +9,7 @@ // whose header lists peers + the peer files themselves, vary the // peers' headers, and inspect exit code + stderr. -import { spawnSync } from '@socketsecurity/lib-stable/spawn' +import { spawnSync } from '@socketsecurity/lib-stable/spawn/spawn' import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import path from 'node:path' import os from 'node:os' diff --git a/scripts/test/check-lock-step-refs.test.mts b/scripts/test/check-lock-step-refs.test.mts index 2c7ad42..6958a85 100644 --- a/scripts/test/check-lock-step-refs.test.mts +++ b/scripts/test/check-lock-step-refs.test.mts @@ -11,7 +11,7 @@ // script from that cwd and inspect exit code + stderr/stdout. Each test // owns its own tmpdir to avoid cross-pollution. -import { spawnSync } from '@socketsecurity/lib-stable/spawn' +import { spawnSync } from '@socketsecurity/lib-stable/spawn/spawn' import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import path from 'node:path' import os from 'node:os' diff --git a/scripts/test/install-git-hooks.test.mts b/scripts/test/install-git-hooks.test.mts index 6b8e451..81ca3dd 100644 --- a/scripts/test/install-git-hooks.test.mts +++ b/scripts/test/install-git-hooks.test.mts @@ -18,7 +18,7 @@ // resolve REPO_ROOT to the real repo and write to the real git config // instead of the tmpdir, which is what we want to verify. -import { spawnSync } from '@socketsecurity/lib-stable/spawn' +import { spawnSync } from '@socketsecurity/lib-stable/spawn/spawn' import { copyFileSync, mkdirSync, diff --git a/scripts/update.mts b/scripts/update.mts index f384b58..f90b857 100644 --- a/scripts/update.mts +++ b/scripts/update.mts @@ -19,7 +19,7 @@ * scripts/ dir and wire it in via a `"update": "node scripts/update.mts"` * package.json entry. */ -import { spawn } from '@socketsecurity/lib-stable/spawn' +import { spawn } from '@socketsecurity/lib-stable/spawn/spawn' async function run(cmd: string, args: string[]): Promise { try { diff --git a/scripts/validate-bundle-deps.mts b/scripts/validate-bundle-deps.mts index 111007b..c71bb93 100644 --- a/scripts/validate-bundle-deps.mts +++ b/scripts/validate-bundle-deps.mts @@ -23,10 +23,15 @@ const logger = getDefaultLogger() const __dirname = path.dirname(fileURLToPath(import.meta.url)) const rootPath = path.join(__dirname, '..') -// Node.js builtins to ignore (including node: prefix variants) +// Node.js builtins to ignore (including node: prefix variants). +// node:smol-* are Socket SEA-bundled optional builtins (smol-util, smol-primordial); +// they appear in dist behind `mod.isBuiltin('node:smol-util')` guards and are only +// resolvable in SEA binaries, so they should never be expected in dependencies. +const SOCKET_SEA_BUILTINS = ['node:smol-util', 'node:smol-primordial'] const BUILTIN_MODULES = new Set([ ...builtinModules, ...builtinModules.map(m => `node:${m}`), + ...SOCKET_SEA_BUILTINS, ]) /** @@ -364,16 +369,14 @@ async function validateBundleDeps(): Promise { // externals + bundled are Set — use for...of, the // canonical fix for set / map / iterable iteration. - for (let i = 0, { length } = externals; i < length; i += 1) { - const ext = externals[i]! + for (const ext of externals) { const packageName = getPackageName(ext) if (packageName && !BUILTIN_MODULES.has(packageName)) { allExternals.add(packageName) } } - for (let i = 0, { length } = bundled; i < length; i += 1) { - const bun = bundled[i]! + for (const bun of bundled) { allBundled.add(bun) } } diff --git a/scripts/validate-file-size.mts b/scripts/validate-file-size.mts index 19b624a..41b3bcc 100644 --- a/scripts/validate-file-size.mts +++ b/scripts/validate-file-size.mts @@ -23,6 +23,19 @@ const rootPath = path.join(__dirname, '..') // Maximum file size: 2MB (2,097,152 bytes) const MAX_FILE_SIZE = 2 * 1024 * 1024 +// Allowlisted large files: fleet-canonical assets whose size is bounded by +// the upstream they ship, not by repo authoring. acorn.wasm is the AST +// parser shared by AST-based oxlint plugin rules + hooks; its ~3MB is the +// upstream build artifact. Two paths because socket-lib vendors its own +// copy at vendor/acorn-wasm/ (so the lib package's own AST helpers can +// load without a node_modules round-trip). Adding a path here is +// intentional — it should only happen for files the fleet jointly owns, +// not per-repo binary leaks. +const ALLOWED_LARGE_FILES = new Set([ + '.claude/hooks/_shared/acorn/acorn.wasm', + 'vendor/acorn-wasm/acorn.wasm', +]) + // Directories to skip const SKIP_DIRS = new Set([ '.cache', @@ -95,6 +108,9 @@ async function scanDirectory( const stats = await fs.stat(fullPath) if (stats.size > MAX_FILE_SIZE) { const relativePath = path.relative(rootPath, fullPath) + if (ALLOWED_LARGE_FILES.has(relativePath)) { + continue + } violations.push({ file: relativePath, size: stats.size,