diff --git a/CLAUDE.md b/CLAUDE.md index 73465cb56..423e5752d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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/` diff --git a/docs/build-guide.md b/docs/build-guide.md index 5a13a0cde..0c083a1da 100644 --- a/docs/build-guide.md +++ b/docs/build-guide.md @@ -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/) @@ -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 @@ -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) @@ -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. @@ -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/-/node` | | `binject` | Binary injection tool | `binject/-/binject` | -| `yoga-layout` | Terminal layout WASM | `yoga-layout/assets/yoga-sync-*.mjs` | | `models` | AI models for analysis | `models/` | ### Cache Management diff --git a/packages/build-infra/README.md b/packages/build-infra/README.md index e14184def..2154c1f10 100644 --- a/packages/build-infra/README.md +++ b/packages/build-infra/README.md @@ -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 @@ -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)` @@ -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 @@ -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', ) ``` @@ -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 ``` diff --git a/packages/cli/.config/esbuild.cli.mjs b/packages/cli/.config/esbuild.cli.mjs index 634a33bbc..f6ea19897 100644 --- a/packages/cli/.config/esbuild.cli.mjs +++ b/packages/cli/.config/esbuild.cli.mjs @@ -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) { diff --git a/packages/cli/scripts/build-js.mjs b/packages/cli/scripts/build-js.mjs index ef7899381..8dfdc7c9e 100644 --- a/packages/cli/scripts/build-js.mjs +++ b/packages/cli/scripts/build-js.mjs @@ -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', diff --git a/packages/cli/scripts/build.mjs b/packages/cli/scripts/build.mjs index 8b69a718a..e85991aa8 100644 --- a/packages/cli/scripts/build.mjs +++ b/packages/cli/scripts/build.mjs @@ -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'], diff --git a/packages/cli/scripts/download-assets.mjs b/packages/cli/scripts/download-assets.mjs index 59a19cb85..d9fa65f99 100644 --- a/packages/cli/scripts/download-assets.mjs +++ b/packages/cli/scripts/download-assets.mjs @@ -5,7 +5,7 @@ * 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: @@ -13,7 +13,6 @@ * 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' @@ -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() @@ -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...`) @@ -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() @@ -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). * diff --git a/packages/cli/src/commands/json/output-cmd-json.mts b/packages/cli/src/commands/json/output-cmd-json.mts index 1ee68ad27..04ffa350f 100644 --- a/packages/cli/src/commands/json/output-cmd-json.mts +++ b/packages/cli/src/commands/json/output-cmd-json.mts @@ -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' @@ -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 diff --git a/packages/cli/src/constants/agents.mts b/packages/cli/src/constants/agents.mts index ff3977225..15ec3ce87 100644 --- a/packages/cli/src/constants/agents.mts +++ b/packages/cli/src/constants/agents.mts @@ -3,7 +3,7 @@ * Functions for package manager version requirements and execution paths. */ -import { existsSync } from 'node:fs' +import fs from 'node:fs' import path from 'node:path' import { whichReal } from '@socketsecurity/lib/bin' @@ -63,7 +63,7 @@ export async function getNpmExecPath(): Promise { // Check npm in the same directory as node. const nodeDir = path.dirname(process.execPath) const npmInNodeDir = path.join(nodeDir, NPM) - if (existsSync(npmInNodeDir)) { + if (fs.existsSync(npmInNodeDir)) { return npmInNodeDir } // Fall back to whichReal. diff --git a/packages/cli/src/utils/ecosystem/environment.mts b/packages/cli/src/utils/ecosystem/environment.mts index aaa5f44c6..0f55a029c 100644 --- a/packages/cli/src/utils/ecosystem/environment.mts +++ b/packages/cli/src/utils/ecosystem/environment.mts @@ -24,7 +24,7 @@ * - Configuring concurrent execution limits */ -import { existsSync, readFileSync } from 'node:fs' +import fs from 'node:fs' import path from 'node:path' import browserslist from 'browserslist' @@ -257,13 +257,13 @@ const LOCKS: Record = { function resolveBinPathSync(binPath: string): string { // Simple implementation that tries to resolve a bin path to its actual entry point. // This is used on Windows to resolve shims like `npm` or `npm.cmd` to their .js entry point. - if (!existsSync(binPath)) { + if (!fs.existsSync(binPath)) { return binPath } try { // Try to read the file synchronously - const content = readFileSync(binPath, 'utf8') + const content = fs.readFileSync(binPath, 'utf8') // Look for common patterns in npm/node shims: // - node "C:\path\to\npm-cli.js" "$@" // - "%_prog%" "%dp0%\node_modules\npm\bin\npm-cli.js" %* @@ -309,7 +309,7 @@ function preferWindowsCmdShim(binPath: string, binName: string): string { const cmdShim = path.join(path.dirname(binPath), `${binName}.cmd`) // Ensure shim exists, otherwise fallback to binPath - return existsSync(cmdShim) ? cmdShim : binPath + return fs.existsSync(cmdShim) ? cmdShim : binPath } async function getAgentExecPath(agent: Agent): Promise { @@ -317,7 +317,7 @@ async function getAgentExecPath(agent: Agent): Promise { if (binName === NPM) { // Try to use getNpmExecPath() first, but verify it exists. const npmPath = preferWindowsCmdShim(await getNpmExecPath(), NPM) - if (existsSync(npmPath)) { + if (fs.existsSync(npmPath)) { return npmPath } // If getNpmExecPath() doesn't exist, try common locations. @@ -325,12 +325,12 @@ async function getAgentExecPath(agent: Agent): Promise { const nodeDir = path.dirname(process.execPath) if (WIN32) { const npmCmdInNodeDir = path.join(nodeDir, `${NPM}.cmd`) - if (existsSync(npmCmdInNodeDir)) { + if (fs.existsSync(npmCmdInNodeDir)) { return npmCmdInNodeDir } } const npmInNodeDir = path.join(nodeDir, NPM) - if (existsSync(npmInNodeDir)) { + if (fs.existsSync(npmInNodeDir)) { return preferWindowsCmdShim(npmInNodeDir, NPM) } // Fall back to which. @@ -343,7 +343,7 @@ async function getAgentExecPath(agent: Agent): Promise { if (binName === PNPM) { // Try to use getPnpmExecPath() first, but verify it exists. const pnpmPath = await getPnpmExecPath() - if (existsSync(pnpmPath)) { + if (fs.existsSync(pnpmPath)) { return pnpmPath } // Fall back to which. @@ -452,7 +452,7 @@ export async function detectPackageEnvironment({ ) : await findUp(PACKAGE_JSON, { cwd }) const pkgPath = - pkgJsonPath && existsSync(pkgJsonPath) + pkgJsonPath && fs.existsSync(pkgJsonPath) ? path.dirname(pkgJsonPath) : undefined const pkgJson = pkgPath ? await readPackageJson(pkgPath) : undefined diff --git a/packages/cli/src/utils/npm/paths.mts b/packages/cli/src/utils/npm/paths.mts index f699add8a..b13b3378e 100755 --- a/packages/cli/src/utils/npm/paths.mts +++ b/packages/cli/src/utils/npm/paths.mts @@ -1,4 +1,4 @@ -import { existsSync } from 'node:fs' +import fs from 'node:fs' import Module from 'node:module' import path from 'node:path' @@ -81,7 +81,7 @@ export function getNpmRequire(): NodeJS.Require { const npmNmPath = path.join(npmDirPath, `${NODE_MODULES}/npm`) _npmRequire = Module.createRequire( path.join( - existsSync(npmNmPath) ? npmNmPath : npmDirPath, + fs.existsSync(npmNmPath) ? npmNmPath : npmDirPath, '', ), ) diff --git a/packages/cli/src/utils/pnpm/lockfile.mts b/packages/cli/src/utils/pnpm/lockfile.mts index 2a811eb5d..9a33fc676 100644 --- a/packages/cli/src/utils/pnpm/lockfile.mts +++ b/packages/cli/src/utils/pnpm/lockfile.mts @@ -1,4 +1,4 @@ -import { existsSync } from 'node:fs' +import fs from 'node:fs' import yaml from 'js-yaml' import semver from 'semver' @@ -88,7 +88,9 @@ export function parsePnpmLockfileVersion(version: unknown): SemVer | undefined { export async function readPnpmLockfile( lockfilePath: string, ): Promise { - return existsSync(lockfilePath) ? await readFileUtf8(lockfilePath) : undefined + return fs.existsSync(lockfilePath) + ? await readFileUtf8(lockfilePath) + : undefined } export function stripLeadingPnpmDepPathSlash(depPath: string): string { diff --git a/packages/cli/src/utils/process/os.mts b/packages/cli/src/utils/process/os.mts index bb876f5b6..825f30a6c 100644 --- a/packages/cli/src/utils/process/os.mts +++ b/packages/cli/src/utils/process/os.mts @@ -26,7 +26,7 @@ * - File permission management */ -import { existsSync, promises as fs, readFileSync } from 'node:fs' +import fs from 'node:fs' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { spawn } from '@socketsecurity/lib/spawn' @@ -142,8 +142,8 @@ function detectMusl(): boolean { // Method 1: Check /etc/os-release for Alpine. try { - if (existsSync('/etc/os-release')) { - const osRelease = readFileSync('/etc/os-release', 'utf8') + if (fs.existsSync('/etc/os-release')) { + const osRelease = fs.readFileSync('/etc/os-release', 'utf8') if (osRelease.includes('Alpine') || osRelease.includes('alpine')) { cachedLibc = 'musl' return true @@ -156,8 +156,8 @@ function detectMusl(): boolean { // Method 2: Check if ldd references musl. try { if ( - existsSync('/lib/ld-musl-x86_64.so.1') || - existsSync('/lib/ld-musl-aarch64.so.1') + fs.existsSync('/lib/ld-musl-x86_64.so.1') || + fs.existsSync('/lib/ld-musl-aarch64.so.1') ) { cachedLibc = 'musl' return true @@ -168,8 +168,8 @@ function detectMusl(): boolean { // Method 3: Check /proc/version for musl indicators. try { - if (existsSync('/proc/version')) { - const version = readFileSync('/proc/version', 'utf8') + if (fs.existsSync('/proc/version')) { + const version = fs.readFileSync('/proc/version', 'utf8') if (version.includes('musl')) { cachedLibc = 'musl' return true @@ -250,7 +250,7 @@ async function ensureExecutable(filePath: string): Promise { } try { - await fs.chmod(filePath, 0o755) + await fs.promises.chmod(filePath, 0o755) logger.log('Set executable permissions') } catch (e) { logger.warn( diff --git a/packages/cli/src/utils/socket/json.mts b/packages/cli/src/utils/socket/json.mts index 15eb0bb90..dd50e6287 100644 --- a/packages/cli/src/utils/socket/json.mts +++ b/packages/cli/src/utils/socket/json.mts @@ -18,7 +18,7 @@ * - Supports both read and write operations */ -import { existsSync, promises as fs, readFileSync } from 'node:fs' +import fs from 'node:fs' import path from 'node:path' import { debugDirNs, debugNs } from '@socketsecurity/lib/debug' @@ -127,14 +127,14 @@ export async function readSocketJson( defaultOnError = false, ): Promise> { const sockJsonPath = path.join(cwd, SOCKET_JSON) - if (!existsSync(sockJsonPath)) { + if (!fs.existsSync(sockJsonPath)) { debugNs('notice', `miss: ${SOCKET_JSON} not found at ${cwd}`) return { ok: true, data: getDefaultSocketJson() } } let json = null try { - json = await fs.readFile(sockJsonPath, 'utf8') + json = await fs.promises.readFile(sockJsonPath, 'utf8') } catch (e) { if (defaultOnError) { logger.warn(`Failed to read ${SOCKET_JSON}, using default`) @@ -188,13 +188,13 @@ export function readSocketJsonSync( defaultOnError = false, ): CResult { const sockJsonPath = path.join(cwd, SOCKET_JSON) - if (!existsSync(sockJsonPath)) { + if (!fs.existsSync(sockJsonPath)) { debugNs('notice', `miss: ${SOCKET_JSON} not found at ${cwd}`) return { ok: true, data: getDefaultSocketJson() } } let jsonContent = null try { - jsonContent = readFileSync(sockJsonPath, 'utf8') + jsonContent = fs.readFileSync(sockJsonPath, 'utf8') } catch (e) { if (defaultOnError) { logger.warn(`Failed to read ${SOCKET_JSON}, using default`) @@ -262,7 +262,7 @@ export async function writeSocketJson( } const filepath = path.join(cwd, SOCKET_JSON) - await fs.writeFile(filepath, `${jsonContent}\n`, 'utf8') + await fs.promises.writeFile(filepath, `${jsonContent}\n`, 'utf8') return { ok: true, data: undefined } } diff --git a/packages/cli/test/unit/commands/json/output-cmd-json.test.mts b/packages/cli/test/unit/commands/json/output-cmd-json.test.mts index 3a7253f2c..92afeaa70 100644 --- a/packages/cli/test/unit/commands/json/output-cmd-json.test.mts +++ b/packages/cli/test/unit/commands/json/output-cmd-json.test.mts @@ -1,23 +1,11 @@ /** - * Unit tests for json command output. - * - * Purpose: - * Tests the output-cmd-json utility for displaying socket.json contents. - * - * Test Coverage: - * - File not found handling - * - Non-file (directory) handling - * - Successful file reading - * - * Related Files: - * - commands/json/output-cmd-json.mts (implementation) + * @fileoverview Unit tests for json command output. */ -import path from 'node:path' +import fs from 'node:fs' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -// Mock dependencies. const mockLogger = vi.hoisted(() => ({ error: vi.fn(), fail: vi.fn(), @@ -30,40 +18,34 @@ vi.mock('@socketsecurity/lib/logger', () => ({ getDefaultLogger: () => mockLogger, })) -vi.mock('node:fs', async () => { - const actual = await vi.importActual('node:fs') - return { - ...actual, - existsSync: vi.fn(), - } -}) +const mockSafeReadFileSync = vi.fn() +const mockSafeStatsSync = vi.fn() vi.mock('@socketsecurity/lib/fs', () => ({ - safeReadFileSync: vi.fn(), - safeStatsSync: vi.fn(), + safeReadFileSync: (...args: unknown[]) => mockSafeReadFileSync(...args), + safeStatsSync: (...args: unknown[]) => mockSafeStatsSync(...args), })) -import { existsSync } from 'node:fs' - -import { safeReadFileSync, safeStatsSync } from '@socketsecurity/lib/fs' - import { outputCmdJson } from '../../../../src/commands/json/output-cmd-json.mts' describe('output-cmd-json', () => { const originalExitCode = process.exitCode + let existsSyncSpy: ReturnType beforeEach(() => { vi.clearAllMocks() process.exitCode = undefined + existsSyncSpy = vi.spyOn(fs, 'existsSync') }) afterEach(() => { + existsSyncSpy.mockRestore() process.exitCode = originalExitCode }) describe('outputCmdJson', () => { it('logs info about target cwd', async () => { - vi.mocked(existsSync).mockReturnValue(false) + existsSyncSpy.mockReturnValue(false) await outputCmdJson('/test/path') @@ -71,7 +53,7 @@ describe('output-cmd-json', () => { }) it('handles socket.json not found', async () => { - vi.mocked(existsSync).mockReturnValue(false) + existsSyncSpy.mockReturnValue(false) await outputCmdJson('/test/path') @@ -82,10 +64,10 @@ describe('output-cmd-json', () => { }) it('handles non-file (directory) path', async () => { - vi.mocked(existsSync).mockReturnValue(true) - vi.mocked(safeStatsSync).mockReturnValue({ + existsSyncSpy.mockReturnValue(true) + mockSafeStatsSync.mockReturnValue({ isFile: () => false, - } as any) + }) await outputCmdJson('/test/path') @@ -97,11 +79,11 @@ describe('output-cmd-json', () => { it('successfully reads and outputs socket.json contents', async () => { const mockContent = JSON.stringify({ version: '1.0.0' }, null, 2) - vi.mocked(existsSync).mockReturnValue(true) - vi.mocked(safeStatsSync).mockReturnValue({ + existsSyncSpy.mockReturnValue(true) + mockSafeStatsSync.mockReturnValue({ isFile: () => true, - } as any) - vi.mocked(safeReadFileSync).mockReturnValue(mockContent) + }) + mockSafeReadFileSync.mockReturnValue(mockContent) await outputCmdJson('/test/path') @@ -113,8 +95,8 @@ describe('output-cmd-json', () => { }) it('handles null safeStatsSync result', async () => { - vi.mocked(existsSync).mockReturnValue(true) - vi.mocked(safeStatsSync).mockReturnValue(null) + existsSyncSpy.mockReturnValue(true) + mockSafeStatsSync.mockReturnValue(null) await outputCmdJson('/test/path') diff --git a/packages/cli/test/unit/constants/agents.test.mts b/packages/cli/test/unit/constants/agents.test.mts index 2863a1e51..a0bae9dbf 100644 --- a/packages/cli/test/unit/constants/agents.test.mts +++ b/packages/cli/test/unit/constants/agents.test.mts @@ -13,24 +13,17 @@ * - constants/agents.mts (implementation) */ +import fs from 'node:fs' + import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' // Mock dependencies using hoisted mocks. const mockWhichReal = vi.hoisted(() => vi.fn()) -const mockExistsSync = vi.hoisted(() => vi.fn()) vi.mock('@socketsecurity/lib/bin', () => ({ whichReal: mockWhichReal, })) -vi.mock('node:fs', async () => { - const actual = await vi.importActual('node:fs') - return { - ...actual, - existsSync: mockExistsSync, - } -}) - import { BUN, getMinimumVersionByAgent, @@ -46,8 +39,11 @@ import { } from '../../../src/constants/agents.mts' describe('agents constants', () => { + let mockExistsSync: ReturnType + beforeEach(() => { vi.clearAllMocks() + mockExistsSync = vi.spyOn(fs, 'existsSync') }) afterEach(() => { diff --git a/packages/cli/test/unit/utils/cli/completion.test.mts b/packages/cli/test/unit/utils/cli/completion.test.mts index 43fb457a3..a6d279eb4 100644 --- a/packages/cli/test/unit/utils/cli/completion.test.mts +++ b/packages/cli/test/unit/utils/cli/completion.test.mts @@ -13,21 +13,11 @@ * - utils/cli/completion.mts (implementation) */ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import fs from 'node:fs' -// Mock fs.existsSync. -const mockExistsSync = vi.hoisted(() => vi.fn()) -vi.mock('node:fs', async importOriginal => { - const actual = (await importOriginal()) as typeof import('node:fs') - return { - ...actual, - default: { - ...actual, - existsSync: mockExistsSync, - }, - existsSync: mockExistsSync, - } -}) +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +let mockExistsSync: ReturnType // Mock getSocketAppDataPath. const mockGetSocketAppDataPath = vi.hoisted(() => vi.fn()) @@ -49,6 +39,11 @@ import { describe('cli/completion', () => { beforeEach(() => { vi.clearAllMocks() + mockExistsSync = vi.spyOn(fs, 'existsSync') + }) + + afterEach(() => { + vi.restoreAllMocks() }) describe('COMPLETION_CMD_PREFIX', () => { diff --git a/packages/cli/test/unit/utils/ecosystem/environment.test.mts b/packages/cli/test/unit/utils/ecosystem/environment.test.mts index 279662a77..556d26a6f 100644 --- a/packages/cli/test/unit/utils/ecosystem/environment.test.mts +++ b/packages/cli/test/unit/utils/ecosystem/environment.test.mts @@ -18,7 +18,9 @@ * - utils/ecosystem/environment.mts (implementation) */ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import fs from 'node:fs' + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { AGENTS, @@ -27,7 +29,7 @@ import { } from '../../../../src/utils/ecosystem/environment.mts' // Mock the dependencies. -const mockExistsSync = vi.hoisted(() => vi.fn()) +let mockExistsSync: ReturnType const mockDefault = vi.hoisted(() => vi.fn()) const mockParse = vi.hoisted(() => vi.fn()) const mockValid = vi.hoisted(() => vi.fn()) @@ -37,14 +39,6 @@ const mockMinor = vi.hoisted(() => vi.fn()) const mockPatch = vi.hoisted(() => vi.fn()) const mockCoerce = vi.hoisted(() => vi.fn()) -vi.mock('node:fs', async importOriginal => { - const actual = (await importOriginal()) as any - return { - ...actual, - existsSync: mockExistsSync, - } -}) - vi.mock('browserslist', () => ({ default: mockDefault.mockReturnValue([]), })) @@ -98,6 +92,7 @@ vi.mock('semver', () => ({ describe('package-environment', () => { beforeEach(() => { vi.clearAllMocks() + mockExistsSync = vi.spyOn(fs, 'existsSync') // Default mock behavior for spawn to get package manager version. mockSpawn.mockResolvedValue({ stdout: '10.0.0', stderr: '', code: 0 }) // Default mock behavior for toEditablePackageJson. @@ -107,6 +102,10 @@ describe('package-environment', () => { })) }) + afterEach(() => { + vi.restoreAllMocks() + }) + describe('AGENTS', () => { it('contains all expected package managers', () => { expect(AGENTS).toContain('npm') diff --git a/packages/cli/test/unit/utils/npm/paths.test.mts b/packages/cli/test/unit/utils/npm/paths.test.mts index 991985060..33f01ebad 100644 --- a/packages/cli/test/unit/utils/npm/paths.test.mts +++ b/packages/cli/test/unit/utils/npm/paths.test.mts @@ -18,12 +18,9 @@ * - utils/npm/paths.mts (implementation) */ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import fs from 'node:fs' -// Mock dependencies. -vi.mock('node:fs', () => ({ - existsSync: vi.fn(), -})) +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('node:module', async importOriginal => { const actual = await importOriginal() @@ -79,6 +76,9 @@ describe('npm-paths utilities', () => { vi.clearAllMocks() vi.resetModules() + // Spy on fs.existsSync for tests that need it. + vi.spyOn(fs, 'existsSync') + // Store original process.exit. originalExit = process.exit // Mock process.exit to prevent actual exits. @@ -97,6 +97,7 @@ describe('npm-paths utilities', () => { afterEach(() => { // Restore original process.exit. process.exit = originalExit + vi.restoreAllMocks() vi.resetModules() }) @@ -226,8 +227,7 @@ describe('npm-paths utilities', () => { }) findNpmDirPathSync.mockReturnValue('/usr/local/lib/node_modules/npm') - const { existsSync } = vi.mocked(await import('node:fs')) - existsSync.mockReturnValue(true) + vi.mocked(fs.existsSync).mockReturnValue(true) const mockRequire = vi.fn() const Module = vi.mocked(await import('node:module')).default @@ -252,8 +252,7 @@ describe('npm-paths utilities', () => { }) findNpmDirPathSync.mockReturnValue('/usr/local/lib/node_modules/npm') - const { existsSync } = vi.mocked(await import('node:fs')) - existsSync.mockReturnValue(false) + vi.mocked(fs.existsSync).mockReturnValue(false) const mockRequire = vi.fn() const Module = vi.mocked(await import('node:module')).default diff --git a/packages/cli/test/unit/utils/pnpm/lockfile.test.mts b/packages/cli/test/unit/utils/pnpm/lockfile.test.mts index bb0af318f..403614114 100644 --- a/packages/cli/test/unit/utils/pnpm/lockfile.test.mts +++ b/packages/cli/test/unit/utils/pnpm/lockfile.test.mts @@ -18,7 +18,9 @@ * - utils/pnpm/lockfile.mts (implementation) */ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import fs from 'node:fs' + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { extractOverridesFromPnpmLockSrc, @@ -32,13 +34,9 @@ import { } from '../../../../src/utils/pnpm/lockfile.mts' // Mock fs module. -const mockExistsSync = vi.hoisted(() => vi.fn()) +let mockExistsSync: ReturnType const mockReadFileUtf8 = vi.hoisted(() => vi.fn()) -vi.mock('node:fs', () => ({ - existsSync: mockExistsSync, -})) - // Mock registry modules. vi.mock('@socketsecurity/lib/fs', () => ({ readFileUtf8: mockReadFileUtf8, @@ -47,6 +45,11 @@ vi.mock('@socketsecurity/lib/fs', () => ({ describe('pnpm utilities', () => { beforeEach(() => { vi.clearAllMocks() + mockExistsSync = vi.spyOn(fs, 'existsSync') + }) + + afterEach(() => { + vi.restoreAllMocks() }) describe('extractOverridesFromPnpmLockSrc', () => { @@ -169,22 +172,16 @@ packages: {}` describe('readPnpmLockfile', () => { it('reads existing lockfile', async () => { - const { existsSync } = await import('node:fs') - const { readFileUtf8 } = await import('@socketsecurity/lib/fs') - mockExistsSync.mockReturnValue(true) mockReadFileUtf8.mockResolvedValue('lockfile content') const result = await readPnpmLockfile('/path/to/pnpm-lock.yaml') expect(result).toBe('lockfile content') - expect(existsSync).toHaveBeenCalledWith('/path/to/pnpm-lock.yaml') - expect(readFileUtf8).toHaveBeenCalledWith('/path/to/pnpm-lock.yaml') + expect(fs.existsSync).toHaveBeenCalledWith('/path/to/pnpm-lock.yaml') + expect(mockReadFileUtf8).toHaveBeenCalledWith('/path/to/pnpm-lock.yaml') }) it('returns undefined for non-existent lockfile', async () => { - // biome-ignore lint/correctness/noUnusedVariables: imported for mocking. - const { existsSync } = await import('node:fs') - mockExistsSync.mockReturnValue(false) const result = await readPnpmLockfile('/path/to/missing.yaml') diff --git a/packages/cli/test/unit/utils/process/os.test.mts b/packages/cli/test/unit/utils/process/os.test.mts index f91da52d1..78de5db1c 100644 --- a/packages/cli/test/unit/utils/process/os.test.mts +++ b/packages/cli/test/unit/utils/process/os.test.mts @@ -2,7 +2,7 @@ * Unit tests for platform detection utilities. */ -import * as fs from 'node:fs' +import fs from 'node:fs' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -28,20 +28,12 @@ vi.mock('@socketsecurity/lib/spawn', () => ({ spawn: vi.fn(), })) -// Mock the fs module. -vi.mock('node:fs', async () => { - const actual = await vi.importActual('node:fs') - return { - ...actual, - existsSync: vi.fn(), - readFileSync: vi.fn(), - } -}) - describe('detectMusl', () => { beforeEach(() => { vi.restoreAllMocks() resetLibcCache() + vi.spyOn(fs, 'existsSync') + vi.spyOn(fs, 'readFileSync') }) afterEach(() => { @@ -154,6 +146,8 @@ describe('getLibcSuffix', () => { beforeEach(() => { vi.restoreAllMocks() resetLibcCache() + vi.spyOn(fs, 'existsSync') + vi.spyOn(fs, 'readFileSync') }) afterEach(() => { @@ -228,7 +222,8 @@ describe('getSocketbinPackageName', () => { vi.restoreAllMocks() resetLibcCache() // Default mock for non-musl systems. - vi.mocked(fs.existsSync).mockReturnValue(false) + vi.spyOn(fs, 'existsSync').mockReturnValue(false) + vi.spyOn(fs, 'readFileSync') }) afterEach(() => { diff --git a/packages/cli/test/unit/utils/socket/json.test.mts b/packages/cli/test/unit/utils/socket/json.test.mts index 960990091..be6ace725 100644 --- a/packages/cli/test/unit/utils/socket/json.test.mts +++ b/packages/cli/test/unit/utils/socket/json.test.mts @@ -18,24 +18,16 @@ * - utils/socket/json.mts (implementation) */ -import { beforeEach, describe, expect, it, vi } from 'vitest' - -// Mock dependencies BEFORE imports. -const { mockExistsSync } = vi.hoisted(() => ({ mockExistsSync: vi.fn() })) -const { mockReadFileSync } = vi.hoisted(() => ({ mockReadFileSync: vi.fn() })) -const { mockReadFile } = vi.hoisted(() => ({ mockReadFile: vi.fn() })) -const { mockWriteFile } = vi.hoisted(() => ({ mockWriteFile: vi.fn() })) -const { mockStat } = vi.hoisted(() => ({ mockStat: vi.fn() })) - -vi.mock('node:fs', () => ({ - existsSync: mockExistsSync, - readFileSync: mockReadFileSync, - promises: { - readFile: mockReadFile, - writeFile: mockWriteFile, - stat: mockStat, - }, -})) +import fs from 'node:fs' +import path from 'node:path' + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +let mockExistsSync: ReturnType> +let mockReadFileSync: ReturnType> +let mockReadFile: ReturnType> +let mockWriteFile: ReturnType> +let mockStat: ReturnType> const mockLogger = vi.hoisted(() => ({ error: vi.fn(), @@ -51,9 +43,6 @@ vi.mock('@socketsecurity/lib/logger', () => ({ logger: mockLogger, })) -import { promises as fs } from 'node:fs' -import path from 'node:path' - import { SOCKET_JSON, SOCKET_WEBSITE_URL, @@ -71,6 +60,15 @@ import { describe('socket-json utilities', () => { beforeEach(() => { vi.clearAllMocks() + mockExistsSync = vi.spyOn(fs, 'existsSync') + mockReadFileSync = vi.spyOn(fs, 'readFileSync') + mockReadFile = vi.spyOn(fs.promises, 'readFile') + mockWriteFile = vi.spyOn(fs.promises, 'writeFile') + mockStat = vi.spyOn(fs.promises, 'stat') + }) + + afterEach(() => { + vi.restoreAllMocks() }) describe('getDefaultSocketJson', () => { @@ -338,7 +336,7 @@ describe('socket-json utilities', () => { const result = await writeSocketJson('/test/dir', mockJson as any) expect(result.ok).toBe(true) - expect(fs.writeFile).toHaveBeenCalledWith( + expect(fs.promises.writeFile).toHaveBeenCalledWith( path.join('/test/dir', SOCKET_JSON), expect.stringContaining('"version": 1'), 'utf8', @@ -361,7 +359,7 @@ describe('socket-json utilities', () => { mockWriteFile.mockResolvedValue(undefined) await writeSocketJson('/test/dir', mockJson) - expect(fs.writeFile).toHaveBeenCalledWith( + expect(fs.promises.writeFile).toHaveBeenCalledWith( expect.any(String), expect.stringMatching(/\n$/), 'utf8', diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index feddd4e43..6251ed0cd 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -50,7 +50,6 @@ catalog: '@socketsecurity/sdk': 3.4.1 '@types/adm-zip': 0.5.7 '@types/cmd-shim': 5.0.2 - '@types/ink': 2.0.3 '@types/js-yaml': 4.0.9 '@types/micromatch': 4.0.9 '@types/mock-fs': 4.13.4 @@ -97,9 +96,6 @@ catalog: husky: 9.1.7 ignore: 7.0.5 indent-string: npm:@socketregistry/indent-string@^1.0.14 - ink: 6.3.1 - ink-table: 3.1.0 - ink-text-input: 6.0.0 is-core-module: npm:@socketregistry/is-core-module@^1.0.11 isarray: npm:@socketregistry/isarray@^1.0.8 js-yaml: npm:@zkochan/js-yaml@0.0.10 @@ -152,5 +148,4 @@ catalog: yaml: 2.8.1 yargs-parser: 21.1.1 yoctocolors-cjs: 2.1.3 - yoga-layout: 3.2.1 zod: 4.1.8