diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..51b97172c --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,340 @@ +'use strict' + +const path = require('node:path') + +const { + convertIgnorePatternToMinimatch, + includeIgnoreFile, +} = require('@eslint/compat') +const js = require('@eslint/js') +const tsParser = require('@typescript-eslint/parser') +const { + createTypeScriptImportResolver, +} = require('eslint-import-resolver-typescript') +const importXPlugin = require('eslint-plugin-import-x') +const nodePlugin = require('eslint-plugin-n') +const sortDestructureKeysPlugin = require('eslint-plugin-sort-destructure-keys') +const unicornPlugin = require('eslint-plugin-unicorn') +const globals = require('globals') +const tsEslint = require('typescript-eslint') + +const constants = require('@socketsecurity/registry/lib/constants') +const { BIOME_JSON, GITIGNORE, LATEST, TSCONFIG_JSON } = constants + +const { flatConfigs: origImportXFlatConfigs } = importXPlugin + +const rootPath = __dirname +const rootTsConfigPath = path.join(rootPath, TSCONFIG_JSON) + +const nodeGlobalsConfig = Object.fromEntries( + Object.entries(globals.node).map(([k]) => [k, 'readonly']), +) + +const biomeConfigPath = path.join(rootPath, BIOME_JSON) +const biomeConfig = require(biomeConfigPath) +const biomeIgnores = { + name: 'Imported biome.json ignore patterns', + ignores: biomeConfig.files.includes + .filter(p => p.startsWith('!')) + .map(p => convertIgnorePatternToMinimatch(p.slice(1))), +} + +const gitignorePath = path.join(rootPath, GITIGNORE) +const gitIgnores = includeIgnoreFile(gitignorePath) + +if (process.env.LINT_DIST) { + const isNotDistGlobPattern = p => !/(?:^|[\\/])dist/.test(p) + biomeIgnores.ignores = biomeIgnores.ignores?.filter(isNotDistGlobPattern) + gitIgnores.ignores = gitIgnores.ignores?.filter(isNotDistGlobPattern) +} + +if (process.env.LINT_EXTERNAL) { + const isNotExternalGlobPattern = p => !/(?:^|[\\/])external/.test(p) + biomeIgnores.ignores = biomeIgnores.ignores?.filter(isNotExternalGlobPattern) + gitIgnores.ignores = gitIgnores.ignores?.filter(isNotExternalGlobPattern) +} + +const sharedPlugins = { + 'sort-destructure-keys': sortDestructureKeysPlugin, + unicorn: unicornPlugin, +} + +const sharedRules = { + 'unicorn/consistent-function-scoping': 'error', + curly: 'error', + 'line-comment-position': ['error', { position: 'above' }], + 'no-await-in-loop': 'error', + 'no-control-regex': 'error', + 'no-empty': ['error', { allowEmptyCatch: true }], + 'no-new': 'error', + 'no-proto': 'error', + 'no-undef': 'error', + 'no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_|^this$', + ignoreRestSiblings: true, + varsIgnorePattern: '^_', + }, + ], + 'no-var': 'error', + 'no-warning-comments': ['warn', { terms: ['fixme'] }], + 'prefer-const': 'error', + 'sort-destructure-keys/sort-destructure-keys': 'error', + 'sort-imports': ['error', { ignoreDeclarationSort: true }], +} + +const sharedRulesForImportX = { + ...origImportXFlatConfigs.recommended.rules, + 'import-x/extensions': [ + 'error', + 'never', + { + cjs: 'ignorePackages', + js: 'ignorePackages', + json: 'always', + mjs: 'ignorePackages', + mts: 'ignorePackages', + ts: 'ignorePackages', + }, + ], + 'import-x/order': [ + 'warn', + { + groups: [ + 'builtin', + 'external', + 'internal', + ['parent', 'sibling', 'index'], + 'type', + ], + pathGroups: [ + { + pattern: '@socket{registry,security}/**', + group: 'internal', + }, + ], + pathGroupsExcludedImportTypes: ['type'], + 'newlines-between': 'always', + alphabetize: { + order: 'asc', + }, + }, + ], +} + +const sharedRulesForNode = { + 'n/exports-style': ['error', 'module.exports'], + 'n/no-missing-require': ['off'], + // The n/no-unpublished-bin rule does does not support non-trivial glob + // patterns used in package.json "files" fields. In those cases we simplify + // the glob patterns used. + 'n/no-unpublished-bin': 'error', + 'n/no-unsupported-features/es-builtins': 'error', + 'n/no-unsupported-features/es-syntax': 'error', + 'n/no-unsupported-features/node-builtins': [ + 'error', + { + ignores: [ + 'fetch', + 'fs.promises.cp', + 'module.enableCompileCache', + 'readline/promises', + 'test', + 'test.describe', + ], + version: constants.maintainedNodeVersions.current, + }, + ], + 'n/prefer-node-protocol': 'error', +} + +function getImportXFlatConfigs(isEsm) { + return { + recommended: { + ...origImportXFlatConfigs.recommended, + languageOptions: { + ...origImportXFlatConfigs.recommended.languageOptions, + ecmaVersion: LATEST, + sourceType: isEsm ? 'module' : 'script', + }, + rules: { + ...sharedRulesForImportX, + 'import-x/no-named-as-default-member': 'off', + }, + }, + typescript: { + ...origImportXFlatConfigs.typescript, + plugins: origImportXFlatConfigs.recommended.plugins, + settings: { + ...origImportXFlatConfigs.typescript.settings, + 'import-x/resolver-next': [ + createTypeScriptImportResolver({ + project: rootTsConfigPath, + }), + ], + }, + rules: { + ...sharedRulesForImportX, + // TypeScript compilation already ensures that named imports exist in + // the referenced module. + 'import-x/named': 'off', + 'import-x/no-named-as-default-member': 'off', + 'import-x/no-unresolved': 'off', + }, + }, + } +} + +const importFlatConfigsForScript = getImportXFlatConfigs(false) +const importFlatConfigsForModule = getImportXFlatConfigs(true) + +module.exports = [ + gitIgnores, + biomeIgnores, + { + files: ['**/*.{cts,mts,ts}'], + ...js.configs.recommended, + ...importFlatConfigsForModule.typescript, + languageOptions: { + ...js.configs.recommended.languageOptions, + ...importFlatConfigsForModule.typescript.languageOptions, + globals: { + ...js.configs.recommended.languageOptions?.globals, + ...importFlatConfigsForModule.typescript.languageOptions?.globals, + ...nodeGlobalsConfig, + BufferConstructor: 'readonly', + BufferEncoding: 'readonly', + NodeJS: 'readonly', + }, + parser: tsParser, + parserOptions: { + ...js.configs.recommended.languageOptions?.parserOptions, + ...importFlatConfigsForModule.typescript.languageOptions?.parserOptions, + projectService: { + ...importFlatConfigsForModule.typescript.languageOptions + ?.parserOptions?.projectService, + allowDefaultProject: [ + // Allow configs. + '*.config.mts', + // Allow paths like src/utils/*.test.mts. + 'src/*/*.test.mts', + // Allow paths like src/commands/optimize/*.test.mts. + 'src/*/*/*.test.mts', + 'test/*.mts', + ], + defaultProject: 'tsconfig.json', + tsconfigRootDir: rootPath, + // Need this to glob the test files in /src. Otherwise it won't work. + maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING: 1_000_000, + }, + }, + }, + linterOptions: { + ...js.configs.recommended.linterOptions, + ...importFlatConfigsForModule.typescript.linterOptions, + reportUnusedDisableDirectives: 'off', + }, + plugins: { + ...js.configs.recommended.plugins, + ...importFlatConfigsForModule.typescript.plugins, + ...nodePlugin.configs['flat/recommended-module'].plugins, + ...sharedPlugins, + '@typescript-eslint': tsEslint.plugin, + }, + rules: { + ...js.configs.recommended.rules, + ...importFlatConfigsForModule.typescript.rules, + ...nodePlugin.configs['flat/recommended-module'].rules, + ...sharedRulesForNode, + ...sharedRules, + '@typescript-eslint/array-type': ['error', { default: 'array-simple' }], + '@typescript-eslint/consistent-type-assertions': [ + 'error', + { assertionStyle: 'as' }, + ], + '@typescript-eslint/no-misused-new': 'error', + '@typescript-eslint/no-this-alias': [ + 'error', + { allowDestructuring: true }, + ], + // Returning unawaited promises in a try/catch/finally is dangerous + // (the `catch` won't catch if the promise is rejected, and the `finally` + // won't wait for the promise to resolve). Returning unawaited promises + // elsewhere is probably fine, but this lint rule doesn't have a way + // to only apply to try/catch/finally (the 'in-try-catch' option *enforces* + // not awaiting promises *outside* of try/catch/finally, which is not what + // we want), and it's nice to await before returning anyways, since you get + // a slightly more comprehensive stack trace upon promise rejection. + '@typescript-eslint/return-await': ['error', 'always'], + // Disable the following rules because they don't play well with TypeScript. + 'n/hashbang': 'off', + 'n/no-extraneous-import': 'off', + 'n/no-missing-import': 'off', + 'no-redeclare': 'off', + 'no-unused-vars': 'off', + }, + }, + { + files: ['**/*.{cjs,js}'], + ...js.configs.recommended, + ...importFlatConfigsForScript.recommended, + ...nodePlugin.configs['flat/recommended-script'], + languageOptions: { + ...js.configs.recommended.languageOptions, + ...importFlatConfigsForModule.recommended.languageOptions, + ...nodePlugin.configs['flat/recommended-script'].languageOptions, + globals: { + ...js.configs.recommended.languageOptions?.globals, + ...importFlatConfigsForModule.recommended.languageOptions?.globals, + ...nodePlugin.configs['flat/recommended-script'].languageOptions + ?.globals, + ...nodeGlobalsConfig, + }, + }, + plugins: { + ...js.configs.recommended.plugins, + ...importFlatConfigsForScript.recommended.plugins, + ...nodePlugin.configs['flat/recommended-script'].plugins, + ...sharedPlugins, + }, + rules: { + ...js.configs.recommended.rules, + ...importFlatConfigsForScript.recommended.rules, + ...nodePlugin.configs['flat/recommended-script'].rules, + ...sharedRulesForNode, + ...sharedRules, + }, + }, + { + files: ['**/*.mjs'], + ...js.configs.recommended, + ...importFlatConfigsForModule.recommended, + ...nodePlugin.configs['flat/recommended-module'], + languageOptions: { + ...js.configs.recommended.languageOptions, + ...importFlatConfigsForModule.recommended.languageOptions, + ...nodePlugin.configs['flat/recommended-module'].languageOptions, + globals: { + ...js.configs.recommended.languageOptions?.globals, + ...importFlatConfigsForModule.recommended.languageOptions?.globals, + ...nodePlugin.configs['flat/recommended-module'].languageOptions + ?.globals, + ...nodeGlobalsConfig, + }, + }, + plugins: { + ...js.configs.recommended.plugins, + ...importFlatConfigsForModule.recommended.plugins, + ...nodePlugin.configs['flat/recommended-module'].plugins, + ...sharedPlugins, + }, + rules: { + ...js.configs.recommended.rules, + ...importFlatConfigsForModule.recommended.rules, + ...nodePlugin.configs['flat/recommended-module'].rules, + ...sharedRulesForNode, + ...sharedRules, + }, + }, +] diff --git a/package.json b/package.json index fb149f814..c15dc1b84 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "@socketsecurity/config": "catalog:", "@socketsecurity/lib": "catalog:", "@socketsecurity/registry": "catalog:", - "@socketsecurity/sdk": "catalog:", + "@socketsecurity/sdk":"file:///Users/billli/code/socketdev/socket-sdk-js/socketsecurity-sdk-3.1.3.tgz", "@types/cmd-shim": "catalog:", "@types/ink": "catalog:", "@types/js-yaml": "catalog:", diff --git a/packages/bootstrap/.config/esbuild.npm.config.mjs b/packages/bootstrap/.config/esbuild.npm.config.mjs index 8092048fa..c3e35d33f 100644 --- a/packages/bootstrap/.config/esbuild.npm.config.mjs +++ b/packages/bootstrap/.config/esbuild.npm.config.mjs @@ -6,13 +6,13 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' +import { unicodeTransformPlugin } from 'build-infra/lib/esbuild-plugin-unicode-transform' import { build } from 'esbuild' import semver from 'semver' -import { unicodeTransformPlugin } from 'build-infra/lib/esbuild-plugin-unicode-transform' -import nodeVersionConfig from '../node-version.json' with { type: 'json' } import socketPackageJson from '../../socket/package.json' with { type: 'json' } +import nodeVersionConfig from '../node-version.json' with { type: 'json' } const __dirname = path.dirname(fileURLToPath(import.meta.url)) const rootPath = path.resolve(__dirname, '..') @@ -25,7 +25,9 @@ const config = { define: { __MIN_NODE_VERSION__: JSON.stringify(nodeVersionConfig.versionSemver), __SOCKET_CLI_VERSION__: JSON.stringify(socketPackageJson.version), - __SOCKET_CLI_VERSION_MAJOR__: JSON.stringify(semver.major(socketPackageJson.version)), + __SOCKET_CLI_VERSION_MAJOR__: JSON.stringify( + semver.major(socketPackageJson.version), + ), }, entryPoints: [path.join(rootPath, 'src', 'bootstrap-npm.mts')], external: [], @@ -39,7 +41,8 @@ const config = { plugins: [unicodeTransformPlugin()], target: 'node18', treeShaking: true, - write: false, // Plugin needs to transform output. + // Plugin needs to transform output. + write: false, } // Run build if invoked directly. diff --git a/packages/bootstrap/.config/esbuild.sea.config.mjs b/packages/bootstrap/.config/esbuild.sea.config.mjs index 9dfbefeb4..6d0f6c170 100644 --- a/packages/bootstrap/.config/esbuild.sea.config.mjs +++ b/packages/bootstrap/.config/esbuild.sea.config.mjs @@ -6,13 +6,13 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' +import { unicodeTransformPlugin } from 'build-infra/lib/esbuild-plugin-unicode-transform' import { build } from 'esbuild' import semver from 'semver' -import { unicodeTransformPlugin } from 'build-infra/lib/esbuild-plugin-unicode-transform' -import nodeVersionConfig from '../node-version.json' with { type: 'json' } import socketPackageJson from '../../socket/package.json' with { type: 'json' } +import nodeVersionConfig from '../node-version.json' with { type: 'json' } const __dirname = path.dirname(fileURLToPath(import.meta.url)) const rootPath = path.resolve(__dirname, '..') @@ -25,7 +25,9 @@ const config = { define: { __MIN_NODE_VERSION__: JSON.stringify(nodeVersionConfig.versionSemver), __SOCKET_CLI_VERSION__: JSON.stringify(socketPackageJson.version), - __SOCKET_CLI_VERSION_MAJOR__: JSON.stringify(semver.major(socketPackageJson.version)), + __SOCKET_CLI_VERSION_MAJOR__: JSON.stringify( + semver.major(socketPackageJson.version), + ), }, entryPoints: [path.join(rootPath, 'src', 'bootstrap-sea.mts')], external: [], @@ -39,7 +41,8 @@ const config = { plugins: [unicodeTransformPlugin()], target: 'node18', treeShaking: true, - write: false, // Plugin needs to transform output. + // Plugin needs to transform output. + write: false, } // Run build if invoked directly. diff --git a/packages/bootstrap/scripts/build.mjs b/packages/bootstrap/scripts/build.mjs index c2ddec494..cf3f34203 100644 --- a/packages/bootstrap/scripts/build.mjs +++ b/packages/bootstrap/scripts/build.mjs @@ -1,4 +1,3 @@ -#!/usr/bin/env node /** * Build script for Socket bootstrap package. * @@ -12,8 +11,8 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' import path from 'node:path' -import { brotliCompressSync, constants as zlibConstants } from 'node:zlib' import { fileURLToPath } from 'node:url' +import { brotliCompressSync, constants as zlibConstants } from 'node:zlib' import { build } from 'esbuild' import colors from 'yoctocolors-cjs' @@ -41,8 +40,13 @@ function compressFile(filePath) { const brPath = `${filePath}.br` writeFileSync(brPath, compressed) - const reductionPercent = ((1 - compressed.length / uncompressed.length) * 100).toFixed(1) - console.log(` Compressed: ${(compressed.length / 1024).toFixed(2)} KB (${reductionPercent}% reduction)`) + const reductionPercent = ( + (1 - compressed.length / uncompressed.length) * + 100 + ).toFixed(1) + console.log( + ` Compressed: ${(compressed.length / 1024).toFixed(2)} KB (${reductionPercent}% reduction)`, + ) } console.log('Building Socket bootstrap with esbuild...\\n') diff --git a/packages/bootstrap/src/bootstrap-npm.mts b/packages/bootstrap/src/bootstrap-npm.mts index 132425e66..868468453 100644 --- a/packages/bootstrap/src/bootstrap-npm.mts +++ b/packages/bootstrap/src/bootstrap-npm.mts @@ -19,12 +19,14 @@ async function main() { // Run the bootstrap. main() - .then((exitCode) => { + .then(exitCode => { // Exit with the code returned by the CLI. process.exit(exitCode) }) - .catch((e) => { + .catch(e => { const logger = getDefaultLogger() - logger.error(`Bootstrap error: ${e instanceof Error ? e.message : String(e)}`) + logger.error( + `Bootstrap error: ${e instanceof Error ? e.message : String(e)}`, + ) process.exit(1) }) diff --git a/packages/bootstrap/src/bootstrap-sea.mts b/packages/bootstrap/src/bootstrap-sea.mts index b36a6c351..40edfd84b 100644 --- a/packages/bootstrap/src/bootstrap-sea.mts +++ b/packages/bootstrap/src/bootstrap-sea.mts @@ -11,7 +11,11 @@ // Load Intl polyfill FIRST for ICU-disabled builds (if SEA uses minimal Node.js). import '@socketsecurity/cli/src/polyfills/intl-stub/index.mts' -import { findAndExecuteCli, getArgs, SOCKET_CLI_VERSION } from './shared/bootstrap-shared.mjs' +import { + findAndExecuteCli, + getArgs, + SOCKET_CLI_VERSION, +} from './shared/bootstrap-shared.mjs' async function main() { const args = getArgs() @@ -27,14 +31,16 @@ async function main() { // Run the bootstrap. main() - .then((exitCode) => { + .then(exitCode => { // Exit with the code returned by the CLI (or 0 if bootstrap was skipped). if (exitCode !== 0) { process.exit(exitCode) } }) - .catch((e) => { + .catch(e => { // Use process.stderr.write() directly to avoid console access during early bootstrap. - process.stderr.write(`Bootstrap error: ${e instanceof Error ? e.message : String(e)}\n`) + process.stderr.write( + `Bootstrap error: ${e instanceof Error ? e.message : String(e)}\n`, + ) process.exit(1) }) diff --git a/packages/bootstrap/src/shared/bootstrap-shared.mjs b/packages/bootstrap/src/shared/bootstrap-shared.mjs index 9b63556a0..91bdbfcfe 100644 --- a/packages/bootstrap/src/shared/bootstrap-shared.mjs +++ b/packages/bootstrap/src/shared/bootstrap-shared.mjs @@ -7,14 +7,16 @@ import { existsSync, mkdirSync } from 'node:fs' import { homedir } from 'node:os' import path from 'node:path' +import { gte } from 'semver' + import { whichReal } from '@socketsecurity/lib/bin' import { SOCKET_IPC_HANDSHAKE } from '@socketsecurity/lib/constants/socket' import { downloadPackage } from '@socketsecurity/lib/dlx-package' import { envAsBoolean } from '@socketsecurity/lib/env' import { getDefaultLogger } from '@socketsecurity/lib/logger' -import { Spinner, withSpinner } from '@socketsecurity/lib/spinner' import { spawn } from '@socketsecurity/lib/spawn' -import { gte } from 'semver' +import { Spinner, withSpinner } from '@socketsecurity/lib/spinner' + import { SOCKET_CLI_ISSUES_URL } from '../../../cli/src/constants/socket.mts' const logger = getDefaultLogger() @@ -26,7 +28,7 @@ export const SOCKET_DLX_DIR = path.join(homedir(), '.socket', '_dlx') * Set to '1', 'true', or 'yes' to disable forwarding (useful for e2e testing). */ const SOCKET_CLI_DISABLE_NODE_FORWARDING = envAsBoolean( - process.env.SOCKET_CLI_DISABLE_NODE_FORWARDING + process.env.SOCKET_CLI_DISABLE_NODE_FORWARDING, ) /** @@ -53,7 +55,14 @@ export function getCliPaths(cliPackageDir) { throw new Error('CLI package directory not initialized') } return { - cliEntry: path.join(cliPackageDir, 'node_modules', '@socketsecurity', 'cli', 'dist', 'index.js'), + cliEntry: path.join( + cliPackageDir, + 'node_modules', + '@socketsecurity', + 'cli', + 'dist', + 'index.js', + ), } } @@ -165,7 +174,12 @@ export async function executeCli(cliPath, args) { } catch (e) { // Spawn throws when child exits with non-zero code. // Extract the exit code from the error. - if (e && typeof e === 'object' && 'code' in e && typeof e.code === 'number') { + if ( + e && + typeof e === 'object' && + 'code' in e && + typeof e.code === 'number' + ) { return e.code } throw e @@ -210,8 +224,12 @@ export async function downloadCli() { logger.error('') // @ts-expect-error - Injected by esbuild define. if (!INLINED_SOCKET_BOOTSTRAP_PUBLISHED_BUILD) { - logger.error('For local development, set SOCKET_CLI_LOCAL_PATH to your CLI build:') - logger.error(` export SOCKET_CLI_LOCAL_PATH=/path/to/socket-cli/packages/cli/dist/index.js`) + logger.error( + 'For local development, set SOCKET_CLI_LOCAL_PATH to your CLI build:', + ) + logger.error( + ` export SOCKET_CLI_LOCAL_PATH=/path/to/socket-cli/packages/cli/dist/index.js`, + ) logger.error('') logger.error('Or try:') } else { diff --git a/packages/build-infra/lib/esbuild-plugin-dead-code-elimination.mjs b/packages/build-infra/lib/esbuild-plugin-dead-code-elimination.mjs index 978db3a9f..f72c33aa9 100644 --- a/packages/build-infra/lib/esbuild-plugin-dead-code-elimination.mjs +++ b/packages/build-infra/lib/esbuild-plugin-dead-code-elimination.mjs @@ -16,7 +16,8 @@ import { parse } from '@babel/parser' import { default as traverseImport } from '@babel/traverse' import MagicString from 'magic-string' -const traverse = typeof traverseImport === 'function' ? traverseImport : traverseImport.default +const traverse = + typeof traverseImport === 'function' ? traverseImport : traverseImport.default /** * Evaluate a test expression to determine if it's a constant boolean. @@ -129,7 +130,7 @@ export function deadCodeEliminationPlugin() { return { name: 'dead-code-elimination', setup(build) { - build.onEnd((result) => { + build.onEnd(result => { const outputs = result.outputFiles if (!outputs || outputs.length === 0) { return diff --git a/packages/build-infra/lib/esbuild-plugin-unicode-transform.mjs b/packages/build-infra/lib/esbuild-plugin-unicode-transform.mjs index 6df6ff467..51fcb2eb1 100644 --- a/packages/build-infra/lib/esbuild-plugin-unicode-transform.mjs +++ b/packages/build-infra/lib/esbuild-plugin-unicode-transform.mjs @@ -23,7 +23,7 @@ export function unicodeTransformPlugin() { return { name: 'unicode-transform', setup(build) { - build.onEnd((result) => { + build.onEnd(result => { const outputs = result.outputFiles if (!outputs || outputs.length === 0) { return diff --git a/packages/build-infra/lib/extraction-cache.mjs b/packages/build-infra/lib/extraction-cache.mjs index 0c10f1fa4..83099507f 100644 --- a/packages/build-infra/lib/extraction-cache.mjs +++ b/packages/build-infra/lib/extraction-cache.mjs @@ -27,9 +27,9 @@ import { getDefaultLogger } from '@socketsecurity/lib/logger' * @returns {Promise} True if extraction needed, false if cached */ export async function shouldExtract({ - sourcePaths, - outputPath, hashPattern = /Source hash: ([a-f0-9]{64})/, + outputPath, + sourcePaths, validateOutput, }) { // Normalize to array. diff --git a/packages/build-infra/lib/script-runner.mjs b/packages/build-infra/lib/script-runner.mjs index 923e3530e..0f1df2808 100644 --- a/packages/build-infra/lib/script-runner.mjs +++ b/packages/build-infra/lib/script-runner.mjs @@ -16,7 +16,12 @@ import { spawn } from '@socketsecurity/lib/spawn' * @param {object} options - Spawn options * @returns {Promise<{code: number, stdout?: string, stderr?: string}>} */ -export async function runPnpmScript(packageName, scriptName, args = [], options = {}) { +export async function runPnpmScript( + packageName, + scriptName, + args = [], + options = {}, +) { const pnpmArgs = ['--filter', packageName, 'run', scriptName, ...args] return spawn('pnpm', pnpmArgs, { diff --git a/packages/build-infra/lib/setup-helpers.mjs b/packages/build-infra/lib/setup-helpers.mjs index 62fb942fa..39d3202d4 100644 --- a/packages/build-infra/lib/setup-helpers.mjs +++ b/packages/build-infra/lib/setup-helpers.mjs @@ -3,9 +3,9 @@ * Provides helpers for checking and installing required development tools. */ -import { spawn } from '@socketsecurity/lib/spawn' -import { getDefaultLogger } from '@socketsecurity/lib/logger' import { WIN32 } from '@socketsecurity/lib/constants/platform' +import { getDefaultLogger } from '@socketsecurity/lib/logger' +import { spawn } from '@socketsecurity/lib/spawn' const logger = getDefaultLogger() @@ -55,7 +55,7 @@ export async function getVersion(command, args = ['--version']) { */ export function parseVersion(versionString) { const match = versionString.match(/(\d+)\.(\d+)\.(\d+)/) - if (!match) return null + if (!match) {return null} return { major: Number.parseInt(match[1], 10), minor: Number.parseInt(match[2], 10), @@ -71,9 +71,9 @@ export function parseVersion(versionString) { * @returns {number} -1 if a < b, 0 if a === b, 1 if a > b */ export function compareVersions(a, b) { - if (a.major !== b.major) return a.major < b.major ? -1 : 1 - if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1 - if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1 + if (a.major !== b.major) {return a.major < b.major ? -1 : 1} + if (a.minor !== b.minor) {return a.minor < b.minor ? -1 : 1} + if (a.patch !== b.patch) {return a.patch < b.patch ? -1 : 1} return 0 } @@ -110,7 +110,8 @@ export async function installHomebrew() { logger.step('Installing Homebrew...') logger.info('This requires sudo access and may take a few minutes') - const installScript = '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"' + const installScript = + '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"' const result = await spawn('bash', ['-c', installScript], { stdio: 'inherit', @@ -140,7 +141,8 @@ export async function installChocolatey() { logger.step('Installing Chocolatey...') logger.info('This requires admin access and may take a few minutes') - const installScript = 'Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString(\'https://community.chocolatey.org/install.ps1\'))' + const installScript = + "Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))" const result = await spawn('powershell', ['-Command', installScript], { stdio: 'inherit', @@ -163,7 +165,7 @@ export async function installChocolatey() { * @returns {Promise} True if installation succeeded */ export async function installWithHomebrew(packageName) { - if (!await hasHomebrew()) { + if (!(await hasHomebrew())) { logger.error('Homebrew not available') return false } @@ -190,7 +192,7 @@ export async function installWithHomebrew(packageName) { * @returns {Promise} True if installation succeeded */ export async function installWithChocolatey(packageName) { - if (!await hasChocolatey()) { + if (!(await hasChocolatey())) { logger.error('Chocolatey not available') return false } @@ -232,8 +234,10 @@ export async function installGhCli({ autoInstall = false } = {}) { // Windows: Try Chocolatey. if (WIN32) { - if (!await hasChocolatey()) { - logger.info('Chocolatey not found. Install Chocolatey first to auto-install gh CLI.') + if (!(await hasChocolatey())) { + logger.info( + 'Chocolatey not found. Install Chocolatey first to auto-install gh CLI.', + ) logger.info('Chocolatey: https://chocolatey.org/install') logger.info('gh CLI: https://cli.github.com/') return false @@ -242,8 +246,10 @@ export async function installGhCli({ autoInstall = false } = {}) { } // macOS/Linux: Try Homebrew. - if (!await hasHomebrew()) { - logger.info('Homebrew not found. Install Homebrew first to auto-install gh CLI.') + if (!(await hasHomebrew())) { + logger.info( + 'Homebrew not found. Install Homebrew first to auto-install gh CLI.', + ) logger.info('Homebrew: https://brew.sh/') logger.info('gh CLI: https://cli.github.com/') return false diff --git a/packages/build-infra/lib/unicode-property-escape-transform.mjs b/packages/build-infra/lib/unicode-property-escape-transform.mjs index c1bc36e6b..4e3976fce 100644 --- a/packages/build-infra/lib/unicode-property-escape-transform.mjs +++ b/packages/build-infra/lib/unicode-property-escape-transform.mjs @@ -9,7 +9,8 @@ import { parse } from '@babel/parser' import { default as traverseImport } from '@babel/traverse' import MagicString from 'magic-string' -const traverse = typeof traverseImport === 'function' ? traverseImport : traverseImport.default +const traverse = + typeof traverseImport === 'function' ? traverseImport : traverseImport.default /** * Map of Unicode property escapes to explicit character ranges. @@ -22,102 +23,115 @@ export const unicodePropertyMap = { __proto__: null, // Special properties. - 'Default_Ignorable_Code_Point': '\\u00AD\\u034F\\u061C\\u115F-\\u1160\\u17B4-\\u17B5\\u180B-\\u180D\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u206F\\u3164\\uFE00-\\uFE0F\\uFEFF\\uFFA0\\uFFF0-\\uFFF8', - 'ASCII': '\\x00-\\x7F', - 'ASCII_Hex_Digit': '0-9A-Fa-f', - 'Alphabetic': 'A-Za-z\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE', + Default_Ignorable_Code_Point: + '\\u00AD\\u034F\\u061C\\u115F-\\u1160\\u17B4-\\u17B5\\u180B-\\u180D\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u206F\\u3164\\uFE00-\\uFE0F\\uFEFF\\uFFA0\\uFFF0-\\uFFF8', + ASCII: '\\x00-\\x7F', + ASCII_Hex_Digit: '0-9A-Fa-f', + Alphabetic: + 'A-Za-z\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE', // General categories - Letter. - 'Letter': 'A-Za-z\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE', - 'L': 'A-Za-z\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE', - 'Lowercase_Letter': 'a-z\\u00B5\\u00DF-\\u00F6\\u00F8-\\u00FF', - 'Ll': 'a-z\\u00B5\\u00DF-\\u00F6\\u00F8-\\u00FF', - 'Uppercase_Letter': 'A-Z\\u00C0-\\u00D6\\u00D8-\\u00DE', - 'Lu': 'A-Z\\u00C0-\\u00D6\\u00D8-\\u00DE', - 'Titlecase_Letter': '\\u01C5\\u01C8\\u01CB\\u01F2', - 'Lt': '\\u01C5\\u01C8\\u01CB\\u01F2', - 'Modifier_Letter': '\\u02B0-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE', - 'Lm': '\\u02B0-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE', - 'Other_Letter': '\\u00AA\\u00BA', - 'Lo': '\\u00AA\\u00BA', + Letter: + 'A-Za-z\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE', + L: 'A-Za-z\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE', + Lowercase_Letter: 'a-z\\u00B5\\u00DF-\\u00F6\\u00F8-\\u00FF', + Ll: 'a-z\\u00B5\\u00DF-\\u00F6\\u00F8-\\u00FF', + Uppercase_Letter: 'A-Z\\u00C0-\\u00D6\\u00D8-\\u00DE', + Lu: 'A-Z\\u00C0-\\u00D6\\u00D8-\\u00DE', + Titlecase_Letter: '\\u01C5\\u01C8\\u01CB\\u01F2', + Lt: '\\u01C5\\u01C8\\u01CB\\u01F2', + Modifier_Letter: + '\\u02B0-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE', + Lm: '\\u02B0-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE', + Other_Letter: '\\u00AA\\u00BA', + Lo: '\\u00AA\\u00BA', // General categories - Mark. - 'Mark': '\\u0300-\\u036F\\u0483-\\u0489\\u0591-\\u05BD\\u05BF\\u05C1-\\u05C2\\u05C4-\\u05C5\\u05C7\\u0610-\\u061A\\u064B-\\u065F\\u0670\\u06D6-\\u06DC\\u06DF-\\u06E4\\u06E7-\\u06E8\\u06EA-\\u06ED', - 'M': '\\u0300-\\u036F\\u0483-\\u0489\\u0591-\\u05BD\\u05BF\\u05C1-\\u05C2\\u05C4-\\u05C5\\u05C7\\u0610-\\u061A\\u064B-\\u065F\\u0670\\u06D6-\\u06DC\\u06DF-\\u06E4\\u06E7-\\u06E8\\u06EA-\\u06ED', - 'Nonspacing_Mark': '\\u0300-\\u036F\\u0483-\\u0489\\u0591-\\u05BD\\u05BF\\u05C1-\\u05C2\\u05C4-\\u05C5\\u05C7', - 'Mn': '\\u0300-\\u036F\\u0483-\\u0489\\u0591-\\u05BD\\u05BF\\u05C1-\\u05C2\\u05C4-\\u05C5\\u05C7', - 'Spacing_Mark': '\\u0903\\u093B\\u093E-\\u0940\\u0949-\\u094C\\u094E-\\u094F', - 'Mc': '\\u0903\\u093B\\u093E-\\u0940\\u0949-\\u094C\\u094E-\\u094F', - 'Enclosing_Mark': '\\u0488-\\u0489', - 'Me': '\\u0488-\\u0489', + Mark: '\\u0300-\\u036F\\u0483-\\u0489\\u0591-\\u05BD\\u05BF\\u05C1-\\u05C2\\u05C4-\\u05C5\\u05C7\\u0610-\\u061A\\u064B-\\u065F\\u0670\\u06D6-\\u06DC\\u06DF-\\u06E4\\u06E7-\\u06E8\\u06EA-\\u06ED', + M: '\\u0300-\\u036F\\u0483-\\u0489\\u0591-\\u05BD\\u05BF\\u05C1-\\u05C2\\u05C4-\\u05C5\\u05C7\\u0610-\\u061A\\u064B-\\u065F\\u0670\\u06D6-\\u06DC\\u06DF-\\u06E4\\u06E7-\\u06E8\\u06EA-\\u06ED', + Nonspacing_Mark: + '\\u0300-\\u036F\\u0483-\\u0489\\u0591-\\u05BD\\u05BF\\u05C1-\\u05C2\\u05C4-\\u05C5\\u05C7', + Mn: '\\u0300-\\u036F\\u0483-\\u0489\\u0591-\\u05BD\\u05BF\\u05C1-\\u05C2\\u05C4-\\u05C5\\u05C7', + Spacing_Mark: '\\u0903\\u093B\\u093E-\\u0940\\u0949-\\u094C\\u094E-\\u094F', + Mc: '\\u0903\\u093B\\u093E-\\u0940\\u0949-\\u094C\\u094E-\\u094F', + Enclosing_Mark: '\\u0488-\\u0489', + Me: '\\u0488-\\u0489', // General categories - Number. - 'Number': '0-9\\u00B2-\\u00B3\\u00B9\\u00BC-\\u00BE', - 'N': '0-9\\u00B2-\\u00B3\\u00B9\\u00BC-\\u00BE', - 'Decimal_Number': '0-9', - 'Nd': '0-9', - 'Letter_Number': '\\u16EE-\\u16F0\\u2160-\\u2182\\u2185-\\u2188\\u3007\\u3021-\\u3029\\u3038-\\u303A', - 'Nl': '\\u16EE-\\u16F0\\u2160-\\u2182\\u2185-\\u2188\\u3007\\u3021-\\u3029\\u3038-\\u303A', - 'Other_Number': '\\u00B2-\\u00B3\\u00B9\\u00BC-\\u00BE', - 'No': '\\u00B2-\\u00B3\\u00B9\\u00BC-\\u00BE', + Number: '0-9\\u00B2-\\u00B3\\u00B9\\u00BC-\\u00BE', + N: '0-9\\u00B2-\\u00B3\\u00B9\\u00BC-\\u00BE', + Decimal_Number: '0-9', + Nd: '0-9', + Letter_Number: + '\\u16EE-\\u16F0\\u2160-\\u2182\\u2185-\\u2188\\u3007\\u3021-\\u3029\\u3038-\\u303A', + Nl: '\\u16EE-\\u16F0\\u2160-\\u2182\\u2185-\\u2188\\u3007\\u3021-\\u3029\\u3038-\\u303A', + Other_Number: '\\u00B2-\\u00B3\\u00B9\\u00BC-\\u00BE', + No: '\\u00B2-\\u00B3\\u00B9\\u00BC-\\u00BE', // General categories - Punctuation. - 'Punctuation': '!-#%-\\*,-\\/:;\\?@\\[-\\]_\\{\\}\\u00A1\\u00A7\\u00AB\\u00B6-\\u00B7\\u00BB\\u00BF', - 'P': '!-#%-\\*,-\\/:;\\?@\\[-\\]_\\{\\}\\u00A1\\u00A7\\u00AB\\u00B6-\\u00B7\\u00BB\\u00BF', - 'Connector_Punctuation': '_\\u203F-\\u2040', - 'Pc': '_\\u203F-\\u2040', - 'Dash_Punctuation': '\\-\\u2010-\\u2015', - 'Pd': '\\-\\u2010-\\u2015', - 'Open_Punctuation': '\\(\\[\\{', - 'Ps': '\\(\\[\\{', - 'Close_Punctuation': '\\)\\]\\}', - 'Pe': '\\)\\]\\}', - 'Initial_Punctuation': '\\u00AB', - 'Pi': '\\u00AB', - 'Final_Punctuation': '\\u00BB', - 'Pf': '\\u00BB', - 'Other_Punctuation': '!-#%-\\*,\\.\\/:;\\?@\\\\\\u00A1\\u00A7\\u00B6-\\u00B7\\u00BF', - 'Po': '!-#%-\\*,\\.\\/:;\\?@\\\\\\u00A1\\u00A7\\u00B6-\\u00B7\\u00BF', + Punctuation: + '!-#%-\\*,-\\/:;\\?@\\[-\\]_\\{\\}\\u00A1\\u00A7\\u00AB\\u00B6-\\u00B7\\u00BB\\u00BF', + P: '!-#%-\\*,-\\/:;\\?@\\[-\\]_\\{\\}\\u00A1\\u00A7\\u00AB\\u00B6-\\u00B7\\u00BB\\u00BF', + Connector_Punctuation: '_\\u203F-\\u2040', + Pc: '_\\u203F-\\u2040', + Dash_Punctuation: '\\-\\u2010-\\u2015', + Pd: '\\-\\u2010-\\u2015', + Open_Punctuation: '\\(\\[\\{', + Ps: '\\(\\[\\{', + Close_Punctuation: '\\)\\]\\}', + Pe: '\\)\\]\\}', + Initial_Punctuation: '\\u00AB', + Pi: '\\u00AB', + Final_Punctuation: '\\u00BB', + Pf: '\\u00BB', + Other_Punctuation: + '!-#%-\\*,\\.\\/:;\\?@\\\\\\u00A1\\u00A7\\u00B6-\\u00B7\\u00BF', + Po: '!-#%-\\*,\\.\\/:;\\?@\\\\\\u00A1\\u00A7\\u00B6-\\u00B7\\u00BF', // General categories - Symbol. - 'Symbol': '\\$\\+<->\\^`\\|~\\u00A2-\\u00A6\\u00A8-\\u00A9\\u00AC\\u00AE-\\u00B1\\u00B4\\u00B8\\u00D7\\u00F7', - 'S': '\\$\\+<->\\^`\\|~\\u00A2-\\u00A6\\u00A8-\\u00A9\\u00AC\\u00AE-\\u00B1\\u00B4\\u00B8\\u00D7\\u00F7', - 'Math_Symbol': '\\+<->\\|~\\u00AC\\u00B1\\u00D7\\u00F7', - 'Sm': '\\+<->\\|~\\u00AC\\u00B1\\u00D7\\u00F7', - 'Currency_Symbol': '\\$\\u00A2-\\u00A5', - 'Sc': '\\$\\u00A2-\\u00A5', - 'Modifier_Symbol': '\\^`\\u00A8\\u00AF\\u00B4\\u00B8', - 'Sk': '\\^`\\u00A8\\u00AF\\u00B4\\u00B8', - 'Other_Symbol': '\\u00A6\\u00A9\\u00AE\\u00B0', - 'So': '\\u00A6\\u00A9\\u00AE\\u00B0', + Symbol: + '\\$\\+<->\\^`\\|~\\u00A2-\\u00A6\\u00A8-\\u00A9\\u00AC\\u00AE-\\u00B1\\u00B4\\u00B8\\u00D7\\u00F7', + S: '\\$\\+<->\\^`\\|~\\u00A2-\\u00A6\\u00A8-\\u00A9\\u00AC\\u00AE-\\u00B1\\u00B4\\u00B8\\u00D7\\u00F7', + Math_Symbol: '\\+<->\\|~\\u00AC\\u00B1\\u00D7\\u00F7', + Sm: '\\+<->\\|~\\u00AC\\u00B1\\u00D7\\u00F7', + Currency_Symbol: '\\$\\u00A2-\\u00A5', + Sc: '\\$\\u00A2-\\u00A5', + Modifier_Symbol: '\\^`\\u00A8\\u00AF\\u00B4\\u00B8', + Sk: '\\^`\\u00A8\\u00AF\\u00B4\\u00B8', + Other_Symbol: '\\u00A6\\u00A9\\u00AE\\u00B0', + So: '\\u00A6\\u00A9\\u00AE\\u00B0', // General categories - Separator. - 'Separator': ' \\u00A0\\u1680\\u2000-\\u200A\\u2028-\\u2029\\u202F\\u205F\\u3000', - 'Z': ' \\u00A0\\u1680\\u2000-\\u200A\\u2028-\\u2029\\u202F\\u205F\\u3000', - 'Space_Separator': ' \\u00A0\\u1680\\u2000-\\u200A\\u202F\\u205F\\u3000', - 'Zs': ' \\u00A0\\u1680\\u2000-\\u200A\\u202F\\u205F\\u3000', - 'Line_Separator': '\\u2028', - 'Zl': '\\u2028', - 'Paragraph_Separator': '\\u2029', - 'Zp': '\\u2029', + Separator: + ' \\u00A0\\u1680\\u2000-\\u200A\\u2028-\\u2029\\u202F\\u205F\\u3000', + Z: ' \\u00A0\\u1680\\u2000-\\u200A\\u2028-\\u2029\\u202F\\u205F\\u3000', + Space_Separator: ' \\u00A0\\u1680\\u2000-\\u200A\\u202F\\u205F\\u3000', + Zs: ' \\u00A0\\u1680\\u2000-\\u200A\\u202F\\u205F\\u3000', + Line_Separator: '\\u2028', + Zl: '\\u2028', + Paragraph_Separator: '\\u2029', + Zp: '\\u2029', // General categories - Other. - 'Other': '\\x00-\\x1F\\x7F-\\x9F\\u00AD', - 'C': '\\x00-\\x1F\\x7F-\\x9F\\u00AD', - 'Control': '\\x00-\\x1F\\x7F-\\x9F', - 'Cc': '\\x00-\\x1F\\x7F-\\x9F', - 'Format': '\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB', - 'Cf': '\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB', - 'Surrogate': '\\uD800-\\uDFFF', - 'Cs': '\\uD800-\\uDFFF', - 'Private_Use': '\\uE000-\\uF8FF', - 'Co': '\\uE000-\\uF8FF', - 'Unassigned': '\\u0378-\\u0379\\u0380-\\u0383\\u038B\\u038D\\u03A2', - 'Cn': '\\u0378-\\u0379\\u0380-\\u0383\\u038B\\u038D\\u03A2', + Other: '\\x00-\\x1F\\x7F-\\x9F\\u00AD', + C: '\\x00-\\x1F\\x7F-\\x9F\\u00AD', + Control: '\\x00-\\x1F\\x7F-\\x9F', + Cc: '\\x00-\\x1F\\x7F-\\x9F', + Format: + '\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB', + Cf: '\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB', + Surrogate: '\\uD800-\\uDFFF', + Cs: '\\uD800-\\uDFFF', + Private_Use: '\\uE000-\\uF8FF', + Co: '\\uE000-\\uF8FF', + Unassigned: '\\u0378-\\u0379\\u0380-\\u0383\\u038B\\u038D\\u03A2', + Cn: '\\u0378-\\u0379\\u0380-\\u0383\\u038B\\u038D\\u03A2', // Emoji properties. - 'Extended_Pictographic': '\\u00A9\\u00AE\\u203C\\u2049\\u2122\\u2139\\u2194-\\u2199\\u21A9-\\u21AA\\u231A-\\u231B\\u2328\\u23CF\\u23E9-\\u23F3\\u23F8-\\u23FA\\u24C2\\u25AA-\\u25AB\\u25B6\\u25C0\\u25FB-\\u25FE\\u2600-\\u2604\\u260E\\u2611\\u2614-\\u2615\\u2618\\u261D\\u2620\\u2622-\\u2623\\u2626\\u262A\\u262E-\\u262F\\u2638-\\u263A\\u2640\\u2642\\u2648-\\u2653\\u265F-\\u2660\\u2663\\u2665-\\u2666\\u2668\\u267B\\u267E-\\u267F\\u2692-\\u2697\\u2699\\u269B-\\u269C\\u26A0-\\u26A1\\u26A7\\u26AA-\\u26AB\\u26B0-\\u26B1\\u26BD-\\u26BE\\u26C4-\\u26C5\\u26C8\\u26CE-\\u26CF\\u26D1\\u26D3-\\u26D4\\u26E9-\\u26EA\\u26F0-\\u26F5\\u26F7-\\u26FA\\u26FD\\u2702\\u2705\\u2708-\\u270D\\u270F\\u2712\\u2714\\u2716\\u271D\\u2721\\u2728\\u2733-\\u2734\\u2744\\u2747\\u274C\\u274E\\u2753-\\u2755\\u2757\\u2763-\\u2764\\u2795-\\u2797\\u27A1\\u27B0\\u27BF\\u2934-\\u2935\\u2B05-\\u2B07\\u2B1B-\\u2B1C\\u2B50\\u2B55\\u3030\\u303D\\u3297\\u3299', - 'RGI_Emoji': '\\u00A9\\u00AE\\u203C\\u2049\\u2122\\u2139\\u2194-\\u2199\\u21A9-\\u21AA\\u231A-\\u231B\\u2328\\u23CF\\u23E9-\\u23F3\\u23F8-\\u23FA\\u24C2\\u25AA-\\u25AB\\u25B6\\u25C0\\u25FB-\\u25FE\\u2600-\\u2604\\u260E\\u2611\\u2614-\\u2615\\u2618\\u261D\\u2620\\u2622-\\u2623\\u2626\\u262A\\u262E-\\u262F\\u2638-\\u263A\\u2640\\u2642\\u2648-\\u2653\\u265F-\\u2660\\u2663\\u2665-\\u2666\\u2668\\u267B\\u267E-\\u267F\\u2692-\\u2697\\u2699\\u269B-\\u269C\\u26A0-\\u26A1\\u26A7\\u26AA-\\u26AB\\u26B0-\\u26B1\\u26BD-\\u26BE\\u26C4-\\u26C5\\u26C8\\u26CE-\\u26CF\\u26D1\\u26D3-\\u26D4\\u26E9-\\u26EA\\u26F0-\\u26F5\\u26F7-\\u26FA\\u26FD\\u2702\\u2705\\u2708-\\u270D\\u270F\\u2712\\u2714\\u2716\\u271D\\u2721\\u2728\\u2733-\\u2734\\u2744\\u2747\\u274C\\u274E\\u2753-\\u2755\\u2757\\u2763-\\u2764\\u2795-\\u2797\\u27A1\\u27B0\\u27BF\\u2934-\\u2935\\u2B05-\\u2B07\\u2B1B-\\u2B1C\\u2B50\\u2B55\\u3030\\u303D\\u3297\\u3299', + Extended_Pictographic: + '\\u00A9\\u00AE\\u203C\\u2049\\u2122\\u2139\\u2194-\\u2199\\u21A9-\\u21AA\\u231A-\\u231B\\u2328\\u23CF\\u23E9-\\u23F3\\u23F8-\\u23FA\\u24C2\\u25AA-\\u25AB\\u25B6\\u25C0\\u25FB-\\u25FE\\u2600-\\u2604\\u260E\\u2611\\u2614-\\u2615\\u2618\\u261D\\u2620\\u2622-\\u2623\\u2626\\u262A\\u262E-\\u262F\\u2638-\\u263A\\u2640\\u2642\\u2648-\\u2653\\u265F-\\u2660\\u2663\\u2665-\\u2666\\u2668\\u267B\\u267E-\\u267F\\u2692-\\u2697\\u2699\\u269B-\\u269C\\u26A0-\\u26A1\\u26A7\\u26AA-\\u26AB\\u26B0-\\u26B1\\u26BD-\\u26BE\\u26C4-\\u26C5\\u26C8\\u26CE-\\u26CF\\u26D1\\u26D3-\\u26D4\\u26E9-\\u26EA\\u26F0-\\u26F5\\u26F7-\\u26FA\\u26FD\\u2702\\u2705\\u2708-\\u270D\\u270F\\u2712\\u2714\\u2716\\u271D\\u2721\\u2728\\u2733-\\u2734\\u2744\\u2747\\u274C\\u274E\\u2753-\\u2755\\u2757\\u2763-\\u2764\\u2795-\\u2797\\u27A1\\u27B0\\u27BF\\u2934-\\u2935\\u2B05-\\u2B07\\u2B1B-\\u2B1C\\u2B50\\u2B55\\u3030\\u303D\\u3297\\u3299', + RGI_Emoji: + '\\u00A9\\u00AE\\u203C\\u2049\\u2122\\u2139\\u2194-\\u2199\\u21A9-\\u21AA\\u231A-\\u231B\\u2328\\u23CF\\u23E9-\\u23F3\\u23F8-\\u23FA\\u24C2\\u25AA-\\u25AB\\u25B6\\u25C0\\u25FB-\\u25FE\\u2600-\\u2604\\u260E\\u2611\\u2614-\\u2615\\u2618\\u261D\\u2620\\u2622-\\u2623\\u2626\\u262A\\u262E-\\u262F\\u2638-\\u263A\\u2640\\u2642\\u2648-\\u2653\\u265F-\\u2660\\u2663\\u2665-\\u2666\\u2668\\u267B\\u267E-\\u267F\\u2692-\\u2697\\u2699\\u269B-\\u269C\\u26A0-\\u26A1\\u26A7\\u26AA-\\u26AB\\u26B0-\\u26B1\\u26BD-\\u26BE\\u26C4-\\u26C5\\u26C8\\u26CE-\\u26CF\\u26D1\\u26D3-\\u26D4\\u26E9-\\u26EA\\u26F0-\\u26F5\\u26F7-\\u26FA\\u26FD\\u2702\\u2705\\u2708-\\u270D\\u270F\\u2712\\u2714\\u2716\\u271D\\u2721\\u2728\\u2733-\\u2734\\u2744\\u2747\\u274C\\u274E\\u2753-\\u2755\\u2757\\u2763-\\u2764\\u2795-\\u2797\\u27A1\\u27B0\\u27BF\\u2934-\\u2935\\u2B05-\\u2B07\\u2B1B-\\u2B1C\\u2B50\\u2B55\\u3030\\u303D\\u3297\\u3299', } /** @@ -160,13 +174,17 @@ function transformRegexPattern(pattern) { * But when writing back into source code, we need to re-escape them. */ function escapeForStringLiteral(str) { - return str - .replace(/\\/g, '\\\\') // Backslash must be doubled. - .replace(/"/g, '\\"') // Escape quotes if needed (handled by keeping original quotes). - .replace(/'/g, "\\'") // Escape single quotes if needed. + return ( + str + // Backslash must be doubled. + .replace(/\\/g, '\\\\') + // Escape quotes if needed (handled by keeping original quotes). + .replace(/"/g, '\\"') + // Escape single quotes if needed. + .replace(/'/g, "\\'") + ) } - /** * Transform Unicode property escapes in regex patterns for ICU-free environments. * @@ -193,8 +211,8 @@ export function transformUnicodePropertyEscapes(content) { traverse(ast, { RegExpLiteral(path) { const { node } = path - const { pattern, flags } = node - const { start, end } = node + const { flags, pattern } = node + const { end, start } = node // Check if this regex has /u or /v flags. const hasUFlag = flags.includes('u') @@ -276,7 +294,10 @@ export function transformUnicodePropertyEscapes(content) { const flagsArg = node.arguments[1] // Both arguments must be string literals. - if (patternArg.type !== 'StringLiteral' || flagsArg.type !== 'StringLiteral') { + if ( + patternArg.type !== 'StringLiteral' || + flagsArg.type !== 'StringLiteral' + ) { return } @@ -315,10 +336,18 @@ export function transformUnicodePropertyEscapes(content) { const escapedPattern = escapeForStringLiteral(transformedPattern) // Replace pattern. - s.overwrite(patternArg.start, patternArg.end, `${patternQuote}${escapedPattern}${patternQuote}`) + s.overwrite( + patternArg.start, + patternArg.end, + `${patternQuote}${escapedPattern}${patternQuote}`, + ) // Replace flags. - s.overwrite(flagsArg.start, flagsArg.end, `${flagsQuote}${newFlags}${flagsQuote}`) + s.overwrite( + flagsArg.start, + flagsArg.end, + `${flagsQuote}${newFlags}${flagsQuote}`, + ) } }, }) diff --git a/packages/cli-with-sentry/.config/esbuild.config.mjs b/packages/cli-with-sentry/.config/esbuild.config.mjs index 21e412b74..84f9d69b0 100644 --- a/packages/cli-with-sentry/.config/esbuild.config.mjs +++ b/packages/cli-with-sentry/.config/esbuild.config.mjs @@ -3,11 +3,12 @@ * Builds a Sentry-enabled version of the CLI with error reporting. */ -import { build } from 'esbuild' import { mkdirSync, writeFileSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' +import { build } from 'esbuild' + // Import base esbuild config from main CLI. import baseConfig from '../../cli/.config/esbuild.cli.build.mjs' diff --git a/packages/cli-with-sentry/.config/esbuild.inject.config.mjs b/packages/cli-with-sentry/.config/esbuild.inject.config.mjs index e819e3cbc..8fd0fb88a 100644 --- a/packages/cli-with-sentry/.config/esbuild.inject.config.mjs +++ b/packages/cli-with-sentry/.config/esbuild.inject.config.mjs @@ -28,7 +28,8 @@ const config = { // With platform: 'node', esbuild automatically externalizes all Node.js built-ins. external: [ - 'node-gyp', // Required for require.resolve('node-gyp/package.json') + // Required for require.resolve('node-gyp/package.json') + 'node-gyp', ], // Suppress warnings for intentional CommonJS compatibility code. diff --git a/packages/cli-with-sentry/scripts/build.mjs b/packages/cli-with-sentry/scripts/build.mjs index a674f4a84..f4168156b 100644 --- a/packages/cli-with-sentry/scripts/build.mjs +++ b/packages/cli-with-sentry/scripts/build.mjs @@ -23,15 +23,11 @@ async function main() { // Build CLI bundle. const logger = getDefaultLogger() logger.info('Building CLI bundle...') - let result = await spawn( - 'node', - ['.config/esbuild.cli-sentry.build.mjs'], - { - shell: WIN32, - stdio: 'inherit', - cwd: rootPath, - }, - ) + let result = await spawn('node', ['.config/esbuild.cli-sentry.build.mjs'], { + shell: WIN32, + stdio: 'inherit', + cwd: rootPath, + }) if (result.code !== 0) { throw new Error(`CLI bundle build failed with exit code ${result.code}`) } @@ -57,7 +53,9 @@ async function main() { cwd: rootPath, }) if (result.code !== 0) { - throw new Error(`Shadow npm inject build failed with exit code ${result.code}`) + throw new Error( + `Shadow npm inject build failed with exit code ${result.code}`, + ) } logger.success('Built shadow npm inject') diff --git a/packages/cli-with-sentry/scripts/compress-cli.mjs b/packages/cli-with-sentry/scripts/compress-cli.mjs index af9fae085..ecd28f0f5 100644 --- a/packages/cli-with-sentry/scripts/compress-cli.mjs +++ b/packages/cli-with-sentry/scripts/compress-cli.mjs @@ -36,7 +36,8 @@ const originalSize = cliCode.length // Compress with brotli (max quality for best compression). const compressed = brotliCompressSync(cliCode, { params: { - [0]: 11, // BROTLI_PARAM_QUALITY: 11 (max quality). + // BROTLI_PARAM_QUALITY: 11 (max quality). + [0]: 11, }, }) const compressedSize = compressed.length diff --git a/packages/cli-with-sentry/scripts/verify-package.mjs b/packages/cli-with-sentry/scripts/verify-package.mjs index 2ee46009f..383dcf5fa 100644 --- a/packages/cli-with-sentry/scripts/verify-package.mjs +++ b/packages/cli-with-sentry/scripts/verify-package.mjs @@ -3,9 +3,10 @@ import path from 'node:path' import process from 'node:process' import { fileURLToPath } from 'node:url' -import { getDefaultLogger } from '@socketsecurity/lib/logger' import colors from 'yoctocolors-cjs' +import { getDefaultLogger } from '@socketsecurity/lib/logger' + const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const packageRoot = path.resolve(__dirname, '..') diff --git a/packages/cli-with-sentry/test/package.test.mjs b/packages/cli-with-sentry/test/package.test.mjs index 6fcf645a9..b4ed1e010 100644 --- a/packages/cli-with-sentry/test/package.test.mjs +++ b/packages/cli-with-sentry/test/package.test.mjs @@ -2,8 +2,7 @@ * @fileoverview Tests for @socketsecurity/cli-with-sentry package structure and configuration. */ -import { existsSync } from 'node:fs' -import { promises as fs } from 'node:fs' +import { existsSync, promises as fs } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' @@ -117,18 +116,12 @@ describe('@socketsecurity/cli-with-sentry package', () => { }) it('should have esbuild config', () => { - const esbuildPath = path.join( - configDir, - 'esbuild.cli-sentry.build.mjs', - ) + const esbuildPath = path.join(configDir, 'esbuild.cli-sentry.build.mjs') expect(existsSync(esbuildPath)).toBe(true) }) it('esbuild config should import base config', async () => { - const esbuildPath = path.join( - configDir, - 'esbuild.cli-sentry.build.mjs', - ) + const esbuildPath = path.join(configDir, 'esbuild.cli-sentry.build.mjs') const content = await fs.readFile(esbuildPath, 'utf-8') expect(content).toContain( @@ -137,10 +130,7 @@ describe('@socketsecurity/cli-with-sentry package', () => { }) it('esbuild config should enable Sentry build flag', async () => { - const esbuildPath = path.join( - configDir, - 'esbuild.cli-sentry.build.mjs', - ) + const esbuildPath = path.join(configDir, 'esbuild.cli-sentry.build.mjs') const content = await fs.readFile(esbuildPath, 'utf-8') expect(content).toContain('INLINED_SOCKET_CLI_SENTRY_BUILD') @@ -148,20 +138,14 @@ describe('@socketsecurity/cli-with-sentry package', () => { }) it('esbuild config should use CLI dispatch with Sentry entry point', async () => { - const esbuildPath = path.join( - configDir, - 'esbuild.cli-sentry.build.mjs', - ) + const esbuildPath = path.join(configDir, 'esbuild.cli-sentry.build.mjs') const content = await fs.readFile(esbuildPath, 'utf-8') expect(content).toContain('cli-dispatch-with-sentry.mts') }) it('esbuild config should call build() when run', async () => { - const esbuildPath = path.join( - configDir, - 'esbuild.cli-sentry.build.mjs', - ) + const esbuildPath = path.join(configDir, 'esbuild.cli-sentry.build.mjs') const content = await fs.readFile(esbuildPath, 'utf-8') expect(content).toContain('build(config)') @@ -246,9 +230,7 @@ describe('@socketsecurity/cli-with-sentry package', () => { expect(pkgJson.publishConfig).toBeDefined() expect(pkgJson.publishConfig.access).toBe('public') - expect(pkgJson.publishConfig.registry).toBe( - 'https://registry.npmjs.org/', - ) + expect(pkgJson.publishConfig.registry).toBe('https://registry.npmjs.org/') }) }) diff --git a/packages/cli/package.json b/packages/cli/package.json index cb4cbea3d..61bb67948 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -85,7 +85,9 @@ "@socketsecurity/config": "catalog:", "@socketsecurity/lib": "catalog:", "@socketsecurity/registry": "catalog:", - "@socketsecurity/sdk": "catalog:", + + "@socketsecurity/sdk":"file:///Users/billli/code/socketdev/socket-sdk-js/socketsecurity-sdk-3.1.3.tgz", + "@types/react": "^19.2.2", "ajv-dist": "catalog:", "ansi-regex": "catalog:", "brace-expansion": "catalog:", diff --git a/packages/cli/scripts/claude.mjs b/packages/cli/scripts/claude.mjs index 323ea81b2..7ab7a9061 100644 --- a/packages/cli/scripts/claude.mjs +++ b/packages/cli/scripts/claude.mjs @@ -8,9 +8,9 @@ import { spawn } from 'node:child_process' import crypto from 'node:crypto' import { existsSync, + promises as fs, readFileSync, writeFileSync, - promises as fs, } from 'node:fs' import os from 'node:os' import path from 'node:path' diff --git a/packages/cli/scripts/compress-cli.mjs b/packages/cli/scripts/compress-cli.mjs index f9106ff04..7369e1cf2 100644 --- a/packages/cli/scripts/compress-cli.mjs +++ b/packages/cli/scripts/compress-cli.mjs @@ -37,7 +37,8 @@ const originalSize = cliCode.length // Compress with brotli (max quality for best compression). const compressed = brotliCompressSync(cliCode, { params: { - [0]: 11, // BROTLI_PARAM_QUALITY: 11 (max quality). + // BROTLI_PARAM_QUALITY: 11 (max quality). + [0]: 11, }, }) const compressedSize = compressed.length diff --git a/packages/cli/scripts/cover.mjs b/packages/cli/scripts/cover.mjs index cf3e35faf..532fc2c07 100644 --- a/packages/cli/scripts/cover.mjs +++ b/packages/cli/scripts/cover.mjs @@ -219,8 +219,10 @@ async function main() { // Combine and clean output - remove ANSI color codes and spinner artifacts const ansiRegex = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g') const output = (codeCoverageResult.stdout + codeCoverageResult.stderr) - .replace(ansiRegex, '') // Remove ANSI color codes - .replace(/(?:✧|︎|⚡)\s*/g, '') // Remove spinner artifacts + // Remove ANSI color codes + .replace(ansiRegex, '') + // Remove spinner artifacts + .replace(/(?:✧|︎|⚡)\s*/g, '') .trim() // Extract test summary (Test Files ... Duration) @@ -254,11 +256,16 @@ async function main() { if (coverageHeaderMatch && allFilesMatch) { if (!values.summary) { logger.log(' % Coverage report from v8') - logger.log(coverageHeaderMatch[1]) // Top border - logger.log(coverageHeaderMatch[2]) // Header row - logger.log(coverageHeaderMatch[1]) // Middle border - logger.log(allFilesMatch[0]) // All files row - logger.log(coverageHeaderMatch[1]) // Bottom border + // Top border + logger.log(coverageHeaderMatch[1]) + // Header row + logger.log(coverageHeaderMatch[2]) + // Middle border + logger.log(coverageHeaderMatch[1]) + // All files row + logger.log(allFilesMatch[0]) + // Bottom border + logger.log(coverageHeaderMatch[1]) logger.log('') } diff --git a/packages/cli/scripts/e2e.mjs b/packages/cli/scripts/e2e.mjs index 2ca1619cb..cb11b8fe3 100644 --- a/packages/cli/scripts/e2e.mjs +++ b/packages/cli/scripts/e2e.mjs @@ -99,7 +99,8 @@ async function runVitest(binaryType) { { env: { ...process.env, - RUN_E2E_TESTS: '1', // Automatically enable tests when explicitly running e2e.mjs. + // Automatically enable tests when explicitly running e2e.mjs. + RUN_E2E_TESTS: '1', ...envVars, }, stdio: 'inherit', diff --git a/packages/cli/scripts/integration.mjs b/packages/cli/scripts/integration.mjs index efe8c8783..2a545d9f0 100644 --- a/packages/cli/scripts/integration.mjs +++ b/packages/cli/scripts/integration.mjs @@ -103,7 +103,8 @@ async function runVitest(binaryType) { cwd: ROOT_DIR, env: { ...process.env, - RUN_INTEGRATION_TESTS: '1', // Automatically enable tests when explicitly running integration.mjs. + // Automatically enable tests when explicitly running integration.mjs. + RUN_INTEGRATION_TESTS: '1', ...envVars, }, stdio: 'inherit', diff --git a/packages/cli/scripts/utils/patches.mjs b/packages/cli/scripts/utils/patches.mjs index 4c2305194..1da7fa936 100644 --- a/packages/cli/scripts/utils/patches.mjs +++ b/packages/cli/scripts/utils/patches.mjs @@ -96,7 +96,8 @@ export async function startPatch(packageSpec) { // First, try to run pnpm patch to see if directory already exists. let result = await spawn('pnpm', ['patch', packageSpec], { shell: WIN32, - stdio: ['inherit', 'pipe', 'pipe'], // Capture stdout and stderr. + // Capture stdout and stderr. + stdio: ['inherit', 'pipe', 'pipe'], stdioString: true, }) diff --git a/packages/cli/src/cli-entry.mts b/packages/cli/src/cli-entry.mts index 92fb1f607..1accb24f2 100755 --- a/packages/cli/src/cli-entry.mts +++ b/packages/cli/src/cli-entry.mts @@ -2,6 +2,7 @@ // Set global Socket theme for consistent CLI branding. import { setTheme } from '@socketsecurity/lib/themes' + setTheme('socket') import path from 'node:path' @@ -48,6 +49,12 @@ import { failMsgWithBadge } from './utils/error/fail-msg-with-badge.mts' import { serializeResultJson } from './utils/output/result-json.mts' import { runPreflightDownloads } from './utils/preflight/downloads.mts' import { isSeaBinary } from './utils/sea/detect.mts' +import { + finalizeTelemetry, + trackCliComplete, + trackCliError, + trackCliStart, +} from './utils/telemetry/integration.mts' import { scheduleUpdateCheck } from './utils/update/manager.mts' import { dlxManifest } from '@socketsecurity/lib/dlx-manifest' @@ -104,12 +111,63 @@ async function writeBootstrapManifestEntry(): Promise { } } +/** + * Global start time for CLI execution. + * Used by global error handlers to calculate duration. + */ +let globalCliStartTime: number = Date.now() + +/** + * Global exception handler for uncaught errors. + * Tracks telemetry and exits with error code. + */ +process.on('uncaughtException', (error: Error) => { + logger.error('\n') + logger.error('Uncaught exception:') + logger.error(stackWithCauses(error)) + + // Track error with telemetry using global start time. + trackCliError(process.argv, globalCliStartTime, error, 1) + .then(() => finalizeTelemetry()) + .catch(telemetryError => { + // Silently ignore telemetry errors in exception handler. + debug(`Failed to track uncaught exception: ${telemetryError}`) + }) +}) + +/** + * Global handler for unhandled promise rejections. + * Tracks telemetry and exits with error code. + */ +process.on('unhandledRejection', (reason: unknown) => { + const error = reason instanceof Error ? reason : new Error(String(reason)) + + logger.error('\n') + logger.error('Unhandled promise rejection:') + logger.error(stackWithCauses(error)) + + // Track error with telemetry using global start time. + trackCliError(process.argv, globalCliStartTime, error, 1) + .then(() => finalizeTelemetry()) + .catch(telemetryError => { + // Silently ignore telemetry errors in rejection handler. + debug(`Failed to track unhandled rejection: ${telemetryError}`) + }) + .finally(() => { + // eslint-disable-next-line n/no-process-exit + process.exit(1) + }) +}) + void (async () => { - // Skip update checks in test environments. - if (!ENV.VITEST && !ENV.CI) { + // Skip update checks in test environments or when explicitly disabled. + // Note: Update checks create HTTP connections that may delay process exit by up to 30s + // due to keep-alive timeouts. Set SOCKET_CLI_SKIP_UPDATE_CHECK=1 to disable. + if (!ENV.VITEST && !ENV.CI && !ENV.SOCKET_CLI_SKIP_UPDATE_CHECK) { const registryUrl = lookupRegistryUrl() // Unified update notifier handles both SEA and npm automatically. - await scheduleUpdateCheck({ + // Fire-and-forget: Don't await to avoid blocking on HTTP keep-alive timeouts. + scheduleUpdateCheck({ authInfo: lookupRegistryAuthToken(registryUrl, { recursive: true }), name: isSeaBinary() ? SOCKET_CLI_BIN_NAME @@ -120,7 +178,8 @@ void (async () => { // Write manifest entry if launched via bootstrap (SEA/smol). // Bootstrap passes spec and cache dir via env vars. - await writeBootstrapManifestEntry() + // Fire-and-forget: Don't await to avoid blocking. + writeBootstrapManifestEntry() // Background preflight downloads for optional dependencies. // This silently downloads @coana-tech/cli and @socketbin/cli-ai in the @@ -128,6 +187,11 @@ void (async () => { runPreflightDownloads() } + // Track CLI start with argv for telemetry. + const cliCommandStartTime = await trackCliStart(process.argv) + // Update global start time for error handlers. + globalCliStartTime = cliCommandStartTime + try { await meowWithSubcommands( { @@ -138,11 +202,25 @@ void (async () => { }, { aliases: rootAliases }, ) + + // Track completion or error based on exit code. + const exitCode = process.exitCode ?? 0 + if (exitCode !== 0) { + // Non-zero exit code without exception. + const error = new Error(`CLI exited with code ${exitCode}`) + await trackCliError(process.argv, cliCommandStartTime, error, exitCode) + } else { + // Success. + await trackCliComplete(process.argv, cliCommandStartTime, exitCode) + } } catch (e) { process.exitCode = 1 debug('CLI uncaught error') debugDir(e) + // Track CLI error for exceptions. + await trackCliError(process.argv, cliCommandStartTime, e, process.exitCode) + let errorBody: string | undefined let errorTitle: string let errorMessage = '' @@ -192,5 +270,22 @@ void (async () => { } await captureException(e) + } finally { + // Finalize telemetry to ensure all events are sent. + // This runs on both success and error paths. + await finalizeTelemetry() + } +})().catch(async err => { + // Fatal error in main async function. + console.error('Fatal error:', err) + + // Try to finalize telemetry even on fatal errors. + try { + await finalizeTelemetry() + } catch (_telemetryErr) { + // Silently ignore telemetry errors in fatal error handler. } -})() + + // eslint-disable-next-line n/no-process-exit + process.exit(1) +}) diff --git a/packages/cli/src/commands/bundler/cmd-bundler.mts b/packages/cli/src/commands/bundler/cmd-bundler.mts index 5d72307a4..837594e1e 100644 --- a/packages/cli/src/commands/bundler/cmd-bundler.mts +++ b/packages/cli/src/commands/bundler/cmd-bundler.mts @@ -27,6 +27,10 @@ import { commonFlags } from '../../flags.mts' import { meowOrExit } from '../../utils/cli/with-subcommands.mjs' import { spawnSfwDlx } from '../../utils/dlx/spawn.mjs' import { filterFlags } from '../../utils/process/cmd.mts' +import { + trackSubprocessExit, + trackSubprocessStart, +} from '../../utils/telemetry/integration.mjs' import type { CliCommandConfig, @@ -101,6 +105,9 @@ async function run( // Set default exit code to 1 (failure). Will be overwritten on success. process.exitCode = 1 + // Track subprocess start. + const subprocessStartTime = await trackSubprocessStart(CMD_NAME) + // Forward arguments to sfw (Socket Firewall) using Socket's dlx. const { spawnPromise } = await spawnSfwDlx(['bundler', ...argsToForward], { stdio: 'inherit', @@ -111,7 +118,9 @@ async function run( const { process: childProcess } = spawnPromise as any childProcess.on( 'exit', - (code: number | null, signalName: NodeJS.Signals | null) => { + async (code: number | null, signalName: NodeJS.Signals | null) => { + await trackSubprocessExit(CMD_NAME, subprocessStartTime, code) + if (signalName) { process.kill(process.pid, signalName) } else if (typeof code === 'number') { diff --git a/packages/cli/src/commands/cargo/cmd-cargo.mts b/packages/cli/src/commands/cargo/cmd-cargo.mts index 1d063955d..91f602d7f 100644 --- a/packages/cli/src/commands/cargo/cmd-cargo.mts +++ b/packages/cli/src/commands/cargo/cmd-cargo.mts @@ -27,6 +27,10 @@ import { commonFlags } from '../../flags.mts' import { meowOrExit } from '../../utils/cli/with-subcommands.mjs' import { spawnSfwDlx } from '../../utils/dlx/spawn.mjs' import { filterFlags } from '../../utils/process/cmd.mts' +import { + trackSubprocessExit, + trackSubprocessStart, +} from '../../utils/telemetry/integration.mjs' import type { CliCommandConfig, @@ -101,6 +105,9 @@ async function run( // Set default exit code to 1 (failure). Will be overwritten on success. process.exitCode = 1 + // Track subprocess start. + const subprocessStartTime = await trackSubprocessStart(CMD_NAME) + // Forward arguments to sfw (Socket Firewall) using Socket's dlx. const { spawnPromise } = await spawnSfwDlx(['cargo', ...argsToForward], { stdio: 'inherit', @@ -111,7 +118,9 @@ async function run( const { process: childProcess } = spawnPromise as any childProcess.on( 'exit', - (code: number | null, signalName: NodeJS.Signals | null) => { + async (code: number | null, signalName: NodeJS.Signals | null) => { + await trackSubprocessExit(CMD_NAME, subprocessStartTime, code) + if (signalName) { process.kill(process.pid, signalName) } else if (typeof code === 'number') { diff --git a/packages/cli/src/commands/gem/cmd-gem.mts b/packages/cli/src/commands/gem/cmd-gem.mts index 1a7a52597..dfff7428c 100644 --- a/packages/cli/src/commands/gem/cmd-gem.mts +++ b/packages/cli/src/commands/gem/cmd-gem.mts @@ -27,6 +27,10 @@ import { commonFlags } from '../../flags.mts' import { meowOrExit } from '../../utils/cli/with-subcommands.mjs' import { spawnSfwDlx } from '../../utils/dlx/spawn.mjs' import { filterFlags } from '../../utils/process/cmd.mts' +import { + trackSubprocessExit, + trackSubprocessStart, +} from '../../utils/telemetry/integration.mjs' import type { CliCommandConfig, @@ -101,6 +105,9 @@ async function run( // Set default exit code to 1 (failure). Will be overwritten on success. process.exitCode = 1 + // Track subprocess start. + const subprocessStartTime = await trackSubprocessStart(CMD_NAME) + // Forward arguments to sfw (Socket Firewall) using Socket's dlx. const { spawnPromise } = await spawnSfwDlx(['gem', ...argsToForward], { stdio: 'inherit', @@ -111,7 +118,9 @@ async function run( const { process: childProcess } = spawnPromise as any childProcess.on( 'exit', - (code: number | null, signalName: NodeJS.Signals | null) => { + async (code: number | null, signalName: NodeJS.Signals | null) => { + await trackSubprocessExit(CMD_NAME, subprocessStartTime, code) + if (signalName) { process.kill(process.pid, signalName) } else if (typeof code === 'number') { diff --git a/packages/cli/src/commands/go/cmd-go.mts b/packages/cli/src/commands/go/cmd-go.mts index 82232ea72..df38f2f4b 100644 --- a/packages/cli/src/commands/go/cmd-go.mts +++ b/packages/cli/src/commands/go/cmd-go.mts @@ -28,6 +28,10 @@ import { commonFlags } from '../../flags.mts' import { meowOrExit } from '../../utils/cli/with-subcommands.mjs' import { spawnSfwDlx } from '../../utils/dlx/spawn.mjs' import { filterFlags } from '../../utils/process/cmd.mts' +import { + trackSubprocessExit, + trackSubprocessStart, +} from '../../utils/telemetry/integration.mjs' import type { CliCommandConfig, @@ -103,6 +107,9 @@ async function run( // Set default exit code to 1 (failure). Will be overwritten on success. process.exitCode = 1 + // Track subprocess start. + const subprocessStartTime = await trackSubprocessStart(CMD_NAME) + // Forward arguments to sfw (Socket Firewall) using Socket's dlx. const { spawnPromise } = await spawnSfwDlx(['go', ...argsToForward], { stdio: 'inherit', @@ -113,7 +120,9 @@ async function run( const { process: childProcess } = spawnPromise as any childProcess.on( 'exit', - (code: number | null, signalName: NodeJS.Signals | null) => { + async (code: number | null, signalName: NodeJS.Signals | null) => { + await trackSubprocessExit(CMD_NAME, subprocessStartTime, code) + if (signalName) { process.kill(process.pid, signalName) } else if (typeof code === 'number') { diff --git a/packages/cli/src/commands/npm/cmd-npm.mts b/packages/cli/src/commands/npm/cmd-npm.mts index 844db5e37..e718cd37a 100644 --- a/packages/cli/src/commands/npm/cmd-npm.mts +++ b/packages/cli/src/commands/npm/cmd-npm.mts @@ -12,6 +12,10 @@ import shadowNpmBin from '../../shadow/npm/bin.mts' import { meowOrExit } from '../../utils/cli/with-subcommands.mjs' import { getFlagApiRequirementsOutput } from '../../utils/output/formatting.mts' import { filterFlags } from '../../utils/process/cmd.mts' +import { + trackSubprocessExit, + trackSubprocessStart, +} from '../../utils/telemetry/integration.mts' import type { CliCommandConfig, @@ -84,6 +88,10 @@ async function run( const argsToForward = filterFlags(argv, { ...commonFlags, ...outputFlags }, [ FLAG_JSON, ]) + + // Track subprocess start. + const subprocessStartTime = await trackSubprocessStart(NPM) + const { spawnPromise } = await shadowNpmBin(argsToForward, { stdio: 'inherit', }) @@ -92,7 +100,10 @@ async function run( // See https://nodejs.org/api/child_process.html#event-exit. spawnPromise.process.on( 'exit', - (code: number | null, signalName: NodeJS.Signals | null) => { + async (code: number | null, signalName: NodeJS.Signals | null) => { + // Track subprocess exit and flush telemetry. + await trackSubprocessExit(NPM, subprocessStartTime, code) + if (signalName) { process.kill(process.pid, signalName) } else if (typeof code === 'number') { diff --git a/packages/cli/src/commands/npx/cmd-npx.mts b/packages/cli/src/commands/npx/cmd-npx.mts index 774f08644..252c2c2d2 100644 --- a/packages/cli/src/commands/npx/cmd-npx.mts +++ b/packages/cli/src/commands/npx/cmd-npx.mts @@ -10,6 +10,10 @@ import { commonFlags } from '../../flags.mts' import shadowNpxBin from '../../shadow/npx/bin.mts' import { meowOrExit } from '../../utils/cli/with-subcommands.mjs' import { getFlagApiRequirementsOutput } from '../../utils/output/formatting.mts' +import { + trackSubprocessExit, + trackSubprocessStart, +} from '../../utils/telemetry/integration.mjs' import type { CliCommandConfig, @@ -76,6 +80,9 @@ async function run( process.exitCode = 1 + // Track subprocess start. + const subprocessStartTime = await trackSubprocessStart(CMD_NAME) + const { spawnPromise } = await shadowNpxBin(argv, { stdio: 'inherit' }) // Handle exit codes and signals using event-based pattern. @@ -83,7 +90,9 @@ async function run( const { process: childProcess } = spawnPromise as any childProcess.on( 'exit', - (code: number | null, signalName: NodeJS.Signals | null) => { + async (code: number | null, signalName: NodeJS.Signals | null) => { + await trackSubprocessExit(CMD_NAME, subprocessStartTime, code) + if (signalName) { process.kill(process.pid, signalName) } else if (typeof code === 'number') { diff --git a/packages/cli/src/commands/nuget/cmd-nuget.mts b/packages/cli/src/commands/nuget/cmd-nuget.mts index eb00da825..b974a82aa 100644 --- a/packages/cli/src/commands/nuget/cmd-nuget.mts +++ b/packages/cli/src/commands/nuget/cmd-nuget.mts @@ -27,6 +27,10 @@ import { commonFlags } from '../../flags.mts' import { meowOrExit } from '../../utils/cli/with-subcommands.mjs' import { spawnSfwDlx } from '../../utils/dlx/spawn.mjs' import { filterFlags } from '../../utils/process/cmd.mts' +import { + trackSubprocessExit, + trackSubprocessStart, +} from '../../utils/telemetry/integration.mjs' import type { CliCommandConfig, @@ -101,6 +105,9 @@ async function run( // Set default exit code to 1 (failure). Will be overwritten on success. process.exitCode = 1 + // Track subprocess start. + const subprocessStartTime = await trackSubprocessStart(CMD_NAME) + // Forward arguments to sfw (Socket Firewall) using Socket's dlx. const { spawnPromise } = await spawnSfwDlx(['nuget', ...argsToForward], { stdio: 'inherit', @@ -111,7 +118,9 @@ async function run( const { process: childProcess } = spawnPromise as any childProcess.on( 'exit', - (code: number | null, signalName: NodeJS.Signals | null) => { + async (code: number | null, signalName: NodeJS.Signals | null) => { + await trackSubprocessExit(CMD_NAME, subprocessStartTime, code) + if (signalName) { process.kill(process.pid, signalName) } else if (typeof code === 'number') { diff --git a/packages/cli/src/commands/pip/cmd-pip.mts b/packages/cli/src/commands/pip/cmd-pip.mts index ce5dd56b3..e52710130 100644 --- a/packages/cli/src/commands/pip/cmd-pip.mts +++ b/packages/cli/src/commands/pip/cmd-pip.mts @@ -33,6 +33,10 @@ import { commonFlags } from '../../flags.mts' import { meowOrExit } from '../../utils/cli/with-subcommands.mjs' import { spawnSfwDlx } from '../../utils/dlx/spawn.mjs' import { filterFlags } from '../../utils/process/cmd.mts' +import { + trackSubprocessExit, + trackSubprocessStart, +} from '../../utils/telemetry/integration.mts' import type { CliCommandConfig, @@ -145,6 +149,9 @@ async function run( // Set default exit code to 1 (failure). Will be overwritten on success. process.exitCode = 1 + // Track subprocess start. + const subprocessStartTime = await trackSubprocessStart(pipBinName) + // Forward arguments to sfw (Socket Firewall) using Socket's dlx. const { spawnPromise } = await spawnSfwDlx([pipBinName, ...argsToForward], { stdio: 'inherit', @@ -155,7 +162,10 @@ async function run( const { process: childProcess } = spawnPromise as any childProcess.on( 'exit', - (code: number | null, signalName: NodeJS.Signals | null) => { + async (code: number | null, signalName: NodeJS.Signals | null) => { + // Track subprocess exit and flush telemetry. + await trackSubprocessExit(pipBinName, subprocessStartTime, code) + if (signalName) { process.kill(process.pid, signalName) } else if (typeof code === 'number') { diff --git a/packages/cli/src/commands/pnpm/cmd-pnpm.mts b/packages/cli/src/commands/pnpm/cmd-pnpm.mts index 84eff5b03..ef1c9fa6d 100644 --- a/packages/cli/src/commands/pnpm/cmd-pnpm.mts +++ b/packages/cli/src/commands/pnpm/cmd-pnpm.mts @@ -7,6 +7,10 @@ import { meowOrExit } from '../../utils/cli/with-subcommands.mjs' import { spawnSfwDlx } from '../../utils/dlx/spawn.mjs' import { getFlagApiRequirementsOutput } from '../../utils/output/formatting.mts' import { filterFlags } from '../../utils/process/cmd.mts' +import { + trackSubprocessExit, + trackSubprocessStart, +} from '../../utils/telemetry/integration.mjs' import type { CliCommandConfig, @@ -80,6 +84,9 @@ async function run( // Set default exit code to 1 (failure). Will be overwritten on success. process.exitCode = 1 + // Track subprocess start. + const subprocessStartTime = await trackSubprocessStart(CMD_NAME) + // Forward arguments to sfw (Socket Firewall) using Socket's dlx. const { spawnPromise } = await spawnSfwDlx(['pnpm', ...filteredArgv], { stdio: 'inherit', @@ -90,7 +97,10 @@ async function run( const { process: childProcess } = spawnPromise as any childProcess.on( 'exit', - (code: number | null, signalName: NodeJS.Signals | null) => { + async (code: number | null, signalName: NodeJS.Signals | null) => { + // Track subprocess exit and flush telemetry. + await trackSubprocessExit(CMD_NAME, subprocessStartTime, code) + if (signalName) { process.kill(process.pid, signalName) } else if (typeof code === 'number') { diff --git a/packages/cli/src/commands/scan/cmd-scan-create.mts b/packages/cli/src/commands/scan/cmd-scan-create.mts index d61c9a635..b1df84201 100644 --- a/packages/cli/src/commands/scan/cmd-scan-create.mts +++ b/packages/cli/src/commands/scan/cmd-scan-create.mts @@ -31,6 +31,7 @@ import { socketDashboardLink } from '../../utils/terminal/link.mts' import { checkCommandInput } from '../../utils/validation/check-input.mts' import { detectManifestActions } from '../manifest/detect-manifest-actions.mts' + import type { REPORT_LEVEL } from './types.mts' import type { MeowFlags } from '../../flags.mts' import type { diff --git a/packages/cli/src/commands/scan/handle-create-new-scan.mts b/packages/cli/src/commands/scan/handle-create-new-scan.mts index 27c537ac3..336bf47d8 100644 --- a/packages/cli/src/commands/scan/handle-create-new-scan.mts +++ b/packages/cli/src/commands/scan/handle-create-new-scan.mts @@ -1,9 +1,9 @@ import path from 'node:path' -import { debug, debugDir } from '@socketsecurity/lib/debug' -import { getDefaultLogger } from '@socketsecurity/lib/logger' import { getDefaultSpinner } from '@socketsecurity/lib/spinner' -import { pluralize } from '@socketsecurity/lib/words' +import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' +import { logger } from '@socketsecurity/registry/lib/logger' +import { pluralize } from '@socketsecurity/registry/lib/words' import { fetchCreateOrgFullScan } from './fetch-create-org-full-scan.mts' import { fetchSupportedScanFileNames } from './fetch-supported-scan-file-names.mts' @@ -11,9 +11,11 @@ import { finalizeTier1Scan } from './finalize-tier1-scan.mts' import { handleScanReport } from './handle-scan-report.mts' import { outputCreateNewScan } from './output-create-new-scan.mts' import { performReachabilityAnalysis } from './perform-reachability-analysis.mts' -import { DOT_SOCKET_DOT_FACTS_JSON } from '../../constants/paths.mts' -import { FOLD_SETTING_VERSION } from '../../constants/reporting.mjs' -import { getPackageFilesForScan } from '../../utils/fs/path-resolve.mjs' +import { + DOT_SOCKET_DOT_FACTS_JSON, + FOLD_SETTING_VERSION, +} from '../../constants.mts' +import { getPackageFilesForScan } from '../../utils/fs/path-resolve.mts' import { readOrDefaultSocketJson } from '../../utils/socket/json.mts' import { socketDocsLink } from '../../utils/terminal/link.mts' import { checkCommandInput } from '../../utils/validation/check-input.mts' @@ -23,8 +25,7 @@ import { generateAutoManifest } from '../manifest/generate_auto_manifest.mts' import type { ReachabilityOptions } from './perform-reachability-analysis.mts' import type { REPORT_LEVEL } from './types.mts' import type { OutputKind } from '../../types.mts' -import type { Remap } from '@socketsecurity/lib/objects' -const logger = getDefaultLogger() +import type { Remap } from '@socketsecurity/registry/lib/objects' export type HandleCreateNewScanConfig = { autoManifest: boolean @@ -73,8 +74,8 @@ export async function handleCreateNewScan({ targets, tmp, }: HandleCreateNewScanConfig): Promise { - debug(`Creating new scan for ${orgSlug}/${repoName}`) - debugDir({ + debugFn('notice', `Creating new scan for ${orgSlug}/${repoName}`) + debugDir('inspect', { autoManifest, branchName, commitHash, @@ -91,10 +92,10 @@ export async function handleCreateNewScan({ if (autoManifest) { logger.info('Auto-generating manifest files ...') - debug('Auto-manifest mode enabled') + debugFn('notice', 'Auto-manifest mode enabled') const sockJson = readOrDefaultSocketJson(cwd) const detected = await detectManifestActions(sockJson, cwd) - debugDir({ detected }) + debugDir('inspect', { detected }) await generateAutoManifest({ detected, cwd, @@ -105,28 +106,32 @@ export async function handleCreateNewScan({ } const spinner = getDefaultSpinner() - const supportedFilesCResult = await fetchSupportedScanFileNames({ - spinner: spinner ?? undefined, - }) + + const supportedFilesCResult = await fetchSupportedScanFileNames({ spinner }) if (!supportedFilesCResult.ok) { - debug('Failed to fetch supported scan file names') - debugDir({ supportedFilesCResult }) + debugFn('warn', 'Failed to fetch supported scan file names') + debugDir('inspect', { supportedFilesCResult }) await outputCreateNewScan(supportedFilesCResult, { interactive, outputKind, }) return } - debug(`Fetched ${supportedFilesCResult.data['size']} supported file types`) + debugFn( + 'notice', + `Fetched ${supportedFilesCResult.data['size']} supported file types`, + ) - spinner?.start('Searching for local files to include in scan...') + spinner.start('Searching for local files to include in scan...') const supportedFiles = supportedFilesCResult.data const packagePaths = await getPackageFilesForScan(targets, supportedFiles, { cwd, }) - spinner?.successAndStop('Finished searching for local files.') + spinner.successAndStop( + `Found ${packagePaths.length} ${pluralize('file', packagePaths.length)} to include in scan.`, + ) const wasValidInput = checkCommandInput(outputKind, { nook: true, @@ -136,15 +141,19 @@ export async function handleCreateNewScan({ 'TARGET (file/dir) must contain matching / supported file types for a scan', }) if (!wasValidInput) { - debug('No eligible files found to scan') + debugFn('warn', 'No eligible files found to scan') return } - debugDir({ packagePaths }) + logger.success( + `Found ${packagePaths.length} local ${pluralize('file', packagePaths.length)}`, + ) + + debugDir('inspect', { packagePaths }) if (readOnly) { logger.log('[ReadOnly] Bailing now') - debug('Read-only mode, exiting early') + debugFn('notice', 'Read-only mode, exiting early') return } @@ -155,10 +164,10 @@ export async function handleCreateNewScan({ if (reach.runReachabilityAnalysis) { logger.error('') logger.info('Starting reachability analysis...') - debug('Reachability analysis enabled') - debugDir({ reachabilityOptions: reach }) + debugFn('notice', 'Reachability analysis enabled') + debugDir('inspect', { reachabilityOptions: reach }) - spinner?.start() + spinner.start() const reachResult = await performReachabilityAnalysis({ branchName, @@ -167,11 +176,11 @@ export async function handleCreateNewScan({ packagePaths, reachabilityOptions: reach, repoName, - spinner: spinner ?? undefined, + spinner, target: targets[0]!, }) - spinner?.stop() + spinner.stop() if (!reachResult.ok) { await outputCreateNewScan(reachResult, { interactive, outputKind }) @@ -194,24 +203,18 @@ export async function handleCreateNewScan({ tier1ReachabilityScanId = reachResult.data?.tier1ReachabilityScanId } - // Display final file count after all modifications. - logger.success( - `Found ${scanPaths.length} ${pluralize('file', { count: scanPaths.length })} to include in scan`, - ) - const fullScanCResult = await fetchCreateOrgFullScan( scanPaths, orgSlug, { - branchName, commitHash, commitMessage, committers, pullRequest, repoName, + branchName, }, { - commandPath: 'socket scan create', cwd, defaultBranch, pendingHead, @@ -252,7 +255,7 @@ export async function handleCreateNewScan({ ) } } else { - spinner?.stop() + spinner.stop() await outputCreateNewScan(fullScanCResult, { interactive, outputKind }) } diff --git a/packages/cli/src/commands/scan/handle-scan-reach.mts b/packages/cli/src/commands/scan/handle-scan-reach.mts index 727c46e08..b9895519f 100644 --- a/packages/cli/src/commands/scan/handle-scan-reach.mts +++ b/packages/cli/src/commands/scan/handle-scan-reach.mts @@ -1,11 +1,11 @@ -import { getDefaultLogger } from '@socketsecurity/lib/logger' import { getDefaultSpinner } from '@socketsecurity/lib/spinner' -import { pluralize } from '@socketsecurity/lib/words' +import { logger } from '@socketsecurity/registry/lib/logger' +import { pluralize } from '@socketsecurity/registry/lib/words' import { fetchSupportedScanFileNames } from './fetch-supported-scan-file-names.mts' import { outputScanReach } from './output-scan-reach.mts' import { performReachabilityAnalysis } from './perform-reachability-analysis.mts' -import { getPackageFilesForScan } from '../../utils/fs/path-resolve.mjs' +import { getPackageFilesForScan } from '../../utils/fs/path-resolve.mts' import { checkCommandInput } from '../../utils/validation/check-input.mts' import type { ReachabilityOptions } from './perform-reachability-analysis.mts' @@ -16,7 +16,6 @@ export type HandleScanReachConfig = { interactive: boolean orgSlug: string outputKind: OutputKind - outputPath: string reachabilityOptions: ReachabilityOptions targets: string[] } @@ -26,26 +25,23 @@ export async function handleScanReach({ interactive: _interactive, orgSlug, outputKind, - outputPath, reachabilityOptions, targets, }: HandleScanReachConfig) { const spinner = getDefaultSpinner() // Get supported file names - const supportedFilesCResult = await fetchSupportedScanFileNames({ - spinner: spinner ?? undefined, - }) + const supportedFilesCResult = await fetchSupportedScanFileNames({ spinner }) if (!supportedFilesCResult.ok) { await outputScanReach(supportedFilesCResult, { cwd, outputKind, - outputPath, + outputPath: '', }) return } - spinner?.start( + spinner.start( 'Searching for local manifest files to include in reachability analysis...', ) @@ -54,8 +50,8 @@ export async function handleScanReach({ cwd, }) - spinner?.successAndStop( - `Found ${packagePaths.length} ${pluralize('manifest file', { count: packagePaths.length })} for reachability analysis.`, + spinner.successAndStop( + `Found ${packagePaths.length} ${pluralize('manifest file', packagePaths.length)} for reachability analysis.`, ) const wasValidInput = checkCommandInput(outputKind, { @@ -69,25 +65,24 @@ export async function handleScanReach({ return } - const logger = getDefaultLogger() logger.success( - `Found ${packagePaths.length} local ${pluralize('file', { count: packagePaths.length })}`, + `Found ${packagePaths.length} local ${pluralize('file', packagePaths.length)}`, ) - spinner?.start('Running reachability analysis...') + spinner.start('Running reachability analysis...') const result = await performReachabilityAnalysis({ cwd, orgSlug, - outputPath, packagePaths, reachabilityOptions, - spinner: spinner ?? undefined, + spinner, target: targets[0]!, uploadManifests: true, }) - spinner?.stop() + spinner.stop() + const outputPath = result.ok ? result.data.reachabilityReport : '' await outputScanReach(result, { cwd, outputKind, outputPath }) } diff --git a/packages/cli/src/commands/uv/cmd-uv.mts b/packages/cli/src/commands/uv/cmd-uv.mts index b5fd79821..be00af6a3 100644 --- a/packages/cli/src/commands/uv/cmd-uv.mts +++ b/packages/cli/src/commands/uv/cmd-uv.mts @@ -27,6 +27,10 @@ import { commonFlags } from '../../flags.mts' import { meowOrExit } from '../../utils/cli/with-subcommands.mjs' import { spawnSfwDlx } from '../../utils/dlx/spawn.mjs' import { filterFlags } from '../../utils/process/cmd.mts' +import { + trackSubprocessExit, + trackSubprocessStart, +} from '../../utils/telemetry/integration.mjs' import type { CliCommandConfig, @@ -101,6 +105,9 @@ async function run( // Set default exit code to 1 (failure). Will be overwritten on success. process.exitCode = 1 + // Track subprocess start. + const subprocessStartTime = await trackSubprocessStart(CMD_NAME) + // Forward arguments to sfw (Socket Firewall) using Socket's dlx. const { spawnPromise } = await spawnSfwDlx(['uv', ...argsToForward], { stdio: 'inherit', @@ -111,7 +118,10 @@ async function run( const { process: childProcess } = spawnPromise as any childProcess.on( 'exit', - (code: number | null, signalName: NodeJS.Signals | null) => { + async (code: number | null, signalName: NodeJS.Signals | null) => { + // Track subprocess exit and flush telemetry. + await trackSubprocessExit(CMD_NAME, subprocessStartTime, code) + if (signalName) { process.kill(process.pid, signalName) } else if (typeof code === 'number') { diff --git a/packages/cli/src/commands/yarn/cmd-yarn.mts b/packages/cli/src/commands/yarn/cmd-yarn.mts index c2fdacfb2..89e9d2527 100644 --- a/packages/cli/src/commands/yarn/cmd-yarn.mts +++ b/packages/cli/src/commands/yarn/cmd-yarn.mts @@ -7,6 +7,10 @@ import { meowOrExit } from '../../utils/cli/with-subcommands.mjs' import { spawnSfwDlx } from '../../utils/dlx/spawn.mjs' import { getFlagApiRequirementsOutput } from '../../utils/output/formatting.mts' import { filterFlags } from '../../utils/process/cmd.mts' +import { + trackSubprocessExit, + trackSubprocessStart, +} from '../../utils/telemetry/integration.mjs' import type { CliCommandConfig, @@ -79,6 +83,9 @@ async function run( // Set default exit code to 1 (failure). Will be overwritten on success. process.exitCode = 1 + // Track subprocess start. + const subprocessStartTime = await trackSubprocessStart(CMD_NAME) + // Forward arguments to sfw (Socket Firewall) using Socket's dlx. const { spawnPromise } = await spawnSfwDlx(['yarn', ...filteredArgv], { stdio: 'inherit', @@ -89,7 +96,10 @@ async function run( const { process: childProcess } = spawnPromise as any childProcess.on( 'exit', - (code: number | null, signalName: NodeJS.Signals | null) => { + async (code: number | null, signalName: NodeJS.Signals | null) => { + // Track subprocess exit and flush telemetry. + await trackSubprocessExit(CMD_NAME, subprocessStartTime, code) + if (signalName) { process.kill(process.pid, signalName) } else if (typeof code === 'number') { diff --git a/packages/cli/src/constants/env.mts b/packages/cli/src/constants/env.mts index eae3a5882..a317acdf2 100644 --- a/packages/cli/src/constants/env.mts +++ b/packages/cli/src/constants/env.mts @@ -62,6 +62,7 @@ import { SOCKET_CLI_ORG_SLUG } from '../env/socket-cli-org-slug.mts' import { SOCKET_CLI_PYCLI_LOCAL_PATH } from '../env/socket-cli-pycli-local-path.mts' import { SOCKET_CLI_SEA_NODE_VERSION } from '../env/socket-cli-sea-node-version.mts' import { SOCKET_CLI_SFW_LOCAL_PATH } from '../env/socket-cli-sfw-local-path.mts' +import { SOCKET_CLI_SKIP_UPDATE_CHECK } from '../env/socket-cli-skip-update-check.mts' import { SOCKET_CLI_VIEW_ALL_RISKS } from '../env/socket-cli-view-all-risks.mts' import { getSynpVersion } from '../env/synp-version.mts' import { TEMP } from '../env/temp.mts' @@ -119,6 +120,7 @@ export { SOCKET_CLI_PYCLI_LOCAL_PATH, SOCKET_CLI_SEA_NODE_VERSION, SOCKET_CLI_SFW_LOCAL_PATH, + SOCKET_CLI_SKIP_UPDATE_CHECK, SOCKET_CLI_VIEW_ALL_RISKS, TEMP, TERM, @@ -198,6 +200,7 @@ const envSnapshot = { SOCKET_CLI_PYCLI_LOCAL_PATH, SOCKET_CLI_SEA_NODE_VERSION, SOCKET_CLI_SFW_LOCAL_PATH, + SOCKET_CLI_SKIP_UPDATE_CHECK, SOCKET_CLI_VIEW_ALL_RISKS, TEMP, TERM, diff --git a/packages/cli/src/env/socket-cli-skip-update-check.mts b/packages/cli/src/env/socket-cli-skip-update-check.mts new file mode 100644 index 000000000..d00ac2694 --- /dev/null +++ b/packages/cli/src/env/socket-cli-skip-update-check.mts @@ -0,0 +1,9 @@ +/** + * SOCKET_CLI_SKIP_UPDATE_CHECK environment variable snapshot. + * When set to a truthy value, disables background update checks. + * This prevents 30-second delays caused by HTTP keep-alive connections. + */ + +import { env } from 'node:process' + +export const SOCKET_CLI_SKIP_UPDATE_CHECK = env['SOCKET_CLI_SKIP_UPDATE_CHECK'] diff --git a/packages/cli/src/meow.mts b/packages/cli/src/meow.mts index b3e682a32..c53e9c1b6 100644 --- a/packages/cli/src/meow.mts +++ b/packages/cli/src/meow.mts @@ -177,12 +177,14 @@ export default function meow< const showHelp = (exitCode = 2) => { logger.log(fullHelp) + console.log(`process.exit called at meow.mts:181 with code ${exitCode}`) // eslint-disable-next-line n/no-process-exit -- Required for CLI exit behavior. process.exit(exitCode) } const showVersion = () => { logger.log(pkg['version'] || '0.0.0') + console.log('process.exit called at meow.mts:187 with code 0') // eslint-disable-next-line n/no-process-exit -- Required for CLI exit behavior. process.exit(0) } diff --git a/packages/cli/src/npm-cli.mts b/packages/cli/src/npm-cli.mts index 7aa80341d..037bdc912 100644 --- a/packages/cli/src/npm-cli.mts +++ b/packages/cli/src/npm-cli.mts @@ -20,6 +20,9 @@ export default async function runNpmCli() { if (result.signal) { process.kill(process.pid, result.signal) } else if (typeof result.code === 'number') { + console.log( + `process.exit called at npm-cli.mts:24 with code ${result.code}`, + ) // eslint-disable-next-line n/no-process-exit process.exit(result.code) } @@ -29,6 +32,7 @@ export default async function runNpmCli() { if (import.meta.url === `file://${process.argv[1]}`) { runNpmCli().catch(error => { logger.error('Socket npm wrapper error:', error) + console.log('process.exit called at npm-cli.mts:33') // eslint-disable-next-line n/no-process-exit process.exit(1) }) diff --git a/packages/cli/src/npx-cli.mts b/packages/cli/src/npx-cli.mts index 127b9b5e6..55096651b 100644 --- a/packages/cli/src/npx-cli.mts +++ b/packages/cli/src/npx-cli.mts @@ -18,6 +18,9 @@ export default async function runNpxCli() { if (result.signal) { process.kill(process.pid, result.signal) } else if (typeof result.code === 'number') { + console.log( + `process.exit called at npx-cli.mts:22 with code ${result.code}`, + ) // eslint-disable-next-line n/no-process-exit process.exit(result.code) } @@ -27,6 +30,7 @@ export default async function runNpxCli() { if (import.meta.url === `file://${process.argv[1]}`) { runNpxCli().catch(error => { logger.error('Socket npx wrapper error:', error) + console.log('process.exit called at npx-cli.mts:31') // eslint-disable-next-line n/no-process-exit process.exit(1) }) diff --git a/packages/cli/src/pnpm-cli.mts b/packages/cli/src/pnpm-cli.mts index 640da74e9..882a9f933 100644 --- a/packages/cli/src/pnpm-cli.mts +++ b/packages/cli/src/pnpm-cli.mts @@ -20,6 +20,9 @@ export default async function runPnpmCli() { if (result.signal) { process.kill(process.pid, result.signal) } else if (typeof result.code === 'number') { + console.log( + `process.exit called at pnpm-cli.mts:24 with code ${result.code}`, + ) // eslint-disable-next-line n/no-process-exit process.exit(result.code) } @@ -29,6 +32,7 @@ export default async function runPnpmCli() { if (import.meta.url === `file://${process.argv[1]}`) { runPnpmCli().catch(error => { logger.error('Socket pnpm wrapper error:', error) + console.log('process.exit called at pnpm-cli.mts:33') // eslint-disable-next-line n/no-process-exit process.exit(1) }) diff --git a/packages/cli/src/utils/cli/with-subcommands.mts b/packages/cli/src/utils/cli/with-subcommands.mts index ee76efc3d..d17a1b785 100644 --- a/packages/cli/src/utils/cli/with-subcommands.mts +++ b/packages/cli/src/utils/cli/with-subcommands.mts @@ -868,6 +868,9 @@ export async function meowWithSubcommands( } if (!helpFlag && dryRun) { logger.log(`${DRY_RUN_LABEL}: No-op, call a sub-command; ok`) + console.log( + 'process.exit called at utils/cli/with-subcommands.mts:873 with code 0', + ) // Exit immediately to prevent tests from hanging waiting for stdin. // eslint-disable-next-line n/no-process-exit -- Required for dry-run mode. process.exit(0) @@ -988,6 +991,9 @@ export function meowOrExit( // Meow doesn't detect 'version' as an unknown flag, so we do the leg work here. if (versionFlag && !hasOwn(cliConfig.flags, 'version')) { logger.error('Unknown flag\n--version') + console.log( + 'process.exit called at utils/cli/with-subcommands.mts:994 with code 2', + ) // eslint-disable-next-line n/no-process-exit process.exit(2) // This line is never reached in production, but helps tests. diff --git a/packages/cli/src/utils/fs/glob.mts b/packages/cli/src/utils/fs/glob.mts index f3d7ab27f..760883642 100644 --- a/packages/cli/src/utils/fs/glob.mts +++ b/packages/cli/src/utils/fs/glob.mts @@ -1,4 +1,3 @@ -import os from 'node:os' import path from 'node:path' import fastGlob from 'fast-glob' @@ -6,14 +5,13 @@ import ignore from 'ignore' import micromatch from 'micromatch' import { parse as yamlParse } from 'yaml' -import { isDirSync, safeReadFile } from '@socketsecurity/lib/fs' -import { defaultIgnore } from '@socketsecurity/lib/globs' -import { readPackageJson } from '@socketsecurity/lib/packages' -import { NODE_MODULES } from '@socketsecurity/lib/paths/dirnames' -import { transform } from '@socketsecurity/lib/streams' -import { isNonEmptyString } from '@socketsecurity/lib/strings' +import { isDirSync, safeReadFile } from '@socketsecurity/registry/lib/fs' +import { defaultIgnore } from '@socketsecurity/registry/lib/globs' +import { readPackageJson } from '@socketsecurity/registry/lib/packages' +import { transform } from '@socketsecurity/registry/lib/streams' +import { isNonEmptyString } from '@socketsecurity/registry/lib/strings' -import { PNPM } from '../../constants/agents.mjs' +import { NODE_MODULES, PNPM } from '../../constants.mts' import type { Agent } from '../ecosystem/environment.mts' import type { SocketYml } from '@socketsecurity/config' @@ -46,23 +44,19 @@ async function getWorkspaceGlobs( agent: Agent, cwd = process.cwd(), ): Promise { - let workspacePatterns: string[] | undefined + let workspacePatterns: unknown if (agent === PNPM) { const workspacePath = path.join(cwd, 'pnpm-workspace.yaml') const yml = await safeReadFile(workspacePath) if (yml) { try { - const ymlStr = typeof yml === 'string' ? yml : yml.toString('utf8') - workspacePatterns = yamlParse(ymlStr)?.packages + workspacePatterns = yamlParse(yml)?.packages } catch {} } } else { - const pkgWorkspaces = (await readPackageJson(cwd, { throws: false })) - ?.workspaces - // Workspaces can be an array or an object with a packages property. - workspacePatterns = Array.isArray(pkgWorkspaces) - ? pkgWorkspaces - : pkgWorkspaces?.packages + workspacePatterns = (await readPackageJson(cwd, { throws: false }))?.[ + 'workspaces' + ] } return Array.isArray(workspacePatterns) ? workspacePatterns @@ -79,8 +73,8 @@ function ignoreFileLinesToGlobPatterns( const base = path.relative(cwd, path.dirname(filepath)).replace(/\\/g, '/') const patterns = [] for (let i = 0, { length } = lines; i < length; i += 1) { - const pattern = lines[i]?.trim() - if (pattern && pattern.length > 0 && pattern.charCodeAt(0) !== 35 /*'#'*/) { + const pattern = lines[i]!.trim() + if (pattern.length > 0 && pattern.charCodeAt(0) !== 35 /*'#'*/) { patterns.push( ignorePatternToMinimatch( pattern.length && pattern.charCodeAt(0) === 33 /*'!'*/ @@ -176,12 +170,8 @@ export function getSupportedFilePatterns( const patterns: string[] = [] for (const key of Object.keys(supportedFiles)) { const supported = supportedFiles[key] - if (supported && typeof supported === 'object') { - patterns.push( - ...(Object.values(supported) as Array<{ pattern: string }>).map( - p => `**/${p.pattern}`, - ), - ) + if (supported) { + patterns.push(...Object.values(supported).map(p => `**/${p.pattern}`)) } } return patterns @@ -221,16 +211,13 @@ export async function globWithGitIgnore( ignore: DEFAULT_IGNORE_FOR_GIT_IGNORE, }) for await (const ignorePatterns of transform( - gitIgnoreStream as AsyncIterable, - async (filepath: string) => { - const content = await safeReadFile(filepath) - const contentStr = content - ? typeof content === 'string' - ? content - : content.toString('utf8') - : '' - return ignoreFileToGlobPatterns(contentStr, filepath, cwd) - }, + gitIgnoreStream, + async (filepath: string) => + ignoreFileToGlobPatterns( + (await safeReadFile(filepath)) ?? '', + filepath, + cwd, + ), { concurrency: 8 }, )) { for (const p of ignorePatterns) { @@ -246,13 +233,14 @@ export async function globWithGitIgnore( } } - const globOptions: GlobOptions = { + const globOptions = { + __proto__: null, absolute: true, cwd, dot: true, - ignore: hasNegatedPattern ? [...defaultIgnore] : [...ignores], + ignore: hasNegatedPattern ? defaultIgnore : [...ignores], ...additionalOptions, - } + } as GlobOptions if (!hasNegatedPattern) { return await fastGlob.glob(patterns as string[], globOptions) @@ -286,7 +274,7 @@ export async function globWorkspace( ? await fastGlob.glob(workspaceGlobs, { absolute: true, cwd, - ignore: [...defaultIgnore], + ignore: defaultIgnore, }) : [] } @@ -299,31 +287,23 @@ export function isReportSupportedFile( return micromatch.some(filepath, patterns) } -/** - * Expand tilde (~) to home directory. - */ -function expandTildePath(p: string): string { - if (p === '~' || p.startsWith('~/') || p.startsWith('~\\')) { - const homeDir = os.homedir() - return p === '~' ? homeDir : path.join(homeDir, p.slice(2)) - } - return p -} - export function pathsToGlobPatterns( paths: string[] | readonly string[], + cwd?: string | undefined, ): string[] { + // TODO: Does not support `~/` paths. return paths.map(p => { - // Expand tilde paths. - const expanded = expandTildePath(p) // Convert current directory references to glob patterns. - if (expanded === '.' || expanded === './') { + if (p === '.' || p === './') { return '**/*' } + const absolutePath = path.isAbsolute(p) + ? p + : path.resolve(cwd ?? process.cwd(), p) // If the path is a directory, scan it recursively for all files. - if (isDirSync(expanded)) { - return `${expanded}/**/*` + if (isDirSync(absolutePath)) { + return `${p}/**/*` } - return expanded + return p }) } diff --git a/packages/cli/src/utils/fs/path-resolve.mts b/packages/cli/src/utils/fs/path-resolve.mts index 436d063b7..1d73b296a 100644 --- a/packages/cli/src/utils/fs/path-resolve.mts +++ b/packages/cli/src/utils/fs/path-resolve.mts @@ -127,10 +127,13 @@ export async function getPackageFilesForScan( ...options, } as PackageFilesForScanOptions - const filepaths = await globWithGitIgnore(pathsToGlobPatterns(inputPaths), { - cwd, - socketConfig, - }) + const filepaths = await globWithGitIgnore( + pathsToGlobPatterns(inputPaths, options?.cwd), + { + cwd, + socketConfig, + }, + ) return filterBySupportedScanFiles(filepaths!, supportedFiles) } diff --git a/packages/cli/src/utils/memoization.mts b/packages/cli/src/utils/memoization.mts index fae1964ca..402b457f6 100644 --- a/packages/cli/src/utils/memoization.mts +++ b/packages/cli/src/utils/memoization.mts @@ -123,8 +123,11 @@ export function memoize( * * @example * const fetchUser = memoizeAsync(async (id: string) => { - * const response = await fetch(`/api/users/${id}`) - * return response.json() + * const https = await import('node:https') + * const response = await new Promise((resolve, reject) => { + * https.request(`https://api.example.com/users/${id}`, {agent: false}, resolve).on('error', reject).end() + * }) + * return response * }, { ttl: 300000, name: 'fetchUser' }) * * await fetchUser('123') // Fetches from API diff --git a/packages/cli/src/utils/process/runner.mts b/packages/cli/src/utils/process/runner.mts index 8efb8a554..cc69334e0 100644 --- a/packages/cli/src/utils/process/runner.mts +++ b/packages/cli/src/utils/process/runner.mts @@ -7,6 +7,11 @@ import { Spinner as createSpinner } from '@socketsecurity/lib/spinner' import { ensureIpcInStdio } from '../../shadow/stdio-ipc.mjs' import { debugNs } from '../debug.mts' import { formatExternalCliError } from '../error/display.mts' +import { + trackSubprocessComplete, + trackSubprocessError, + trackSubprocessStart, +} from '../telemetry/integration.mts' import type { IpcObject } from '../../constants/shadow.mts' import type { CResult } from '../../types.mjs' @@ -57,6 +62,9 @@ export async function runExternalCommand( let spinner: Spinner | undefined + // Track subprocess start for telemetry. + const subprocessStartTime = await trackSubprocessStart(command) + try { // Start spinner if requested. if (showSpinner) { @@ -104,6 +112,30 @@ export async function runExternalCommand( debugNs('stdio', `Command completed with exit code: ${exitCode}`) + // Track subprocess completion or error based on exit code. + if (exitCode !== 0) { + // Non-zero exit code is an error. + const error = new Error( + `Command failed with exit code ${exitCode}: ${command} ${args.join(' ')}`, + ) + await trackSubprocessError( + command, + subprocessStartTime, + error, + exitCode, + { + stderr_length: stderr.length, + stdout_length: stdout.length, + }, + ) + } else { + // Zero exit code is success. + await trackSubprocessComplete(command, subprocessStartTime, exitCode, { + stderr_length: stderr.length, + stdout_length: stdout.length, + }) + } + // If buffered and has output, log it. if (bufferOutput && stdout) { const logger = getDefaultLogger() @@ -132,6 +164,9 @@ export async function runExternalCommand( ? Number((e as { code: unknown }).code) : 1 + // Track subprocess error for telemetry. + await trackSubprocessError(command, subprocessStartTime, e, exitCode) + const errorMessage = formatExternalCliError(command, e, { verbose: false, }) diff --git a/packages/cli/src/utils/socket/sdk.mts b/packages/cli/src/utils/socket/sdk.mts index 739defc6d..8ce211150 100644 --- a/packages/cli/src/utils/socket/sdk.mts +++ b/packages/cli/src/utils/socket/sdk.mts @@ -43,9 +43,15 @@ import { import ENV from '../../constants/env.mts' import { TOKEN_PREFIX_LENGTH } from '../../constants/socket.mts' import { getConfigValueOrUndef } from '../config.mts' +import { trackCliEvent } from '../telemetry/integration.mts' import type { CResult } from '../../types.mts' -import type { FileValidationResult } from '@socketsecurity/sdk' +import type { + FileValidationResult, + RequestInfo, + ResponseInfo, +} from '@socketsecurity/sdk' + const logger = getDefaultLogger() const TOKEN_VISIBLE_LENGTH = 5 @@ -182,6 +188,39 @@ export async function setupSdk( version: ENV.INLINED_SOCKET_CLI_VERSION || '0.0.0', homepage: ENV.INLINED_SOCKET_CLI_HOMEPAGE || 'https://socket.dev/cli', }), + hooks: { + onRequest: (info: RequestInfo) => { + // Track API request event. + void trackCliEvent('api_request', process.argv, { + method: info.method, + timeout: info.timeout, + url: info.url, + }) + }, + onResponse: (info: ResponseInfo) => { + // Track API response event. + const metadata = { + duration: info.duration, + method: info.method, + status: info.status, + statusText: info.statusText, + url: info.url, + headers: info.headers, + } + + if (info.error) { + // Track as error event if request failed. + void trackCliEvent('api_error', process.argv, { + ...metadata, + error_message: info.error.message, + error_type: info.error.constructor.name, + }) + } else { + // Track as successful response. + void trackCliEvent('api_response', process.argv, metadata) + } + }, + }, }), } } diff --git a/packages/cli/src/utils/telemetry/integration.mts b/packages/cli/src/utils/telemetry/integration.mts new file mode 100644 index 000000000..4738112c7 --- /dev/null +++ b/packages/cli/src/utils/telemetry/integration.mts @@ -0,0 +1,451 @@ +/** + * Telemetry integration helpers for Socket CLI. + * Provides utilities for tracking common CLI events and subprocess executions. + * + * Usage: + * ```typescript + * import { + * trackCliStart, + * trackCliEvent, + * trackCliComplete, + * trackCliError, + * trackSubprocessStart, + * trackSubprocessComplete, + * trackSubprocessError + * } from './utils/telemetry/integration.mts' + * + * // Track main CLI execution. + * const startTime = await trackCliStart(process.argv) + * await trackCliComplete(process.argv, startTime, 0) + * + * // Track custom event with optional metadata. + * await trackCliEvent('custom_event', process.argv, { key: 'value' }) + * + * // Track subprocess/forked CLI execution. + * const subStart = await trackSubprocessStart('npm', { cwd: '/path' }) + * await trackSubprocessComplete('npm', subStart, 0, { stdout_length: 1234 }) + * + * // On subprocess error. + * await trackSubprocessError('npm', subStart, error, 1) + * ``` + */ +import { homedir } from 'node:os' +import process from 'node:process' + +import { debug } from '@socketsecurity/lib/debug' + +import { TelemetryService } from './service.mts' +import { CONFIG_KEY_DEFAULT_ORG } from '../../constants/config.mjs' +import { getCliVersion } from '../../constants/env.mts' +import { getConfigValueOrUndef } from '../config.mts' + +import type { TelemetryContext } from './types.mts' + +/** + * Flush any pending telemetry events. + * This should be called before process.exit to ensure telemetry is sent. + * + * @returns Promise that resolves when flush completes. + */ +export async function finalizeTelemetry(): Promise { + const instance = TelemetryService.getCurrentInstance() + if (instance) { + debug('Flushing telemetry before exit') + await instance.flush() + } +} + +/** + * Track subprocess exit and finalize telemetry. + * This is a convenience function that tracks completion/error based on exit code + * and ensures telemetry is flushed before returning. + * + * @param command - Command name (e.g., 'npm', 'pip'). + * @param startTime - Start timestamp from trackSubprocessStart. + * @param exitCode - Process exit code (null treated as error). + * @returns Promise that resolves when tracking and flush complete. + * + * @example + * ```typescript + * await trackSubprocessExit(NPM, subprocessStartTime, code) + * ``` + */ +export async function trackSubprocessExit( + command: string, + startTime: number, + exitCode: number | null, +): Promise { + // Track subprocess completion or error based on exit code. + if (exitCode !== null && exitCode !== 0) { + const error = new Error(`${command} exited with code ${exitCode}`) + await trackSubprocessError(command, startTime, error, exitCode) + } else if (exitCode === 0) { + await trackSubprocessComplete(command, startTime, exitCode) + } + + // Flush telemetry to ensure events are sent before exit. + await finalizeTelemetry() +} + +const WRAPPER_CLI = ['npm', 'yarn', 'pip'] + +const API_TOKEN_FLAGS = ['--api-token', '--token', '-t'] + +/** + * Calculate duration from start timestamp. + * + * @param startTime - Start timestamp from Date.now(). + * @returns Duration in milliseconds. + */ +function calculateDuration(startTime: number): number { + return Date.now() - startTime +} + +/** + * Normalize exit code to a number with default fallback. + * + * @param exitCode - Exit code (may be string, number, null, or undefined). + * @param defaultValue - Default value if exitCode is not a number. + * @returns Normalized exit code. + */ +function normalizeExitCode( + exitCode: string | number | null | undefined, + defaultValue: number, +): number { + return typeof exitCode === 'number' ? exitCode : defaultValue +} + +/** + * Normalize error to Error object. + * + * @param error - Unknown error value. + * @returns Error object. + */ +function normalizeError(error: unknown): Error { + return error instanceof Error ? error : new Error(String(error)) +} + +/** + * Build context for the current telemetry entry. + * + * The context contains the current execution context, in which all CLI invocation should have access to. + * + * @param argv Command line arguments. + * @returns Telemetry context object. + */ +function buildContext(argv: string[]): TelemetryContext { + return { + arch: process.arch, + argv: sanitizeArgv(argv), + node_version: process.version, + platform: process.platform, + version: getCliVersion(), + } +} + +/** + * Sanitize argv to remove sensitive information. + * Removes API tokens, file paths with usernames, and other PII. + * Also strips arguments after wrapper CLIs to avoid leaking package names. + * + * @param argv Raw command line arguments (full process.argv including execPath and script). + * @returns Sanitized argv array. + * + * @example + * // Input: ['node', 'socket', 'npm', 'install', '@my/private-package', '--token', 'sktsec_abc123'] + * // Output: ['npm', 'install'] + */ +function sanitizeArgv(argv: string[]): string[] { + // Strip the first two values to drop the execPath and script. + const withoutPathAndScript = argv.slice(2) + + // Then strip arguments after wrapper CLIs to avoid leaking package names. + const wrapperIndex = withoutPathAndScript.findIndex(arg => + WRAPPER_CLI.includes(arg), + ) + let strippedArgv = withoutPathAndScript + + if (wrapperIndex !== -1) { + // Keep only wrapper + first command (e.g., ['npm']). + const endIndex = wrapperIndex + 1 + strippedArgv = withoutPathAndScript.slice(0, endIndex) + } + + // Then sanitize remaining arguments. + return strippedArgv.map((arg, index) => { + // Check if previous arg was an API token flag. + if (index > 0) { + const prevArg = strippedArgv[index - 1] + if (prevArg && API_TOKEN_FLAGS.includes(prevArg)) { + return '[REDACTED]' + } + } + + // Redact anything that looks like a socket API token. + if (arg.startsWith('sktsec_') || arg.match(/^[a-f0-9]{32,}$/i)) { + return '[REDACTED]' + } + + // Remove user home directory from file paths. + const homeDir = homedir() + if (homeDir) { + return arg.replace(new RegExp(homeDir, 'g'), '~') + } + + return arg + }) +} + +/** + * Sanitize error attribute to remove user specific paths. + * Replaces user home directory and other sensitive paths. + * + * @param input Raw input. + * @returns Sanitized input. + */ +function sanitizeErrorAttribute(input: string | undefined): string | undefined { + if (!input) { + return undefined + } + + // Remove user home directory. + const homeDir = homedir() + if (homeDir) { + return input.replace(new RegExp(homeDir, 'g'), '~') + } + + return input +} + +/** + * Generic event tracking function. + * Tracks any telemetry event with optional error details and flush. + * + * @param eventType Type of event to track. + * @param context Event context. + * @param metadata Event metadata. + * @param options Optional configuration. + * @returns Promise that resolves when tracking completes. + */ +export async function trackEvent( + eventType: string, + context: TelemetryContext, + metadata: Record = {}, + options: { + error?: Error | undefined + flush?: boolean | undefined + } = {}, +): Promise { + try { + const orgSlug = getConfigValueOrUndef(CONFIG_KEY_DEFAULT_ORG) + + if (orgSlug) { + const telemetry = await TelemetryService.getTelemetryClient(orgSlug) + debug(`Got telemetry service for org: ${orgSlug}`) + + const event = { + context, + event_sender_created_at: new Date().toISOString(), + event_type: eventType, + ...(Object.keys(metadata).length > 0 && { metadata }), + ...(options.error && { + error: { + message: sanitizeErrorAttribute(options.error.message), + stack: sanitizeErrorAttribute(options.error.stack), + type: options.error.constructor.name, + }, + }), + } + + telemetry.track(event) + + // Flush events if requested. + if (options.flush) { + await telemetry.flush() + } + } + } catch (err) { + // Telemetry errors should never block CLI execution. + debug(`Failed to track event ${eventType}: ${err}`) + } +} + +/** + * Track CLI initialization event. + * Should be called at the start of CLI execution. + * + * @param argv Command line arguments (process.argv). + * @returns Start timestamp for duration calculation. + */ +export async function trackCliStart(argv: string[]): Promise { + debug('Capture start of command') + + const startTime = Date.now() + + await trackEvent('cli_start', buildContext(argv)) + + return startTime +} + +/** + * Track a generic CLI event with optional metadata. + * Use this for tracking custom events during CLI execution. + * + * @param eventType Type of event to track. + * @param argv Command line arguments (process.argv). + * @param metadata Optional additional metadata to include with the event. + */ +export async function trackCliEvent( + eventType: string, + argv: string[], + metadata?: Record | undefined, +): Promise { + debug(`Tracking CLI event: ${eventType}`) + + await trackEvent(eventType, buildContext(argv), metadata) +} + +/** + * Track CLI completion event. + * Should be called on successful CLI exit. + * + * @param argv + * @param startTime Start timestamp from trackCliStart. + * @param exitCode Process exit code (default: 0). + */ +export async function trackCliComplete( + argv: string[], + startTime: number, + exitCode?: string | number | undefined | null, +): Promise { + debug('Capture end of command') + + await trackEvent( + 'cli_complete', + buildContext(argv), + { + duration: calculateDuration(startTime), + exit_code: normalizeExitCode(exitCode, 0), + }, + { + flush: true, + }, + ) +} + +/** + * Track CLI error event. + * Should be called when CLI exits with an error. + * + * @param argv + * @param startTime Start timestamp from trackCliStart. + * @param error Error that occurred. + * @param exitCode Process exit code (default: 1). + */ +export async function trackCliError( + argv: string[], + startTime: number, + error: unknown, + exitCode?: number | string | undefined | null, +): Promise { + debug('Capture error and stack trace of command') + + await trackEvent( + 'cli_error', + buildContext(argv), + { + duration: calculateDuration(startTime), + exit_code: normalizeExitCode(exitCode, 1), + }, + { + error: normalizeError(error), + flush: true, + }, + ) +} + +/** + * Track subprocess/command start event. + * + * Use this when spawning external commands like npm, npx, coana, cdxgen, etc. + * + * @param command Command being executed (e.g., 'npm', 'npx', 'coana'). + * @param metadata Optional additional metadata (e.g., cwd, purpose). + * @returns Start timestamp for duration calculation. + */ +export async function trackSubprocessStart( + command: string, + metadata?: Record | undefined, +): Promise { + debug(`Tracking subprocess start: ${command}`) + + const startTime = Date.now() + + await trackEvent('subprocess_start', buildContext(process.argv), { + command, + ...metadata, + }) + + return startTime +} + +/** + * Track subprocess/command completion event. + * + * Should be called when spawned command completes successfully. + * + * @param command Command that was executed. + * @param startTime Start timestamp from trackSubprocessStart. + * @param exitCode Process exit code. + * @param metadata Optional additional metadata (e.g., stdout length, stderr length). + */ +export async function trackSubprocessComplete( + command: string, + startTime: number, + exitCode: number | null, + metadata?: Record | undefined, +): Promise { + debug(`Tracking subprocess complete: ${command}`) + + await trackEvent('subprocess_complete', buildContext(process.argv), { + command, + duration: calculateDuration(startTime), + exit_code: normalizeExitCode(exitCode, 0), + ...metadata, + }) +} + +/** + * Track subprocess/command error event. + * + * Should be called when spawned command fails or throws error. + * + * @param command Command that was executed. + * @param startTime Start timestamp from trackSubprocessStart. + * @param error Error that occurred. + * @param exitCode Process exit code. + * @param metadata Optional additional metadata. + */ +export async function trackSubprocessError( + command: string, + startTime: number, + error: unknown, + exitCode?: number | null | undefined, + metadata?: Record | undefined, +): Promise { + debug(`Tracking subprocess error: ${command}`) + + await trackEvent( + 'subprocess_error', + buildContext(process.argv), + { + command, + duration: calculateDuration(startTime), + exit_code: normalizeExitCode(exitCode, 1), + ...metadata, + }, + { + error: normalizeError(error), + }, + ) +} diff --git a/packages/cli/src/utils/telemetry/service.mts b/packages/cli/src/utils/telemetry/service.mts new file mode 100644 index 000000000..505f7fd13 --- /dev/null +++ b/packages/cli/src/utils/telemetry/service.mts @@ -0,0 +1,390 @@ +/** + * Telemetry service for Socket CLI. + * Manages event collection, batching, and submission to Socket API. + * + * IMPORTANT: Telemetry is ALWAYS scoped to an organization. + * Cannot track telemetry without an org context. + * + * Features: + * - Singleton pattern (one instance per process) + * - Organization-scoped tracking (required) + * - Event batching (configurable batch size) + * - Periodic flush (configurable interval) + * - Automatic session ID assignment + * - Explicit finalization via destroy() for controlled cleanup + * - Graceful degradation (errors don't block CLI) + * + * @example + * ```typescript + * // Get telemetry client (returns singleton instance) + * const telemetry = await TelemetryService.getTelemetryClient('my-org') + * + * // Track an event (session_id is auto-set) + * telemetry.track({ + * event_sender_created_at: new Date().toISOString(), + * event_type: 'cli_start', + * context: { + * version: '2.2.15', + * platform: process.platform, + * node_version: process.version, + * arch: process.arch, + * argv: process.argv.slice(2) + * } + * }) + * + * // Flush is automatic on batch size, but can be called manually + * await telemetry.flush() + * + * // Always call destroy() before exit to flush remaining events + * await telemetry.destroy() + * ``` + */ + +import { randomUUID } from 'node:crypto' + +import { debug, debugDir } from '@socketsecurity/lib/debug' + +import { setupSdk } from '../socket/sdk.mts' + +import type { TelemetryEvent } from './types.mts' +import type { + PostOrgTelemetryPayload, + TelemetryConfig, +} from '@socketsecurity/sdk' + +/** + * Process-wide session ID. + * Generated once per CLI invocation and shared across all telemetry instances. + */ +const SESSION_ID = randomUUID() + +/** + * Default telemetry configuration. + * Used as fallback if API config fetch fails. + */ +const DEFAULT_TELEMETRY_CONFIG = { + telemetry: { + enabled: false, + }, +} as TelemetryConfig + +/** + * Static configuration for telemetry service behavior. + */ +const TELEMETRY_SERVICE_CONFIG = { + batch_size: 10, + flush_interval: 1_000, // 1 second. + flush_timeout: 5_000, // 5 seconds maximum for flush operations. +} as const + +/** + * Singleton instance holder. + */ +interface TelemetryServiceInstance { + current: TelemetryService | null +} + +/** + * Singleton telemetry service instance holder. + * Only one instance exists per process. + */ +const telemetryServiceInstance: TelemetryServiceInstance = { + current: null, +} + +/** + * Wrap a promise with a timeout. + * Rejects if promise doesn't resolve within timeout. + * + * @param promise Promise to wrap. + * @param timeoutMs Timeout in milliseconds. + * @param errorMessage Error message if timeout occurs. + * @returns Promise that resolves or times out. + */ +function withTimeout( + promise: Promise, + timeoutMs: number, + errorMessage: string, +): Promise { + return Promise.race([ + promise, + new Promise((_, reject) => { + setTimeout(() => { + reject(new Error(errorMessage)) + }, timeoutMs) + }), + ]) +} + +/** + * Centralized telemetry service for Socket CLI. + * Telemetry is always scoped to an organization. + * Singleton pattern ensures only one instance exists per process. + */ +export class TelemetryService { + private readonly orgSlug: string + private config: TelemetryConfig | null = null + private eventQueue: TelemetryEvent[] = [] + private flushTimer: NodeJS.Timeout | null = null + private isDestroyed = false + + /** + * Private constructor. + * Requires organization slug. + * + * @param orgSlug - Organization identifier. + */ + private constructor(orgSlug: string) { + this.orgSlug = orgSlug + debug( + `Telemetry service created for org '${orgSlug}' with session ID: ${SESSION_ID}`, + ) + } + + /** + * Get the current telemetry instance if one exists. + * Does not create a new instance. + * + * @returns Current telemetry instance or null if none exists. + */ + static getCurrentInstance(): TelemetryService | null { + return telemetryServiceInstance.current + } + + /** + * Get telemetry client for an organization. + * Creates and initializes client if it doesn't exist. + * Returns existing instance if already initialized. + * + * @param orgSlug - Organization identifier (required). + * @returns Initialized telemetry service instance. + */ + static async getTelemetryClient(orgSlug: string): Promise { + // Return existing instance if already initialized. + if (telemetryServiceInstance.current) { + debug( + `Telemetry already initialized for org: ${telemetryServiceInstance.current.orgSlug}`, + ) + return telemetryServiceInstance.current + } + + const instance = new TelemetryService(orgSlug) + + try { + const sdkResult = await setupSdk() + if (!sdkResult.ok) { + debug('Failed to setup SDK for telemetry, using default config') + instance.config = DEFAULT_TELEMETRY_CONFIG + telemetryServiceInstance.current = instance + return instance + } + + const sdk = sdkResult.data + const configResult = await sdk.getTelemetryConfig(orgSlug) + + if (configResult.success) { + instance.config = configResult.data + debug( + `Telemetry configuration fetched successfully: enabled=${instance.config.telemetry.enabled}`, + ) + debugDir({ config: instance.config }) + + // Start periodic flush if enabled. + if (instance.config.telemetry.enabled) { + instance.startPeriodicFlush() + } + } else { + debug(`Failed to fetch telemetry config: ${configResult.error}`) + instance.config = DEFAULT_TELEMETRY_CONFIG + } + } catch (e) { + debug(`Error initializing telemetry: ${e}`) + instance.config = DEFAULT_TELEMETRY_CONFIG + } + + // Only set singleton instance after full initialization. + telemetryServiceInstance.current = instance + return instance + } + + /** + * Track a telemetry event. + * Adds event to queue for batching and eventual submission. + * + * @param event - Telemetry event to track (session_id is optional and will be auto-set). + */ + track(event: Omit): void { + debug('Incoming track event request') + + if (this.isDestroyed) { + debug('Telemetry service destroyed, ignoring event') + return + } + + if (!this.config?.telemetry.enabled) { + debug(`Telemetry disabled, skipping event: ${event.event_type}`) + return + } + + // Create complete event with session_id and org_slug. + const completeEvent: TelemetryEvent = { + ...event, + session_id: SESSION_ID, + } + + debug(`Tracking telemetry event: ${completeEvent.event_type}`) + debugDir(completeEvent) + + this.eventQueue.push(completeEvent) + + // Auto-flush if batch size reached. + const batchSize = TELEMETRY_SERVICE_CONFIG.batch_size + if (this.eventQueue.length >= batchSize) { + debug(`Batch size reached (${batchSize}), flushing events`) + void this.flush() + } + } + + /** + * Flush all queued events to the API. + * Returns immediately if no events queued or telemetry disabled. + * Times out after configured flush_timeout to prevent blocking CLI exit. + */ + async flush(): Promise { + if (this.isDestroyed) { + debug('Telemetry service destroyed, cannot flush') + return + } + + if (this.eventQueue.length === 0) { + debug('No events to flush') + return + } + + if (!this.config?.telemetry.enabled) { + debug('Telemetry disabled, clearing queue without sending') + this.eventQueue = [] + return + } + + const eventsToSend = [...this.eventQueue] + this.eventQueue = [] + + debug(`Flushing ${eventsToSend.length} telemetry events`) + + try { + await withTimeout( + this.sendEvents(eventsToSend), + TELEMETRY_SERVICE_CONFIG.flush_timeout, + `Telemetry flush timed out after ${TELEMETRY_SERVICE_CONFIG.flush_timeout}ms`, + ) + + debug( + `Telemetry events sent successfully (${eventsToSend.length} events)`, + ) + } catch (e) { + debug(`Error flushing telemetry: ${e}`) + // Events are discarded on error to prevent infinite growth. + } + } + + /** + * Send events to the API. + * Extracted as separate method for timeout wrapping. + * + * @param events Events to send. + */ + private async sendEvents(events: TelemetryEvent[]): Promise { + const sdkResult = await setupSdk() + if (!sdkResult.ok) { + debug('Failed to setup SDK for flush, events discarded') + return + } + + const sdk = sdkResult.data + + // Send events individually (no batch endpoint available). + for (const event of events) { + const result = await sdk.postOrgTelemetry( + this.orgSlug, + event as unknown as PostOrgTelemetryPayload, + ) + + if (result.success) { + debug('Telemetry sent to telemetry:') + debugDir(event) + } else { + debug(`Failed to send telemetry event: ${result.error}`) + } + } + } + + /** + * Destroy the telemetry service for this organization. + * Flushes remaining events and clears all state. + * Idempotent - safe to call multiple times. + */ + async destroy(): Promise { + if (this.isDestroyed) { + debug('Telemetry service already destroyed, skipping') + return + } + + debug(`Destroying telemetry service for org: ${this.orgSlug}`) + + // Mark as destroyed immediately to prevent concurrent destroy() calls. + this.isDestroyed = true + + // Stop periodic flush. + if (this.flushTimer) { + clearInterval(this.flushTimer) + this.flushTimer = null + } + + // Flush remaining events with timeout. + const eventsToFlush = [...this.eventQueue] + this.eventQueue = [] + + if (eventsToFlush.length > 0 && this.config?.telemetry.enabled) { + debug(`Flushing ${eventsToFlush.length} events before destroy`) + try { + await withTimeout( + this.sendEvents(eventsToFlush), + TELEMETRY_SERVICE_CONFIG.flush_timeout, + `Telemetry flush during destroy timed out after ${TELEMETRY_SERVICE_CONFIG.flush_timeout}ms`, + ) + debug('Events flushed successfully during destroy') + } catch (e) { + debug(`Error flushing telemetry during destroy: ${e}`) + } + } + + this.config = null + + // Clear singleton instance. + telemetryServiceInstance.current = null + + debug(`Telemetry service destroyed for org: ${this.orgSlug}`) + } + + /** + * Start periodic flush timer. + */ + private startPeriodicFlush(): void { + if (this.flushTimer) { + return + } + + const flushInterval = TELEMETRY_SERVICE_CONFIG.flush_interval + + this.flushTimer = setInterval(() => { + debug('Periodic flush triggered') + void this.flush() + }, flushInterval) + + // Don't keep process alive for telemetry. + this.flushTimer.unref() + + debug(`Periodic flush started with interval: ${flushInterval}ms`) + } +} diff --git a/packages/cli/src/utils/telemetry/types.mts b/packages/cli/src/utils/telemetry/types.mts new file mode 100644 index 000000000..60b7786ec --- /dev/null +++ b/packages/cli/src/utils/telemetry/types.mts @@ -0,0 +1,42 @@ +/** + * Telemetry types for Socket CLI. + * Defines the structure of telemetry events and related data. + */ + +/** + * Error details for telemetry events. + */ +export interface TelemetryEventError { + /** Error class/type name. */ + type: string + /** Error message. */ + message: string | undefined + /** Stack trace (sanitized). */ + stack?: string | undefined +} + +/** + * Telemetry Context. + * + * This represent how the cli was invoked and met. + */ +export interface TelemetryContext { + version: string + platform: string + node_version: string + arch: string + argv: string[] +} + +/** + * Telemetry event structure. + * All telemetry events must follow this schema. + */ +export interface TelemetryEvent { + event_sender_created_at: string + event_type: string + context: TelemetryContext + session_id?: string + metadata?: Record + error?: TelemetryEventError | undefined +} diff --git a/packages/cli/src/utils/update/checker.mts b/packages/cli/src/utils/update/checker.mts index 16fb55163..0dd07dbe0 100644 --- a/packages/cli/src/utils/update/checker.mts +++ b/packages/cli/src/utils/update/checker.mts @@ -19,6 +19,9 @@ * - Version compatibility checks */ +import https from 'node:https' +import { URL } from 'node:url' + import semver from 'semver' import { NPM_REGISTRY_URL } from '@socketsecurity/lib/constants/agents' @@ -90,7 +93,9 @@ function isUpdateAvailable(current: string, latest: string): boolean { */ const NetworkUtils = { /** - * Fetch package information from npm registry. + * Fetch package information from npm registry using https.request(). + * Uses Node.js built-in https module to avoid keep-alive connection pooling + * that causes 30-second delays in process exit. */ async fetch( url: string, @@ -103,76 +108,86 @@ const NetworkUtils = { const { authInfo } = { __proto__: null, ...options } as FetchOptions - const headers = new Headers({ + const parsedUrl = new URL(url) + const headers: Record = { Accept: 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*', 'User-Agent': 'socket-cli-updater/1.0', - }) + } if ( authInfo && isNonEmptyString(authInfo.token) && isNonEmptyString(authInfo.type) ) { - headers.set('Authorization', `${authInfo.type} ${authInfo.token}`) + headers['Authorization'] = `${authInfo.type} ${authInfo.token}` } - const aborter = new AbortController() - const signal = aborter.signal - - // Set up timeout. - const timeout = setTimeout(() => { - aborter.abort() - }, timeoutMs) - - // Also listen for process exit. - const exitHandler = () => aborter.abort() - onExit(exitHandler) - - try { - const request = await fetch(url, { - headers, - signal, - // Additional fetch options for reliability. - redirect: 'follow', - keepalive: false, + return new Promise((resolve, reject) => { + const req = https.request( + { + agent: false, // Disable connection pooling. + headers, + hostname: parsedUrl.hostname, + method: 'GET', + path: parsedUrl.pathname + parsedUrl.search, + port: parsedUrl.port, + timeout: timeoutMs, + }, + res => { + let data = '' + + res.on('data', chunk => { + data += chunk + }) + + res.on('end', () => { + try { + if (res.statusCode !== 200) { + reject( + new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`), + ) + return + } + + const json = JSON.parse(data) as unknown + + if (!json || typeof json !== 'object') { + reject(new Error('Invalid JSON response from registry')) + return + } + + resolve(json as { version?: string }) + } catch (parseError) { + const contentType = res.headers['content-type'] + if (!contentType || !contentType.includes('application/json')) { + debug(`Unexpected content type: ${contentType}`) + } + reject( + new Error( + `Failed to parse JSON response: ${parseError instanceof Error ? parseError.message : String(parseError)}`, + ), + ) + } + }) + }, + ) + + req.on('timeout', () => { + req.destroy() + reject(new Error(`Request timed out after ${timeoutMs}ms`)) }) - if (!request.ok) { - throw new Error(`HTTP ${request.status}: ${request.statusText}`) - } - - const contentType = request.headers.get('content-type') - let json: unknown - - try { - json = await request.json() - } catch (parseError) { - // Only warn about content type if JSON parsing actually fails. - if (!contentType || !contentType.includes('application/json')) { - debug(`Unexpected content type: ${contentType}`) - } - throw new Error( - `Failed to parse JSON response: ${parseError instanceof Error ? parseError.message : String(parseError)}`, - ) - } + req.on('error', error => { + reject(new Error(`Network request failed: ${error.message}`)) + }) - if (!json || typeof json !== 'object') { - throw new Error('Invalid JSON response from registry') - } + // Also listen for process exit. + const exitHandler = () => req.destroy() + onExit(exitHandler) - return json as { version?: string } - } catch (error) { - if (error instanceof Error) { - if (error.name === 'AbortError') { - throw new Error(`Request timed out after ${timeoutMs}ms`) - } - throw new Error(`Network request failed: ${error.message}`) - } - throw new Error(`Unknown network error: ${String(error)}`) - } finally { - clearTimeout(timeout) - } + req.end() + }) }, /** diff --git a/packages/cli/src/yarn-cli.mts b/packages/cli/src/yarn-cli.mts index 2779c26bd..ad73ca85b 100644 --- a/packages/cli/src/yarn-cli.mts +++ b/packages/cli/src/yarn-cli.mts @@ -24,6 +24,7 @@ export default async function runYarnCli() { if (signalName) { process.kill(process.pid, signalName) } else if (typeof code === 'number') { + console.log(`process.exit called at yarn-cli.mts:28 with code ${code}`) // eslint-disable-next-line n/no-process-exit process.exit(code) } @@ -37,6 +38,7 @@ export default async function runYarnCli() { if (import.meta.url === `file://${process.argv[1]}`) { runYarnCli().catch(error => { logger.error('Socket yarn wrapper error:', error) + console.log('process.exit called at yarn-cli.mts:41') // eslint-disable-next-line n/no-process-exit process.exit(1) }) diff --git a/packages/cli/test/unit/commands/audit-log/handle-audit-log.test.mts b/packages/cli/test/unit/commands/audit-log/handle-audit-log.test.mts index 5b74a7425..bacaa5d0f 100644 --- a/packages/cli/test/unit/commands/audit-log/handle-audit-log.test.mts +++ b/packages/cli/test/unit/commands/audit-log/handle-audit-log.test.mts @@ -186,9 +186,7 @@ describe('handleAuditLog', () => { }) it('handles empty audit logs', async () => { - const { fetchAuditLog } = await import( - '../../../../src/commands/audit-log/fetch-audit-log.mts' - ) + await import('../../../../src/commands/audit-log/fetch-audit-log.mts') const { outputAuditLog } = await import( '../../../../src/commands/audit-log/output-audit-log.mts' ) @@ -208,9 +206,7 @@ describe('handleAuditLog', () => { }) it('handles fetch errors', async () => { - const { fetchAuditLog } = await import( - '../../../../src/commands/audit-log/fetch-audit-log.mts' - ) + await import('../../../../src/commands/audit-log/fetch-audit-log.mts') const { outputAuditLog } = await import( '../../../../src/commands/audit-log/output-audit-log.mts' ) diff --git a/packages/cli/test/unit/commands/config/handle-config-auto.test.mts b/packages/cli/test/unit/commands/config/handle-config-auto.test.mts index 07f5cf7c6..05ff10ecb 100644 --- a/packages/cli/test/unit/commands/config/handle-config-auto.test.mts +++ b/packages/cli/test/unit/commands/config/handle-config-auto.test.mts @@ -97,9 +97,6 @@ describe('handleConfigAuto', () => { }) it('handles markdown output format', async () => { - const { discoverConfigValue } = await import( - '../../../../src/commands/config/discover-config-value.mts' - ) const { outputConfigAuto } = await import( '../../../../src/commands/config/output-config-auto.mts' ) @@ -133,9 +130,6 @@ describe('handleConfigAuto', () => { }) it('handles text output format', async () => { - const { discoverConfigValue } = await import( - '../../../../src/commands/config/discover-config-value.mts' - ) const { outputConfigAuto } = await import( '../../../../src/commands/config/output-config-auto.mts' ) diff --git a/packages/cli/test/unit/commands/config/handle-config-get.test.mts b/packages/cli/test/unit/commands/config/handle-config-get.test.mts index 9523f6372..9830acf04 100644 --- a/packages/cli/test/unit/commands/config/handle-config-get.test.mts +++ b/packages/cli/test/unit/commands/config/handle-config-get.test.mts @@ -143,7 +143,6 @@ describe('handleConfigGet', () => { }) it('handles empty config value', async () => { - const { getConfigValue } = await import('../../../../src/utils/config.mts') const { outputConfigGet } = await import( '../../../../src/commands/config/output-config-get.mts' ) @@ -160,7 +159,6 @@ describe('handleConfigGet', () => { }) it('handles undefined config value', async () => { - const { getConfigValue } = await import('../../../../src/utils/config.mts') const { outputConfigGet } = await import( '../../../../src/commands/config/output-config-get.mts' ) diff --git a/packages/cli/test/unit/commands/config/handle-config-set.test.mts b/packages/cli/test/unit/commands/config/handle-config-set.test.mts index dc00e3730..30f2280e5 100644 --- a/packages/cli/test/unit/commands/config/handle-config-set.test.mts +++ b/packages/cli/test/unit/commands/config/handle-config-set.test.mts @@ -137,9 +137,6 @@ describe('handleConfigSet', () => { it('logs debug information', async () => { const { debug, debugDir } = await import('@socketsecurity/lib/debug') - const { updateConfigValue } = await import( - '../../../../src/utils/config.mts' - ) mockUpdateConfigValue.mockReturnValue(createSuccessResult('debug-value')) @@ -162,9 +159,6 @@ describe('handleConfigSet', () => { it('logs debug information on failure', async () => { const { debug } = await import('@socketsecurity/lib/debug') - const { updateConfigValue } = await import( - '../../../../src/utils/config.mts' - ) mockUpdateConfigValue.mockReturnValue(createErrorResult('Failed')) diff --git a/packages/cli/test/unit/commands/config/handle-config-unset.test.mts b/packages/cli/test/unit/commands/config/handle-config-unset.test.mts index dceeba714..96e6e71b0 100644 --- a/packages/cli/test/unit/commands/config/handle-config-unset.test.mts +++ b/packages/cli/test/unit/commands/config/handle-config-unset.test.mts @@ -147,9 +147,6 @@ describe('handleConfigUnset', () => { }) it('handles text output', async () => { - const { updateConfigValue } = await import( - '../../../../src/utils/config.mts' - ) const { outputConfigUnset } = await import( '../../../../src/commands/config/output-config-unset.mts' ) diff --git a/packages/cli/test/unit/commands/fix/ghsa-tracker.test.mts b/packages/cli/test/unit/commands/fix/ghsa-tracker.test.mts index 8b64659ec..85a89642c 100644 --- a/packages/cli/test/unit/commands/fix/ghsa-tracker.test.mts +++ b/packages/cli/test/unit/commands/fix/ghsa-tracker.test.mts @@ -97,7 +97,6 @@ describe('ghsa-tracker', () => { }) it('creates new tracker when file does not exist', async () => { - const { readJson } = await import('@socketsecurity/lib/fs') mockReadJson.mockRejectedValue(new Error('ENOENT')) const result = await loadGhsaTracker(mockCwd) @@ -109,7 +108,6 @@ describe('ghsa-tracker', () => { }) it('handles null tracker data', async () => { - const { readJson } = await import('@socketsecurity/lib/fs') mockReadJson.mockResolvedValue(null) const result = await loadGhsaTracker(mockCwd) @@ -149,7 +147,7 @@ describe('ghsa-tracker', () => { describe('markGhsaFixed', () => { it('adds new GHSA fix record', async () => { - const { readJson, writeJson } = await import('@socketsecurity/lib/fs') + const { writeJson } = await import('@socketsecurity/lib/fs') const existingTracker: GhsaTracker = { version: 1, fixed: [], @@ -176,7 +174,7 @@ describe('ghsa-tracker', () => { }) it('replaces existing GHSA fix record', async () => { - const { readJson, writeJson } = await import('@socketsecurity/lib/fs') + const { writeJson } = await import('@socketsecurity/lib/fs') const existingTracker: GhsaTracker = { version: 1, fixed: [ @@ -213,7 +211,6 @@ describe('ghsa-tracker', () => { }) it('sorts records by fixedAt descending', async () => { - const { readJson, writeJson } = await import('@socketsecurity/lib/fs') const existingTracker: GhsaTracker = { version: 1, fixed: [ @@ -237,7 +234,6 @@ describe('ghsa-tracker', () => { }) it('handles errors gracefully', async () => { - const { readJson } = await import('@socketsecurity/lib/fs') mockReadJson.mockRejectedValue(new Error('Permission denied')) // Should not throw. @@ -249,7 +245,6 @@ describe('ghsa-tracker', () => { describe('isGhsaFixed', () => { it('returns true for fixed GHSA', async () => { - const { readJson } = await import('@socketsecurity/lib/fs') const tracker: GhsaTracker = { version: 1, fixed: [ @@ -270,7 +265,6 @@ describe('ghsa-tracker', () => { }) it('returns false for unfixed GHSA', async () => { - const { readJson } = await import('@socketsecurity/lib/fs') const tracker: GhsaTracker = { version: 1, fixed: [], @@ -284,7 +278,6 @@ describe('ghsa-tracker', () => { }) it('returns false on error', async () => { - const { readJson } = await import('@socketsecurity/lib/fs') mockReadJson.mockRejectedValue(new Error('Read error')) const result = await isGhsaFixed(mockCwd, 'GHSA-1234-5678-90ab') @@ -295,7 +288,6 @@ describe('ghsa-tracker', () => { describe('getFixedGhsas', () => { it('returns all fixed GHSA records', async () => { - const { readJson } = await import('@socketsecurity/lib/fs') const tracker: GhsaTracker = { version: 1, fixed: [ @@ -323,7 +315,6 @@ describe('ghsa-tracker', () => { }) it('returns empty array on error', async () => { - const { readJson } = await import('@socketsecurity/lib/fs') mockReadJson.mockRejectedValue(new Error('Read error')) const result = await getFixedGhsas(mockCwd) diff --git a/packages/cli/test/unit/commands/manifest/handle-manifest-conda.test.mts b/packages/cli/test/unit/commands/manifest/handle-manifest-conda.test.mts index 576dcf498..ae6ac1eb2 100644 --- a/packages/cli/test/unit/commands/manifest/handle-manifest-conda.test.mts +++ b/packages/cli/test/unit/commands/manifest/handle-manifest-conda.test.mts @@ -48,12 +48,10 @@ vi.mock('../../../../src/commands/manifest/output-requirements.mts', () => ({ describe('handleManifestConda', () => { it('converts conda file and outputs requirements successfully', async () => { - const { convertCondaToRequirements } = await import( + await import( '../../../../src/commands/manifest/convert-conda-to-requirements.mts' ) - const { outputRequirements } = await import( - '../../../../src/commands/manifest/output-requirements.mts' - ) + await import('../../../../src/commands/manifest/output-requirements.mts') const mockConvert = mockConvertCondaToRequirements const mockOutput = mockOutputRequirements @@ -86,12 +84,10 @@ describe('handleManifestConda', () => { }) it('handles conversion failure', async () => { - const { convertCondaToRequirements } = await import( + await import( '../../../../src/commands/manifest/convert-conda-to-requirements.mts' ) - const { outputRequirements } = await import( - '../../../../src/commands/manifest/output-requirements.mts' - ) + await import('../../../../src/commands/manifest/output-requirements.mts') const mockConvert = mockConvertCondaToRequirements const mockOutput = mockOutputRequirements @@ -111,12 +107,10 @@ describe('handleManifestConda', () => { }) it('handles different output formats', async () => { - const { convertCondaToRequirements } = await import( + await import( '../../../../src/commands/manifest/convert-conda-to-requirements.mts' ) - const { outputRequirements } = await import( - '../../../../src/commands/manifest/output-requirements.mts' - ) + await import('../../../../src/commands/manifest/output-requirements.mts') const mockConvert = mockConvertCondaToRequirements const mockOutput = mockOutputRequirements @@ -143,7 +137,7 @@ describe('handleManifestConda', () => { }) it('handles verbose mode', async () => { - const { convertCondaToRequirements } = await import( + await import( '../../../../src/commands/manifest/convert-conda-to-requirements.mts' ) const mockConvert = mockConvertCondaToRequirements @@ -166,7 +160,7 @@ describe('handleManifestConda', () => { }) it('handles different working directories', async () => { - const { convertCondaToRequirements } = await import( + await import( '../../../../src/commands/manifest/convert-conda-to-requirements.mts' ) const mockConvert = mockConvertCondaToRequirements diff --git a/packages/cli/test/unit/commands/repository/fetch-list-repos.test.mts b/packages/cli/test/unit/commands/repository/fetch-list-repos.test.mts index cc32148aa..2426c131f 100644 --- a/packages/cli/test/unit/commands/repository/fetch-list-repos.test.mts +++ b/packages/cli/test/unit/commands/repository/fetch-list-repos.test.mts @@ -192,7 +192,7 @@ describe('fetchListRepos', () => { }) it('handles empty results on specific page', async () => { - const { mockSdk } = await setupSdkMockSuccess('listRepositories', { + await setupSdkMockSuccess('listRepositories', { results: [], nextPage: null, }) diff --git a/packages/cli/test/unit/commands/scan/handle-create-new-scan.test.mts b/packages/cli/test/unit/commands/scan/handle-create-new-scan.test.mts index fb60bfdf9..d074729e4 100644 --- a/packages/cli/test/unit/commands/scan/handle-create-new-scan.test.mts +++ b/packages/cli/test/unit/commands/scan/handle-create-new-scan.test.mts @@ -150,9 +150,7 @@ describe('handleCreateNewScan', () => { const { getPackageFilesForScan } = await import( '../../../../src/utils/fs/path-resolve.mts' ) - const { checkCommandInput } = await import( - '../../../../src/utils/validation/check-input.mts' - ) + await import('../../../../src/utils/validation/check-input.mts') const { fetchCreateOrgFullScan } = await import( '../../../../src/commands/scan/fetch-create-org-full-scan.mts' ) @@ -202,15 +200,13 @@ describe('handleCreateNewScan', () => { const { generateAutoManifest } = await import( '../../../../src/commands/manifest/generate_auto_manifest.mts' ) - const { fetchSupportedScanFileNames } = await import( + await import( '../../../../src/commands/scan/fetch-supported-scan-file-names.mts' ) const { getPackageFilesForScan } = await import( '../../../../src/utils/fs/path-resolve.mts' ) - const { checkCommandInput } = await import( - '../../../../src/utils/validation/check-input.mts' - ) + await import('../../../../src/utils/validation/check-input.mts') mockReadOrDefaultSocketJson.mockReturnValue({}) mockDetectManifestActions.mockResolvedValue({ detected: true }) @@ -298,12 +294,8 @@ describe('handleCreateNewScan', () => { const { getPackageFilesForScan } = await import( '../../../../src/utils/fs/path-resolve.mts' ) - const { checkCommandInput } = await import( - '../../../../src/utils/validation/check-input.mts' - ) - const { fetchCreateOrgFullScan } = await import( - '../../../../src/commands/scan/fetch-create-org-full-scan.mts' - ) + await import('../../../../src/utils/validation/check-input.mts') + await import('../../../../src/commands/scan/fetch-create-org-full-scan.mts') const { finalizeTier1Scan } = await import( '../../../../src/commands/scan/finalize-tier1-scan.mts' ) @@ -339,18 +331,12 @@ describe('handleCreateNewScan', () => { }) it('handles scan report generation', async () => { - const { fetchSupportedScanFileNames } = await import( + await import( '../../../../src/commands/scan/fetch-supported-scan-file-names.mts' ) - const { getPackageFilesForScan } = await import( - '../../../../src/utils/fs/path-resolve.mts' - ) - const { checkCommandInput } = await import( - '../../../../src/utils/validation/check-input.mts' - ) - const { fetchCreateOrgFullScan } = await import( - '../../../../src/commands/scan/fetch-create-org-full-scan.mts' - ) + await import('../../../../src/utils/fs/path-resolve.mts') + await import('../../../../src/utils/validation/check-input.mts') + await import('../../../../src/commands/scan/fetch-create-org-full-scan.mts') const { handleScanReport } = await import( '../../../../src/commands/scan/handle-scan-report.mts' ) @@ -379,7 +365,7 @@ describe('handleCreateNewScan', () => { }) it('handles fetch supported files failure', async () => { - const { fetchSupportedScanFileNames } = await import( + await import( '../../../../src/commands/scan/fetch-supported-scan-file-names.mts' ) const { outputCreateNewScan } = await import( diff --git a/packages/cli/test/unit/commands/scan/handle-delete-scan.test.mts b/packages/cli/test/unit/commands/scan/handle-delete-scan.test.mts index d33eb7610..420962777 100644 --- a/packages/cli/test/unit/commands/scan/handle-delete-scan.test.mts +++ b/packages/cli/test/unit/commands/scan/handle-delete-scan.test.mts @@ -41,9 +41,7 @@ vi.mock('../../../../src/commands/scan/output-delete-scan.mts', () => ({ describe('handleDeleteScan', () => { it('deletes scan and outputs result successfully', async () => { - const { outputDeleteScan } = await import( - '../../../../src/commands/scan/output-delete-scan.mts' - ) + await import('../../../../src/commands/scan/output-delete-scan.mts') const mockFetch = mockFetchDeleteOrgFullScan const mockOutput = mockOutputDeleteScan @@ -63,9 +61,7 @@ describe('handleDeleteScan', () => { }) it('handles deletion failure', async () => { - const { outputDeleteScan } = await import( - '../../../../src/commands/scan/output-delete-scan.mts' - ) + await import('../../../../src/commands/scan/output-delete-scan.mts') const mockFetch = mockFetchDeleteOrgFullScan const mockOutput = mockOutputDeleteScan @@ -81,9 +77,7 @@ describe('handleDeleteScan', () => { }) it('handles markdown output format', async () => { - const { outputDeleteScan } = await import( - '../../../../src/commands/scan/output-delete-scan.mts' - ) + await import('../../../../src/commands/scan/output-delete-scan.mts') const mockFetch = mockFetchDeleteOrgFullScan const mockOutput = mockOutputDeleteScan @@ -116,9 +110,7 @@ describe('handleDeleteScan', () => { }) it('handles text output format', async () => { - const { outputDeleteScan } = await import( - '../../../../src/commands/scan/output-delete-scan.mts' - ) + await import('../../../../src/commands/scan/output-delete-scan.mts') const mockFetch = mockFetchDeleteOrgFullScan const mockOutput = mockOutputDeleteScan diff --git a/packages/cli/test/unit/commands/scan/handle-list-scans.test.mts b/packages/cli/test/unit/commands/scan/handle-list-scans.test.mts index 18b0d177d..9fa1de85b 100644 --- a/packages/cli/test/unit/commands/scan/handle-list-scans.test.mts +++ b/packages/cli/test/unit/commands/scan/handle-list-scans.test.mts @@ -41,12 +41,8 @@ vi.mock('../../../../src/commands/scan/output-list-scans.mts', () => ({ describe('handleListScans', () => { it('fetches and outputs scan list successfully', async () => { - const { fetchOrgFullScanList } = await import( - '../../../../src/commands/scan/fetch-list-scans.mts' - ) - const { outputListScans } = await import( - '../../../../src/commands/scan/output-list-scans.mts' - ) + await import('../../../../src/commands/scan/fetch-list-scans.mts') + await import('../../../../src/commands/scan/output-list-scans.mts') const mockFetch = mockFetchOrgFullScanList const mockOutput = mockOutputListScans @@ -101,12 +97,8 @@ describe('handleListScans', () => { }) it('handles fetch failure', async () => { - const { fetchOrgFullScanList } = await import( - '../../../../src/commands/scan/fetch-list-scans.mts' - ) - const { outputListScans } = await import( - '../../../../src/commands/scan/output-list-scans.mts' - ) + await import('../../../../src/commands/scan/fetch-list-scans.mts') + await import('../../../../src/commands/scan/output-list-scans.mts') const mockFetch = mockFetchOrgFullScanList const mockOutput = mockOutputListScans @@ -129,9 +121,7 @@ describe('handleListScans', () => { }) it('handles pagination parameters', async () => { - const { fetchOrgFullScanList } = await import( - '../../../../src/commands/scan/fetch-list-scans.mts' - ) + await import('../../../../src/commands/scan/fetch-list-scans.mts') const mockFetch = mockFetchOrgFullScanList mockFetch.mockResolvedValue(createSuccessResult([])) @@ -160,12 +150,8 @@ describe('handleListScans', () => { }) it('handles markdown output format', async () => { - const { fetchOrgFullScanList } = await import( - '../../../../src/commands/scan/fetch-list-scans.mts' - ) - const { outputListScans } = await import( - '../../../../src/commands/scan/output-list-scans.mts' - ) + await import('../../../../src/commands/scan/fetch-list-scans.mts') + await import('../../../../src/commands/scan/output-list-scans.mts') const mockFetch = mockFetchOrgFullScanList const mockOutput = mockOutputListScans @@ -187,9 +173,7 @@ describe('handleListScans', () => { }) it('handles filtering by branch and repository', async () => { - const { fetchOrgFullScanList } = await import( - '../../../../src/commands/scan/fetch-list-scans.mts' - ) + await import('../../../../src/commands/scan/fetch-list-scans.mts') const mockFetch = mockFetchOrgFullScanList mockFetch.mockResolvedValue(createSuccessResult([])) diff --git a/packages/cli/test/unit/commands/scan/handle-scan-metadata.test.mts b/packages/cli/test/unit/commands/scan/handle-scan-metadata.test.mts index 84536bac7..42d8a00c1 100644 --- a/packages/cli/test/unit/commands/scan/handle-scan-metadata.test.mts +++ b/packages/cli/test/unit/commands/scan/handle-scan-metadata.test.mts @@ -41,12 +41,8 @@ vi.mock('../../../../src/commands/scan/output-scan-metadata.mts', () => ({ describe('handleOrgScanMetadata', () => { it('fetches and outputs scan metadata successfully', async () => { - const { fetchScanMetadata } = await import( - '../../../../src/commands/scan/fetch-scan-metadata.mts' - ) - const { outputScanMetadata } = await import( - '../../../../src/commands/scan/output-scan-metadata.mts' - ) + await import('../../../../src/commands/scan/fetch-scan-metadata.mts') + await import('../../../../src/commands/scan/output-scan-metadata.mts') const mockFetch = mockFetchScanMetadata const mockOutput = mockOutputScanMetadata @@ -71,12 +67,8 @@ describe('handleOrgScanMetadata', () => { }) it('handles fetch failure', async () => { - const { fetchScanMetadata } = await import( - '../../../../src/commands/scan/fetch-scan-metadata.mts' - ) - const { outputScanMetadata } = await import( - '../../../../src/commands/scan/output-scan-metadata.mts' - ) + await import('../../../../src/commands/scan/fetch-scan-metadata.mts') + await import('../../../../src/commands/scan/output-scan-metadata.mts') const mockFetch = mockFetchScanMetadata const mockOutput = mockOutputScanMetadata @@ -92,12 +84,8 @@ describe('handleOrgScanMetadata', () => { }) it('handles markdown output format', async () => { - const { fetchScanMetadata } = await import( - '../../../../src/commands/scan/fetch-scan-metadata.mts' - ) - const { outputScanMetadata } = await import( - '../../../../src/commands/scan/output-scan-metadata.mts' - ) + await import('../../../../src/commands/scan/fetch-scan-metadata.mts') + await import('../../../../src/commands/scan/output-scan-metadata.mts') const mockFetch = mockFetchScanMetadata const mockOutput = mockOutputScanMetadata @@ -118,12 +106,8 @@ describe('handleOrgScanMetadata', () => { }) it('handles different scan IDs', async () => { - const { fetchScanMetadata } = await import( - '../../../../src/commands/scan/fetch-scan-metadata.mts' - ) - const { outputScanMetadata } = await import( - '../../../../src/commands/scan/output-scan-metadata.mts' - ) + await import('../../../../src/commands/scan/fetch-scan-metadata.mts') + await import('../../../../src/commands/scan/output-scan-metadata.mts') const mockFetch = mockFetchScanMetadata const _mockOutput = mockOutputScanMetadata @@ -145,12 +129,8 @@ describe('handleOrgScanMetadata', () => { }) it('handles text output with detailed metadata', async () => { - const { fetchScanMetadata } = await import( - '../../../../src/commands/scan/fetch-scan-metadata.mts' - ) - const { outputScanMetadata } = await import( - '../../../../src/commands/scan/output-scan-metadata.mts' - ) + await import('../../../../src/commands/scan/fetch-scan-metadata.mts') + await import('../../../../src/commands/scan/output-scan-metadata.mts') const mockFetch = mockFetchScanMetadata const mockOutput = mockOutputScanMetadata diff --git a/packages/cli/test/unit/commands/scan/handle-scan-view.test.mts b/packages/cli/test/unit/commands/scan/handle-scan-view.test.mts index 259be8a6c..f02ee4491 100644 --- a/packages/cli/test/unit/commands/scan/handle-scan-view.test.mts +++ b/packages/cli/test/unit/commands/scan/handle-scan-view.test.mts @@ -101,9 +101,7 @@ describe('handleScanView', () => { }) it('handles markdown output', async () => { - const { fetchScan } = await import( - '../../../../../src/commands/scan/fetch-scan.mts' - ) + await import('../../../../../src/commands/scan/fetch-scan.mts') const { outputScanView } = await import( '../../../../../src/commands/scan/output-scan-view.mts' ) @@ -130,9 +128,7 @@ describe('handleScanView', () => { }) it('handles empty file path', async () => { - const { fetchScan } = await import( - '../../../../../src/commands/scan/fetch-scan.mts' - ) + await import('../../../../../src/commands/scan/fetch-scan.mts') const { outputScanView } = await import( '../../../../../src/commands/scan/output-scan-view.mts' ) @@ -155,9 +151,7 @@ describe('handleScanView', () => { }) it('handles different scan statuses', async () => { - const { fetchScan } = await import( - '../../../../../src/commands/scan/fetch-scan.mts' - ) + await import('../../../../../src/commands/scan/fetch-scan.mts') const { outputScanView } = await import( '../../../../../src/commands/scan/output-scan-view.mts' ) diff --git a/packages/cli/test/unit/utils/telemetry/integration.test.mts b/packages/cli/test/unit/utils/telemetry/integration.test.mts new file mode 100644 index 000000000..1f6d2a723 --- /dev/null +++ b/packages/cli/test/unit/utils/telemetry/integration.test.mts @@ -0,0 +1,644 @@ +/** + * Unit tests for telemetry integration helpers. + * + * Purpose: + * Tests telemetry tracking utilities for CLI lifecycle and subprocess events. + * + * Test Coverage: + * - CLI lifecycle tracking (start, complete, error) + * - Subprocess tracking (start, complete, error, exit) + * - Argument sanitization (tokens, paths, package names) + * - Context building (version, platform, node version, arch) + * - Error normalization and sanitization + * - Event metadata handling + * - Telemetry finalization and flushing + * + * Testing Approach: + * Mocks TelemetryService and SDK to test integration logic without network calls. + * + * Related Files: + * - utils/telemetry/integration.mts (implementation) + * - utils/telemetry/service.mts (service implementation) + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +// Mock TelemetryService. +const mockTrack = vi.hoisted(() => vi.fn()) +const mockFlush = vi.hoisted(() => vi.fn()) +const mockGetTelemetryClient = vi.hoisted(() => + vi.fn(() => + Promise.resolve({ + flush: mockFlush, + track: mockTrack, + }), + ), +) +const mockGetCurrentInstance = vi.hoisted(() => + vi.fn(() => ({ + flush: mockFlush, + track: mockTrack, + })), +) + +vi.mock('../../../../src/utils/telemetry/service.mts', () => ({ + TelemetryService: { + getCurrentInstance: mockGetCurrentInstance, + getTelemetryClient: mockGetTelemetryClient, + }, +})) + +// Mock debug functions. +const mockDebug = vi.hoisted(() => vi.fn()) +vi.mock('@socketsecurity/lib/debug', () => ({ + debug: mockDebug, +})) + +// Mock config function. +const mockGetConfigValueOrUndef = vi.hoisted(() => vi.fn(() => 'test-org')) +vi.mock('../../../../src/utils/config.mts', () => ({ + getConfigValueOrUndef: mockGetConfigValueOrUndef, +})) + +// Mock constants. +vi.mock('../../../../src/constants/env.mts', () => ({ + getCliVersion: () => '2.2.15', +})) + +import { + finalizeTelemetry, + trackCliComplete, + trackCliError, + trackCliEvent, + trackCliStart, + trackEvent, + trackSubprocessComplete, + trackSubprocessError, + trackSubprocessExit, + trackSubprocessStart, +} from '../../../../src/utils/telemetry/integration.mts' + +describe('telemetry integration', () => { + beforeEach(() => { + vi.clearAllMocks() + mockGetConfigValueOrUndef.mockReturnValue('test-org') + }) + + describe('finalizeTelemetry', () => { + it('flushes telemetry when instance exists', async () => { + await finalizeTelemetry() + + expect(mockGetCurrentInstance).toHaveBeenCalled() + expect(mockFlush).toHaveBeenCalled() + }) + + it('does nothing when no instance exists', async () => { + mockGetCurrentInstance.mockReturnValueOnce(null) + + await finalizeTelemetry() + + expect(mockGetCurrentInstance).toHaveBeenCalled() + expect(mockFlush).not.toHaveBeenCalled() + }) + }) + + describe('trackEvent', () => { + const mockContext = { + arch: 'x64', + argv: ['scan'], + node_version: 'v20.0.0', + platform: 'darwin', + version: '2.2.15', + } + + it('tracks event with context and metadata', async () => { + await trackEvent('test_event', mockContext, { foo: 'bar' }) + + expect(mockGetTelemetryClient).toHaveBeenCalledWith('test-org') + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + context: mockContext, + event_type: 'test_event', + metadata: { foo: 'bar' }, + }), + ) + }) + + it('tracks event with error details', async () => { + const error = new Error('Test error') + await trackEvent('test_event', mockContext, {}, { error }) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + error: { + message: 'Test error', + stack: expect.any(String), + type: 'Error', + }, + }), + ) + }) + + it('flushes when flush option is true', async () => { + await trackEvent('test_event', mockContext, {}, { flush: true }) + + expect(mockFlush).toHaveBeenCalled() + }) + + it('does not track when org slug is undefined', async () => { + mockGetConfigValueOrUndef.mockReturnValueOnce(undefined) + + await trackEvent('test_event', mockContext) + + expect(mockGetTelemetryClient).not.toHaveBeenCalled() + expect(mockTrack).not.toHaveBeenCalled() + }) + + it('does not throw when telemetry client fails', async () => { + mockGetTelemetryClient.mockRejectedValueOnce( + new Error('Client creation failed'), + ) + + await expect(trackEvent('test_event', mockContext)).resolves.not.toThrow() + }) + + it('omits metadata when empty', async () => { + await trackEvent('test_event', mockContext, {}) + + expect(mockTrack).toHaveBeenCalledWith( + expect.not.objectContaining({ + metadata: expect.anything(), + }), + ) + }) + }) + + describe('trackCliStart', () => { + it('returns start timestamp', async () => { + const startTime = await trackCliStart(['node', 'socket', 'scan']) + + expect(typeof startTime).toBe('number') + expect(startTime).toBeGreaterThan(0) + }) + + it('tracks cli_start event with sanitized argv', async () => { + await trackCliStart(['node', 'socket', 'scan', '--token', 'sktsec_abc']) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + argv: ['scan', '--token', '[REDACTED]'], + }), + event_type: 'cli_start', + }), + ) + }) + }) + + describe('trackCliEvent', () => { + it('tracks custom event with metadata', async () => { + await trackCliEvent('custom_event', ['node', 'socket', 'scan'], { + key: 'value', + }) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + event_type: 'custom_event', + metadata: { key: 'value' }, + }), + ) + }) + + it('tracks custom event without metadata', async () => { + await trackCliEvent('custom_event', ['node', 'socket', 'scan']) + + expect(mockTrack).toHaveBeenCalledWith( + expect.not.objectContaining({ + metadata: expect.anything(), + }), + ) + }) + }) + + describe('trackCliComplete', () => { + it('tracks cli_complete event with duration', async () => { + const startTime = Date.now() - 1000 + await trackCliComplete(['node', 'socket', 'scan'], startTime, 0) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + event_type: 'cli_complete', + metadata: expect.objectContaining({ + duration: expect.any(Number), + exit_code: 0, + }), + }), + ) + expect(mockFlush).toHaveBeenCalled() + }) + + it('normalizes exit code when string', async () => { + const startTime = Date.now() + await trackCliComplete(['node', 'socket', 'scan'], startTime, '0') + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + metadata: expect.objectContaining({ + exit_code: 0, + }), + }), + ) + }) + + it('uses default exit code when null', async () => { + const startTime = Date.now() + await trackCliComplete(['node', 'socket', 'scan'], startTime, null) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + metadata: expect.objectContaining({ + exit_code: 0, + }), + }), + ) + }) + }) + + describe('trackCliError', () => { + it('tracks cli_error event with error details', async () => { + const startTime = Date.now() - 500 + const error = new Error('Test error') + + await trackCliError(['node', 'socket', 'scan'], startTime, error, 1) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + error: expect.objectContaining({ + message: 'Test error', + type: 'Error', + }), + event_type: 'cli_error', + metadata: expect.objectContaining({ + duration: expect.any(Number), + exit_code: 1, + }), + }), + ) + expect(mockFlush).toHaveBeenCalled() + }) + + it('normalizes non-Error objects', async () => { + const startTime = Date.now() + await trackCliError(['node', 'socket', 'scan'], startTime, 'string error') + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + error: expect.objectContaining({ + message: 'string error', + type: 'Error', + }), + }), + ) + }) + + it('uses default exit code when not provided', async () => { + const startTime = Date.now() + const error = new Error('Test') + + await trackCliError(['node', 'socket', 'scan'], startTime, error) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + metadata: expect.objectContaining({ + exit_code: 1, + }), + }), + ) + }) + }) + + describe('trackSubprocessStart', () => { + it('returns start timestamp', async () => { + const startTime = await trackSubprocessStart('npm') + + expect(typeof startTime).toBe('number') + expect(startTime).toBeGreaterThan(0) + }) + + it('tracks subprocess_start event with command', async () => { + await trackSubprocessStart('npm', { cwd: '/path' }) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + event_type: 'subprocess_start', + metadata: expect.objectContaining({ + command: 'npm', + cwd: '/path', + }), + }), + ) + }) + + it('tracks subprocess_start without metadata', async () => { + await trackSubprocessStart('coana') + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + metadata: expect.objectContaining({ + command: 'coana', + }), + }), + ) + }) + }) + + describe('trackSubprocessComplete', () => { + it('tracks subprocess_complete event with duration', async () => { + const startTime = Date.now() - 2000 + await trackSubprocessComplete('npm', startTime, 0) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + event_type: 'subprocess_complete', + metadata: expect.objectContaining({ + command: 'npm', + duration: expect.any(Number), + exit_code: 0, + }), + }), + ) + }) + + it('includes additional metadata', async () => { + const startTime = Date.now() + await trackSubprocessComplete('npm', startTime, 0, { stdout_length: 1234 }) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + metadata: expect.objectContaining({ + stdout_length: 1234, + }), + }), + ) + }) + }) + + describe('trackSubprocessError', () => { + it('tracks subprocess_error event with error details', async () => { + const startTime = Date.now() - 1000 + const error = new Error('Subprocess failed') + + await trackSubprocessError('npm', startTime, error, 1) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + error: expect.objectContaining({ + message: 'Subprocess failed', + type: 'Error', + }), + event_type: 'subprocess_error', + metadata: expect.objectContaining({ + command: 'npm', + duration: expect.any(Number), + exit_code: 1, + }), + }), + ) + }) + + it('includes additional metadata', async () => { + const startTime = Date.now() + const error = new Error('Test') + + await trackSubprocessError('npm', startTime, error, 1, { stderr: 'log' }) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + metadata: expect.objectContaining({ + stderr: 'log', + }), + }), + ) + }) + }) + + describe('trackSubprocessExit', () => { + it('tracks completion when exit code is 0', async () => { + const startTime = Date.now() + await trackSubprocessExit('npm', startTime, 0) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + event_type: 'subprocess_complete', + }), + ) + expect(mockFlush).toHaveBeenCalled() + }) + + it('tracks error when exit code is non-zero', async () => { + const startTime = Date.now() + await trackSubprocessExit('npm', startTime, 1) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + error: expect.objectContaining({ + message: 'npm exited with code 1', + }), + event_type: 'subprocess_error', + }), + ) + expect(mockFlush).toHaveBeenCalled() + }) + + it('does not track when exit code is null', async () => { + const startTime = Date.now() + await trackSubprocessExit('npm', startTime, null) + + expect(mockTrack).not.toHaveBeenCalled() + expect(mockFlush).toHaveBeenCalled() + }) + }) + + describe('argv sanitization', () => { + it('strips node and script paths', async () => { + await trackCliStart(['node', '/path/socket', 'scan']) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + argv: ['scan'], + }), + }), + ) + }) + + it('redacts API tokens after flags', async () => { + await trackCliStart([ + 'node', + 'socket', + 'scan', + '--api-token', + 'sktsec_secret', + ]) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + argv: ['scan', '--api-token', '[REDACTED]'], + }), + }), + ) + }) + + it('redacts socket tokens starting with sktsec_', async () => { + await trackCliStart(['node', 'socket', 'scan', 'sktsec_abc123def']) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + argv: ['scan', '[REDACTED]'], + }), + }), + ) + }) + + it('redacts hex tokens', async () => { + await trackCliStart([ + 'node', + 'socket', + 'scan', + 'abcdef1234567890abcdef1234567890', + ]) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + argv: ['scan', '[REDACTED]'], + }), + }), + ) + }) + + it('replaces home directory with tilde', async () => { + const homeDir = require('node:os').homedir() + await trackCliStart([ + 'node', + 'socket', + 'scan', + `${homeDir}/projects/app`, + ]) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + argv: ['scan', '~/projects/app'], + }), + }), + ) + }) + + it('strips arguments after npm wrapper', async () => { + await trackCliStart([ + 'node', + 'socket', + 'npm', + 'install', + '@my/private-package', + ]) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + argv: ['npm'], + }), + }), + ) + }) + + it('strips arguments after yarn wrapper', async () => { + await trackCliStart(['node', 'socket', 'yarn', 'add', 'private-pkg']) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + argv: ['yarn'], + }), + }), + ) + }) + + it('strips arguments after pip wrapper', async () => { + await trackCliStart(['node', 'socket', 'pip', 'install', 'flask']) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + argv: ['pip'], + }), + }), + ) + }) + + it('preserves non-wrapper commands fully', async () => { + await trackCliStart(['node', 'socket', 'scan', '--json', '--all']) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + argv: ['scan', '--json', '--all'], + }), + }), + ) + }) + }) + + describe('context building', () => { + it('includes CLI version', async () => { + await trackCliStart(['node', 'socket', 'scan']) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + version: '2.2.15', + }), + }), + ) + }) + + it('includes platform', async () => { + await trackCliStart(['node', 'socket', 'scan']) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + platform: process.platform, + }), + }), + ) + }) + + it('includes node version', async () => { + await trackCliStart(['node', 'socket', 'scan']) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + node_version: process.version, + }), + }), + ) + }) + + it('includes architecture', async () => { + await trackCliStart(['node', 'socket', 'scan']) + + expect(mockTrack).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + arch: process.arch, + }), + }), + ) + }) + }) +}) diff --git a/packages/cli/test/unit/utils/telemetry/service.test.mts b/packages/cli/test/unit/utils/telemetry/service.test.mts new file mode 100644 index 000000000..7a71737a8 --- /dev/null +++ b/packages/cli/test/unit/utils/telemetry/service.test.mts @@ -0,0 +1,643 @@ +/** + * Unit tests for telemetry service. + * + * Purpose: + * Tests TelemetryService singleton and event management. Validates service lifecycle, event batching, and API integration. + * + * Test Coverage: + * - Singleton pattern (getTelemetryClient, getCurrentInstance) + * - Event tracking and batching + * - Periodic and manual flushing + * - Service initialization and configuration + * - Session ID generation and assignment + * - Error handling and graceful degradation + * - Service destruction and cleanup + * - Timeout handling for flush operations + * + * Testing Approach: + * Mocks SDK and tests service behavior with various configurations. + * Uses fake timers to test periodic flush behavior. + * + * Related Files: + * - utils/telemetry/service.mts (implementation) + * - utils/telemetry/types.mts (types) + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +// Mock SDK setup. +const mockPostOrgTelemetry = vi.hoisted(() => + vi.fn(() => Promise.resolve({ success: true })), +) +const mockGetTelemetryConfig = vi.hoisted(() => + vi.fn(() => + Promise.resolve({ + data: { telemetry: { enabled: true } }, + success: true, + }), + ), +) +const mockSetupSdk = vi.hoisted(() => + vi.fn(() => + Promise.resolve({ + data: { + getTelemetryConfig: mockGetTelemetryConfig, + postOrgTelemetry: mockPostOrgTelemetry, + }, + ok: true, + }), + ), +) + +vi.mock('../../../../src/utils/socket/sdk.mts', () => ({ + setupSdk: mockSetupSdk, +})) + +// Mock debug functions. +const mockDebug = vi.hoisted(() => vi.fn()) +const mockDebugDir = vi.hoisted(() => vi.fn()) + +vi.mock('@socketsecurity/lib/debug', () => ({ + debug: mockDebug, + debugDir: mockDebugDir, +})) + +import { TelemetryService } from '../../../../src/utils/telemetry/service.mts' + +import type { TelemetryEvent } from '../../../../src/utils/telemetry/types.mts' + +describe('TelemetryService', () => { + beforeEach(async () => { + vi.clearAllMocks() + vi.restoreAllMocks() + + // Reset singleton instance. + const instance = TelemetryService.getCurrentInstance() + if (instance) { + await instance.destroy() + } + + // Reset mock implementations. + mockSetupSdk.mockResolvedValue({ + data: { + getTelemetryConfig: mockGetTelemetryConfig, + postOrgTelemetry: mockPostOrgTelemetry, + }, + ok: true, + }) + + mockGetTelemetryConfig.mockResolvedValue({ + data: { telemetry: { enabled: true } }, + success: true, + }) + + mockPostOrgTelemetry.mockResolvedValue({ success: true }) + }) + + describe('singleton pattern', () => { + it('creates new instance when none exists', async () => { + const client = await TelemetryService.getTelemetryClient('test-org') + + expect(client).toBeDefined() + expect(TelemetryService.getCurrentInstance()).toBe(client) + }) + + it('returns existing instance on subsequent calls', async () => { + const client1 = await TelemetryService.getTelemetryClient('test-org') + const client2 = await TelemetryService.getTelemetryClient('test-org') + + expect(client1).toBe(client2) + }) + + it('getCurrentInstance returns null when no instance exists', async () => { + expect(TelemetryService.getCurrentInstance()).toBeNull() + }) + }) + + describe('initialization', () => { + it('fetches telemetry configuration on creation', async () => { + await TelemetryService.getTelemetryClient('test-org') + + expect(mockSetupSdk).toHaveBeenCalled() + expect(mockGetTelemetryConfig).toHaveBeenCalledWith('test-org') + }) + + it('uses default config when SDK setup fails', async () => { + mockSetupSdk.mockResolvedValueOnce({ ok: false }) + + const client = await TelemetryService.getTelemetryClient('test-org') + + expect(client).toBeDefined() + expect(mockGetTelemetryConfig).not.toHaveBeenCalled() + }) + + it('uses default config when config fetch fails', async () => { + mockGetTelemetryConfig.mockResolvedValueOnce({ + error: 'Config fetch failed', + success: false, + }) + + const client = await TelemetryService.getTelemetryClient('test-org') + + expect(client).toBeDefined() + }) + + it('uses default config when initialization throws', async () => { + mockSetupSdk.mockRejectedValueOnce(new Error('Network error')) + + const client = await TelemetryService.getTelemetryClient('test-org') + + expect(client).toBeDefined() + }) + }) + + describe('event tracking', () => { + it('tracks event with session_id', async () => { + const client = await TelemetryService.getTelemetryClient('test-org') + + const event: Omit = { + context: { + arch: 'x64', + argv: ['scan'], + node_version: 'v20.0.0', + platform: 'darwin', + version: '2.2.15', + }, + event_sender_created_at: new Date().toISOString(), + event_type: 'cli_start', + } + + client.track(event) + + // Verify event is queued (not sent immediately). + expect(mockPostOrgTelemetry).not.toHaveBeenCalled() + }) + + it('includes metadata when provided', async () => { + const client = await TelemetryService.getTelemetryClient('test-org') + + const event: Omit = { + context: { + arch: 'x64', + argv: ['scan'], + node_version: 'v20.0.0', + platform: 'darwin', + version: '2.2.15', + }, + event_sender_created_at: new Date().toISOString(), + event_type: 'cli_complete', + metadata: { + duration: 1000, + exit_code: 0, + }, + } + + client.track(event) + + await client.flush() + + expect(mockPostOrgTelemetry).toHaveBeenCalledWith( + 'test-org', + expect.objectContaining({ + metadata: { + duration: 1000, + exit_code: 0, + }, + }), + ) + }) + + it('includes error when provided', async () => { + const client = await TelemetryService.getTelemetryClient('test-org') + + const event: Omit = { + context: { + arch: 'x64', + argv: ['scan'], + node_version: 'v20.0.0', + platform: 'darwin', + version: '2.2.15', + }, + error: { + message: 'Test error', + stack: 'stack trace', + type: 'Error', + }, + event_sender_created_at: new Date().toISOString(), + event_type: 'cli_error', + } + + client.track(event) + + await client.flush() + + expect(mockPostOrgTelemetry).toHaveBeenCalledWith( + 'test-org', + expect.objectContaining({ + error: { + message: 'Test error', + stack: 'stack trace', + type: 'Error', + }, + }), + ) + }) + + it('ignores events when telemetry disabled', async () => { + mockGetTelemetryConfig.mockResolvedValueOnce({ + data: { telemetry: { enabled: false } }, + success: true, + }) + + const client = await TelemetryService.getTelemetryClient('test-org') + + const event: Omit = { + context: { + arch: 'x64', + argv: ['scan'], + node_version: 'v20.0.0', + platform: 'darwin', + version: '2.2.15', + }, + event_sender_created_at: new Date().toISOString(), + event_type: 'cli_start', + } + + client.track(event) + + await client.flush() + + expect(mockPostOrgTelemetry).not.toHaveBeenCalled() + }) + + it('ignores events after destroy', async () => { + const client = await TelemetryService.getTelemetryClient('test-org') + + await client.destroy() + + const event: Omit = { + context: { + arch: 'x64', + argv: ['scan'], + node_version: 'v20.0.0', + platform: 'darwin', + version: '2.2.15', + }, + event_sender_created_at: new Date().toISOString(), + event_type: 'cli_start', + } + + client.track(event) + + expect(mockPostOrgTelemetry).not.toHaveBeenCalled() + }) + }) + + describe('batching', () => { + it('auto-flushes when batch size reached', async () => { + const client = await TelemetryService.getTelemetryClient('test-org') + + const baseEvent: Omit = { + context: { + arch: 'x64', + argv: ['scan'], + node_version: 'v20.0.0', + platform: 'darwin', + version: '2.2.15', + }, + event_sender_created_at: new Date().toISOString(), + event_type: 'cli_start', + } + + // Track 10 events (batch size). + for (let i = 0; i < 10; i++) { + client.track(baseEvent) + } + + // Wait for async flush to complete. + await new Promise(resolve => { + setTimeout(resolve, 100) + }) + + expect(mockPostOrgTelemetry).toHaveBeenCalledTimes(10) + }) + + it('does not flush before batch size reached', async () => { + const client = await TelemetryService.getTelemetryClient('test-org') + + const event: Omit = { + context: { + arch: 'x64', + argv: ['scan'], + node_version: 'v20.0.0', + platform: 'darwin', + version: '2.2.15', + }, + event_sender_created_at: new Date().toISOString(), + event_type: 'cli_start', + } + + // Track fewer than batch size events. + client.track(event) + client.track(event) + + expect(mockPostOrgTelemetry).not.toHaveBeenCalled() + }) + }) + + describe('flushing', () => { + it('sends all queued events', async () => { + // Clear any previous calls before this test. + mockPostOrgTelemetry.mockClear() + + const client = await TelemetryService.getTelemetryClient('test-org') + + const event: Omit = { + context: { + arch: 'x64', + argv: ['scan'], + node_version: 'v20.0.0', + platform: 'darwin', + version: '2.2.15', + }, + event_sender_created_at: new Date().toISOString(), + event_type: 'cli_start', + } + + client.track(event) + client.track(event) + client.track(event) + + await client.flush() + + expect(mockPostOrgTelemetry).toHaveBeenCalledTimes(3) + }) + + it('clears queue after successful flush', async () => { + const client = await TelemetryService.getTelemetryClient('test-org') + + const event: Omit = { + context: { + arch: 'x64', + argv: ['scan'], + node_version: 'v20.0.0', + platform: 'darwin', + version: '2.2.15', + }, + event_sender_created_at: new Date().toISOString(), + event_type: 'cli_start', + } + + client.track(event) + + await client.flush() + await client.flush() + + // Second flush should not send anything. + expect(mockPostOrgTelemetry).toHaveBeenCalledTimes(1) + }) + + it('does nothing when queue is empty', async () => { + const client = await TelemetryService.getTelemetryClient('test-org') + + await client.flush() + + expect(mockPostOrgTelemetry).not.toHaveBeenCalled() + }) + + it('discards events on flush error', async () => { + const client = await TelemetryService.getTelemetryClient('test-org') + + mockPostOrgTelemetry.mockRejectedValueOnce(new Error('Network error')) + + const event: Omit = { + context: { + arch: 'x64', + argv: ['scan'], + node_version: 'v20.0.0', + platform: 'darwin', + version: '2.2.15', + }, + event_sender_created_at: new Date().toISOString(), + event_type: 'cli_start', + } + + client.track(event) + + await client.flush() + + // Events should be discarded even after error. + await client.flush() + + expect(mockPostOrgTelemetry).toHaveBeenCalledTimes(1) + }) + + it('does not flush after destroy', async () => { + const client = await TelemetryService.getTelemetryClient('test-org') + + await client.destroy() + await client.flush() + + expect(mockPostOrgTelemetry).not.toHaveBeenCalled() + }) + + it('handles flush timeout', async () => { + const client = await TelemetryService.getTelemetryClient('test-org') + + // Make postOrgTelemetry hang longer than timeout. + mockPostOrgTelemetry.mockImplementationOnce( + () => + new Promise(resolve => { + setTimeout(() => { + resolve({ success: true }) + }, 10_000) + }), + ) + + const event: Omit = { + context: { + arch: 'x64', + argv: ['scan'], + node_version: 'v20.0.0', + platform: 'darwin', + version: '2.2.15', + }, + event_sender_created_at: new Date().toISOString(), + event_type: 'cli_start', + } + + client.track(event) + + // Flush should timeout and not throw. + await expect(client.flush()).resolves.not.toThrow() + }) + + it('clears queue when telemetry disabled', async () => { + mockGetTelemetryConfig.mockResolvedValueOnce({ + data: { telemetry: { enabled: false } }, + success: true, + }) + + const client = await TelemetryService.getTelemetryClient('test-org') + + const event: Omit = { + context: { + arch: 'x64', + argv: ['scan'], + node_version: 'v20.0.0', + platform: 'darwin', + version: '2.2.15', + }, + event_sender_created_at: new Date().toISOString(), + event_type: 'cli_start', + } + + client.track(event) + + await client.flush() + + expect(mockPostOrgTelemetry).not.toHaveBeenCalled() + }) + }) + + describe('destroy', () => { + it('flushes remaining events', async () => { + const client = await TelemetryService.getTelemetryClient('test-org') + + const event: Omit = { + context: { + arch: 'x64', + argv: ['scan'], + node_version: 'v20.0.0', + platform: 'darwin', + version: '2.2.15', + }, + event_sender_created_at: new Date().toISOString(), + event_type: 'cli_start', + } + + client.track(event) + + await client.destroy() + + expect(mockPostOrgTelemetry).toHaveBeenCalled() + }) + + it('clears singleton instance', async () => { + const client = await TelemetryService.getTelemetryClient('test-org') + + await client.destroy() + + expect(TelemetryService.getCurrentInstance()).toBeNull() + }) + + it('is idempotent', async () => { + const client = await TelemetryService.getTelemetryClient('test-org') + + await client.destroy() + await client.destroy() + + // No error should occur. + expect(TelemetryService.getCurrentInstance()).toBeNull() + }) + + it('handles flush timeout during destroy', async () => { + const client = await TelemetryService.getTelemetryClient('test-org') + + // Make postOrgTelemetry hang longer than timeout. + mockPostOrgTelemetry.mockImplementationOnce( + () => + new Promise(resolve => { + setTimeout(() => { + resolve({ success: true }) + }, 10_000) + }), + ) + + const event: Omit = { + context: { + arch: 'x64', + argv: ['scan'], + node_version: 'v20.0.0', + platform: 'darwin', + version: '2.2.15', + }, + event_sender_created_at: new Date().toISOString(), + event_type: 'cli_start', + } + + client.track(event) + + await expect(client.destroy()).resolves.not.toThrow() + }) + + it('does not flush when telemetry disabled', async () => { + mockGetTelemetryConfig.mockResolvedValueOnce({ + data: { telemetry: { enabled: false } }, + success: true, + }) + + const client = await TelemetryService.getTelemetryClient('test-org') + + const event: Omit = { + context: { + arch: 'x64', + argv: ['scan'], + node_version: 'v20.0.0', + platform: 'darwin', + version: '2.2.15', + }, + event_sender_created_at: new Date().toISOString(), + event_type: 'cli_start', + } + + client.track(event) + + await client.destroy() + + expect(mockPostOrgTelemetry).not.toHaveBeenCalled() + }) + }) + + describe('session ID', () => { + it('assigns same session_id to all events in a session', async () => { + const client = await TelemetryService.getTelemetryClient('test-org') + + const event1: Omit = { + context: { + arch: 'x64', + argv: ['scan'], + node_version: 'v20.0.0', + platform: 'darwin', + version: '2.2.15', + }, + event_sender_created_at: new Date().toISOString(), + event_type: 'cli_start', + } + + const event2: Omit = { + context: { + arch: 'x64', + argv: ['scan'], + node_version: 'v20.0.0', + platform: 'darwin', + version: '2.2.15', + }, + event_sender_created_at: new Date().toISOString(), + event_type: 'cli_complete', + } + + client.track(event1) + client.track(event2) + + await client.flush() + + const sessionIds = mockPostOrgTelemetry.mock.calls.map( + call => call[1].session_id, + ) + + expect(sessionIds[0]).toBeDefined() + expect(sessionIds[0]).toBe(sessionIds[1]) + }) + }) +}) diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 0ecd24baf..cc67c10c7 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -1,6 +1,11 @@ { "extends": "../../.config/tsconfig.base.json", - "include": ["src/**/*.mts", "src/**/*.d.ts", "src/**/*.tsx", "test/helpers/**/*.mts"], + "include": [ + "src/**/*.mts", + "src/**/*.d.ts", + "src/**/*.tsx", + "test/helpers/**/*.mts" + ], "exclude": [ ".cache/**", ".claude/**", @@ -14,6 +19,6 @@ "src/commands/analytics/output-analytics.mts", "src/commands/audit-log/output-audit-log.mts", "src/commands/threat-feed/output-threat-feed.mts", - "test/helpers/**/*.test.mts", + "test/helpers/**/*.test.mts" ] } diff --git a/packages/cli/vitest.e2e.config.mts b/packages/cli/vitest.e2e.config.mts index bd4ed84fc..b25b3d7c7 100644 --- a/packages/cli/vitest.e2e.config.mts +++ b/packages/cli/vitest.e2e.config.mts @@ -9,9 +9,7 @@ export default defineConfig({ test: { globals: false, environment: 'node', - include: [ - '**/*.e2e.test.{mts,ts}', - ], + include: ['**/*.e2e.test.{mts,ts}'], exclude: [ '**/node_modules/**', '**/dist/**', diff --git a/packages/socket/scripts/build.mjs b/packages/socket/scripts/build.mjs index 2a729cc0d..517355e72 100644 --- a/packages/socket/scripts/build.mjs +++ b/packages/socket/scripts/build.mjs @@ -7,17 +7,17 @@ * 3. Copies LICENSE, CHANGELOG.md, and logo images from repo root */ -import { existsSync, mkdirSync, writeFileSync } from 'node:fs' -import { promises as fs } from 'node:fs' +import { existsSync, promises as fs, mkdirSync, writeFileSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' import { build } from 'esbuild' -import { getSpinner } from '@socketsecurity/lib/constants/process' + import { WIN32 } from '@socketsecurity/lib/constants/platform' +import { getSpinner } from '@socketsecurity/lib/constants/process' import { getDefaultLogger } from '@socketsecurity/lib/logger' -import { Spinner, withSpinner } from '@socketsecurity/lib/spinner' import { spawn } from '@socketsecurity/lib/spawn' +import { Spinner, withSpinner } from '@socketsecurity/lib/spinner' import seaConfig from './esbuild.bootstrap.config.mjs' @@ -30,11 +30,11 @@ const logger = getDefaultLogger() async function ensureBootstrapPackageBuilt() { const bootstrapSource = path.join( monorepoRoot, - 'packages/bootstrap/src/bootstrap-npm.mts' + 'packages/bootstrap/src/bootstrap-npm.mts', ) const bootstrapDist = path.join( monorepoRoot, - 'packages/bootstrap/dist/bootstrap-npm.js' + 'packages/bootstrap/dist/bootstrap-npm.js', ) logger.group('Checking bootstrap package') @@ -66,7 +66,7 @@ async function ensureBootstrapPackageBuilt() { cwd: monorepoRoot, shell: WIN32, stdio: 'pipe', - } + }, ) if (spawnResult.code !== 0) { diff --git a/packages/socket/scripts/esbuild.bootstrap.config.mjs b/packages/socket/scripts/esbuild.bootstrap.config.mjs index 6bdca63d6..908d10194 100644 --- a/packages/socket/scripts/esbuild.bootstrap.config.mjs +++ b/packages/socket/scripts/esbuild.bootstrap.config.mjs @@ -6,9 +6,9 @@ import { readFileSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' +import { deadCodeEliminationPlugin } from 'build-infra/lib/esbuild-plugin-dead-code-elimination' import semver from 'semver' -import { deadCodeEliminationPlugin } from 'build-infra/lib/esbuild-plugin-dead-code-elimination' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const packageRoot = path.resolve(__dirname, '..') @@ -35,7 +35,7 @@ export default { __MIN_NODE_VERSION__: JSON.stringify(minNodeVersion), __SOCKET_CLI_VERSION__: JSON.stringify(cliVersion), __SOCKET_CLI_VERSION_MAJOR__: JSON.stringify(cliVersionMajor), - 'INLINED_SOCKET_BOOTSTRAP_PUBLISHED_BUILD': 'true', + INLINED_SOCKET_BOOTSTRAP_PUBLISHED_BUILD: 'true', }, entryPoints: [path.join(bootstrapPackage, 'src', 'bootstrap-npm.mts')], external: [], diff --git a/packages/socket/scripts/verify-package.mjs b/packages/socket/scripts/verify-package.mjs index 2dbd74a7c..411402ff9 100644 --- a/packages/socket/scripts/verify-package.mjs +++ b/packages/socket/scripts/verify-package.mjs @@ -3,9 +3,10 @@ import path from 'node:path' import process from 'node:process' import { fileURLToPath } from 'node:url' -import { getDefaultLogger } from '@socketsecurity/lib/logger' import colors from 'yoctocolors-cjs' +import { getDefaultLogger } from '@socketsecurity/lib/logger' + const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const packageRoot = path.resolve(__dirname, '..') diff --git a/packages/socket/test/bootstrap.test.mjs b/packages/socket/test/bootstrap.test.mjs index 8a454ca1f..d542a4130 100644 --- a/packages/socket/test/bootstrap.test.mjs +++ b/packages/socket/test/bootstrap.test.mjs @@ -7,14 +7,13 @@ */ import { spawnSync } from 'node:child_process' -import { existsSync } from 'node:fs' -import { promises as fs } from 'node:fs' +import { existsSync, promises as fs } from 'node:fs' import { homedir, platform, tmpdir } from 'node:os' import path from 'node:path' import { fileURLToPath } from 'node:url' import { brotliCompressSync } from 'node:zlib' -import { describe, expect, it, beforeEach, afterEach } from 'vitest' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const packageDir = path.join(__dirname, '..') @@ -61,7 +60,9 @@ describe('socket package', () => { expect(pkgJson.optionalDependencies).toBeDefined() for (const platformPkg of expectedPlatforms) { - expect(pkgJson.optionalDependencies[platformPkg]).toMatch(/^\^?\d+\.\d+\.\d+/) + expect(pkgJson.optionalDependencies[platformPkg]).toMatch( + /^\^?\d+\.\d+\.\d+/, + ) } }) @@ -125,7 +126,8 @@ describe('socket package', () => { const result = spawnSync(process.execPath, [bootstrapPath, '--version'], { stdio: ['ignore', 'pipe', 'pipe'], - timeout: 60000, // 60s for npm download. + // 60s for npm download. + timeout: 60_000, }) // Should succeed. @@ -136,9 +138,17 @@ describe('socket package', () => { expect(stdout).toMatch(/\d+\.\d+\.\d+/) // Should have cached CLI. - const cliPath = path.join(testDir, '.socket', '_dlx', 'cli', 'dist', 'cli.js') + const cliPath = path.join( + testDir, + '.socket', + '_dlx', + 'cli', + 'dist', + 'cli.js', + ) expect(existsSync(cliPath)).toBe(true) - }, 120000) // 2 min timeout + // 2 min timeout + }, 120_000) it('should use local CLI path when SOCKET_CLI_LOCAL_PATH is set', async () => { // Create mock CLI directory. @@ -237,15 +247,19 @@ describe('socket package', () => { const invalidCliPath = path.join(invalidCliDir, 'bad-cli.js') await fs.writeFile(invalidCliPath, 'this is not valid javascript {{{') - const result = spawnSync(process.execPath, [bootstrapPath, '--version'], { - env: { - ...process.env, - HOME: testDir, - SOCKET_CLI_LOCAL_PATH: invalidCliPath, + const result = spawnSync( + process.execPath, + [bootstrapPath, '--version'], + { + env: { + ...process.env, + HOME: testDir, + SOCKET_CLI_LOCAL_PATH: invalidCliPath, + }, + stdio: ['ignore', 'pipe', 'pipe'], + timeout: 5000, }, - stdio: ['ignore', 'pipe', 'pipe'], - timeout: 5000, - }) + ) // Should fail gracefully. expect(result.status).not.toBe(0) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 557039c52..9a28c6642 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,9 +105,6 @@ catalogs: '@socketsecurity/registry': specifier: 2.0.2 version: 2.0.2 - '@socketsecurity/sdk': - specifier: 3.1.3 - version: 3.1.3 '@types/cmd-shim': specifier: 5.0.2 version: 5.0.2 @@ -479,8 +476,8 @@ importers: specifier: 'catalog:' version: 2.0.2(typescript@5.9.3) '@socketsecurity/sdk': - specifier: 'catalog:' - version: 3.1.3 + specifier: file:///Users/billli/code/socketdev/socket-sdk-js/socketsecurity-sdk-3.1.3.tgz + version: file:../../code/socketdev/socket-sdk-js/socketsecurity-sdk-3.1.3.tgz(typescript@5.9.3) '@types/cmd-shim': specifier: 'catalog:' version: 5.0.2 @@ -794,8 +791,11 @@ importers: specifier: 'catalog:' version: 2.0.2(typescript@5.9.3) '@socketsecurity/sdk': - specifier: 'catalog:' - version: 3.1.3 + specifier: file:///Users/billli/code/socketdev/socket-sdk-js/socketsecurity-sdk-3.1.3.tgz + version: file:../../code/socketdev/socket-sdk-js/socketsecurity-sdk-3.1.3.tgz(typescript@5.9.3) + '@types/react': + specifier: ^19.2.2 + version: 19.2.2 ajv-dist: specifier: 'catalog:' version: 8.17.1 @@ -936,16 +936,16 @@ importers: specifier: 'catalog:' version: 0.25.11 - packages/socketbin-cli-alpine-arm64: {} - - packages/socketbin-cli-alpine-x64: {} - packages/socketbin-cli-darwin-arm64: {} packages/socketbin-cli-darwin-x64: {} packages/socketbin-cli-linux-arm64: {} + packages/socketbin-cli-linux-musl-arm64: {} + + packages/socketbin-cli-linux-musl-x64: {} + packages/socketbin-cli-linux-x64: {} packages/socketbin-cli-win32-arm64: {} @@ -2305,9 +2305,10 @@ packages: typescript: optional: true - '@socketsecurity/sdk@3.1.3': - resolution: {integrity: sha512-GSOysDaKLAtXY6ZLJdf3YKPliCPxsa84J4JXBzTOZA6X+xSdcOPu1423VUmr5tG9ZJTRWH5z0fMNq3EyeALONA==} - engines: {node: '>=18', pnpm: '>=10.16.0'} + '@socketsecurity/sdk@file:../../code/socketdev/socket-sdk-js/socketsecurity-sdk-3.1.3.tgz': + resolution: {integrity: sha512-kE25ZK8z/66x8mZ76jU+kYOZOVifUKj5lPosjuTcGf3/hSPPEvd0qi+KApao7vGMG9N/wnreoW2ixXzSb8vPBg==, tarball: file:../../code/socketdev/socket-sdk-js/socketsecurity-sdk-3.1.3.tgz} + version: 3.1.3 + engines: {node: '>=18', pnpm: '>=10.22.0'} '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} @@ -6812,7 +6813,11 @@ snapshots: optionalDependencies: typescript: 5.9.3 - '@socketsecurity/sdk@3.1.3': {} + '@socketsecurity/sdk@file:../../code/socketdev/socket-sdk-js/socketsecurity-sdk-3.1.3.tgz(typescript@5.9.3)': + dependencies: + '@socketsecurity/lib': 4.3.0(typescript@5.9.3) + transitivePeerDependencies: + - typescript '@standard-schema/spec@1.0.0': {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 048a02cd2..74c490869 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -14,6 +14,8 @@ catalog: '@babel/traverse': 7.28.4 '@babel/types': 7.28.5 '@biomejs/biome': 2.2.4 + '@coana-tech/cli': 14.12.51 + '@cyclonedx/cdxgen': 11.11.0 '@dotenvx/dotenvx': 1.49.0 '@eslint/compat': 1.3.2 '@eslint/js': 9.35.0 @@ -39,7 +41,6 @@ catalog: '@socketsecurity/config': 3.0.1 '@socketsecurity/lib': 4.3.0 '@socketsecurity/registry': 2.0.2 - '@socketsecurity/sdk': 3.1.3 '@types/cmd-shim': 5.0.2 '@types/ink': 2.0.3 '@types/js-yaml': 4.0.9 diff --git a/scripts/apply-socket-mods.mjs b/scripts/apply-socket-mods.mjs index 5864293b2..c063fe80e 100644 --- a/scripts/apply-socket-mods.mjs +++ b/scripts/apply-socket-mods.mjs @@ -11,9 +11,10 @@ import { readFile, writeFile } from 'node:fs/promises' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' -import { getDefaultLogger } from '@socketsecurity/lib/logger' + import colors from 'yoctocolors-cjs' +import { getDefaultLogger } from '@socketsecurity/lib/logger' const logger = getDefaultLogger() const __filename = fileURLToPath(import.meta.url) @@ -95,7 +96,9 @@ async function fixV8IncludePaths() { await writeFile(filePath, content, 'utf8') } } catch (e) { - logger.warn(` ${colors.yellow('⚠')} Could not fix ${file}: ${e.message}`) + logger.warn( + ` ${colors.yellow('⚠')} Could not fix ${file}: ${e.message}`, + ) } } @@ -128,7 +131,9 @@ const { getAsset: getAssetInternal, getAssetKeys: getAssetKeysInternal } = inter logger.log(' ℹ️ lib/sea.js already modified or structure changed') } } catch (e) { - logger.warn(` ${colors.yellow('⚠')} Could not modify lib/sea.js: ${e.message}`) + logger.warn( + ` ${colors.yellow('⚠')} Could not modify lib/sea.js: ${e.message}`, + ) } logger.log(`${colors.green('✓')} SEA detection enabled`) @@ -155,6 +160,9 @@ async function main() { // Run main function main().catch(error => { - logger.error(`${colors.red('✗')} Failed to apply modifications:`, error.message) + logger.error( + `${colors.red('✗')} Failed to apply modifications:`, + error.message, + ) process.exitCode = 1 }) diff --git a/scripts/build-all-from-source.mjs b/scripts/build-all-from-source.mjs index c4fe86058..5f9ddefa5 100644 --- a/scripts/build-all-from-source.mjs +++ b/scripts/build-all-from-source.mjs @@ -31,7 +31,7 @@ const ROOT_DIR = path.join(__dirname, '..') // Parse arguments. const args = process.argv.slice(2) const FORCE_BUILD = args.includes('--force') -const specificPackage = args.find((arg) => !arg.startsWith('--')) +const specificPackage = args.find(arg => !arg.startsWith('--')) // Build packages in order (dependencies first). const PACKAGES = [ @@ -39,7 +39,8 @@ const PACKAGES = [ name: 'build-infra', description: 'Shared build infrastructure', path: 'packages/build-infra', - build: false, // No build needed (utilities only) + // No build needed (utilities only) + build: false, }, { name: 'node-smol-builder', @@ -136,7 +137,7 @@ async function main() { let packagesToBuild = PACKAGES if (specificPackage) { - const pkg = PACKAGES.find((p) => p.name === specificPackage) + const pkg = PACKAGES.find(p => p.name === specificPackage) if (!pkg) { logger.fail(`Unknown package: ${specificPackage}`) logger.info('') @@ -176,7 +177,9 @@ async function main() { logger.info(`Total time: ${totalMinutes}m ${totalSeconds}s`) logger.log('') logger.info('Build artifacts:') - logger.info(' node-smol-builder: packages/node-smol-builder/build/out/Release/node') + logger.info( + ' node-smol-builder: packages/node-smol-builder/build/out/Release/node', + ) logger.info(' onnx-runtime: packages/onnx-runtime/build/wasm/') logger.info(' codet5-models: packages/codet5-models/build/models/') logger.info(' yoga-layout: packages/yoga-layout/build/wasm/') @@ -189,7 +192,7 @@ async function main() { } // Run main function. -main().catch((e) => { +main().catch(e => { logger.fail(`Build failed: ${e.message}`) process.exit(1) }) diff --git a/scripts/build-binaries.mjs b/scripts/build-binaries.mjs index ff113d842..6f3c44dae 100755 --- a/scripts/build-binaries.mjs +++ b/scripts/build-binaries.mjs @@ -1,4 +1,3 @@ -#!/usr/bin/env node /** * Build Socket binaries (WASM, SEA, smol) without running tests. * @@ -37,11 +36,19 @@ const tasks = [] // Build bootstrap first (required for all binaries). console.log('\n🔨 Building bootstrap...\n') -tasks.push({ name: 'bootstrap', cmd: 'pnpm', args: ['--filter', '@socketsecurity/bootstrap', 'run', 'build'] }) +tasks.push({ + name: 'bootstrap', + cmd: 'pnpm', + args: ['--filter', '@socketsecurity/bootstrap', 'run', 'build'], +}) // Build CLI (required for binaries). console.log('🔨 Building CLI...\n') -tasks.push({ name: 'cli', cmd: 'pnpm', args: ['--filter', '@socketsecurity/cli', 'run', 'build'] }) +tasks.push({ + name: 'cli', + cmd: 'pnpm', + args: ['--filter', '@socketsecurity/cli', 'run', 'build'], +}) // Build WASM (slowest, optional). if (buildWasm) { @@ -52,20 +59,30 @@ if (buildWasm) { // Build SEA binary. if (buildSea) { - const seaArgs = ['--filter', '@socketsecurity/node-sea-builder', 'run', 'build'] - if (values.dev) seaArgs.push('--dev') - if (values.prod) seaArgs.push('--prod') - if (values.clean) seaArgs.push('--clean') + const seaArgs = [ + '--filter', + '@socketsecurity/node-sea-builder', + 'run', + 'build', + ] + if (values.dev) {seaArgs.push('--dev')} + if (values.prod) {seaArgs.push('--prod')} + if (values.clean) {seaArgs.push('--clean')} console.log('🔨 Building SEA binary...\n') tasks.push({ name: 'sea', cmd: 'pnpm', args: seaArgs }) } // Build smol binary. if (buildSmol) { - const smolArgs = ['--filter', '@socketsecurity/node-smol-builder', 'run', 'build'] - if (values.dev) smolArgs.push('--dev') - if (values.prod) smolArgs.push('--prod') - if (values.clean) smolArgs.push('--clean') + const smolArgs = [ + '--filter', + '@socketsecurity/node-smol-builder', + 'run', + 'build', + ] + if (values.dev) {smolArgs.push('--dev')} + if (values.prod) {smolArgs.push('--prod')} + if (values.clean) {smolArgs.push('--clean')} console.log('🔨 Building smol binary...\n') tasks.push({ name: 'smol', cmd: 'pnpm', args: smolArgs }) } @@ -78,7 +95,7 @@ async function runTask(task) { shell: WIN32, }) - proc.on('close', (code) => { + proc.on('close', code => { if (code === 0) { resolve() } else { @@ -88,7 +105,7 @@ async function runTask(task) { }) } -(async () => { +;(async () => { const startTime = Date.now() for (const task of tasks) { diff --git a/scripts/build.mjs b/scripts/build.mjs index fe9ace0ef..c19b168cf 100755 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -1,4 +1,3 @@ -#!/usr/bin/env node /** * Comprehensive build script with intelligent caching. @@ -23,11 +22,11 @@ import path from 'node:path' import process from 'node:process' import { fileURLToPath } from 'node:url' +import colors from 'yoctocolors-cjs' + import { WIN32 } from '@socketsecurity/lib/constants/platform' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { spawn } from '@socketsecurity/lib/spawn' -import colors from 'yoctocolors-cjs' - const logger = getDefaultLogger() const __filename = fileURLToPath(import.meta.url) @@ -50,7 +49,7 @@ const TARGET_PACKAGES = { sea: '@socketbin/node-sea-builder-builder', socket: 'socket', 'win32-arm64': '@socketbin/cli-win32-arm64', - 'win32-x64': '@socketbin/cli-win32-x64' + 'win32-x64': '@socketbin/cli-win32-x64', } const PLATFORM_TARGETS = [ @@ -61,7 +60,7 @@ const PLATFORM_TARGETS = [ 'linux-arm64', 'linux-x64', 'win32-arm64', - 'win32-x64' + 'win32-x64', ] /** @@ -139,7 +138,17 @@ function parseArgs() { target = `${platform}-${arch}` } - return { arch, buildArgs, force, help, parallel, platform, platforms, target, targets } + return { + arch, + buildArgs, + force, + help, + parallel, + platform, + platforms, + target, + targets, + } } /** @@ -150,13 +159,25 @@ function showHelp() { logger.log(`${colors.blue('Socket CLI Build System')}`) logger.log('') logger.log('Usage:') - logger.log(' pnpm run build # Smart build (skips unchanged)') + logger.log( + ' pnpm run build # Smart build (skips unchanged)', + ) logger.log(' pnpm run build --force # Force rebuild all') - logger.log(' pnpm run build --target # Build specific target') - logger.log(' pnpm run build --platform

--arch # Build specific platform/arch') - logger.log(' pnpm run build --targets # Build multiple targets') - logger.log(' pnpm run build --platforms # Build all platform binaries') - logger.log(' pnpm run build --platforms --parallel # Build platforms in parallel') + logger.log( + ' pnpm run build --target # Build specific target', + ) + logger.log( + ' pnpm run build --platform

--arch # Build specific platform/arch', + ) + logger.log( + ' pnpm run build --targets # Build multiple targets', + ) + logger.log( + ' pnpm run build --platforms # Build all platform binaries', + ) + logger.log( + ' pnpm run build --platforms --parallel # Build platforms in parallel', + ) logger.log(' pnpm run build --help # Show this help') logger.log('') logger.log('Default Build Order:') @@ -205,7 +226,9 @@ async function buildPackage(pkg, force) { const skip = !needsBuild(pkg, force) if (skip) { - logger.log(`${colors.cyan('→')} ${pkg.name}: ${colors.gray('skipped (up to date)')}`) + logger.log( + `${colors.cyan('→')} ${pkg.name}: ${colors.gray('skipped (up to date)')}`, + ) return { success: true, skipped: true } } @@ -222,11 +245,15 @@ async function buildPackage(pkg, force) { const duration = ((Date.now() - startTime) / 1000).toFixed(1) if (result.code !== 0) { - logger.log(`${colors.red('✗')} ${pkg.name}: ${colors.red('failed')} (${duration}s)`) + logger.log( + `${colors.red('✗')} ${pkg.name}: ${colors.red('failed')} (${duration}s)`, + ) return { success: false, skipped: false } } - logger.log(`${colors.green('✓')} ${pkg.name}: ${colors.green('built')} (${duration}s)`) + logger.log( + `${colors.green('✓')} ${pkg.name}: ${colors.green('built')} (${duration}s)`, + ) return { success: true, skipped: false } } @@ -301,17 +328,13 @@ async function runTargetedBuild(target, buildArgs) { const packageFilter = TARGET_PACKAGES[target] if (!packageFilter) { logger.error(`Unknown build target: ${target}`) - logger.error(`Available targets: ${Object.keys(TARGET_PACKAGES).join(', ')}`) + logger.error( + `Available targets: ${Object.keys(TARGET_PACKAGES).join(', ')}`, + ) process.exit(1) } - const pnpmArgs = [ - '--filter', - packageFilter, - 'run', - 'build', - ...buildArgs - ] + const pnpmArgs = ['--filter', packageFilter, 'run', 'build', ...buildArgs] const result = await spawn('pnpm', pnpmArgs, { shell: WIN32, @@ -333,13 +356,7 @@ async function buildTarget(target, buildArgs) { const startTime = Date.now() logger.log(`${colors.cyan('→')} [${target}] Starting build...`) - const pnpmArgs = [ - '--filter', - packageFilter, - 'run', - 'build', - ...buildArgs - ] + const pnpmArgs = ['--filter', packageFilter, 'run', 'build', ...buildArgs] const result = await spawn('pnpm', pnpmArgs, { shell: WIN32, @@ -349,7 +366,9 @@ async function buildTarget(target, buildArgs) { const duration = ((Date.now() - startTime) / 1000).toFixed(1) if (result.code === 0) { - logger.log(`${colors.green('✓')} [${target}] Build succeeded (${duration}s)`) + logger.log( + `${colors.green('✓')} [${target}] Build succeeded (${duration}s)`, + ) return { success: true, target, duration } } @@ -367,7 +386,9 @@ async function buildTarget(target, buildArgs) { async function runParallelBuilds(targetsToBuild, buildArgs) { logger.log('') logger.log('='.repeat(60)) - logger.log(`${colors.blue('Building ' + targetsToBuild.length + ' targets in parallel')}`) + logger.log( + `${colors.blue('Building ' + targetsToBuild.length + ' targets in parallel')}`, + ) logger.log('='.repeat(60)) logger.log('') logger.log(`Targets: ${targetsToBuild.join(', ')}`) @@ -375,7 +396,7 @@ async function runParallelBuilds(targetsToBuild, buildArgs) { const startTime = Date.now() const results = await Promise.allSettled( - targetsToBuild.map(target => buildTarget(target, buildArgs)) + targetsToBuild.map(target => buildTarget(target, buildArgs)), ) const totalDuration = ((Date.now() - startTime) / 1000).toFixed(1) @@ -386,8 +407,13 @@ async function runParallelBuilds(targetsToBuild, buildArgs) { logger.log('='.repeat(60)) logger.log('') - const successful = results.filter(r => r.status === 'fulfilled' && r.value.success).length - const failed = results.filter(r => r.status === 'rejected' || (r.status === 'fulfilled' && !r.value.success)).length + const successful = results.filter( + r => r.status === 'fulfilled' && r.value.success, + ).length + const failed = results.filter( + r => + r.status === 'rejected' || (r.status === 'fulfilled' && !r.value.success), + ).length logger.log(`${colors.green('Succeeded:')} ${successful}`) logger.log(`${colors.red('Failed:')} ${failed}`) @@ -411,7 +437,9 @@ async function runParallelBuilds(targetsToBuild, buildArgs) { async function runSequentialBuilds(targetsToBuild, buildArgs) { logger.log('') logger.log('='.repeat(60)) - logger.log(`${colors.blue('Building ' + targetsToBuild.length + ' targets sequentially')}`) + logger.log( + `${colors.blue('Building ' + targetsToBuild.length + ' targets sequentially')}`, + ) logger.log('='.repeat(60)) logger.log('') logger.log(`Targets: ${targetsToBuild.join(', ')}`) @@ -446,7 +474,9 @@ async function runSequentialBuilds(targetsToBuild, buildArgs) { logger.log('') if (failed > 0) { - logger.log(`${colors.red('✗')} Build failed at target: ${results.find(r => !r.success)?.target}`) + logger.log( + `${colors.red('✗')} Build failed at target: ${results.find(r => !r.success)?.target}`, + ) logger.log('') process.exit(1) } diff --git a/scripts/check-build-deps.mjs b/scripts/check-build-deps.mjs index 1a45a7116..7c5da3811 100644 --- a/scripts/check-build-deps.mjs +++ b/scripts/check-build-deps.mjs @@ -13,10 +13,10 @@ import { existsSync } from 'node:fs' import { platform } from 'node:os' -import { spawn } from '@socketsecurity/lib/spawn' -import { getDefaultLogger } from '@socketsecurity/lib/logger' import colors from 'yoctocolors-cjs' +import { getDefaultLogger } from '@socketsecurity/lib/logger' +import { spawn } from '@socketsecurity/lib/spawn' const logger = getDefaultLogger() const IS_MACOS = platform() === 'darwin' @@ -70,7 +70,8 @@ async function checkDiskSpace() { const lines = result.stdout.trim().split('\n') if (lines.length > 1) { const parts = lines[1].split(/\s+/) - return parts[3] // Available space + // Available space + return parts[3] } } } catch { @@ -164,7 +165,9 @@ async function main() { const gcc = await commandExists('gcc') const gccVersion = gcc ? await getVersion('gcc') : null checks.push({ name: 'gcc', required: true, found: gcc, version: gccVersion }) - logger.log(` ${gcc ? `${colors.green('✓')}` : `${colors.red('✗')}`} gcc: ${gccVersion || 'not found'}`) + logger.log( + ` ${gcc ? `${colors.green('✓')}` : `${colors.red('✗')}`} gcc: ${gccVersion || 'not found'}`, + ) if (!gcc) { hasErrors = true } @@ -172,7 +175,9 @@ async function main() { const gxx = await commandExists('g++') const gxxVersion = gxx ? await getVersion('g++') : null checks.push({ name: 'g++', required: true, found: gxx, version: gxxVersion }) - logger.log(` ${gxx ? `${colors.green('✓')}` : `${colors.red('✗')}`} g++: ${gxxVersion || 'not found'}`) + logger.log( + ` ${gxx ? `${colors.green('✓')}` : `${colors.red('✗')}`} g++: ${gxxVersion || 'not found'}`, + ) if (!gxx) { hasErrors = true } @@ -185,7 +190,9 @@ async function main() { found: make, version: makeVersion, }) - logger.log(` ${make ? `${colors.green('✓')}` : `${colors.red('✗')}`} make: ${makeVersion || 'not found'}`) + logger.log( + ` ${make ? `${colors.green('✓')}` : `${colors.red('✗')}`} make: ${makeVersion || 'not found'}`, + ) if (!make) { hasErrors = true } @@ -208,7 +215,9 @@ async function main() { const git = await commandExists('git') const gitVersion = git ? await getVersion('git') : null checks.push({ name: 'git', required: true, found: git, version: gitVersion }) - logger.log(` ${git ? `${colors.green('✓')}` : `${colors.red('✗')}`} git: ${gitVersion || 'not found'}`) + logger.log( + ` ${git ? `${colors.green('✓')}` : `${colors.red('✗')}`} git: ${gitVersion || 'not found'}`, + ) if (!git) { hasErrors = true } @@ -225,12 +234,12 @@ async function main() { if (IS_MACOS) { logger.log(' ℹ️ UPX: not used on macOS (incompatible with code signing)') } else { - logger.log(` ${upx ? `${colors.green('✓')}` : `${colors.yellow('⚠')} `} upx: ${upxVersion || 'not found'}`) + logger.log( + ` ${upx ? `${colors.green('✓')}` : `${colors.yellow('⚠')} `} upx: ${upxVersion || 'not found'}`, + ) if (!upx) { hasWarnings = true - logger.log( - ' UPX enables 30-50% binary compression on Linux/Windows', - ) + logger.log(' UPX enables 30-50% binary compression on Linux/Windows') logger.log( ' Build will succeed without UPX but produce larger binaries', ) @@ -247,9 +256,7 @@ async function main() { logger.log('') // Check existing build - const nodeBuilt = existsSync( - 'build/node-smol/out/Release/node', - ) + const nodeBuilt = existsSync('build/node-smol/out/Release/node') if (nodeBuilt) { logger.log(`${colors.green('✓')} Custom Node.js binary already built`) logger.log(' Location: build/node-smol/out/Release/node') @@ -297,9 +304,7 @@ async function main() { logger.log('') } else if (IS_WINDOWS) { logger.log(' Windows (Chocolatey):') - logger.log( - ' $ choco install visualstudio2022buildtools python git upx', - ) + logger.log(' $ choco install visualstudio2022buildtools python git upx') logger.log('') logger.log(' Or use WSL2 (recommended):') logger.log(' $ wsl --install -d Ubuntu') diff --git a/scripts/check-version-consistency.mjs b/scripts/check-version-consistency.mjs index 3be18d343..14b5d4a33 100644 --- a/scripts/check-version-consistency.mjs +++ b/scripts/check-version-consistency.mjs @@ -2,9 +2,10 @@ import { promises as fs } from 'node:fs' import path from 'node:path' import process from 'node:process' import { fileURLToPath } from 'node:url' -import { getDefaultLogger } from '@socketsecurity/lib/logger' + import colors from 'yoctocolors-cjs' +import { getDefaultLogger } from '@socketsecurity/lib/logger' const logger = getDefaultLogger() const __filename = fileURLToPath(import.meta.url) @@ -41,7 +42,9 @@ async function checkVersionConsistency() { // Only check root package.json if the expected version looks like a socketbin version // (contains timestamp like YYYYMMDD.HHmmss) or if root package version matches. const isSocketbinVersion = /\d{8}\.\d{6}/.test(cleanVersion) - const shouldCheckRoot = isSocketbinVersion ? mainPkg.version === cleanVersion : true + const shouldCheckRoot = isSocketbinVersion + ? mainPkg.version === cleanVersion + : true if (shouldCheckRoot) { checked.push({ diff --git a/scripts/check.mjs b/scripts/check.mjs index 2cf0a48d2..06111f823 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -87,7 +87,13 @@ async function runEslintCheck(options = {}) { } // Run lint across affected packages. - return await runAcrossPackages(packages, 'lint', [], quiet, 'Running ESLint checks') + return await runAcrossPackages( + packages, + 'lint', + [], + quiet, + 'Running ESLint checks', + ) } /** @@ -107,7 +113,13 @@ async function runTypeCheck(options = {}) { } // Run type check across packages. - return await runAcrossPackages(packages, 'type', [], quiet, 'Running TypeScript checks') + return await runAcrossPackages( + packages, + 'type', + [], + quiet, + 'Running TypeScript checks', + ) } async function main() { @@ -141,11 +153,15 @@ async function main() { logger.log(' --changed Check packages with changed files') logger.log(' --quiet, --silent Suppress progress messages') logger.log('\nExamples:') - logger.log(' pnpm check # Run all checks on changed packages') + logger.log( + ' pnpm check # Run all checks on changed packages', + ) logger.log(' pnpm check --all # Run all checks on all packages') logger.log(' pnpm check --lint # Run ESLint only') logger.log(' pnpm check --types # Run TypeScript only') - logger.log(' pnpm check --lint --staged # Run ESLint on staged packages') + logger.log( + ' pnpm check --lint --staged # Run ESLint on staged packages', + ) process.exitCode = 0 return } diff --git a/scripts/clean-cache.mjs b/scripts/clean-cache.mjs index 15871bfd5..98a1ebd99 100755 --- a/scripts/clean-cache.mjs +++ b/scripts/clean-cache.mjs @@ -1,4 +1,3 @@ -#!/usr/bin/env node /** * Clean stale caches across all packages. * @@ -10,8 +9,8 @@ import { readdirSync, rmSync, statSync } from 'node:fs' import { join } from 'node:path' -import { parseArgs } from 'node:util' import { fileURLToPath } from 'node:url' +import { parseArgs } from 'node:util' const __dirname = fileURLToPath(new URL('.', import.meta.url)) const ROOT_DIR = join(__dirname, '..') @@ -70,7 +69,9 @@ function analyzeCacheDir(cacheDir) { path: itemPath, size: getDirSize(itemPath), mtime: stats.mtime, - ageD: Math.floor((Date.now() - stats.mtime.getTime()) / (1000 * 60 * 60 * 24)), + ageD: Math.floor( + (Date.now() - stats.mtime.getTime()) / (1000 * 60 * 60 * 24), + ), }) } } @@ -107,9 +108,10 @@ function getDirSize(dir) { * Format bytes to human readable. */ function formatSize(bytes) { - if (bytes < 1024) return `${bytes} B` - if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` - if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB` + if (bytes < 1024) {return `${bytes} B`} + if (bytes < 1024 * 1024) {return `${(bytes / 1024).toFixed(1)} KB`} + if (bytes < 1024 * 1024 * 1024) + {return `${(bytes / (1024 * 1024)).toFixed(1)} MB`} return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB` } @@ -120,7 +122,9 @@ if (!cacheDirs.length) { process.exit(0) } -console.log(`Found ${cacheDirs.length} cache director${cacheDirs.length === 1 ? 'y' : 'ies'}:\n`) +console.log( + `Found ${cacheDirs.length} cache director${cacheDirs.length === 1 ? 'y' : 'ies'}:\n`, +) let totalDeleted = 0 let totalSize = 0 @@ -138,7 +142,9 @@ for (const { package: pkg, path: cacheDir } of cacheDirs) { if (cleanAll) { // Delete everything. for (const entry of entries) { - console.log(` ${dryRun ? '[DRY RUN]' : '✗'} ${entry.name} (${formatSize(entry.size)}, ${entry.ageD}d old)`) + console.log( + ` ${dryRun ? '[DRY RUN]' : '✗'} ${entry.name} (${formatSize(entry.size)}, ${entry.ageD}d old)`, + ) if (!dryRun) { rmSync(entry.path, { recursive: true, force: true }) } @@ -149,10 +155,14 @@ for (const { package: pkg, path: cacheDir } of cacheDirs) { // Keep most recent, delete older ones. const [latest, ...older] = entries - console.log(` ✓ ${latest.name} (${formatSize(latest.size)}, ${latest.ageD}d old) - KEEP`) + console.log( + ` ✓ ${latest.name} (${formatSize(latest.size)}, ${latest.ageD}d old) - KEEP`, + ) for (const entry of older) { - console.log(` ${dryRun ? '[DRY RUN]' : '✗'} ${entry.name} (${formatSize(entry.size)}, ${entry.ageD}d old)`) + console.log( + ` ${dryRun ? '[DRY RUN]' : '✗'} ${entry.name} (${formatSize(entry.size)}, ${entry.ageD}d old)`, + ) if (!dryRun) { rmSync(entry.path, { recursive: true, force: true }) } diff --git a/scripts/clean.mjs b/scripts/clean.mjs index 8b008181b..6ec35fff0 100644 --- a/scripts/clean.mjs +++ b/scripts/clean.mjs @@ -119,9 +119,7 @@ async function main() { logger.log('\nUsage: pnpm clean [options]') logger.log('\nOptions:') logger.log(' --help Show this help message') - logger.log( - ' --all Clean everything (default if no flags)', - ) + logger.log(' --all Clean everything (default if no flags)') logger.log(' --cache Clean cache directories') logger.log(' --coverage Clean coverage reports') logger.log(' --dist Clean build output') diff --git a/scripts/create-sea-symlinks.mjs b/scripts/create-sea-symlinks.mjs index 9458e870a..4e91553d2 100644 --- a/scripts/create-sea-symlinks.mjs +++ b/scripts/create-sea-symlinks.mjs @@ -19,9 +19,10 @@ import { promises as fs } from 'node:fs' import path from 'node:path' import process from 'node:process' -import { getDefaultLogger } from '@socketsecurity/lib/logger' + import colors from 'yoctocolors-cjs' +import { getDefaultLogger } from '@socketsecurity/lib/logger' const logger = getDefaultLogger() const COMMANDS = ['socket-npm', 'socket-npx', 'socket-pnpm', 'socket-yarn'] diff --git a/scripts/fix.mjs b/scripts/fix.mjs index 253b08224..3b96f0c61 100644 --- a/scripts/fix.mjs +++ b/scripts/fix.mjs @@ -20,7 +20,6 @@ import { getDefaultLogger } from '@socketsecurity/lib/logger' import { spawn } from '@socketsecurity/lib/spawn' import { printHeader } from '@socketsecurity/lib/stdio/header' - const logger = getDefaultLogger() async function main() { const { values } = parseArgs({ diff --git a/scripts/generate-node-patches.mjs b/scripts/generate-node-patches.mjs index d7e15ad4d..ca332ff23 100644 --- a/scripts/generate-node-patches.mjs +++ b/scripts/generate-node-patches.mjs @@ -14,10 +14,10 @@ import { mkdir, writeFile } from 'node:fs/promises' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' -import { spawn } from '@socketsecurity/lib/spawn' -import { getDefaultLogger } from '@socketsecurity/lib/logger' import colors from 'yoctocolors-cjs' +import { getDefaultLogger } from '@socketsecurity/lib/logger' +import { spawn } from '@socketsecurity/lib/spawn' const logger = getDefaultLogger() const __filename = fileURLToPath(import.meta.url) @@ -180,7 +180,10 @@ async function main() { try { patches.push(await generateV8IncludePathsPatch()) } catch (e) { - logger.error(`${colors.red('✗')} Failed to generate V8 include paths patch:`, e.message) + logger.error( + `${colors.red('✗')} Failed to generate V8 include paths patch:`, + e.message, + ) } try { @@ -199,9 +202,7 @@ async function main() { logger.log('') logger.log('📝 Next steps:') logger.log(' 1. Review the generated patches') - logger.log( - ' 2. Update build-yao-pkg-node.mjs to reference new patch files', - ) + logger.log(' 2. Update build-yao-pkg-node.mjs to reference new patch files') logger.log(' 3. Test the build with new patches') logger.log('') } diff --git a/scripts/lib/build-exec.mjs b/scripts/lib/build-exec.mjs index fd25ebbb9..eea8f1c55 100644 --- a/scripts/lib/build-exec.mjs +++ b/scripts/lib/build-exec.mjs @@ -9,7 +9,6 @@ import { spawn } from '@socketsecurity/lib/spawn' import { saveBuildLog } from './build-helpers.mjs' - const logger = getDefaultLogger() /** * Execute a command and stream output. diff --git a/scripts/lib/build-helpers.mjs b/scripts/lib/build-helpers.mjs index 5aeec7cbe..0ebebce9e 100644 --- a/scripts/lib/build-helpers.mjs +++ b/scripts/lib/build-helpers.mjs @@ -7,10 +7,10 @@ import { promises as fs, statfsSync } from 'node:fs' import { join } from 'node:path' -import { spawn } from '@socketsecurity/lib/spawn' -import { getDefaultLogger } from '@socketsecurity/lib/logger' import colors from 'yoctocolors-cjs' +import { getDefaultLogger } from '@socketsecurity/lib/logger' +import { spawn } from '@socketsecurity/lib/spawn' const logger = getDefaultLogger() /** @@ -47,7 +47,8 @@ export async function checkDiskSpace(path) { return { availableGB: Math.floor(availableGB), availableBytes, - sufficient: availableGB >= 5, // Need 5GB for build. + // Need 5GB for build. + sufficient: availableGB >= 5, } } catch (e) { // If we can't check, assume it's fine (don't block builds). @@ -358,7 +359,8 @@ export function estimateBuildTime(cpuCount) { // 4 cores: ~60 min // 2 cores: ~90 min - const baseTime = 300 // 300 seconds for 10 cores. + // 300 seconds for 10 cores. + const baseTime = 300 const adjustedTime = (baseTime * 10) / cpuCount const minutes = Math.round(adjustedTime / 60) @@ -430,11 +432,9 @@ export async function smokeTestBinary(binaryPath, env = {}) { } // Test 2: Execute simple JS. - const jsResult = await execCapture( - binaryPath, - ['-e', 'logger.log("OK")'], - { env }, - ) + const jsResult = await execCapture(binaryPath, ['-e', 'logger.log("OK")'], { + env, + }) if (jsResult.code !== 0 || jsResult.stdout !== 'OK') { return { passed: false, reason: 'JS execution failed' } } @@ -498,7 +498,9 @@ export async function installHomebrew(exec) { logger.log('') return true } catch (e) { - logger.error(`${colors.red('✗')} Homebrew installation failed: ${e.message}`) + logger.error( + `${colors.red('✗')} Homebrew installation failed: ${e.message}`, + ) logger.error('Install manually: https://brew.sh') logger.error() return false @@ -523,7 +525,9 @@ export async function installBrewPackage(packageName, exec) { logger.log('') return true } catch (e) { - logger.error(`${colors.red('✗')} ${packageName} installation failed: ${e.message}`) + logger.error( + `${colors.red('✗')} ${packageName} installation failed: ${e.message}`, + ) logger.error(`Try manually: brew install ${packageName}`) logger.error() return false diff --git a/scripts/lib/build-output.mjs b/scripts/lib/build-output.mjs index 9164119e6..b55de31bb 100644 --- a/scripts/lib/build-output.mjs +++ b/scripts/lib/build-output.mjs @@ -6,7 +6,6 @@ import { getDefaultLogger } from '@socketsecurity/lib/logger' - const logger = getDefaultLogger() /** * Print section header. diff --git a/scripts/lib/patch-validator.mjs b/scripts/lib/patch-validator.mjs index 79302537b..0289689ab 100644 --- a/scripts/lib/patch-validator.mjs +++ b/scripts/lib/patch-validator.mjs @@ -20,9 +20,10 @@ export function parsePatchMetadata(patchContent) { } for (const line of lines) { + // Stop at first non-comment if (!line.startsWith('#')) { break - } // Stop at first non-comment + } // Parse metadata directives. if (line.includes('@node-versions:')) { diff --git a/scripts/lint.mjs b/scripts/lint.mjs index 0b5f72a41..63e730c13 100644 --- a/scripts/lint.mjs +++ b/scripts/lint.mjs @@ -126,8 +126,11 @@ async function main() { // Display what we're linting. if (!quiet) { const modeText = mode === 'all' ? 'all packages' : `${mode} packages` - logger.step(`Linting ${modeText} (${packages.length} package${packages.length > 1 ? 's' : ''})`) - logger.error('') // Blank line. + logger.step( + `Linting ${modeText} (${packages.length} package${packages.length > 1 ? 's' : ''})`, + ) + // Blank line. + logger.error('') } // Run lint across affected packages. diff --git a/scripts/llm/compute-embeddings-pure.mjs b/scripts/llm/compute-embeddings-pure.mjs index b14240227..ec706f3b1 100644 --- a/scripts/llm/compute-embeddings-pure.mjs +++ b/scripts/llm/compute-embeddings-pure.mjs @@ -8,9 +8,9 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' import * as ort from 'onnxruntime-node' -import { getDefaultLogger } from '@socketsecurity/lib/logger' import colors from 'yoctocolors-cjs' +import { getDefaultLogger } from '@socketsecurity/lib/logger' const logger = getDefaultLogger() const __dirname = path.dirname(fileURLToPath(import.meta.url)) diff --git a/scripts/llm/download-minilm.mjs b/scripts/llm/download-minilm.mjs index e5b73b659..0de6b6fe5 100644 --- a/scripts/llm/download-minilm.mjs +++ b/scripts/llm/download-minilm.mjs @@ -27,9 +27,10 @@ import { promises as fs } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' -import { getDefaultLogger } from '@socketsecurity/lib/logger' + import colors from 'yoctocolors-cjs' +import { getDefaultLogger } from '@socketsecurity/lib/logger' const logger = getDefaultLogger() const __dirname = path.dirname(fileURLToPath(import.meta.url)) diff --git a/scripts/llm/generate-semantic-index.mjs b/scripts/llm/generate-semantic-index.mjs index 11426d192..7cf5bdc98 100644 --- a/scripts/llm/generate-semantic-index.mjs +++ b/scripts/llm/generate-semantic-index.mjs @@ -28,9 +28,10 @@ import { readFileSync, writeFileSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' -import { getDefaultLogger } from '@socketsecurity/lib/logger' + import colors from 'yoctocolors-cjs' +import { getDefaultLogger } from '@socketsecurity/lib/logger' const logger = getDefaultLogger() // Get the directory of this script file. @@ -148,7 +149,8 @@ function extractWords(text) { .toLowerCase() .replace(/[^\w\s-]/g, '') .split(/\s+/) - .filter(w => w.length > 2) // Filter short words. + // Filter short words. + .filter(w => w.length > 2) // Canonicalize. return words.map(canonicalize) diff --git a/scripts/llm/generate-skill-embeddings.mjs b/scripts/llm/generate-skill-embeddings.mjs index e81ad8c12..913754f77 100644 --- a/scripts/llm/generate-skill-embeddings.mjs +++ b/scripts/llm/generate-skill-embeddings.mjs @@ -9,9 +9,9 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' import { pipeline } from '@xenova/transformers' -import { getDefaultLogger } from '@socketsecurity/lib/logger' import colors from 'yoctocolors-cjs' +import { getDefaultLogger } from '@socketsecurity/lib/logger' const logger = getDefaultLogger() const __dirname = path.dirname(fileURLToPath(import.meta.url)) diff --git a/scripts/optimize-binary-size.mjs b/scripts/optimize-binary-size.mjs index 07ee41a41..c852c8524 100755 --- a/scripts/optimize-binary-size.mjs +++ b/scripts/optimize-binary-size.mjs @@ -1,4 +1,3 @@ -#!/usr/bin/env node /** * Binary Size Optimization Script * @@ -20,12 +19,12 @@ import { platform as osPlatform } from 'node:os' import path from 'node:path' import { fileURLToPath } from 'node:url' +import colors from 'yoctocolors-cjs' + import { WIN32 } from '@socketsecurity/lib/constants/platform' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { spawn } from '@socketsecurity/lib/spawn' -import colors from 'yoctocolors-cjs' - const logger = getDefaultLogger() const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -121,7 +120,7 @@ async function optimizeDarwin(binaryPath) { // strip and llvm-strip already handle this well. const afterSize = await getFileSizeMB(binaryPath) - const savings = ((beforeSize - afterSize) / beforeSize * 100).toFixed(1) + const savings = (((beforeSize - afterSize) / beforeSize) * 100).toFixed(1) logger.log(`\n After: ${afterSize} MB (${savings}% reduction)`) // Re-sign binary if on macOS ARM64 (required). @@ -130,7 +129,11 @@ async function optimizeDarwin(binaryPath) { await exec('codesign', ['--force', '--sign', '-', binaryPath]) } - return { before: parseFloat(beforeSize), after: parseFloat(afterSize), savings: parseFloat(savings) } + return { + before: parseFloat(beforeSize), + after: parseFloat(afterSize), + savings: parseFloat(savings), + } } /** @@ -168,10 +171,14 @@ async function optimizeLinux(binaryPath) { } const afterSize = await getFileSizeMB(binaryPath) - const savings = ((beforeSize - afterSize) / beforeSize * 100).toFixed(1) + const savings = (((beforeSize - afterSize) / beforeSize) * 100).toFixed(1) logger.log(`\n After: ${afterSize} MB (${savings}% reduction)`) - return { before: parseFloat(beforeSize), after: parseFloat(afterSize), savings: parseFloat(savings) } + return { + before: parseFloat(beforeSize), + after: parseFloat(afterSize), + savings: parseFloat(savings), + } } /** @@ -195,10 +202,14 @@ async function optimizeWindows(binaryPath) { } const afterSize = await getFileSizeMB(binaryPath) - const savings = ((beforeSize - afterSize) / beforeSize * 100).toFixed(1) + const savings = (((beforeSize - afterSize) / beforeSize) * 100).toFixed(1) logger.log(`\n After: ${afterSize} MB (${savings}% reduction)`) - return { before: parseFloat(beforeSize), after: parseFloat(afterSize), savings: parseFloat(savings) } + return { + before: parseFloat(beforeSize), + after: parseFloat(afterSize), + savings: parseFloat(savings), + } } /** @@ -268,7 +279,12 @@ async function optimizeAllBinaries() { for (const pkg of packages) { if (pkg.startsWith('socketbin-cli-')) { - const binPath = path.join(packagesDir, pkg, 'bin', file.replace('*', '')) + const binPath = path.join( + packagesDir, + pkg, + 'bin', + file.replace('*', ''), + ) if (existsSync(binPath)) { const stats = await fs.stat(binPath) // Only process actual binaries (>1MB), not placeholders. @@ -305,7 +321,7 @@ async function optimizeAllBinaries() { */ async function main() { logger.log('⚡ Socket CLI Binary Size Optimizer') - logger.log('=' .repeat(50)) + logger.log('='.repeat(50)) let results = [] @@ -319,11 +335,17 @@ async function main() { } else { logger.error(`\n${colors.red('✗')} Error: No binary specified`) logger.log('\nUsage:') - logger.log(' node scripts/optimize-binary-size.mjs [--platform=]') + logger.log( + ' node scripts/optimize-binary-size.mjs [--platform=]', + ) logger.log(' node scripts/optimize-binary-size.mjs --all') logger.log('\nExamples:') - logger.log(' node scripts/optimize-binary-size.mjs packages/socketbin-cli-darwin-arm64/bin/socket') - logger.log(' node scripts/optimize-binary-size.mjs build/out/Release/node --platform=linux') + logger.log( + ' node scripts/optimize-binary-size.mjs packages/socketbin-cli-darwin-arm64/bin/socket', + ) + logger.log( + ' node scripts/optimize-binary-size.mjs build/out/Release/node --platform=linux', + ) logger.log(' node scripts/optimize-binary-size.mjs --all') process.exit(1) } @@ -338,22 +360,29 @@ async function main() { let totalBefore = 0 let totalAfter = 0 - for (const { path: binPath, before, after, savings } of results) { + for (const { after, before, path: binPath, savings } of results) { totalBefore += before totalAfter += after logger.log(` ${path.basename(binPath)}:`) logger.log(` Before: ${before.toFixed(2)} MB`) logger.log(` After: ${after.toFixed(2)} MB`) - logger.log(` Saved: ${(before - after).toFixed(2)} MB (${savings.toFixed(1)}%)`) + logger.log( + ` Saved: ${(before - after).toFixed(2)} MB (${savings.toFixed(1)}%)`, + ) logger.log('') } if (results.length > 1) { - const totalSavings = ((totalBefore - totalAfter) / totalBefore * 100).toFixed(1) + const totalSavings = ( + ((totalBefore - totalAfter) / totalBefore) * + 100 + ).toFixed(1) logger.log(' Total:') logger.log(` Before: ${totalBefore.toFixed(2)} MB`) logger.log(` After: ${totalAfter.toFixed(2)} MB`) - logger.log(` Saved: ${(totalBefore - totalAfter).toFixed(2)} MB (${totalSavings}%)`) + logger.log( + ` Saved: ${(totalBefore - totalAfter).toFixed(2)} MB (${totalSavings}%)`, + ) } logger.log(`\n${colors.green('✓')} All optimizations complete!`) diff --git a/scripts/prepare-package-for-publish.mjs b/scripts/prepare-package-for-publish.mjs index 4e60573e3..4d6a7c600 100644 --- a/scripts/prepare-package-for-publish.mjs +++ b/scripts/prepare-package-for-publish.mjs @@ -1,4 +1,3 @@ -#!/usr/bin/env node /** * @fileoverview Helper script to prepare package.json for publishing. * Handles removing private field and optionally setting version. @@ -12,7 +11,9 @@ const packagePath = args[0] const version = args[1] if (!packagePath) { - console.error('Usage: prepare-package-for-publish.mjs [version]') + console.error( + 'Usage: prepare-package-for-publish.mjs [version]', + ) process.exit(1) } diff --git a/scripts/prepublish-socketbin.mjs b/scripts/prepublish-socketbin.mjs index b2769460b..a575a017b 100644 --- a/scripts/prepublish-socketbin.mjs +++ b/scripts/prepublish-socketbin.mjs @@ -8,11 +8,11 @@ import { promises as fs } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' -import { parseArgs } from '@socketsecurity/lib/argv/parse' -import { getDefaultLogger } from '@socketsecurity/lib/logger' import semver from 'semver' import colors from 'yoctocolors-cjs' +import { parseArgs } from '@socketsecurity/lib/argv/parse' +import { getDefaultLogger } from '@socketsecurity/lib/logger' const logger = getDefaultLogger() const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -25,11 +25,18 @@ const rootDir = path.join(__dirname, '..') */ function generateDatetimeVersion(platform, arch, tool = 'cli') { // Read base version from the current package being generated. - const basePackagePath = path.join(rootDir, 'packages', `socketbin-${tool}-${platform}-${arch}`, 'package.json') + const basePackagePath = path.join( + rootDir, + 'packages', + `socketbin-${tool}-${platform}-${arch}`, + 'package.json', + ) let baseVersion = '0.0.0' try { - const basePackage = JSON.parse(require('fs').readFileSync(basePackagePath, 'utf-8')) + const basePackage = JSON.parse( + require('node:fs').readFileSync(basePackagePath, 'utf-8'), + ) const version = basePackage.version || '0.0.0' // Extract just the core version (X.Y.Z), ignoring any prerelease/placeholder text. const versionMatch = version.match(/^(\d+\.\d+\.\d+)/) @@ -63,11 +70,11 @@ const { values } = parseArgs({ const { arch, + method: buildMethod = 'smol', outdir, platform, tool = 'cli', version: providedVersion, - method: buildMethod = 'smol', } = values if (!platform || !arch) { diff --git a/scripts/publish.mjs b/scripts/publish.mjs index d8f30db3c..7aebdc0bc 100755 --- a/scripts/publish.mjs +++ b/scripts/publish.mjs @@ -1,4 +1,3 @@ -#!/usr/bin/env node /** * Unified publish script router. @@ -11,7 +10,6 @@ import { WIN32 } from '@socketsecurity/lib/constants/platform' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { spawn } from '@socketsecurity/lib/spawn' - const logger = getDefaultLogger() const TARGET_PACKAGES = { __proto__: null, @@ -28,7 +26,7 @@ const TARGET_PACKAGES = { sea: '@socketbin/node-sea-builder-builder', socket: 'socket', 'win32-arm64': '@socketbin/cli-win32-arm64', - 'win32-x64': '@socketbin/cli-win32-x64' + 'win32-x64': '@socketbin/cli-win32-x64', } const args = process.argv.slice(2) @@ -48,7 +46,9 @@ async function main() { const packageFilter = TARGET_PACKAGES[target] if (!packageFilter) { logger.error(`Unknown publish target: ${target}`) - logger.error(`Available targets: ${Object.keys(TARGET_PACKAGES).join(', ')}`) + logger.error( + `Available targets: ${Object.keys(TARGET_PACKAGES).join(', ')}`, + ) process.exit(1) } @@ -58,12 +58,7 @@ async function main() { logger.log('Note: Packages are published in dependency order by pnpm') } - const pnpmArgs = [ - '--filter', - packageFilter, - 'publish', - ...publishArgs - ] + const pnpmArgs = ['--filter', packageFilter, 'publish', ...publishArgs] logger.log(`Publishing ${target}...`) logger.log(`Command: pnpm ${pnpmArgs.join(' ')}`) diff --git a/scripts/regenerate-node-patches.mjs b/scripts/regenerate-node-patches.mjs index 1ca026734..03f172536 100644 --- a/scripts/regenerate-node-patches.mjs +++ b/scripts/regenerate-node-patches.mjs @@ -18,10 +18,10 @@ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' -import { spawn } from '@socketsecurity/lib/spawn' -import { getDefaultLogger } from '@socketsecurity/lib/logger' import colors from 'yoctocolors-cjs' +import { getDefaultLogger } from '@socketsecurity/lib/logger' +import { spawn } from '@socketsecurity/lib/spawn' const logger = getDefaultLogger() const __filename = fileURLToPath(import.meta.url) @@ -40,7 +40,9 @@ if (!versionArg) { const NODE_VERSION = versionArg.split('=')[1] if (!NODE_VERSION.startsWith('v')) { - logger.error(`${colors.red('✗')} Version must start with "v" (e.g., v24.10.0)`) + logger.error( + `${colors.red('✗')} Version must start with "v" (e.g., v24.10.0)`, + ) process.exit(1) } @@ -269,7 +271,9 @@ async function main() { logger.log(' 3. Update SOCKET_PATCHES array with new filenames') logger.log(' 4. Test the build') } else { - logger.log(`${colors.yellow('⚠')} No patches were generated (no changes detected)`) + logger.log( + `${colors.yellow('⚠')} No patches were generated (no changes detected)`, + ) } logger.log('') diff --git a/scripts/setup-build-toolchain.mjs b/scripts/setup-build-toolchain.mjs index 5586a525e..31b2b55ad 100755 --- a/scripts/setup-build-toolchain.mjs +++ b/scripts/setup-build-toolchain.mjs @@ -1,4 +1,3 @@ -#!/usr/bin/env node /** * Automatic Build Toolchain Setup Script * @@ -29,9 +28,10 @@ import { mkdir, writeFile } from 'node:fs/promises' import { homedir, platform as osPlatform, tmpdir } from 'node:os' import path from 'node:path' import { fileURLToPath } from 'node:url' -import { getDefaultLogger } from '@socketsecurity/lib/logger' + import colors from 'yoctocolors-cjs' +import { getDefaultLogger } from '@socketsecurity/lib/logger' const logger = getDefaultLogger() const __filename = fileURLToPath(import.meta.url) @@ -40,10 +40,14 @@ const __dirname = path.dirname(__filename) // Parse arguments. const args = process.argv.slice(2) const CHECK_ONLY = args.includes('--check-only') -const INSTALL_EMSCRIPTEN = args.includes('--emscripten') || !args.some(a => a.startsWith('--')) -const INSTALL_RUST = args.includes('--rust') || !args.some(a => a.startsWith('--')) -const INSTALL_PYTHON = args.includes('--python') || !args.some(a => a.startsWith('--')) -const INSTALL_COMPILER = args.includes('--compiler') || !args.some(a => a.startsWith('--')) +const INSTALL_EMSCRIPTEN = + args.includes('--emscripten') || !args.some(a => a.startsWith('--')) +const INSTALL_RUST = + args.includes('--rust') || !args.some(a => a.startsWith('--')) +const INSTALL_PYTHON = + args.includes('--python') || !args.some(a => a.startsWith('--')) +const INSTALL_COMPILER = + args.includes('--compiler') || !args.some(a => a.startsWith('--')) const FORCE_INSTALL = args.includes('--force') const QUIET = args.includes('--quiet') @@ -62,7 +66,7 @@ const PLATFORM = osPlatform() * Log with color support. */ function log(message, color = '') { - if (QUIET && !color.includes('red')) return + if (QUIET && !color.includes('red')) {return} const colors = { red: '\x1b[31m', @@ -155,7 +159,7 @@ function checkEmscripten() { for (const emsdkPath of possiblePaths) { const emsdkEnv = path.join( emsdkPath, - PLATFORM === 'win32' ? 'emsdk_env.bat' : 'emsdk_env.sh' + PLATFORM === 'win32' ? 'emsdk_env.bat' : 'emsdk_env.sh', ) if (existsSync(emsdkEnv)) { @@ -181,7 +185,8 @@ async function installEmscripten() { logInfo('Installing Emscripten SDK...') const emsdkPath = path.join(homedir(), '.emsdk') - const emsdkVersion = '3.1.70' // Pinned version for reproducibility. + // Pinned version for reproducibility. + const emsdkVersion = '3.1.70' try { // Clone emsdk if not exists. @@ -189,7 +194,7 @@ async function installEmscripten() { logInfo(`Cloning emsdk to ${emsdkPath}...`) if ( !exec( - `git clone https://github.com/emscripten-core/emsdk.git "${emsdkPath}"` + `git clone https://github.com/emscripten-core/emsdk.git "${emsdkPath}"`, ) ) { logError('Failed to clone emsdk') @@ -200,7 +205,7 @@ async function installEmscripten() { // Install and activate specific version. const emsdkCmd = path.join( emsdkPath, - PLATFORM === 'win32' ? 'emsdk.bat' : 'emsdk' + PLATFORM === 'win32' ? 'emsdk.bat' : 'emsdk', ) logInfo(`Installing Emscripten ${emsdkVersion}...`) @@ -220,7 +225,7 @@ async function installEmscripten() { await writeFile( activateScript, `#!/bin/bash\nsource "${emsdkPath}/emsdk_env.sh"\n`, - { mode: 0o755 } + { mode: 0o755 }, ) logSuccess('Emscripten SDK installed successfully') @@ -311,7 +316,7 @@ async function installRust() { if (PLATFORM === 'win32') { logInfo('Download Rust from: https://rustup.rs/') logError( - 'Automatic Rust installation not supported on Windows via script' + 'Automatic Rust installation not supported on Windows via script', ) logInfo('Please install manually and run this script again') return false @@ -320,7 +325,7 @@ async function installRust() { logInfo('Installing Rust via rustup...') if ( !exec( - 'curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y' + 'curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y', ) ) { logError('Failed to install Rust') @@ -347,7 +352,7 @@ async function installRust() { exec('brew install wasm-pack') } else if ( !exec( - 'curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh' + 'curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh', ) ) { logError('Failed to install wasm-pack') @@ -430,7 +435,8 @@ async function installPackageManager() { log('') // Install Homebrew using official installation script. - const installCmd = '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"' + const installCmd = + '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"' if (!exec(installCmd)) { logError('Failed to install Homebrew') @@ -456,11 +462,14 @@ async function installPackageManager() { log('') // Install Chocolatey using official PowerShell script. - const installCmd = 'powershell -NoProfile -ExecutionPolicy Bypass -Command "Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString(\'https://community.chocolatey.org/install.ps1\'))"' + const installCmd = + 'powershell -NoProfile -ExecutionPolicy Bypass -Command "Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString(\'https://community.chocolatey.org/install.ps1\'))"' if (!exec(installCmd)) { logError('Failed to install Chocolatey') - logInfo('Visit https://chocolatey.org/install for manual installation instructions') + logInfo( + 'Visit https://chocolatey.org/install for manual installation instructions', + ) return false } @@ -473,7 +482,7 @@ async function installPackageManager() { if (PLATFORM === 'linux') { logError('Linux package managers (apt-get/yum) should be pre-installed') - logInfo('Please install your distribution\'s package manager manually') + logInfo("Please install your distribution's package manager manually") return false } @@ -742,7 +751,7 @@ async function installCompiler() { logError('Chocolatey not found') logInfo( - 'Install Visual Studio Build Tools from: https://visualstudio.microsoft.com/downloads/' + 'Install Visual Studio Build Tools from: https://visualstudio.microsoft.com/downloads/', ) return false } @@ -895,7 +904,6 @@ async function installBuildTools() { } } - /** * Main entry point. */ @@ -906,8 +914,8 @@ async function main() { logInfo(`Platform: ${PLATFORM}`) logInfo(`CI Environment: ${IS_CI ? 'Yes' : 'No'}`) - if (IS_GITHUB_ACTIONS) logInfo('GitHub Actions detected') - if (IS_DOCKER) logInfo('Docker environment detected') + if (IS_GITHUB_ACTIONS) {logInfo('GitHub Actions detected')} + if (IS_DOCKER) {logInfo('Docker environment detected')} log('') // Check package manager FIRST - required for all subsequent installations. diff --git a/scripts/setup-monorepo.mjs b/scripts/setup-monorepo.mjs index 2cf4bbb4b..ee66872c2 100644 --- a/scripts/setup-monorepo.mjs +++ b/scripts/setup-monorepo.mjs @@ -1,4 +1,3 @@ -#!/usr/bin/env node /** * Setup monorepo structure for Socket CLI. * @@ -11,9 +10,10 @@ import { existsSync, promises as fs } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' -import { getDefaultLogger } from '@socketsecurity/lib/logger' + import colors from 'yoctocolors-cjs' +import { getDefaultLogger } from '@socketsecurity/lib/logger' const logger = getDefaultLogger() const __dirname = path.dirname(fileURLToPath(import.meta.url)) diff --git a/scripts/setup.mjs b/scripts/setup.mjs index c8f46e826..8847e7aaa 100644 --- a/scripts/setup.mjs +++ b/scripts/setup.mjs @@ -1,4 +1,3 @@ -#!/usr/bin/env node /** * @fileoverview Developer setup script - checks prerequisites and prepares environment. * @@ -34,9 +33,10 @@ import { existsSync } from 'node:fs' import { mkdir } from 'node:fs/promises' -import { spawn } from '@socketsecurity/lib/spawn' + import { WIN32 } from '@socketsecurity/lib/constants/platform' import { getDefaultLogger } from '@socketsecurity/lib/logger' +import { spawn } from '@socketsecurity/lib/spawn' const logger = getDefaultLogger() @@ -105,7 +105,7 @@ async function getVersion(command, args = ['--version']) { */ function parseVersion(versionString) { const match = versionString.match(/(\d+)\.(\d+)\.(\d+)/) - if (!match) return null + if (!match) {return null} return { major: Number.parseInt(match[1], 10), minor: Number.parseInt(match[2], 10), @@ -118,9 +118,9 @@ function parseVersion(versionString) { * Returns: -1 if a < b, 0 if a === b, 1 if a > b */ function compareVersions(a, b) { - if (a.major !== b.major) return a.major < b.major ? -1 : 1 - if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1 - if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1 + if (a.major !== b.major) {return a.major < b.major ? -1 : 1} + if (a.minor !== b.minor) {return a.minor < b.minor ? -1 : 1} + if (a.patch !== b.patch) {return a.patch < b.patch ? -1 : 1} return 0 } @@ -136,7 +136,8 @@ async function installHomebrew() { logger.step('Installing Homebrew...') logger.info('This requires sudo access and may take a few minutes') - const installScript = '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"' + const installScript = + '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"' const result = await spawn('bash', ['-c', installScript], { stdio: 'inherit', @@ -163,7 +164,8 @@ async function installChocolatey() { logger.step('Installing Chocolatey...') logger.info('This requires admin access and may take a few minutes') - const installScript = 'Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString(\'https://community.chocolatey.org/install.ps1\'))' + const installScript = + "Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))" const result = await spawn('powershell', ['-Command', installScript], { stdio: 'inherit', @@ -183,7 +185,7 @@ async function installChocolatey() { * Install a package using Homebrew (macOS/Linux). */ async function installWithHomebrew(packageName) { - if (!await hasCommand('brew')) { + if (!(await hasCommand('brew'))) { logger.error('Homebrew not available') return false } @@ -207,7 +209,7 @@ async function installWithHomebrew(packageName) { * Install a package using Chocolatey (Windows). */ async function installWithChocolatey(packageName) { - if (!await hasCommand('choco')) { + if (!(await hasCommand('choco'))) { logger.error('Chocolatey not available') return false } @@ -248,14 +250,16 @@ async function ensureGhCli() { // Auto-install mode. if (WIN32) { // Windows: Try Chocolatey. - if (!await hasCommand('choco')) { + if (!(await hasCommand('choco'))) { logger.info('Chocolatey not found (needed for auto-install on Windows)') logger.log('Attempting to install Chocolatey...') const installed = await installChocolatey() if (!installed) { logger.warn('Could not install Chocolatey') logger.info('Install gh CLI manually from: https://cli.github.com/') - logger.info('Or install Chocolatey from: https://chocolatey.org/install') + logger.info( + 'Or install Chocolatey from: https://chocolatey.org/install', + ) return false } } @@ -281,7 +285,7 @@ async function ensureGhCli() { } // macOS/Linux: Try Homebrew. - if (!await hasCommand('brew')) { + if (!(await hasCommand('brew'))) { logger.info('Homebrew not found (needed for auto-install)') logger.log('Attempting to install Homebrew...') const installed = await installHomebrew() @@ -315,7 +319,12 @@ async function ensureGhCli() { /** * Check prerequisite. */ -async function checkPrerequisite({ command, minVersion, name, required = true }) { +async function checkPrerequisite({ + command, + minVersion, + name, + required = true, +}) { const version = await getVersion(command) if (!version) { @@ -369,7 +378,7 @@ async function restoreCache(hasGh) { ['--filter', '@socketsecurity/cli', 'run', 'restore-cache', '--quiet'], { stdio: 'inherit', - } + }, ) if (result.code === 0) { @@ -459,7 +468,9 @@ async function main() { } if (!nodeOk || !pnpmOk) { - logger.error('Required prerequisites missing. Please install and try again.') + logger.error( + 'Required prerequisites missing. Please install and try again.', + ) if (!quiet) { logger.log('') } @@ -498,9 +509,11 @@ async function main() { return 0 } -main().then(code => { - process.exit(code) -}).catch(error => { - logger.error(error.message) - process.exit(1) -}) +main() + .then(code => { + process.exit(code) + }) + .catch(error => { + logger.error(error.message) + process.exit(1) + }) diff --git a/scripts/test-monorepo.mjs b/scripts/test-monorepo.mjs index bf1f3be24..d016ddc1b 100644 --- a/scripts/test-monorepo.mjs +++ b/scripts/test-monorepo.mjs @@ -3,21 +3,22 @@ * Runs tests across affected packages based on changed files. */ +import colors from 'yoctocolors-cjs' + import { isQuiet } from '@socketsecurity/lib/argv/flags' import { parseArgs } from '@socketsecurity/lib/argv/parse' +import { WIN32 } from '@socketsecurity/lib/constants/platform' import { getChangedFiles, getStagedFiles } from '@socketsecurity/lib/git' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { onExit } from '@socketsecurity/lib/signal-exit' -import { ProgressBar } from '@socketsecurity/lib/stdio/progress' +import { isSpawnError, spawn } from '@socketsecurity/lib/spawn' import { printHeader } from '@socketsecurity/lib/stdio/header' -import colors from 'yoctocolors-cjs' +import { ProgressBar } from '@socketsecurity/lib/stdio/progress' import { getAffectedPackages, getPackagesWithScript, } from './utils/monorepo-helper.mjs' -import { WIN32 } from '@socketsecurity/lib/constants/platform' -import { isSpawnError, spawn } from '@socketsecurity/lib/spawn' const logger = getDefaultLogger() @@ -74,7 +75,12 @@ async function getPackagesToTest(options) { /** * Run tests on a specific package with pretty output. */ -async function runPackageTest(pkg, testArgs = [], quiet = false, showProgress = true) { +async function runPackageTest( + pkg, + testArgs = [], + quiet = false, + showProgress = true, +) { const displayName = pkg.displayName || pkg.name if (!quiet && showProgress) { @@ -193,7 +199,8 @@ async function main() { logger.step( `Testing ${modeText} (${packages.length} package${packages.length > 1 ? 's' : ''})`, ) - logger.error('') // Blank line. + // Blank line. + logger.error('') } // Setup progress bar. @@ -229,7 +236,12 @@ async function main() { progressBar.update(completedCount, { pkg: colors.cyan(displayName) }) } - const result = await runPackageTest(pkg, positionals, quiet, !useProgressBar) + const result = await runPackageTest( + pkg, + positionals, + quiet, + !useProgressBar, + ) if (progressBar) { completedCount++ diff --git a/scripts/test-with-custom-node.mjs b/scripts/test-with-custom-node.mjs index c4a96ad44..ea54f9fc0 100755 --- a/scripts/test-with-custom-node.mjs +++ b/scripts/test-with-custom-node.mjs @@ -19,7 +19,6 @@ import { fileURLToPath } from 'node:url' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { spawn } from '@socketsecurity/lib/spawn' - const logger = getDefaultLogger() const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) diff --git a/scripts/test.mjs b/scripts/test.mjs index b202d3430..c0b6524c0 100644 --- a/scripts/test.mjs +++ b/scripts/test.mjs @@ -46,7 +46,13 @@ const nodeModulesBinPath = path.join(rootPath, 'node_modules', '.bin') */ function hasExternalRepos() { const libPath = path.join(rootPath, '..', 'socket-lib', 'package.json') - const registryPath = path.join(rootPath, '..', 'socket-registry', 'registry', 'package.json') + const registryPath = path.join( + rootPath, + '..', + 'socket-registry', + 'registry', + 'package.json', + ) return existsSync(libPath) || existsSync(registryPath) } @@ -193,13 +199,9 @@ async function runCheck() { // Run TypeScript check const tsConfigPath = getTsConfigPath() spinner.start('Checking TypeScript...') - exitCode = await runCommand( - 'tsgo', - ['--noEmit', '-p', tsConfigPath], - { - stdio: 'pipe', - }, - ) + exitCode = await runCommand('tsgo', ['--noEmit', '-p', tsConfigPath], { + stdio: 'pipe', + }) if (exitCode !== 0) { spinner.stop() logger.error('TypeScript check failed') @@ -401,9 +403,7 @@ async function main() { ' pnpm test # Run checks, build, and tests for changed files', ) logger.log(' pnpm test --all # Run all tests') - logger.log( - ' pnpm test --fast # Skip checks for quick testing', - ) + logger.log(' pnpm test --fast # Skip checks for quick testing') logger.log(' pnpm test --cover # Run with coverage report') logger.log(' pnpm test --fast --cover # Quick test with coverage') logger.log(' pnpm test --update # Update test snapshots') diff --git a/scripts/type.mjs b/scripts/type.mjs index 03946d23e..0b9597678 100644 --- a/scripts/type.mjs +++ b/scripts/type.mjs @@ -3,17 +3,17 @@ * Runs type checking across packages with pretty UI. */ +import colors from 'yoctocolors-cjs' + import { isQuiet } from '@socketsecurity/lib/argv/flags' import { parseArgs } from '@socketsecurity/lib/argv/parse' import { WIN32 } from '@socketsecurity/lib/constants/platform' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { spawn } from '@socketsecurity/lib/spawn' import { printFooter, printHeader } from '@socketsecurity/lib/stdio/header' -import colors from 'yoctocolors-cjs' import { getPackagesWithScript } from './utils/monorepo-helper.mjs' - const logger = getDefaultLogger() /** * Run type check on a specific package with pretty output. @@ -25,16 +25,12 @@ async function runPackageTypeCheck(pkg, quiet = false) { logger.progress(`${displayName}: checking types`) } - const result = await spawn( - 'pnpm', - ['--filter', pkg.name, 'run', 'type'], - { - cwd: process.cwd(), - shell: WIN32, - stdio: 'pipe', - stdioString: true, - }, - ) + const result = await spawn('pnpm', ['--filter', pkg.name, 'run', 'type'], { + cwd: process.cwd(), + shell: WIN32, + stdio: 'pipe', + stdioString: true, + }) if (result.code !== 0) { if (!quiet) { @@ -109,7 +105,8 @@ async function main() { logger.step( `Type checking ${packages.length} package${packages.length > 1 ? 's' : ''}`, ) - logger.error('') // Blank line. + // Blank line. + logger.error('') } // Run type check across all packages. diff --git a/scripts/update-socketbin-versions.mjs b/scripts/update-socketbin-versions.mjs index c50808781..7ec86319a 100755 --- a/scripts/update-socketbin-versions.mjs +++ b/scripts/update-socketbin-versions.mjs @@ -1,4 +1,3 @@ -#!/usr/bin/env node /** * @fileoverview Update optionalDependencies in socket package.json with latest @socketbin/* versions from npm. */ @@ -69,9 +68,7 @@ async function main() { }) pkg.optionalDependencies[packageName] = latestVersion - console.log( - ` ${packageName}: ${currentVersion} → ${latestVersion}`, - ) + console.log(` ${packageName}: ${currentVersion} → ${latestVersion}`) } // Write updated package.json. diff --git a/scripts/update.mjs b/scripts/update.mjs index 338bced2f..e3fb323d5 100644 --- a/scripts/update.mjs +++ b/scripts/update.mjs @@ -12,8 +12,8 @@ */ import { isQuiet, isVerbose } from '@socketsecurity/lib/argv/flags' -import { getDefaultLogger } from '@socketsecurity/lib/logger' import { WIN32 } from '@socketsecurity/lib/constants/platform' +import { getDefaultLogger } from '@socketsecurity/lib/logger' import { spawn } from '@socketsecurity/lib/spawn' async function main() { diff --git a/scripts/utils/changed-test-mapper.mjs b/scripts/utils/changed-test-mapper.mjs index 49c19b99d..16eb9a28d 100644 --- a/scripts/utils/changed-test-mapper.mjs +++ b/scripts/utils/changed-test-mapper.mjs @@ -108,7 +108,10 @@ function mapSourceToTests(filepath) { } // External or fixtures changes - if (normalized.startsWith('external/') || normalized.startsWith('fixtures/')) { + if ( + normalized.startsWith('external/') || + normalized.startsWith('fixtures/') + ) { return ['packages/cli/test/integration/'] } diff --git a/scripts/utils/monorepo-helper.mjs b/scripts/utils/monorepo-helper.mjs index 22da3c15f..268c0cedf 100644 --- a/scripts/utils/monorepo-helper.mjs +++ b/scripts/utils/monorepo-helper.mjs @@ -6,11 +6,11 @@ import { existsSync, readFileSync } from 'node:fs' import path from 'node:path' +import colors from 'yoctocolors-cjs' + import { WIN32 } from '@socketsecurity/lib/constants/platform' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { spawn } from '@socketsecurity/lib/spawn' -import colors from 'yoctocolors-cjs' - const logger = getDefaultLogger() /** @@ -112,7 +112,13 @@ export function getAffectedPackages(changedFiles) { * @param {string} [progressMessage] - Optional custom progress message * @returns {Promise} Exit code */ -export async function runPackageScript(pkg, scriptName, args = [], quiet = false, progressMessage = '') { +export async function runPackageScript( + pkg, + scriptName, + args = [], + quiet = false, + progressMessage = '', +) { const displayName = pkg.displayName || pkg.name if (!quiet) { @@ -162,7 +168,13 @@ export async function runPackageScript(pkg, scriptName, args = [], quiet = false * @param {string} [sectionTitle] - Optional section title to use as progress message * @returns {Promise} Exit code (0 if all succeed, first failure code otherwise) */ -export async function runAcrossPackages(packages, scriptName, args = [], quiet = false, sectionTitle = '') { +export async function runAcrossPackages( + packages, + scriptName, + args = [], + quiet = false, + sectionTitle = '', +) { if (!packages.length) { if (!quiet) { logger.substep('No packages to process') @@ -171,8 +183,15 @@ export async function runAcrossPackages(packages, scriptName, args = [], quiet = } for (const pkg of packages) { - const progressMessage = sectionTitle || `${pkg.displayName || pkg.name}: running ${scriptName}` - const exitCode = await runPackageScript(pkg, scriptName, args, quiet, progressMessage) + const progressMessage = + sectionTitle || `${pkg.displayName || pkg.name}: running ${scriptName}` + const exitCode = await runPackageScript( + pkg, + scriptName, + args, + quiet, + progressMessage, + ) if (exitCode !== 0) { return exitCode } diff --git a/scripts/validate-bundle-deps.mjs b/scripts/validate-bundle-deps.mjs index 6a9298ce8..4adc7c037 100644 --- a/scripts/validate-bundle-deps.mjs +++ b/scripts/validate-bundle-deps.mjs @@ -30,9 +30,7 @@ const BUILTIN_MODULES = new Set([ // Packages that are marked external but are excused from validation. // These are packages referenced in bundled code that's never actually called. -const EXCUSED_EXTERNALS = new Set([ - 'node-gyp', -]) +const EXCUSED_EXTERNALS = new Set(['node-gyp']) /** * Find all JavaScript files in dist directory. @@ -236,7 +234,7 @@ async function isDirectDependency(packageName, devDependencies) { // For scoped packages: from '@scope/package' or from "@scope/package" const importPattern = new RegExp( `from\\s+['"]${packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:['"/]|$)`, - 'm' + 'm', ) if (importPattern.test(content)) { return true diff --git a/scripts/validate-file-count.mjs b/scripts/validate-file-count.mjs index cba047b0b..4a4d75d3e 100644 --- a/scripts/validate-file-count.mjs +++ b/scripts/validate-file-count.mjs @@ -1,4 +1,3 @@ -#!/usr/bin/env node /** * @fileoverview Validates that commits don't contain too many files. * @@ -8,20 +7,21 @@ * - Prevents overly large commits that are hard to review */ -import { exec } from 'node:child_process'; -import path from 'node:path'; -import { promisify } from 'node:util'; -import { fileURLToPath } from 'node:url'; -import { getDefaultLogger } from '@socketsecurity/lib/logger'; +import { exec } from 'node:child_process' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { promisify } from 'node:util' -const logger = getDefaultLogger(); -const execAsync = promisify(exec); +import { getDefaultLogger } from '@socketsecurity/lib/logger' -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const rootPath = path.join(__dirname, '..'); +const logger = getDefaultLogger() +const execAsync = promisify(exec) + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const rootPath = path.join(__dirname, '..') // Maximum number of files in a single commit -const MAX_FILES_PER_COMMIT = 50; +const MAX_FILES_PER_COMMIT = 50 /** * Check if too many files are staged for commit. @@ -29,79 +29,85 @@ const MAX_FILES_PER_COMMIT = 50; async function validateStagedFileCount() { try { // Check if we're in a git repository - const { stdout: gitRoot } = await execAsync('git rev-parse --show-toplevel', { - cwd: rootPath, - }); + const { stdout: gitRoot } = await execAsync( + 'git rev-parse --show-toplevel', + { + cwd: rootPath, + }, + ) if (!gitRoot.trim()) { - return null; // Not a git repository + // Not a git repository + return null } // Get list of staged files - const { stdout } = await execAsync('git diff --cached --name-only', { cwd: rootPath }); + const { stdout } = await execAsync('git diff --cached --name-only', { + cwd: rootPath, + }) const stagedFiles = stdout .trim() .split('\n') - .filter(line => line.length > 0); + .filter(line => line.length > 0) if (stagedFiles.length >= MAX_FILES_PER_COMMIT) { return { count: stagedFiles.length, files: stagedFiles, limit: MAX_FILES_PER_COMMIT, - }; + } } - return null; + return null } catch { // Not a git repo or git not available - return null; + return null } } async function main() { try { - const violation = await validateStagedFileCount(); + const violation = await validateStagedFileCount() if (!violation) { - logger.success('Commit size is acceptable'); - process.exitCode = 0; - return; + logger.success('Commit size is acceptable') + process.exitCode = 0 + return } - logger.fail('Too many files staged for commit'); - logger.log(''); - logger.log(`Staged files: ${violation.count}`); - logger.log(`Maximum allowed: ${violation.limit}`); - logger.log(''); - logger.log('Staged files:'); - logger.log(''); + logger.fail('Too many files staged for commit') + logger.log('') + logger.log(`Staged files: ${violation.count}`) + logger.log(`Maximum allowed: ${violation.limit}`) + logger.log('') + logger.log('Staged files:') + logger.log('') // Show first 20 files, then summary if more - const filesToShow = violation.files.slice(0, 20); + const filesToShow = violation.files.slice(0, 20) for (const file of filesToShow) { - logger.log(` ${file}`); + logger.log(` ${file}`) } if (violation.files.length > 20) { - logger.log(` ... and ${violation.files.length - 20} more files`); + logger.log(` ... and ${violation.files.length - 20} more files`) } - logger.log(''); + logger.log('') logger.log( 'Split into smaller commits, check for accidentally staged files, or exclude generated files.', - ); - logger.log(''); + ) + logger.log('') - process.exitCode = 1; + process.exitCode = 1 } catch (error) { - logger.fail(`Validation failed: ${error.message}`); - process.exitCode = 1; + logger.fail(`Validation failed: ${error.message}`) + process.exitCode = 1 } } main().catch(error => { - logger.fail(`Validation failed: ${error}`); - process.exitCode = 1; -}); + logger.fail(`Validation failed: ${error}`) + process.exitCode = 1 +}) diff --git a/scripts/validate-file-size.mjs b/scripts/validate-file-size.mjs index 1eaaae288..7ec974857 100644 --- a/scripts/validate-file-size.mjs +++ b/scripts/validate-file-size.mjs @@ -1,4 +1,3 @@ -#!/usr/bin/env node /** * @fileoverview Validates that no individual files exceed size threshold. * @@ -8,18 +7,20 @@ * - Excludes: node_modules, .git, dist, build, coverage directories */ -import { promises as fs } from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { getDefaultLogger } from '@socketsecurity/lib/logger'; +import { promises as fs } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' -const logger = getDefaultLogger(); +import { getDefaultLogger } from '@socketsecurity/lib/logger' -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const rootPath = path.join(__dirname, '..'); +const logger = getDefaultLogger() + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const rootPath = path.join(__dirname, '..') // Maximum file size: 2MB -const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2,097,152 bytes +// 2,097,152 bytes +const MAX_FILE_SIZE = 2 * 1024 * 1024 // Directories to skip const SKIP_DIRS = new Set([ @@ -37,17 +38,17 @@ const SKIP_DIRS = new Set([ '.vscode', 'tmp', 'external', -]); +]) /** * Format bytes to human-readable size. */ function formatBytes(bytes) { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`; + if (bytes === 0) {return '0 B'} + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}` } /** @@ -55,10 +56,10 @@ function formatBytes(bytes) { */ async function scanDirectory(dir, violations = []) { try { - const entries = await fs.readdir(dir, { withFileTypes: true }); + const entries = await fs.readdir(dir, { withFileTypes: true }) for (const entry of entries) { - const fullPath = path.join(dir, entry.name); + const fullPath = path.join(dir, entry.name) if (entry.isDirectory()) { // Skip excluded directories and hidden directories (except .claude, .config, .github) @@ -69,19 +70,19 @@ async function scanDirectory(dir, violations = []) { entry.name === '.config' || entry.name === '.github') ) { - await scanDirectory(fullPath, violations); + await scanDirectory(fullPath, violations) } } else if (entry.isFile()) { try { - const stats = await fs.stat(fullPath); + const stats = await fs.stat(fullPath) if (stats.size > MAX_FILE_SIZE) { - const relativePath = path.relative(rootPath, fullPath); + const relativePath = path.relative(rootPath, fullPath) violations.push({ file: relativePath, size: stats.size, formattedSize: formatBytes(stats.size), maxSize: formatBytes(MAX_FILE_SIZE), - }); + }) } } catch { // Skip files we can't stat @@ -92,58 +93,60 @@ async function scanDirectory(dir, violations = []) { // Skip directories we can't read } - return violations; + return violations } /** * Validate file sizes in repository. */ async function validateFileSizes() { - const violations = await scanDirectory(rootPath); + const violations = await scanDirectory(rootPath) // Sort by size descending (largest first) - violations.sort((a, b) => b.size - a.size); + violations.sort((a, b) => b.size - a.size) - return violations; + return violations } async function main() { try { - const violations = await validateFileSizes(); + const violations = await validateFileSizes() if (violations.length === 0) { - logger.success('All files are within size limits'); - process.exitCode = 0; - return; + logger.success('All files are within size limits') + process.exitCode = 0 + return } - logger.fail('File size violations found'); - logger.log(''); - logger.log(`Maximum allowed file size: ${formatBytes(MAX_FILE_SIZE)}`); - logger.log(''); - logger.log('Files exceeding limit:'); - logger.log(''); + logger.fail('File size violations found') + logger.log('') + logger.log(`Maximum allowed file size: ${formatBytes(MAX_FILE_SIZE)}`) + logger.log('') + logger.log('Files exceeding limit:') + logger.log('') for (const violation of violations) { - logger.log(` ${violation.file}`); - logger.log(` Size: ${violation.formattedSize}`); - logger.log(` Exceeds limit by: ${formatBytes(violation.size - MAX_FILE_SIZE)}`); - logger.log(''); + logger.log(` ${violation.file}`) + logger.log(` Size: ${violation.formattedSize}`) + logger.log( + ` Exceeds limit by: ${formatBytes(violation.size - MAX_FILE_SIZE)}`, + ) + logger.log('') } logger.log( 'Reduce file sizes, move large files to external storage, or exclude from repository.', - ); - logger.log(''); + ) + logger.log('') - process.exitCode = 1; + process.exitCode = 1 } catch (error) { - logger.fail(`Validation failed: ${error.message}`); - process.exitCode = 1; + logger.fail(`Validation failed: ${error.message}`) + process.exitCode = 1 } } main().catch(error => { - logger.fail(`Validation failed: ${error}`); - process.exitCode = 1; -}); + logger.fail(`Validation failed: ${error}`) + process.exitCode = 1 +}) diff --git a/scripts/validate-markdown-filenames.mjs b/scripts/validate-markdown-filenames.mjs index 42b75573d..b594b476d 100644 --- a/scripts/validate-markdown-filenames.mjs +++ b/scripts/validate-markdown-filenames.mjs @@ -1,4 +1,3 @@ -#!/usr/bin/env node /** * @fileoverview Validates that markdown files follow naming conventions. * @@ -17,15 +16,16 @@ * - NOT be at root level */ -import { promises as fs } from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { getDefaultLogger } from '@socketsecurity/lib/logger'; +import { promises as fs } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' -const logger = getDefaultLogger(); +import { getDefaultLogger } from '@socketsecurity/lib/logger' -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const rootPath = path.join(__dirname, '..'); +const logger = getDefaultLogger() + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const rootPath = path.join(__dirname, '..') // Allowed SCREAMING_CASE markdown files (without .md extension for comparison) const ALLOWED_SCREAMING_CASE = new Set([ @@ -177,8 +177,13 @@ function validateFilename(filePath) { const relativePath = path.relative(rootPath, filePath) // README.md, CHANGELOG.md, and LICENSE are special - allowed anywhere - if (nameWithoutExt === 'README' || nameWithoutExt === 'CHANGELOG' || nameWithoutExt === 'LICENSE') { - return null // Valid - allowed in any location + if ( + nameWithoutExt === 'README' || + nameWithoutExt === 'CHANGELOG' || + nameWithoutExt === 'LICENSE' + ) { + // Valid - allowed in any location + return null } // Check if it's an allowed SCREAMING_CASE file @@ -192,7 +197,8 @@ function validateFilename(filePath) { suggestion: `Move to root, docs/, or .claude/, or rename to ${filename.toLowerCase().replace(/_/g, '-')}`, } } - return null // Valid + // Valid + return null } // Check if it's in SCREAMING_CASE but not allowed @@ -242,7 +248,8 @@ function validateFilename(filePath) { } } - return null // Valid + // Valid + return null } /** @@ -264,49 +271,49 @@ async function validateMarkdownFilenames() { async function main() { try { - const violations = await validateMarkdownFilenames(); + const violations = await validateMarkdownFilenames() if (violations.length === 0) { - logger.success('All markdown filenames follow conventions'); - process.exitCode = 0; - return; + logger.success('All markdown filenames follow conventions') + process.exitCode = 0 + return } - logger.fail('Markdown filename violations found'); - logger.log(''); - logger.log('Special files (allowed anywhere):'); - logger.log(' README.md, CHANGELOG.md, LICENSE'); - logger.log(''); - logger.log('Allowed SCREAMING_CASE files (root, docs/, or .claude/ only):'); - logger.log(' AUTHORS.md, CITATION.md, CLAUDE.md,'); - logger.log(' CODE_OF_CONDUCT.md, CONTRIBUTORS.md, CONTRIBUTING.md,'); - logger.log(' COPYING, CREDITS.md, GOVERNANCE.md, MAINTAINERS.md,'); - logger.log(' NOTICE.md, SECURITY.md, SUPPORT.md, TRADEMARK.md'); - logger.log(''); - logger.log('All other .md files must:'); - logger.log(' - Be lowercase-with-hyphens'); - logger.log(' - Be in docs/ or .claude/ directories (any depth)'); - logger.log(''); + logger.fail('Markdown filename violations found') + logger.log('') + logger.log('Special files (allowed anywhere):') + logger.log(' README.md, CHANGELOG.md, LICENSE') + logger.log('') + logger.log('Allowed SCREAMING_CASE files (root, docs/, or .claude/ only):') + logger.log(' AUTHORS.md, CITATION.md, CLAUDE.md,') + logger.log(' CODE_OF_CONDUCT.md, CONTRIBUTORS.md, CONTRIBUTING.md,') + logger.log(' COPYING, CREDITS.md, GOVERNANCE.md, MAINTAINERS.md,') + logger.log(' NOTICE.md, SECURITY.md, SUPPORT.md, TRADEMARK.md') + logger.log('') + logger.log('All other .md files must:') + logger.log(' - Be lowercase-with-hyphens') + logger.log(' - Be in docs/ or .claude/ directories (any depth)') + logger.log('') for (const violation of violations) { - logger.log(` ${violation.file}`); - logger.log(` Issue: ${violation.issue}`); - logger.log(` Current: ${violation.filename}`); - logger.log(` Suggested: ${violation.suggestion}`); - logger.log(''); + logger.log(` ${violation.file}`) + logger.log(` Issue: ${violation.issue}`) + logger.log(` Current: ${violation.filename}`) + logger.log(` Suggested: ${violation.suggestion}`) + logger.log('') } - logger.log('Rename files to follow conventions.'); - logger.log(''); + logger.log('Rename files to follow conventions.') + logger.log('') - process.exitCode = 1; + process.exitCode = 1 } catch (error) { - logger.fail(`Validation failed: ${error.message}`); - process.exitCode = 1; + logger.fail(`Validation failed: ${error.message}`) + process.exitCode = 1 } } main().catch(error => { - logger.fail(`Validation failed: ${error}`); - process.exitCode = 1; -}); + logger.fail(`Validation failed: ${error}`) + process.exitCode = 1 +}) diff --git a/scripts/validate-no-cdn-refs.mjs b/scripts/validate-no-cdn-refs.mjs index afeecff9b..8f04f3cb5 100644 --- a/scripts/validate-no-cdn-refs.mjs +++ b/scripts/validate-no-cdn-refs.mjs @@ -1,4 +1,3 @@ -#!/usr/bin/env node /** * @fileoverview Validates that there are no CDN references in the codebase. * @@ -16,6 +15,7 @@ import { promises as fs } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' + import { getDefaultLogger } from '@socketsecurity/lib/logger' const logger = getDefaultLogger() diff --git a/scripts/validate-no-link-deps.mjs b/scripts/validate-no-link-deps.mjs index 9698b8eba..d41b1d5d7 100755 --- a/scripts/validate-no-link-deps.mjs +++ b/scripts/validate-no-link-deps.mjs @@ -1,4 +1,3 @@ -#!/usr/bin/env node /** * @fileoverview Validates that no package.json files contain link: dependencies. * Link dependencies are prohibited - use workspace: or catalog: instead. diff --git a/scripts/verify-node-build.mjs b/scripts/verify-node-build.mjs index 3dbb6fdfc..b00c4e758 100644 --- a/scripts/verify-node-build.mjs +++ b/scripts/verify-node-build.mjs @@ -18,10 +18,10 @@ import { platform } from 'node:os' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' -import { getDefaultLogger } from '@socketsecurity/lib/logger' -import { spawn } from '@socketsecurity/lib/spawn' import colors from 'yoctocolors-cjs' +import { getDefaultLogger } from '@socketsecurity/lib/logger' +import { spawn } from '@socketsecurity/lib/spawn' const logger = getDefaultLogger() const __filename = fileURLToPath(import.meta.url) @@ -333,13 +333,9 @@ async function testBinary() { success(`Binary version: ${version}`) // Test 2: Execute simple script. - const execResult = await execCapture( - nodeBinary, - ['-e', 'logger.log("OK")'], - { - env: { ...process.env, PKG_EXECPATH: '' }, - }, - ) + const execResult = await execCapture(nodeBinary, ['-e', 'logger.log("OK")'], { + env: { ...process.env, PKG_EXECPATH: '' }, + }) if (execResult.code !== 0 || execResult.stdout !== 'OK') { error('Binary failed to execute simple script') @@ -424,7 +420,8 @@ async function verifySignature() { if (result.code !== 0) { warn('Binary is not signed (may cause issues on macOS)') warn('Run: codesign --sign - --force out/Release/node') - return true // Not fatal. + // Not fatal. + return true } success('Binary is properly signed for macOS') diff --git a/scripts/wasm/benchmark-build.mjs b/scripts/wasm/benchmark-build.mjs index 67cc5c25e..51f4da4f2 100644 --- a/scripts/wasm/benchmark-build.mjs +++ b/scripts/wasm/benchmark-build.mjs @@ -12,13 +12,12 @@ import { existsSync, promises as fs } from 'node:fs' import path from 'node:path' -import { fileURLToPath } from 'node:url' import { performance } from 'node:perf_hooks' +import { fileURLToPath } from 'node:url' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { spawn } from '@socketsecurity/lib/spawn' - const logger = getDefaultLogger() const __dirname = path.dirname(fileURLToPath(import.meta.url)) const buildScript = path.join(__dirname, 'build-unified-wasm.mjs') @@ -158,10 +157,7 @@ async function displaySizes() { const wasmMB = (sizes.wasmSize / 1024 / 1024).toFixed(2) const syncMB = (sizes.syncSize / 1024 / 1024).toFixed(2) - const compressionRatio = ( - (sizes.syncSize / sizes.wasmSize) * - 100 - ).toFixed(1) + const compressionRatio = ((sizes.syncSize / sizes.wasmSize) * 100).toFixed(1) logger.log(` WASM (raw): ${wasmMB} MB`) logger.log(` JS (compressed): ${syncMB} MB`) @@ -195,7 +191,8 @@ async function main() { // Run prod build. if (!devOnly) { if (devResult) { - logger.log('') // Spacing. + // Spacing. + logger.log('') } prodResult = await benchmarkBuild('production') if (!prodResult) { diff --git a/scripts/wasm/build-model-packages.mjs b/scripts/wasm/build-model-packages.mjs index 4ff84ea0c..4edf9e6b4 100755 --- a/scripts/wasm/build-model-packages.mjs +++ b/scripts/wasm/build-model-packages.mjs @@ -1,4 +1,3 @@ -#!/usr/bin/env node /** * Build separate WASM model packages for npm distribution. * @@ -16,17 +15,21 @@ */ import { execSync } from 'node:child_process' -import { existsSync, mkdirSync, promises as fs } from 'node:fs' +import { existsSync, promises as fs, mkdirSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' -import { getDefaultLogger } from '@socketsecurity/lib/logger' + import colors from 'yoctocolors-cjs' +import { getDefaultLogger } from '@socketsecurity/lib/logger' const logger = getDefaultLogger() const __dirname = path.dirname(fileURLToPath(import.meta.url)) const rootPath = path.join(__dirname, '../..') -const wasmBundlePath = path.join(rootPath, 'packages/node-smol-builder/wasm-bundle') +const wasmBundlePath = path.join( + rootPath, + 'packages/node-smol-builder/wasm-bundle', +) const packagesPath = path.join(rootPath, 'packages') // Parse command line arguments. @@ -96,7 +99,7 @@ async function buildWasm(modelName, feature) { // Copy built WASM. const wasmSource = path.join( wasmBundlePath, - 'target/wasm32-unknown-unknown/release/socket_ai.wasm' + 'target/wasm32-unknown-unknown/release/socket_ai.wasm', ) const wasmBuild = path.join(buildDir, `${modelName}.wasm`) @@ -128,14 +131,19 @@ async function optimizeWasm(inputPath, modelName) { const optimizedPath = path.join(buildDir, `${modelName}.optimized.wasm`) exec( - `wasm-opt -Oz --enable-simd --enable-bulk-memory ${inputPath} -o ${optimizedPath}` + `wasm-opt -Oz --enable-simd --enable-bulk-memory ${inputPath} -o ${optimizedPath}`, ) const originalSize = (await fs.stat(inputPath)).size const optimizedSize = (await fs.stat(optimizedPath)).size - const reduction = (((originalSize - optimizedSize) / originalSize) * 100).toFixed(1) + const reduction = ( + ((originalSize - optimizedSize) / originalSize) * + 100 + ).toFixed(1) - logger.log(` Optimized: ${(optimizedSize / (1024 * 1024)).toFixed(1)} MB (${reduction}% reduction)`) + logger.log( + ` Optimized: ${(optimizedSize / (1024 * 1024)).toFixed(1)} MB (${reduction}% reduction)`, + ) return optimizedPath } @@ -181,7 +189,9 @@ async function main() { logger.log(' - packages/socketbin-minilm-wasm/') logger.log(' - packages/socketbin-codet5-wasm/') logger.log('\nNext steps:') - logger.log(' 1. Test locally: cd packages/socketbin-minilm-wasm && npm pack') + logger.log( + ' 1. Test locally: cd packages/socketbin-minilm-wasm && npm pack', + ) logger.log(' 2. Publish: npm publish') } catch (error) { logger.error(`\n${colors.red('✗')} Build failed:`, error.message) diff --git a/scripts/wasm/build-unified-wasm.mjs b/scripts/wasm/build-unified-wasm.mjs index 7a117fc63..3bb17c194 100644 --- a/scripts/wasm/build-unified-wasm.mjs +++ b/scripts/wasm/build-unified-wasm.mjs @@ -127,7 +127,8 @@ async function installBinaryen() { // Fallback: Download from GitHub releases (all platforms). logger.substep('Downloading pre-built binaryen from GitHub') - const version = 'version_119' // Latest stable as of implementation. + // Latest stable as of implementation. + const version = 'version_119' let platformSuffix = '' if (isWindows) { @@ -246,19 +247,27 @@ const buildEnv = { // Add RUSTFLAGS for additional optimizations (if not already set). if (!buildEnv.RUSTFLAGS) { const rustFlags = [ - '-C target-feature=+simd128', // Enable WASM SIMD (73% browser support) - '-C target-feature=+bulk-memory', // Bulk memory operations (faster copies) - '-C target-feature=+mutable-globals', // Mutable globals support - '-C target-feature=+sign-ext', // Sign extension operations + // Enable WASM SIMD (73% browser support) + '-C target-feature=+simd128', + // Bulk memory operations (faster copies) + '-C target-feature=+bulk-memory', + // Mutable globals support + '-C target-feature=+mutable-globals', + // Sign extension operations + '-C target-feature=+sign-ext', ] // Production-only optimizations. if (!isDev) { rustFlags.push( - '-C link-arg=--strip-debug', // Strip debug info - '-C link-arg=--strip-all', // Strip all symbols - '-C link-arg=-zstack-size=131_072', // Smaller stack size (128KB) - '-C embed-bitcode=yes', // Embed bitcode for LTO + // Strip debug info + '-C link-arg=--strip-debug', + // Strip all symbols + '-C link-arg=--strip-all', + // Smaller stack size (128KB) + '-C link-arg=-zstack-size=131_072', + // Embed bitcode for LTO + '-C embed-bitcode=yes', ) } @@ -310,22 +319,37 @@ try { // Aggressive optimization flags (no backward compat needed). const wasmOptFlags = [ - '-Oz', // Optimize for size - '--enable-simd', // Enable SIMD operations - '--enable-bulk-memory', // Enable bulk memory - '--enable-sign-ext', // Enable sign extension - '--enable-mutable-globals', // Enable mutable globals - '--enable-nontrapping-float-to-int', // Non-trapping float conversions - '--enable-reference-types', // Enable reference types - '--low-memory-unused', // Optimize for low memory usage - '--flatten', // Flatten IR for better optimization - '--rereloop', // Optimize control flow - '--vacuum', // Remove unused code + // Optimize for size + '-Oz', + // Enable SIMD operations + '--enable-simd', + // Enable bulk memory + '--enable-bulk-memory', + // Enable sign extension + '--enable-sign-ext', + // Enable mutable globals + '--enable-mutable-globals', + // Non-trapping float conversions + '--enable-nontrapping-float-to-int', + // Enable reference types + '--enable-reference-types', + // Optimize for low memory usage + '--low-memory-unused', + // Flatten IR for better optimization + '--flatten', + // Optimize control flow + '--rereloop', + // Remove unused code + '--vacuum', ] - const optResult = await exec('wasm-opt', [...wasmOptFlags, wasmFile, '-o', wasmFile], { - stdio: 'inherit', - }) + const optResult = await exec( + 'wasm-opt', + [...wasmOptFlags, wasmFile, '-o', wasmFile], + { + stdio: 'inherit', + }, + ) if (optResult.code === 0) { stats = await fs.stat(wasmFile) @@ -348,7 +372,9 @@ try { // Report final size. if (!optimizationSucceeded) { - logger.info(`Final size: ${(originalSize / 1024 / 1024).toFixed(2)} MB (unoptimized)`) + logger.info( + `Final size: ${(originalSize / 1024 / 1024).toFixed(2)} MB (unoptimized)`, + ) } // Step 5: Embed as base64 in JavaScript. @@ -362,10 +388,14 @@ logger.progress('Compressing with brotli (quality 11 - maximum)') const { constants } = await import('node:zlib') const wasmCompressed = brotliCompressSync(wasmData, { params: { - [constants.BROTLI_PARAM_QUALITY]: 11, // Maximum quality (0-11) - [constants.BROTLI_PARAM_SIZE_HINT]: wasmData.length, // Hint for better compression - [constants.BROTLI_PARAM_LGWIN]: 24, // Maximum window size (10-24) - [constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_GENERIC, // Generic mode for binary data + // Maximum quality (0-11) + [constants.BROTLI_PARAM_QUALITY]: 11, + // Hint for better compression + [constants.BROTLI_PARAM_SIZE_HINT]: wasmData.length, + // Maximum window size (10-24) + [constants.BROTLI_PARAM_LGWIN]: 24, + // Generic mode for binary data + [constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_GENERIC, }, }) const compressionRatio = ( diff --git a/scripts/wasm/check-rust-toolchain.mjs b/scripts/wasm/check-rust-toolchain.mjs index a1bedbd14..3ac515143 100644 --- a/scripts/wasm/check-rust-toolchain.mjs +++ b/scripts/wasm/check-rust-toolchain.mjs @@ -18,7 +18,6 @@ import path from 'node:path' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { spawn } from '@socketsecurity/lib/spawn' - const logger = getDefaultLogger() /** * Execute command and wait for completion. diff --git a/scripts/wasm/download-models.mjs b/scripts/wasm/download-models.mjs index 934802abe..71e310fc7 100644 --- a/scripts/wasm/download-models.mjs +++ b/scripts/wasm/download-models.mjs @@ -52,21 +52,25 @@ const FILES = [ // CodeT5 (needs manual conversion first - see convert-codet5.mjs). { - copyFrom: null, // Set after conversion + // Set after conversion + copyFrom: null, description: 'CodeT5 encoder (int4)', name: 'codet5-encoder-int4.onnx', - url: null, // Needs conversion first + // Needs conversion first + url: null, }, { copyFrom: null, description: 'CodeT5 decoder (int4)', name: 'codet5-decoder-int4.onnx', - url: null, // Needs conversion first + // Needs conversion first + url: null, }, { description: 'CodeT5 tokenizer', name: 'codet5-tokenizer.json', - url: null, // Will be created by convert-codet5.mjs + // Will be created by convert-codet5.mjs + url: null, }, // ONNX Runtime WASM (from node_modules). diff --git a/scripts/wasm/optimize-embedded-wasm.mjs b/scripts/wasm/optimize-embedded-wasm.mjs index 628d9920e..87da64588 100644 --- a/scripts/wasm/optimize-embedded-wasm.mjs +++ b/scripts/wasm/optimize-embedded-wasm.mjs @@ -19,7 +19,6 @@ import { fileURLToPath } from 'node:url' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { spawn } from '@socketsecurity/lib/spawn' - const logger = getDefaultLogger() const __dirname = path.dirname(fileURLToPath(import.meta.url)) const rootPath = path.join(__dirname, '../..') @@ -69,7 +68,7 @@ async function getFileSizeMB(filePath) { * Optimize a single WASM file. */ async function optimizeWasmFile(inputPath, outputPath, options = {}) { - const { name, aggressive = false } = options + const { aggressive = false, name } = options if (!existsSync(inputPath)) { logger.warn(`File not found: ${inputPath}`) @@ -82,7 +81,8 @@ async function optimizeWasmFile(inputPath, outputPath, options = {}) { // Build optimization flags. const flags = [ - '-Oz', // Optimize for size + // Optimize for size + '-Oz', '--enable-simd', '--enable-bulk-memory', '--enable-sign-ext', @@ -97,7 +97,8 @@ async function optimizeWasmFile(inputPath, outputPath, options = {}) { '--flatten', '--rereloop', '--vacuum', - '--dce', // Dead code elimination + // Dead code elimination + '--dce', '--remove-unused-names', '--remove-unused-module-elements', '--strip-debug', @@ -108,9 +109,13 @@ async function optimizeWasmFile(inputPath, outputPath, options = {}) { } try { - const result = await exec('wasm-opt', [...flags, inputPath, '-o', outputPath], { - stdio: 'pipe', - }) + const result = await exec( + 'wasm-opt', + [...flags, inputPath, '-o', outputPath], + { + stdio: 'pipe', + }, + ) if (result.code === 0) { const optimizedSize = await getFileSizeMB(outputPath) @@ -190,7 +195,8 @@ async function main() { totalOriginal += originalSize totalOptimized += optimizedSize - logger.log('') // Spacing. + // Spacing. + logger.log('') } // Summary. diff --git a/vitest.config.mts b/vitest.config.mts index 61a3945b6..8e45928db 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -59,7 +59,7 @@ export default defineConfig({ hookTimeout: 30_000, bail: process.env.CI ? 1 : 0, // Exit on first failure in CI for faster feedback. sequence: { - concurrent: true // Run tests concurrently within suites for better parallelism. + concurrent: true, // Run tests concurrently within suites for better parallelism. }, coverage: { provider: 'v8',