diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f2addfe5..4f925cad7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,10 +108,8 @@ jobs: key: cli-build-${{ runner.os }}-${{ steps.cli-cache-key.outputs.hash }} restore-keys: | cli-build-${{ runner.os }}- - lookup-only: true - name: Build CLI - if: steps.cli-build-cache.outputs.cache-hit != 'true' working-directory: packages/cli run: pnpm run build @@ -155,10 +153,8 @@ jobs: key: cli-build-${{ runner.os }}-${{ steps.cli-cache-key.outputs.hash }} restore-keys: | cli-build-${{ runner.os }}- - lookup-only: true - name: Build CLI - if: steps.cli-build-cache.outputs.cache-hit != 'true' working-directory: packages/cli run: pnpm run build @@ -315,10 +311,8 @@ jobs: key: cli-build-${{ runner.os }}-${{ steps.cli-cache-key.outputs.hash }} restore-keys: | cli-build-${{ runner.os }}- - lookup-only: true - name: Build CLI - if: steps.cli-build-cache.outputs.cache-hit != 'true' working-directory: packages/cli run: pnpm run build diff --git a/packages/cli/package.json b/packages/cli/package.json index 86602abcc..dd2495e58 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -18,6 +18,7 @@ "logo-light.png" ], "scripts": { + "prebuild": "node scripts/generate-packages.mjs", "build": "node --max-old-space-size=8192 --import=./scripts/load.mjs scripts/build.mjs", "build:force": "node --max-old-space-size=8192 --import=./scripts/load.mjs scripts/build.mjs --force", "build:watch": "node --max-old-space-size=8192 --import=./scripts/load.mjs scripts/build.mjs --watch", diff --git a/packages/cli/scripts/build.mjs b/packages/cli/scripts/build.mjs index dce78cf50..17f6cb7d2 100644 --- a/packages/cli/scripts/build.mjs +++ b/packages/cli/scripts/build.mjs @@ -1,8 +1,9 @@ /** * Build script for Socket CLI. - * Options: --quiet, --verbose, --prod, --force, --watch + * Options: --quiet, --verbose, --force, --watch */ +import { copyFileSync } from 'node:fs' import { promises as fs } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' @@ -97,7 +98,6 @@ async function main() { const verbose = isVerbose() const watch = process.argv.includes('--watch') const force = process.argv.includes('--force') - const prod = process.argv.includes('--prod') // Pass --force flag via environment variable. if (force) { @@ -202,19 +202,6 @@ async function main() { command: 'node', args: [...NODE_MEMORY_FLAGS, '.config/esbuild.inject.config.mjs'], }, - // Copy CLI to dist for production builds. - ...(prod - ? [ - { - name: 'Copy CLI to dist', - command: 'node', - args: [ - '-e', - 'require("fs").copyFileSync("build/cli.js", "dist/cli.js")', - ], - }, - ] - : []), ] // Run build steps sequentially. @@ -248,6 +235,9 @@ async function main() { } } + // Copy CLI bundle to dist (required for dist/index.js to work). + copyFileSync('build/cli.js', 'dist/cli.js') + // Post-process: Fix node-gyp strings to prevent bundler issues. if (!quiet && verbose) { log.info('Post-processing build output...') diff --git a/packages/cli/scripts/generate-packages.mjs b/packages/cli/scripts/generate-packages.mjs new file mode 100644 index 000000000..7e5964fd5 --- /dev/null +++ b/packages/cli/scripts/generate-packages.mjs @@ -0,0 +1,20 @@ +/** + * Generate template-based packages required for CLI build. + * Runs the package generation scripts from package-builder. + */ + +import { spawn } from '@socketsecurity/lib/spawn' + +const scripts = [ + '../package-builder/scripts/generate-cli-sentry-package.mjs', + '../package-builder/scripts/generate-socket-package.mjs', + '../package-builder/scripts/generate-socketbin-packages.mjs', +] + +for (const script of scripts) { + const result = await spawn('node', [script], { stdio: 'inherit' }) + if (result.code !== 0) { + // Use nullish coalescing to handle signal-killed processes (code is null). + process.exit(result.code ?? 1) + } +} diff --git a/packages/cli/src/utils/cli/with-subcommands.mts b/packages/cli/src/utils/cli/with-subcommands.mts index ee76efc3d..b250d4118 100644 --- a/packages/cli/src/utils/cli/with-subcommands.mts +++ b/packages/cli/src/utils/cli/with-subcommands.mts @@ -615,7 +615,28 @@ export async function meowWithSubcommands( // If first arg is a flag (starts with --), try Python CLI forwarding. // This enables: socket --repo owner/repo --target-path . - if (commandOrAliasName?.startsWith('--')) { + // Exception: Don't forward Node.js CLI built-in flags (help, version, etc). + const nodeCliFlags = new Set([ + '--compact-header', + '--config', + '--dry-run', + '--help', + '--help-full', + '--json', + '--markdown', + '--no-banner', + '--no-spinner', + '--nobanner', + '--org', + '--spinner', + '--version', + ]) + // Extract base flag name for --flag=value syntax (e.g., '--config=/path' -> '--config'). + const baseFlagName = commandOrAliasName?.split('=')[0] + if ( + commandOrAliasName?.startsWith('--') && + !nodeCliFlags.has(baseFlagName ?? '') + ) { const pythonResult = await spawnSocketPython(argv, { stdio: 'inherit', }) diff --git a/packages/cli/test/unit/shadow/npm-base.test.mts b/packages/cli/test/unit/shadow/npm-base.test.mts index e3bb3070a..39c715d4f 100644 --- a/packages/cli/test/unit/shadow/npm-base.test.mts +++ b/packages/cli/test/unit/shadow/npm-base.test.mts @@ -117,6 +117,11 @@ vi.mock('@socketsecurity/lib/constants/node', () => ({ supportsNodePermissionFlag: vi.fn(() => true), })) +// Mock isDebug to always return false so --loglevel args are added. +vi.mock('@socketsecurity/lib/debug', () => ({ + isDebug: vi.fn(() => false), +})) + describe('shadowNpmBase', () => { const mockSpawnResult = Promise.resolve({ success: true, diff --git a/packages/cli/test/unit/shadow/npm/install.test.mts b/packages/cli/test/unit/shadow/npm/install.test.mts index a1cf1c87d..cbeeaca28 100644 --- a/packages/cli/test/unit/shadow/npm/install.test.mts +++ b/packages/cli/test/unit/shadow/npm/install.test.mts @@ -95,6 +95,11 @@ vi.mock('../../../../src/constants/cli.mts', () => ({ FLAG_LOGLEVEL: '--loglevel', })) +// Mock isDebug to always return false so --loglevel args are added. +vi.mock('@socketsecurity/lib/debug', () => ({ + isDebug: vi.fn(() => false), +})) + describe('shadowNpmInstall', () => { const mockProcess = { send: vi.fn(), diff --git a/packages/cli/test/unit/utils/fs/path-resolve.test.mts b/packages/cli/test/unit/utils/fs/path-resolve.test.mts index dad01b39b..c46994277 100644 --- a/packages/cli/test/unit/utils/fs/path-resolve.test.mts +++ b/packages/cli/test/unit/utils/fs/path-resolve.test.mts @@ -37,6 +37,10 @@ import { createTestWorkspace } from '../../../helpers/workspace-helper.mts' const PACKAGE_JSON = 'package.json' +// Hoisted mocks for better CI reliability. +const mockWhichRealSync = vi.hoisted(() => vi.fn()) +const mockResolveRealBinSync = vi.hoisted(() => vi.fn((p: string) => p)) + // Mock dependencies for new tests. vi.mock('@socketsecurity/lib/bin', async () => { const actual = await vi.importActual< @@ -44,8 +48,8 @@ vi.mock('@socketsecurity/lib/bin', async () => { >('@socketsecurity/lib/bin') return { ...actual, - resolveBinPathSync: vi.fn(p => p), - whichRealSync: vi.fn(), + resolveRealBinSync: mockResolveRealBinSync, + whichRealSync: mockWhichRealSync, } }) @@ -382,11 +386,8 @@ describe('Path Resolve', () => { vi.clearAllMocks() }) - it('finds bin path when available', async () => { - const { whichRealSync } = vi.mocked( - await import('@socketsecurity/lib/bin'), - ) - whichRealSync.mockReturnValue(['/usr/local/bin/npm']) + it('finds bin path when available', () => { + mockWhichRealSync.mockReturnValue(['/usr/local/bin/npm']) const result = findBinPathDetailsSync('npm') @@ -400,10 +401,7 @@ describe('Path Resolve', () => { it('handles shadowed bin paths', async () => { const constants = await import('../../../../src/constants.mts') const shadowBinPath = constants.default.shadowBinPath - const { whichRealSync } = vi.mocked( - await import('@socketsecurity/lib/bin'), - ) - whichRealSync.mockReturnValue([ + mockWhichRealSync.mockReturnValue([ `${shadowBinPath}/npm`, '/usr/local/bin/npm', ]) @@ -417,11 +415,8 @@ describe('Path Resolve', () => { }) }) - it('handles no bin path found', async () => { - const { whichRealSync } = vi.mocked( - await import('@socketsecurity/lib/bin'), - ) - whichRealSync.mockReturnValue(null) + it('handles no bin path found', () => { + mockWhichRealSync.mockReturnValue(null) const result = findBinPathDetailsSync('nonexistent') @@ -432,11 +427,8 @@ describe('Path Resolve', () => { }) }) - it('handles empty array result', async () => { - const { whichRealSync } = vi.mocked( - await import('@socketsecurity/lib/bin'), - ) - whichRealSync.mockReturnValue([]) + it('handles empty array result', () => { + mockWhichRealSync.mockReturnValue([]) const result = findBinPathDetailsSync('npm') @@ -447,11 +439,8 @@ describe('Path Resolve', () => { }) }) - it('handles single string result', async () => { - const { whichRealSync } = vi.mocked( - await import('@socketsecurity/lib/bin'), - ) - whichRealSync.mockReturnValue('/usr/local/bin/npm' as any) + it('handles single string result', () => { + mockWhichRealSync.mockReturnValue('/usr/local/bin/npm' as any) const result = findBinPathDetailsSync('npm') @@ -465,10 +454,7 @@ describe('Path Resolve', () => { it('handles only shadow bin in path', async () => { const constants = await import('../../../../src/constants.mts') const shadowBinPath = constants.default.shadowBinPath - const { whichRealSync } = vi.mocked( - await import('@socketsecurity/lib/bin'), - ) - whichRealSync.mockReturnValue([`${shadowBinPath}/npm`]) + mockWhichRealSync.mockReturnValue([`${shadowBinPath}/npm`]) const result = findBinPathDetailsSync('npm')