diff --git a/package.json b/package.json index 898e6afa9..609daf9d5 100644 --- a/package.json +++ b/package.json @@ -35,9 +35,7 @@ "@socketregistry/packageurl-js": "catalog:", "@socketregistry/yocto-spinner": "catalog:", "@socketsecurity/config": "catalog:", - "@socketsecurity/lib": "catalog:", "@socketsecurity/registry": "catalog:", - "@socketsecurity/sdk": "catalog:", "@types/cmd-shim": "catalog:", "@types/ink": "catalog:", "@types/js-yaml": "catalog:", @@ -110,7 +108,6 @@ "overrides": { "@octokit/graphql": "catalog:", "@octokit/request-error": "catalog:", - "@socketsecurity/lib": "catalog:", "aggregate-error": "catalog:", "ansi-regex": "catalog:", "brace-expansion": "catalog:", diff --git a/packages/bootstrap/package.json b/packages/bootstrap/package.json index 30e4f035c..1eeac88f9 100644 --- a/packages/bootstrap/package.json +++ b/packages/bootstrap/package.json @@ -22,7 +22,7 @@ "@babel/types": "catalog:", "@socketsecurity/build-infra": "workspace:*", "@socketsecurity/cli": "workspace:*", - "@socketsecurity/lib": "catalog:", + "@socketsecurity/lib": "workspace:*", "del-cli": "catalog:", "esbuild": "catalog:", "magic-string": "catalog:", diff --git a/packages/build-infra/package.json b/packages/build-infra/package.json index fa0e20a01..862c84659 100644 --- a/packages/build-infra/package.json +++ b/packages/build-infra/package.json @@ -25,7 +25,7 @@ "dependencies": { "@babel/parser": "catalog:", "@babel/traverse": "catalog:", - "@socketsecurity/lib": "catalog:", + "@socketsecurity/lib": "workspace:*", "magic-string": "catalog:" } } diff --git a/packages/cli/package.json b/packages/cli/package.json index 5e10f0e0b..6f0b8bad0 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -75,7 +75,8 @@ "@coana-tech/cli": "catalog:", "@gitbeaker/rest": "catalog:", "@socketsecurity/build-infra": "workspace:*", - "@socketsecurity/lib": "catalog:", + "@socketsecurity/lib": "workspace:*", + "@socketsecurity/sdk": "workspace:*", "ajv-dist": "catalog:", "compromise": "catalog:", "del-cli": "catalog:", diff --git a/packages/codet5-models-builder/package.json b/packages/codet5-models-builder/package.json index f25a107bf..751b56bf5 100644 --- a/packages/codet5-models-builder/package.json +++ b/packages/codet5-models-builder/package.json @@ -11,6 +11,6 @@ }, "dependencies": { "@socketsecurity/build-infra": "workspace:*", - "@socketsecurity/lib": "catalog:" + "@socketsecurity/lib": "workspace:*" } } diff --git a/packages/lib/.config/esbuild.config.mjs b/packages/lib/.config/esbuild.config.mjs new file mode 100644 index 000000000..56173ab1d --- /dev/null +++ b/packages/lib/.config/esbuild.config.mjs @@ -0,0 +1,348 @@ +/** + * @fileoverview esbuild configuration for socket-lib + * Fast JS compilation with esbuild, declarations with tsgo + */ + +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import fg from 'fast-glob' + +import { getLocalPackageAliases } from '../scripts/utils/get-local-package-aliases.mjs' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const rootPath = path.join(__dirname, '..') +const srcPath = path.join(rootPath, 'src') +const distPath = path.join(rootPath, 'dist') + +// Find all TypeScript source files +const entryPoints = fg.sync('**/*.{ts,mts,cts}', { + cwd: srcPath, + absolute: true, + // Skip declaration files. + ignore: ['**/*.d.ts', '**/types/**', '**/external/**'], +}) + +/** + * Plugin to shorten module paths in bundled output with conflict detection. + * Uses @babel/parser and magic-string for precise AST-based modifications. + */ +function createPathShorteningPlugin() { + return { + name: 'shorten-module-paths', + setup(build) { + build.onEnd(async result => { + if (!result.outputFiles && result.metafile) { + // Dynamic imports to avoid adding to production dependencies + const fs = await import('node:fs/promises') + const { parse } = await import('@babel/parser') + const MagicString = (await import('magic-string')).default + + const outputs = Object.keys(result.metafile.outputs).filter(f => + f.endsWith('.js'), + ) + + for (const outputPath of outputs) { + // eslint-disable-next-line no-await-in-loop + const content = await fs.readFile(outputPath, 'utf8') + const magicString = new MagicString(content) + + // Track module paths and their shortened versions + // Map + const pathMap = new Map() + // Track shortened paths to detect conflicts + // Map + const conflictDetector = new Map() + + /** + * Shorten a module path and detect conflicts. + */ + // eslint-disable-next-line unicorn/consistent-function-scoping + const shortenPath = longPath => { + if (pathMap.has(longPath)) { + return pathMap.get(longPath) + } + + let shortPath = longPath + + // Handle pnpm scoped packages + // node_modules/.pnpm/@scope+pkg@version/node_modules/@scope/pkg/dist/file.js + // -> @scope/pkg/dist/file.js + const scopedPnpmMatch = longPath.match( + /node_modules\/\.pnpm\/@([^+/]+)\+([^@/]+)@[^/]+\/node_modules\/(@[^/]+\/[^/]+)\/(.+)/, + ) + if (scopedPnpmMatch) { + const [, _scope, _pkg, packageName, subpath] = scopedPnpmMatch + shortPath = `${packageName}/${subpath}` + } else { + // Handle pnpm non-scoped packages + // node_modules/.pnpm/pkg@version/node_modules/pkg/dist/file.js + // -> pkg/dist/file.js + const pnpmMatch = longPath.match( + /node_modules\/\.pnpm\/([^@/]+)@[^/]+\/node_modules\/([^/]+)\/(.+)/, + ) + if (pnpmMatch) { + const [, _pkgName, packageName, subpath] = pnpmMatch + shortPath = `${packageName}/${subpath}` + } + } + + // Detect conflicts + if (conflictDetector.has(shortPath)) { + const existingPath = conflictDetector.get(shortPath) + if (existingPath !== longPath) { + // Conflict detected - keep original path + console.warn( + `⚠ Path conflict detected:\n "${shortPath}"\n Maps to: "${existingPath}"\n Also from: "${longPath}"\n Keeping original paths to avoid conflict.`, + ) + shortPath = longPath + } + } else { + conflictDetector.set(shortPath, longPath) + } + + pathMap.set(longPath, shortPath) + return shortPath + } + + // Parse AST to find all string literals containing module paths + try { + const ast = parse(content, { + sourceType: 'module', + plugins: [], + }) + + // Walk through all comments (esbuild puts module paths in comments) + for (const comment of ast.comments || []) { + if ( + comment.type === 'CommentLine' && + comment.value.includes('node_modules') + ) { + const originalPath = comment.value.trim() + const shortPath = shortenPath(originalPath) + + if (shortPath !== originalPath) { + // Replace in comment + const commentStart = comment.start + const commentEnd = comment.end + magicString.overwrite( + commentStart, + commentEnd, + `// ${shortPath}`, + ) + } + } + } + + // Walk through all string literals in __commonJS calls + const walk = node => { + if (!node || typeof node !== 'object') { + return + } + + // Check for string literals containing node_modules paths + if ( + node.type === 'StringLiteral' && + node.value && + node.value.includes('node_modules') + ) { + const originalPath = node.value + const shortPath = shortenPath(originalPath) + + if (shortPath !== originalPath) { + // Replace the string content (keep quotes) + magicString.overwrite( + node.start + 1, + node.end - 1, + shortPath, + ) + } + } + + // Recursively walk all properties + for (const key of Object.keys(node)) { + if (key === 'start' || key === 'end' || key === 'loc') { + continue + } + const value = node[key] + if (Array.isArray(value)) { + for (const item of value) { + walk(item) + } + } else { + walk(value) + } + } + } + + walk(ast.program) + + // Write the modified content + // eslint-disable-next-line no-await-in-loop + await fs.writeFile(outputPath, magicString.toString(), 'utf8') + } catch (error) { + console.error( + `Failed to shorten paths in ${outputPath}:`, + error.message, + ) + // Continue without failing the build + } + } + } + }) + }, + } +} + +/** + * Plugin to resolve internal path aliases (#lib/*, #constants/*, etc.) to relative paths + */ +function createPathAliasPlugin() { + return { + name: 'internal-path-aliases', + setup(build) { + // Map of path aliases to their actual directories + const pathAliases = { + '#lib/': srcPath, + '#constants/': path.join(srcPath, 'constants'), + '#env/': path.join(srcPath, 'env'), + '#packages/': path.join(srcPath, 'packages'), + '#utils/': path.join(srcPath, 'utils'), + '#types': path.join(srcPath, 'types'), + } + + // Intercept imports for path aliases + for (const [alias, basePath] of Object.entries(pathAliases)) { + const isExact = !alias.endsWith('/') + const filter = isExact + ? new RegExp(`^${alias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`) + : new RegExp(`^${alias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`) + + build.onResolve({ filter }, args => { + // Calculate the subpath after the alias + const subpath = isExact ? '' : args.path.slice(alias.length) + const targetPath = subpath ? path.join(basePath, subpath) : basePath + + // Calculate relative path from the importing file to the target + const importer = args.importer || srcPath + const importerDir = path.dirname(importer) + let relativePath = path.relative(importerDir, targetPath) + + // Ensure relative paths start with ./ or ../ + if (!relativePath.startsWith('.')) { + relativePath = `./${relativePath}` + } + + // Normalize to forward slashes for consistency + relativePath = relativePath.replace(/\\/g, '/') + + return { path: relativePath, external: true } + }) + } + }, + } +} + +/** + * Plugin to handle local package aliases when bundle: false + * esbuild's built-in alias only works with bundle: true, so we need a custom plugin + */ +function createAliasPlugin() { + const aliases = getLocalPackageAliases(rootPath) + + // Only create plugin if we have local aliases + if (Object.keys(aliases).length === 0) { + return null + } + + return { + name: 'local-package-aliases', + setup(build) { + // Intercept imports for aliased packages + for (const [packageName, aliasPath] of Object.entries(aliases)) { + // Match both exact package name and subpath imports + build.onResolve( + { filter: new RegExp(`^${packageName}(/|$)`) }, + args => { + // Handle subpath imports like '@socketsecurity/lib/spinner' + const subpath = args.path.slice(packageName.length + 1) + const resolvedPath = subpath + ? path.join(aliasPath, subpath) + : aliasPath + return { path: resolvedPath, external: true } + }, + ) + } + }, + } +} + +// Build configuration for CommonJS output +export const buildConfig = { + entryPoints, + outdir: distPath, + outbase: srcPath, + // Don't bundle - library pattern (each file separate). + bundle: false, + format: 'cjs', + platform: 'node', + target: 'node18', + sourcemap: false, + // Don't minify - this is a library and minification breaks ESM/CJS interop. + minify: false, + // Tree-shaking optimization. + treeShaking: true, + metafile: true, + logLevel: 'info', + + // Use plugin for local package aliases (built-in alias requires bundle: true). + plugins: [ + createPathShorteningPlugin(), + createPathAliasPlugin(), + createAliasPlugin(), + ].filter(Boolean), + + // Note: Cannot use "external" with bundle: false. + // esbuild automatically treats all imports as external when not bundling. + + // Define constants for optimization + define: { + 'process.env.NODE_ENV': JSON.stringify( + process.env.NODE_ENV || 'production', + ), + }, + + // Banner for generated code + banner: { + js: '"use strict";\n/* Socket Lib - Built with esbuild */', + }, +} + +// Watch configuration for development with incremental builds +export const watchConfig = { + ...buildConfig, + minify: false, + sourcemap: 'inline', + logLevel: 'debug', +} + +/** + * Analyze build output for size information + */ +export function analyzeMetafile(metafile) { + const outputs = Object.keys(metafile.outputs) + let totalSize = 0 + + const files = outputs.map(file => { + const output = metafile.outputs[file] + totalSize += output.bytes + return { + name: path.relative(rootPath, file), + size: `${(output.bytes / 1024).toFixed(2)} KB`, + } + }) + + return { + files, + totalSize: `${(totalSize / 1024).toFixed(2)} KB`, + } +} diff --git a/packages/lib/.config/eslint.config.mjs b/packages/lib/.config/eslint.config.mjs new file mode 100644 index 000000000..c7d4044d4 --- /dev/null +++ b/packages/lib/.config/eslint.config.mjs @@ -0,0 +1,368 @@ +import { createRequire } from 'node:module' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import { + convertIgnorePatternToMinimatch, + includeIgnoreFile, +} from '@eslint/compat' +import js from '@eslint/js' +import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript' +import { flatConfigs as origImportXFlatConfigs } from 'eslint-plugin-import-x' +import nodePlugin from 'eslint-plugin-n' +import sortDestructureKeysPlugin from 'eslint-plugin-sort-destructure-keys' +import unicornPlugin from 'eslint-plugin-unicorn' +import globals from 'globals' +import tsEslint from 'typescript-eslint' + +// Resolve current module paths for proper configuration loading. +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const require = createRequire(import.meta.url) + +const rootPath = path.dirname(__dirname) +const rootTsConfigPath = path.join(rootPath, 'tsconfig.json') + +// Convert Node.js globals to readonly format for ESLint configuration. +// This ensures Node.js built-ins are recognized but not modifiable. +const nodeGlobalsConfig = Object.fromEntries( + Object.entries(globals.node).map(([k]) => [k, 'readonly']), +) + +// Import Biome config to synchronize ignore patterns between formatters. +// This reduces configuration duplication and ensures consistent file filtering. +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), + name: 'Imported .gitignore ignore patterns', +} + +// OPTIMIZATION: When LINT_EXTERNAL is set, include external dependencies in linting. +// This is disabled by default for performance since external deps are pre-validated. +// Enable only for comprehensive checks before releases. +if (process.env.LINT_EXTERNAL) { + const isNotExternalGlobPattern = p => !/(?:^|[\\/])external/.test(p) + if (biomeIgnores.ignores) { + biomeIgnores.ignores = biomeIgnores.ignores.filter(isNotExternalGlobPattern) + } + if (gitIgnores.ignores) { + gitIgnores.ignores = gitIgnores.ignores.filter(isNotExternalGlobPattern) + } +} + +// OPTIMIZATION: For socket-lib (single package), no dynamic ignore patterns needed. +// This is a simplified version for non-monorepo usage. +function getIgnores(_isEsm) { + return [] +} + +function getImportXFlatConfigs(isEsm) { + return { + recommended: { + ...origImportXFlatConfigs.recommended, + languageOptions: { + ...origImportXFlatConfigs.recommended.languageOptions, + ecmaVersion: 'latest', + sourceType: isEsm ? 'module' : 'script', + }, + }, + typescript: { + ...origImportXFlatConfigs.typescript, + plugins: { + ...origImportXFlatConfigs.recommended.plugins, + ...origImportXFlatConfigs.typescript.plugins, + }, + settings: { + ...origImportXFlatConfigs.typescript.settings, + 'import-x/resolver-next': [ + createTypeScriptImportResolver({ + project: rootTsConfigPath, + }), + ], + }, + rules: { + ...origImportXFlatConfigs.recommended.rules, + 'import-x/extensions': [ + 'error', + 'never', + { + cjs: 'ignorePackages', + js: 'ignorePackages', + json: 'always', + mjs: 'ignorePackages', + }, + ], + // Disable import ordering auto-fix to prevent conflicts with Biome. + // Biome handles import formatting and organization. + 'import-x/order': 'off', + // 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', + }, + }, + } +} + +function configs(sourceType) { + const isEsm = sourceType === 'module' + const ignores = getIgnores(isEsm) + const importFlatConfigs = getImportXFlatConfigs(isEsm) + const nodePluginConfigs = + nodePlugin.configs[`flat/recommended-${isEsm ? 'module' : 'script'}`] + const sharedPlugins = { + ...nodePluginConfigs.plugins, + 'sort-destructure-keys': sortDestructureKeysPlugin, + unicorn: unicornPlugin, + } + const sharedRules = { + 'line-comment-position': ['error', { position: 'above' }], + 'n/exports-style': ['error', 'module.exports'], + // 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', + 'no-unexpected-multiline': 'off', + 'n/no-unsupported-features/es-builtins': [ + 'error', + { + ignores: ['Object.groupBy'], + version: '>=22', + }, + ], + 'n/no-unsupported-features/es-syntax': [ + 'error', + { + ignores: ['object-map-groupby'], + version: '>=22', + }, + ], + 'n/no-unsupported-features/node-builtins': [ + 'error', + { + ignores: [ + 'buffer.File', + 'buffer.isAscii', + 'buffer.isUtf8', + 'buffer.resolveObjectURL', + 'events.getMaxListeners', + 'fetch', + 'fs.promises.cp', + 'module.isBuiltin', + 'process.features.require_module', + 'ReadableStream', + 'Response', + ], + version: '>=22', + }, + ], + 'n/prefer-node-protocol': 'off', + 'unicorn/consistent-function-scoping': 'error', + curly: 'error', + 'no-await-in-loop': 'error', + // Disable no-control-regex - ANSI escape sequences intentionally use control chars. + // Biome handles this check via lint/suspicious/noControlCharactersInRegex. + 'no-control-regex': 'off', + 'no-empty': ['error', { allowEmptyCatch: true }], + 'no-new': 'error', + 'no-proto': 'error', + 'no-undef': 'error', + 'no-self-assign': ['error', { props: false }], + 'no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_|^this$', + ignoreRestSiblings: true, + varsIgnorePattern: '^_', + }, + ], + 'no-var': 'error', + 'no-warning-comments': 'error', + 'prefer-const': 'error', + 'sort-destructure-keys/sort-destructure-keys': 'error', + // Disable sort-imports to prevent conflicts with Biome. + // Biome handles import formatting and organization. + 'sort-imports': 'off', + } + + return [ + { + ...js.configs.recommended, + ...importFlatConfigs.recommended, + ...nodePluginConfigs, + ignores, + languageOptions: { + ...js.configs.recommended.languageOptions, + ...importFlatConfigs.recommended.languageOptions, + ...nodePluginConfigs.languageOptions, + globals: { + ...js.configs.recommended.languageOptions?.globals, + ...importFlatConfigs.recommended.languageOptions?.globals, + ...nodePluginConfigs.languageOptions?.globals, + ...nodeGlobalsConfig, + NodeJS: false, + }, + sourceType: isEsm ? 'module' : 'script', + }, + plugins: { + ...js.configs.recommended.plugins, + ...importFlatConfigs.recommended.plugins, + ...sharedPlugins, + }, + rules: { + ...js.configs.recommended.rules, + ...importFlatConfigs.recommended.rules, + ...nodePluginConfigs.rules, + ...sharedRules, + }, + }, + { + files: ['**/*.{cts,mts,ts}'], + ...js.configs.recommended, + ...importFlatConfigs.typescript, + ignores, + languageOptions: { + ...js.configs.recommended.languageOptions, + ...importFlatConfigs.typescript.languageOptions, + ecmaVersion: 'latest', + sourceType, + parser: tsEslint.parser, + parserOptions: { + ...importFlatConfigs.typescript.languageOptions?.parserOptions, + project: [ + path.join(rootPath, 'tsconfig.json'), + path.join(rootPath, 'tsconfig.test.json'), + ], + tsconfigRootDir: rootPath, + }, + }, + plugins: { + ...js.configs.recommended.plugins, + ...importFlatConfigs.typescript.plugins, + ...sharedPlugins, + '@typescript-eslint': tsEslint.plugin, + }, + rules: { + ...js.configs.recommended.rules, + ...importFlatConfigs.typescript.rules, + ...sharedRules, + '@typescript-eslint/array-type': ['error', { default: 'array-simple' }], + '@typescript-eslint/consistent-type-assertions': [ + 'error', + { assertionStyle: 'as' }, + ], + '@typescript-eslint/no-extraneous-class': 'off', + '@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'], + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_|^this$', + ignoreRestSiblings: true, + varsIgnorePattern: '^_', + }, + ], + // Disable the following rules because they don't play well with TypeScript. + 'dot-notation': 'off', + 'no-redeclare': 'off', + 'no-unused-vars': 'off', + // Disable node plugin rules that can't resolve TypeScript imports. + 'n/no-missing-import': 'off', + 'n/no-missing-require': 'off', + }, + }, + { + files: ['**/*.d.{cts,mts,ts}'], + ignores, + rules: { + 'n/no-unpublished-import': 'off', + // Disable the following rules because they don't play well with TypeScript. + 'n/no-missing-import': 'off', + // Disable no-unused-vars for type definition files since they contain declarations. + '@typescript-eslint/no-unused-vars': 'off', + 'no-unused-vars': 'off', + }, + }, + ] +} + +export default [ + gitIgnores, + biomeIgnores, + { + ignores: [ + // Dot folders. + '.*/**', + // Nested directories. + '**/coverage/**', + '**/dist/**', + '**/external/**', + '**/node_modules/**', + // Bundled packages. + 'packages/npm/**/package/**', + // Registry paths. + 'registry/src/external/**/*.d.ts', + 'registry/dist/**', + // Generated TypeScript files. + '**/*.d.ts', + '**/*.d.ts.map', + '**/*.tsbuildinfo', + ], + }, + ...configs('script'), + ...configs('module'), + { + // The external directory contains rollup-bundled dependencies that are + // part of the published package. The n/no-unpublished-require rule doesn't + // understand that these files are included via the "files" field, so we + // disable it for registry/lib. The n/no-missing-require rule still runs + // and will catch actual missing dependencies. + files: ['registry/lib/**/*.js', 'registry/lib/**/*.cjs'], + rules: { + 'n/no-unpublished-require': 'off', + }, + }, + { + // Relax rules for test files - testing code has different conventions + files: ['test/**/*.ts', 'test/**/*.mts'], + rules: { + 'n/no-missing-import': 'off', + 'import-x/no-unresolved': 'off', + 'line-comment-position': 'off', + 'unicorn/consistent-function-scoping': 'off', + 'no-undef': 'off', // TypeScript handles this + 'no-import-assign': 'off', // Tests intentionally reassign imports to test immutability + 'no-await-in-loop': 'off', // Tests often need to await in loops + }, + }, + { + // Relax rules for script files + files: ['scripts/**/*.mjs', 'registry/scripts/**/*.mjs'], + rules: { + 'n/no-process-exit': 'off', + 'no-await-in-loop': 'off', + }, + }, +] diff --git a/packages/lib/.config/knip.json b/packages/lib/.config/knip.json new file mode 100644 index 000000000..46df55374 --- /dev/null +++ b/packages/lib/.config/knip.json @@ -0,0 +1,13 @@ +{ + "entry": [ + "perf/npm/json-stable-stringify.perf.ts", + "registry/index.js", + "registry/constants/index.js", + "scripts/**/*.{js,ts}", + "src/**/*.ts", + "test/**/*.ts", + "*.js" + ], + "project": ["perf/**", "registry/**", "scripts/**", "src/**", "test/**"], + "ignore": ["packages/**", "test/npm/packages/**"] +} diff --git a/packages/lib/.config/taze.config.mts b/packages/lib/.config/taze.config.mts new file mode 100644 index 000000000..8a55df65c --- /dev/null +++ b/packages/lib/.config/taze.config.mts @@ -0,0 +1,24 @@ +import { defineConfig } from 'taze' + +export default defineConfig({ + // Exclude these packages. + exclude: [ + 'debug', + 'eslint-plugin-unicorn', + 'make-fetch-happen', + 'minimatch', + 'normalize-package-data', + ], + // Interactive mode disabled for automation. + interactive: false, + // Silent logging. + loglevel: 'silent', + // Only update packages that have been stable for 7 days. + maturityPeriod: 7, + // Update mode: 'latest'. + mode: 'latest', + // Recursive mode to handle all package.json files. + recursive: true, + // Write to package.json automatically. + write: true, +}) diff --git a/packages/lib/.config/tsconfig.check.json b/packages/lib/.config/tsconfig.check.json new file mode 100644 index 000000000..e1e983f39 --- /dev/null +++ b/packages/lib/.config/tsconfig.check.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "..", + "types": ["node", "vitest"], + "skipLibCheck": true, + "strict": false, + "noImplicitAny": false + }, + "include": ["../src/**/*.ts", "../test/**/*.ts", "../test/**/*.mts"], + "exclude": ["../node_modules", "../dist/**/*"] +} diff --git a/packages/lib/.config/tsconfig.external-aliases.json b/packages/lib/.config/tsconfig.external-aliases.json new file mode 100644 index 000000000..638212850 --- /dev/null +++ b/packages/lib/.config/tsconfig.external-aliases.json @@ -0,0 +1,29 @@ +{ + "extends": "./tsconfig.check.json", + "compilerOptions": { + "paths": { + "#constants/*": ["../src/constants/*"], + "#env/*": ["../src/env/*"], + "#lib/*": ["../src/*"], + "#packages/*": ["../src/packages/*"], + "#types": ["../src/types"], + "#utils/*": ["../src/utils/*"], + "cacache": ["../src/external/cacache"], + "make-fetch-happen": ["../src/external/make-fetch-happen"], + "fast-sort": ["../src/external/fast-sort"], + "pacote": ["../src/external/pacote"], + "@socketsecurity/lib": ["../../socket-lib/dist/index.d.ts"], + "@socketsecurity/lib/*": ["../../socket-lib/dist/*"], + "@socketsecurity/registry": [ + "../../socket-registry/registry/dist/index.d.ts" + ], + "@socketsecurity/registry/*": ["../../socket-registry/registry/dist/*"], + "@socketregistry/packageurl-js": [ + "../../socket-packageurl-js/dist/index.d.ts" + ], + "@socketregistry/packageurl-js/*": ["../../socket-packageurl-js/dist/*"], + "@socketsecurity/sdk": ["../../socket-sdk-js/dist/index.d.ts"], + "@socketsecurity/sdk/*": ["../../socket-sdk-js/dist/*"] + } + } +} diff --git a/packages/lib/.config/vitest-global-setup.mts b/packages/lib/.config/vitest-global-setup.mts new file mode 100644 index 000000000..fdc631243 --- /dev/null +++ b/packages/lib/.config/vitest-global-setup.mts @@ -0,0 +1,18 @@ +/** + * @fileoverview Global setup for Vitest. + * Ensures necessary directories exist before running tests. + */ + +import { mkdir } from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const projectRoot = path.resolve(__dirname, '..') + +export async function setup() { + // Ensure coverage/.tmp directory exists to prevent ENOENT errors + // when vitest's v8 coverage provider writes temporary coverage files. + const coverageTmpDir = path.join(projectRoot, 'coverage', '.tmp') + await mkdir(coverageTmpDir, { recursive: true }) +} diff --git a/packages/lib/.config/vitest-plugins/import-transform.mts b/packages/lib/.config/vitest-plugins/import-transform.mts new file mode 100644 index 000000000..ba2c735f7 --- /dev/null +++ b/packages/lib/.config/vitest-plugins/import-transform.mts @@ -0,0 +1,66 @@ +import { resolve } from 'node:path' + +import type { Plugin } from 'vite' + +/** + * Vite plugin to transform ES6 import paths from dist/ to src/ during coverage. + * This allows tests with ES6 imports to load TypeScript source files for instrumentation. + */ +export function createImportTransformPlugin( + isCoverageEnabled: boolean, + projectRoot: string, +): Plugin { + if (!isCoverageEnabled) { + return { name: 'socket:import-transform-noop' } + } + + // projectRoot is the .config directory, so go up one level for the actual project root + const actualProjectRoot = resolve(projectRoot, '..') + + return { + name: 'socket:import-transform', + enforce: 'pre', + + async resolveId(source: string, importer: string | undefined) { + // Handle @socketsecurity/registry imports. + if (source.startsWith('@socketsecurity/registry')) { + const subpath = source.replace('@socketsecurity/registry', '') + // Transform: @socketsecurity/registry → /abs/path/registry/src/index.ts + // Transform: @socketsecurity/registry/lib/foo → /abs/path/registry/src/lib/foo.ts + const targetPath = subpath + ? resolve(actualProjectRoot, `registry/src${subpath}.ts`) + : resolve(actualProjectRoot, 'registry/src/index.ts') + return { id: targetPath } + } + + // Only handle relative imports for dist/ transformation. + if ( + !importer || + (!source.startsWith('./') && !source.startsWith('../')) + ) { + return null + } + + // Check if this is a dist/ import that needs transformation. + if (source.includes('registry/dist/')) { + // Transform: ../../registry/dist/lib/foo.js → registry/src/lib/foo.ts + const transformed = source + .replace(/registry\/dist\//, 'registry/src/') + .replace(/\.js$/, '.ts') + .replace(/\.mjs$/, '.mts') + .replace(/\.cjs$/, '.cts') + + // Resolve to absolute path. + const absolutePath = resolve( + importer.substring(0, importer.lastIndexOf('/')), + transformed, + ) + + // Return the absolute path directly. + return { id: absolutePath } + } + + return null + }, + } +} diff --git a/packages/lib/.config/vitest-plugins/require-transform.mts b/packages/lib/.config/vitest-plugins/require-transform.mts new file mode 100644 index 000000000..39d314c7e --- /dev/null +++ b/packages/lib/.config/vitest-plugins/require-transform.mts @@ -0,0 +1,466 @@ +import { readFileSync } from 'node:fs' +import { dirname, resolve } from 'node:path' + +import { parse } from '@babel/parser' +import type { NodePath } from '@babel/traverse' +import traverseModule from '@babel/traverse' +import * as t from '@babel/types' +import MagicString from 'magic-string' + +import type { Plugin } from 'vite' + +import { getDistDir, srcToDistPath } from './transform-utils.mts' + +// Handle both ESM and CJS exports from @babel/traverse +const traverse = + (traverseModule as { default?: typeof traverseModule }).default || + traverseModule + +/** + * Vite plugin to inline CommonJS require() calls. + * Uses Babel AST parsing for robust detection and MagicString for source transformations. + * Since require() bypasses Vite's module system, we inline simple constant values at transform time. + */ +export function createRequireTransformPlugin(): Plugin { + // Cache for loaded constant values + const constantCache = new Map() + + /** + * Evaluate if a constant file can be safely inlined. + * + * Strategy: + * 1. Check cache first for performance + * 2. Read and parse the TypeScript file into an AST + * 3. Detect if the file has imports (makes it non-inlineable) + * 4. Find the default export and check if it's a safe, simple expression + * 5. Extract the source code of safe expressions for inlining + * + * Returns the stringified value if inlineable, null otherwise. + * + * @param resolvedPath - Absolute path to the constant file + * @returns Source code string to inline, or null if not inlineable + */ + function loadConstant(resolvedPath: string): string | null { + // Step 1: Check cache to avoid re-parsing the same file + if (constantCache.has(resolvedPath)) { + const cached = constantCache.get(resolvedPath) + return cached === undefined ? null : cached + } + + try { + // Step 2: Read the source file from disk + const content = readFileSync(resolvedPath, 'utf8') + + // Step 3: Parse TypeScript source into an Abstract Syntax Tree (AST). + // This allows us to analyze the code structure programmatically. + // Parse as ES module (supports import/export). + // Enable TypeScript syntax parsing. + const ast = parse(content, { + sourceType: 'module', + plugins: ['typescript'], + }) + + // Step 4: Track state during AST traversal. + // Files with imports are too complex to inline. + let hasImports = false + // Will hold the inlineable value. + let defaultExportValue: string | null = null + + // Step 5: Traverse the AST to find imports and exports + // traverse() walks through every node in the syntax tree + traverse(ast, { + // Visitor for import statements (e.g., import foo from './bar') + ImportDeclaration() { + // If the file imports other modules, it has dependencies + // and shouldn't be inlined (too complex) + hasImports = true + }, + + // Visitor for default exports (e.g., export default 'value') + ExportDefaultDeclaration( + nodePath: NodePath, + ) { + const declaration = nodePath.node.declaration + + // Step 6: Check if the exported value is a safe, simple expression + // We only inline literals and simple safe expressions to avoid + // breaking code that requires runtime evaluation + + // Inline primitive literals: strings, numbers, booleans, null, undefined. + // 'hello' or "hello". + // 42 or 3.14. + // true or false. + // null. + // undefined. + // [] or [1, 2, 3]. + // {} or {a: 1}. + if ( + t.isStringLiteral(declaration) || + t.isNumericLiteral(declaration) || + t.isBooleanLiteral(declaration) || + t.isNullLiteral(declaration) || + t.isIdentifier(declaration, { name: 'undefined' }) || + t.isArrayExpression(declaration) || + t.isObjectExpression(declaration) + ) { + // Step 7: Extract the exact source code for this expression + // Using start/end positions from the AST preserves formatting + if ( + typeof declaration.start === 'number' && + typeof declaration.end === 'number' + ) { + defaultExportValue = content.slice( + declaration.start, + declaration.end, + ) + } + } + // Inline binary expressions like: process.platform === 'win32' + else if (t.isBinaryExpression(declaration)) { + // Safe because it's evaluated at load time + if ( + typeof declaration.start === 'number' && + typeof declaration.end === 'number' + ) { + defaultExportValue = content.slice( + declaration.start, + declaration.end, + ) + } + } + // Inline Object.freeze() calls for frozen constants + else if (t.isCallExpression(declaration)) { + // Check if it's ObjectFreeze() or Object.freeze() + if ( + t.isIdentifier(declaration.callee, { name: 'ObjectFreeze' }) || + (t.isMemberExpression(declaration.callee) && + t.isIdentifier(declaration.callee.object, { name: 'Object' }) && + t.isIdentifier(declaration.callee.property, { name: 'freeze' })) + ) { + if ( + typeof declaration.start === 'number' && + typeof declaration.end === 'number' + ) { + defaultExportValue = content.slice( + declaration.start, + declaration.end, + ) + } + } + } + // Inline template literals like: `hello ${world}` + else if (t.isTemplateLiteral(declaration)) { + // Safe if they only contain simple expressions + if ( + typeof declaration.start === 'number' && + typeof declaration.end === 'number' + ) { + defaultExportValue = content.slice( + declaration.start, + declaration.end, + ) + } + } + }, + }) + + // Step 8: Determine if we can inline this constant + // Don't inline if: + // - The file has imports (depends on other modules) + // - No safe default export was found + // - The export is a complex expression (function call, etc.) + if (hasImports || !defaultExportValue) { + constantCache.set(resolvedPath, null) + return null + } + + // Step 9: Cache and return the inlineable value + constantCache.set(resolvedPath, defaultExportValue) + return defaultExportValue + } catch { + // Parse error (invalid syntax) or file doesn't exist. + // Cache null to avoid retrying. + constantCache.set(resolvedPath, null) + return null + } + } + + return { + name: 'socket:require-transform', + + /** + * Transform source code to inline require() calls for constants. + * + * This is called by Vite for each source file during the build/test process. + * + * Strategy: + * 1. Filter to only our lib files to avoid processing unrelated code + * 2. Quick check if file contains require() to skip unnecessary parsing + * 3. Parse the source into an AST for robust detection + * 4. Find all require() CallExpression nodes + * 5. For each require('./X.js'), try to inline the constant value + * 6. Use MagicString to perform precise source code replacements + * 7. Generate source map for debugging + * + * @param code - Source code of the file + * @param id - Absolute path to the file being transformed + * @returns Transformed code with inlined constants, or null if no changes + */ + transform(code: string, id: string) { + // Step 1: Only apply to our registry lib files during coverage + // This prevents accidentally transforming node_modules or test files + if (!id.includes('/registry/src/lib/')) { + return null + } + + // Step 2: Quick early exit optimization + // If the file doesn't contain 'require(', skip expensive AST parsing + if (!code.includes('require(')) { + return null + } + + // Step 3: Initialize MagicString for precise source code manipulation. + // MagicString allows us to replace specific ranges while preserving + // the rest of the source code and generating accurate source maps. + const s = new MagicString(code) + // Track if we made any changes. + let modified = false + + try { + // Step 4: Parse the source file into an AST. + // This gives us a structured representation of the code. + // Support ES modules (import/export). + // Parse TypeScript syntax. + const ast = parse(code, { + sourceType: 'module', + plugins: ['typescript'], + }) + + // Step 5: Traverse the AST to find all require() calls + // traverse() visits every node in the syntax tree + traverse(ast, { + // Visitor for function calls (e.g., require(...), foo(), etc.) + CallExpression(nodePath: NodePath) { + const { node } = nodePath + + // Step 6: Check if this call is specifically require() + // We only want to transform require(), not other function calls. + // Not a require() call, skip. + if (!t.isIdentifier(node.callee, { name: 'require' })) { + return + } + + // Step 7: Handle both string literals and template literals + if (node.arguments.length !== 1) { + return + } + + const arg = node.arguments[0] + + // Handle template literals like `require(\`./\${variable}\`)` + if (t.isTemplateLiteral(arg)) { + // Only handle simple patterns: `./prefix${variable}suffix` + // Check if it starts with ./ or ../ + const firstQuasi = arg.quasis[0] + if ( + !firstQuasi?.value.raw.startsWith('./') && + !firstQuasi?.value.raw.startsWith('../') + ) { + return + } + + // Build a template literal that resolves to dist/ + // For constants/index.ts with require(`./\${k}`): + // Transform to require(`/abs/path/dist/lib/constants/\${k}`) + const distDir = getDistDir(id) + + if (!distDir) { + return + } + + // Reconstruct the template literal with absolute dist path + // Replace the leading ./ or ../ with the absolute dist path + const newQuasis = arg.quasis.map((quasi, i) => { + if (i === 0) { + const raw = quasi.value.raw.replace(/^\.\.?\//, `${distDir}/`) + return t.templateElement({ raw, cooked: raw }, quasi.tail) + } + return quasi + }) + + // Replace the argument + if ( + typeof arg.start === 'number' && + typeof arg.end === 'number' + ) { + s.overwrite( + arg.start, + arg.end, + code + .slice(arg.start, arg.end) + .replace(firstQuasi.value.raw, newQuasis[0].value.raw), + ) + modified = true + } + return + } + + // Handle string literals (existing logic) + if (!t.isStringLiteral(arg)) { + return + } + + const requirePath = arg.value + + // Step 8: Only handle relative requires from our code. + // We don't inline: + // - Absolute requires (e.g., require('fs')) + // - npm package requires (e.g., require('lodash')) + // Not a relative require, skip. + if ( + !requirePath.startsWith('./') && + !requirePath.startsWith('../') + ) { + return + } + + try { + // Step 9: Resolve the require modulePath to the actual TypeScript file. + // Directory of current file. + const currentDir = dirname(id) + + // Special handling for external dependencies that only exist in dist. + // These are bundled modules that don't have TypeScript sources. + const externalModules = [ + 'fast-sort', + 'semver', + 'del', + 'cacache', + 'libnpmpack', + 'pacote', + 'browserslist', + 'yargs-parser', + 'zod', + ] + + // Check if this is an external module require. + const requireName = requirePath + .replace(/^\.\.\/\.\.\//, '') + .replace(/\.js$/, '') + if (externalModules.includes(requireName)) { + // For external modules, directly use the dist version. + const projectRoot = id.split('/registry/')[0] + const distPath = resolve( + projectRoot, + 'registry/dist', + `${requireName}.js`, + ) + + // Replace the require with absolute path to dist. + const stringNode = arg as t.StringLiteral + if ( + typeof stringNode.start === 'number' && + typeof stringNode.end === 'number' + ) { + s.overwrite(stringNode.start, stringNode.end, `'${distPath}'`) + modified = true + } + return + } + + // Convert .js to .ts, or add .ts if no extension. + const tsPath = requirePath.endsWith('.js') + ? requirePath.replace(/\.js$/, '.ts') + : `${requirePath}.ts` + // Absolute file path. + const resolvedPath = resolve(currentDir, tsPath) + + // Step 10: Try to load and inline the constant. + const value = loadConstant(resolvedPath) + if (value !== null) { + // Step 11: Determine what to replace. + // Handle both require('./X.js') and require('./X.js').default. + const parent = nodePath.parent + // Start of require() call. + const replaceStart = node.start + // End of require() call. + let replaceEnd = node.end + + if ( + typeof replaceStart !== 'number' || + typeof replaceEnd !== 'number' + ) { + return + } + + // Check if there's a .default property access after require(). + // Is a property access. + // On the require() result. + // Accessing .default. + if ( + t.isMemberExpression(parent) && + parent.object === node && + t.isIdentifier(parent.property, { name: 'default' }) + ) { + // Replace the entire require('./X.js').default expression + if (typeof parent.end === 'number') { + replaceEnd = parent.end + } + } + + // Step 12: Use MagicString to replace the require with the inlined value + // This preserves the rest of the source code exactly as-is. + s.overwrite(replaceStart, replaceEnd, value) + modified = true + } else { + // Step 11b: If we can't inline, rewrite to use compiled dist/ version. + // During coverage, require() can't load TypeScript files, so we use + // the compiled JavaScript files from dist/ which Node can handle. + const stringNode = arg as t.StringLiteral + const absoluteDistPath = srcToDistPath(resolvedPath) + + if (!absoluteDistPath) { + // Not in lib directory, skip transformation. + return + } + + // Replace the require string with the absolute dist path. + if ( + typeof stringNode.start === 'number' && + typeof stringNode.end === 'number' + ) { + s.overwrite( + stringNode.start, + stringNode.end, + `'${absoluteDistPath}'`, + ) + modified = true + } + } + } catch { + // Resolution error (file doesn't exist, etc.). + // Skip this require() and leave it as-is. + } + }, + }) + } catch { + // Parse error (invalid TypeScript syntax). + // Return null to use original code without transformation. + return null + } + + // Step 13: Return transformed code if we made any changes. + if (modified) { + // Get the transformed source code. + // Generate source map for debugging. + return { + code: s.toString(), + map: s.generateMap({ hires: true }), + } + } + + // No changes made, return null to use original code. + return null + }, + } +} diff --git a/packages/lib/.config/vitest-plugins/transform-utils.mts b/packages/lib/.config/vitest-plugins/transform-utils.mts new file mode 100644 index 000000000..05f09f36c --- /dev/null +++ b/packages/lib/.config/vitest-plugins/transform-utils.mts @@ -0,0 +1,54 @@ +/** @fileoverview Shared utilities for Vitest transform plugins. */ + +import { dirname, resolve } from 'node:path' + +const LIB_MARKER = '/registry/src/lib/' + +interface LibPathInfo { + projectRoot: string + relativeToLib: string +} + +/** + * Extract project root and relative path from a source file path. + */ +export function extractLibPath(filePath: string): LibPathInfo | null { + const libIndex = filePath.indexOf(LIB_MARKER) + if (libIndex === -1) { + return null + } + + return { + projectRoot: filePath.substring(0, libIndex), + relativeToLib: filePath.substring(libIndex + LIB_MARKER.length), + } +} + +/** + * Convert TypeScript source path to compiled JavaScript dist path. + */ +export function srcToDistPath(srcPath: string): string | null { + const extracted = extractLibPath(srcPath) + if (!extracted) { + return null + } + + const { projectRoot, relativeToLib } = extracted + const relativeJsPath = relativeToLib.replace(/\.ts$/, '.js') + + return resolve(projectRoot, 'registry/dist/lib', relativeJsPath) +} + +/** + * Build absolute dist directory path for a source file. + */ +export function getDistDir(srcFilePath: string): string | null { + const extracted = extractLibPath(srcFilePath) + if (!extracted) { + return null + } + + const { projectRoot, relativeToLib } = extracted + + return resolve(projectRoot, 'registry/dist/lib', dirname(relativeToLib)) +} diff --git a/packages/lib/.config/vitest.config.isolated.mts b/packages/lib/.config/vitest.config.isolated.mts new file mode 100644 index 000000000..52cf60e4d --- /dev/null +++ b/packages/lib/.config/vitest.config.isolated.mts @@ -0,0 +1,72 @@ +/** + * @fileoverview Vitest configuration for isolated tests + * Tests that require full isolation due to shared module state + */ + +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const projectRoot = path.resolve(__dirname, '..') + +// Normalize paths for cross-platform glob patterns (forward slashes on Windows) +const toGlobPath = (pathLike: string): string => pathLike.replaceAll('\\', '/') + +export default defineConfig({ + cacheDir: path.resolve(projectRoot, '.cache/vitest-isolated'), + resolve: { + preserveSymlinks: false, + extensions: ['.mts', '.ts', '.mjs', '.js', '.json'], + alias: { + '#env/ci': path.resolve(projectRoot, 'src/env/ci.ts'), + '#env': path.resolve(projectRoot, 'src/env'), + '#constants': path.resolve(projectRoot, 'src/constants'), + '#lib': path.resolve(projectRoot, 'src/lib'), + '#packages': path.resolve(projectRoot, 'src/lib/packages'), + '#types': path.resolve(projectRoot, 'src/types.ts'), + '#utils': path.resolve(projectRoot, 'src/utils'), + cacache: path.resolve(projectRoot, 'src/external/cacache'), + 'make-fetch-happen': path.resolve( + projectRoot, + 'src/external/make-fetch-happen', + ), + 'fast-sort': path.resolve(projectRoot, 'src/external/fast-sort'), + pacote: path.resolve(projectRoot, 'src/external/pacote'), + '@socketregistry/scripts': path.resolve(projectRoot, 'scripts'), + '@socketsecurity/lib/stdio/prompts': path.resolve( + projectRoot, + 'src/stdio/prompts/index.ts', + ), + '@socketsecurity/lib': path.resolve(projectRoot, 'src'), + }, + }, + test: { + globalSetup: [path.resolve(__dirname, 'vitest-global-setup.mts')], + globals: false, + environment: 'node', + include: [ + toGlobPath( + path.resolve(projectRoot, 'test/isolated/**/*.test.{js,ts,mjs,mts}'), + ), + ], + exclude: ['**/node_modules/**', '**/dist/**'], + reporters: ['default'], + // Full isolation for tests that modify shared module state + pool: 'threads', + poolOptions: { + threads: { + singleThread: true, + maxThreads: 1, + minThreads: 1, + isolate: true, + useAtomics: true, + }, + }, + testTimeout: 10_000, + hookTimeout: 10_000, + sequence: { + concurrent: false, + }, + }, +}) diff --git a/packages/lib/.config/vitest.config.mts b/packages/lib/.config/vitest.config.mts new file mode 100644 index 000000000..4ec486c6f --- /dev/null +++ b/packages/lib/.config/vitest.config.mts @@ -0,0 +1,142 @@ +/** + * @fileoverview Vitest configuration for socket-lib + */ + +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const projectRoot = path.resolve(__dirname, '..') + +// Normalize paths for cross-platform glob patterns (forward slashes on Windows) +const toGlobPath = (pathLike: string): string => pathLike.replaceAll('\\', '/') + +// Coverage mode detection +const isCoverageEnabled = + process.env.COVERAGE === 'true' || + process.env.npm_lifecycle_event?.includes('coverage') || + process.argv.some(arg => arg.includes('coverage')) + +export default defineConfig({ + cacheDir: path.resolve(projectRoot, '.cache/vitest'), + resolve: { + preserveSymlinks: false, + extensions: isCoverageEnabled + ? ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs', '.json'] + : ['.mts', '.ts', '.mjs', '.js', '.json'], + alias: { + '#env/ci': path.resolve(projectRoot, 'src/env/ci.ts'), + '#env': path.resolve(projectRoot, 'src/env'), + '#constants': path.resolve(projectRoot, 'src/constants'), + '#lib': path.resolve(projectRoot, 'src/lib'), + '#packages': path.resolve(projectRoot, 'src/lib/packages'), + '#types': path.resolve(projectRoot, 'src/types.ts'), + '#utils': path.resolve(projectRoot, 'src/utils'), + cacache: path.resolve(projectRoot, 'src/external/cacache'), + 'make-fetch-happen': path.resolve( + projectRoot, + 'src/external/make-fetch-happen', + ), + 'fast-sort': path.resolve(projectRoot, 'src/external/fast-sort'), + pacote: path.resolve(projectRoot, 'src/external/pacote'), + '@socketregistry/scripts': path.resolve(projectRoot, 'scripts'), + '@socketsecurity/lib/stdio/prompts': path.resolve( + projectRoot, + 'src/stdio/prompts/index.ts', + ), + '@socketsecurity/lib': path.resolve(projectRoot, 'src'), + }, + }, + test: { + globalSetup: [path.resolve(__dirname, 'vitest-global-setup.mts')], + globals: false, + environment: 'node', + include: [ + toGlobPath( + path.resolve(projectRoot, 'test/**/*.test.{js,ts,mjs,mts,cjs,cts}'), + ), + ], + exclude: [ + '**/node_modules/**', + '**/dist/**', + toGlobPath(path.resolve(projectRoot, 'test/isolated/**')), + ...(process.env.INCLUDE_NPM_TESTS + ? [] + : [toGlobPath(path.resolve(projectRoot, 'test/npm/**'))]), + ], + reporters: ['default'], + // Optimize test execution for speed + // Threads are faster than forks + pool: 'threads', + poolOptions: { + threads: { + // Maximize parallelism for speed + // During coverage, use single thread for deterministic execution + singleThread: isCoverageEnabled, + maxThreads: isCoverageEnabled ? 1 : 16, + minThreads: isCoverageEnabled ? 1 : 4, + // IMPORTANT: isolate: false for performance and test compatibility + // + // Tradeoff Analysis: + // - isolate: true = Full isolation, slower, breaks nock/module mocking + // - isolate: false = Shared worker context, faster, mocking works + // + // We choose isolate: false because: + // 1. Significant performance improvement (faster test runs) + // 2. HTTP mocking works correctly across all test files + // 3. Vi.mock() module mocking functions properly + // 4. Test state pollution is prevented through proper beforeEach/afterEach + // 5. Our tests are designed to clean up after themselves + isolate: false, + useAtomics: true, + }, + }, + // Reduce timeouts for faster failures + testTimeout: 10_000, + hookTimeout: 10_000, + // Speed optimizations + sequence: { + // Run tests concurrently within suites + concurrent: true, + }, + // Bail early on first failure in CI + bail: process.env.CI ? 1 : 0, + server: { + deps: { + inline: isCoverageEnabled ? [/@socketsecurity\/lib/, 'zod'] : ['zod'], + }, + }, + coverage: { + provider: 'v8', + reportsDirectory: 'coverage', + reporter: ['text-summary', 'json', 'html', 'lcov', 'clover'], + exclude: [ + '**/*.config.*', + '**/node_modules/**', + '**/[.]**', + '**/*.d.ts', + '**/virtual:*', + 'coverage/**', + 'test/**', + 'packages/**', + 'perf/**', + 'dist/**', + 'src/external/**', + 'src/types.ts', + 'scripts/**', + ], + include: ['src/**/*.{ts,mts,cts}'], + all: true, + clean: true, + skipFull: false, + ignoreClassMethods: ['constructor'], + thresholds: { + lines: 1, + functions: 68, + branches: 70, + statements: 1, + }, + }, + }, +}) diff --git a/packages/lib/.config/vitest.setup.mts b/packages/lib/.config/vitest.setup.mts new file mode 100644 index 000000000..ec3a68b0e --- /dev/null +++ b/packages/lib/.config/vitest.setup.mts @@ -0,0 +1,29 @@ +/** @fileoverview Vitest setup file for coverage mode require() interception. */ + +// Check if coverage is enabled. +const isCoverageEnabled = + process.argv.includes('--coverage') || process.env.COVERAGE === 'true' + +if (isCoverageEnabled) { + // Intercept require() calls to redirect registry/dist to registry/src. + const Module = require('node:module') + const originalRequire = Module.prototype.require + + Module.prototype.require = function (id: string) { + // Redirect registry/dist to registry/src for coverage. + if (typeof id === 'string' && id.includes('/registry/dist/')) { + const srcPath = id + .replace(/\/registry\/dist\//, '/registry/src/') + .replace(/\.js$/, '.ts') + + // Try to resolve the src path. + try { + return originalRequire.call(this, srcPath) + } catch { + // Fall back to original if src doesn't exist. + } + } + + return originalRequire.call(this, id) + } +} diff --git a/packages/lib/.gitignore b/packages/lib/.gitignore new file mode 100644 index 000000000..4462ea5a9 --- /dev/null +++ b/packages/lib/.gitignore @@ -0,0 +1,30 @@ +.DS_Store +._.DS_Store +Thumbs.db +/.claude +/.env +/.env.local +/.env.*.local +/.pnpmfile.cjs +/.nvm +/.type-coverage +/.vscode +/npm-debug.log +/yarn.lock + +# Editor files +*.swp +*.swo +#*# +.#* +**/.cache +**/coverage +**/dist +**/html +**/node_modules +**/*.tgz +**/*.tmp +**/*.tsbuildinfo + +# Allow specific files +!/.vscode/settings.json diff --git a/packages/lib/CHANGELOG.md b/packages/lib/CHANGELOG.md new file mode 100644 index 000000000..2350b7768 --- /dev/null +++ b/packages/lib/CHANGELOG.md @@ -0,0 +1,1055 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [3.2.8](https://github.com/SocketDev/socket-lib/releases/tag/v3.2.8) - 2025-11-05 + +### Fixed + +- **build**: Fix CommonJS export script edge cases + - Fixed stray semicolons after comment placeholders in transformed modules + - Fixed incorrect transformation of `module.exports.default` to `module.module.exports` + - Ensures external dependencies and default exports work correctly + +## [3.2.7](https://github.com/SocketDev/socket-lib/releases/tag/v3.2.7) - 2025-11-05 + +### Fixed + +- **build-externals**: Disable minification to preserve exports + - External dependencies are no longer minified during bundling + - Prevents export name mangling that breaks CommonJS interop + - Fixes `semver.parse()` and `semver.major()` being undefined + +- **build**: Fix CommonJS export interop for TypeScript default exports + - Modules with `export default` now work without requiring `.default` accessor + +### Changed + +- **docs**: Moved packages README to correct location (`src/packages/README.md`) + +## [3.2.6](https://github.com/SocketDev/socket-lib/releases/tag/v3.2.6) - 2025-11-05 + +### Fixed + +- **logger**: Replace yoctocolors-cjs rgb() with manual ANSI codes + - The yoctocolors-cjs package doesn't have an rgb() method + - Manually construct ANSI escape sequences for RGB colors (ESC[38;2;r;g;bm...ESC[39m) + - Affects `src/logger.ts` and `src/stdio/prompts.ts` applyColor() functions + +## [3.2.5](https://github.com/SocketDev/socket-lib/releases/tag/v3.2.5) - 2025-11-05 + +### Added + +- **scripts**: Add path alias resolution script (`fix-path-aliases.mjs`) + - Resolves internal path aliases (`#lib/*`, `#constants/*`, etc.) to relative paths in built CommonJS files + +- **build**: Integrate path alias resolution into build pipeline + - Add path alias plugin to esbuild config + - Integrate `fix-path-aliases.mjs` into build process + - Ensures path aliases work correctly in compiled CommonJS output + +## [3.2.4](https://github.com/SocketDev/socket-lib/releases/tag/v3.2.4) - 2025-11-04 + +### Added + +- **Logger**: New `time()` method for timing operations with automatic duration reporting + - Starts a named timer and returns a `stop()` function + - Automatically logs completion with formatted duration (e.g., "Operation completed in 1.23s") + - Useful for performance monitoring and debugging + +### Fixed + +- **Spinner effects**: Fixed star spinner frames by adding trailing space for consistent spacing +- **Build system**: Fixed external dependency bundling issues + - Bundle `@npmcli/package-json` with subpath exports support + - Use `src/external` files as bundle entry points for proper module resolution + - Bundle libnpmexec from npm instead of using vendored version + - Prevent circular dependencies with `createForceNodeModulesPlugin()` to force resolution from node_modules + +## [3.2.3](https://github.com/SocketDev/socket-lib/releases/tag/v3.2.3) - 2025-11-03 + +### Internal + +- **Build system**: Added stub infrastructure for external dependency bundling + - Created organized `scripts/build-externals/stubs/` directory with utility and active stubs + - Added conservative stubs for unused dependencies: `encoding`/`iconv-lite` and `debug` + - Reduces external bundle size by ~18KB (9KB from encoding stubs, 9KB from debug stubs) + +## [3.2.2](https://github.com/SocketDev/socket-lib/releases/tag/v3.2.2) - 2025-11-03 + +### Added + +- **DLX**: Binary permission management with chmod 0o755 for all package binaries + - New `makePackageBinsExecutable()` function ensures all binaries in installed packages are executable + - Aligns with npm's cmd-shim approach for binary permissions + - Handles both single and multiple binary packages + - No-op on Windows (permissions not needed) + +- **DLX**: npm-compatible bin resolution via vendored `getBinFromManifest` + - Cherry-picked `getBinFromManifest` from libnpmexec@10.1.8 (~1.5 KB) + - Avoids 1.1 MB bundle by vendoring single function instead of full package + - Provides battle-tested npm bin resolution strategy + - Maintains user-friendly fallbacks for edge cases + +### Changed + +- **DLX**: Enhanced `findBinaryPath()` with npm's resolution strategy + - Primary: npm's `getBinFromManifest` (handles standard cases and aliases) + - Fallback: user-provided `binaryName` parameter + - Fallback: last segment of package name + - Last resort: first binary in list + +### Performance + +- **Optimized package size**: Reduced bundle size through strategic export minimization and vendoring + - Vendored `getBinFromManifest` function instead of bundling full libnpmexec (~1.1 MB savings) + - Minimized external module exports for better tree-shaking: + - `fast-sort`: Now exports only `{ createNewSortInstance }` (2.1 KB, 96% reduction from ~56 KB) + - `fast-glob`: Now exports only `{ globStream }` (82 KB bundle) + - `del`: Now exports only `{ deleteAsync, deleteSync }` (100 KB bundle) + - `streaming-iterables`: Now exports only `{ parallelMap, transform }` (11 KB, 93% reduction from ~168 KB) + - Total savings: ~1.3 MB (1.1 MB from vendoring + 211 KB from minimized exports) + - Establishes pattern for future external module additions + +## [3.2.1](https://github.com/SocketDev/socket-lib/releases/tag/v3.2.1) - 2025-11-02 + +### Changed + +- **Logger/Spinner**: Use module-level constants to prevent duplicate and rogue spinner indicators + - Call `getDefaultLogger()` and `getDefaultSpinner()` once at module scope instead of repeated calls + - Prevents multiple spinner instances that can cause duplicate or lingering indicators in terminal output + - Applied in `src/dlx-manifest.ts`, `src/stdio/mask.ts`, and `src/spinner.ts` + - Follows DRY principle and aligns with socket-registry/socket-sdk-js patterns + +### Fixed + +- **Scripts**: Fixed undefined logger variable in update script + - Replaced undefined `log` references with `_logger` throughout `scripts/update.mjs` + - Resolves ESLint errors that blocked test execution +- **Tests**: Improved stdout test stability by checking call delta instead of absolute counts + - Fixed flaky CI failures where spy call count was 101 instead of expected 100 + - More robust approach handles potential state leakage between tests +- **Tests**: Removed unnecessary 10ms delay in cache-with-ttl test + - Cache with memoization enabled updates in-memory storage synchronously + - Delay was insufficient in CI and unnecessary given synchronous behavior + - Resolves flaky CI failures where cached values returned undefined + +## [3.2.0](https://github.com/SocketDev/socket-lib/releases/tag/v3.2.0) - 2025-11-02 + +### Added + +- **DLX**: Unified manifest for packages and binaries + - Centralized manifest system for tracking DLX-compatible packages + - Simplifies package and binary lookups for dependency-free execution + +## [3.1.3](https://github.com/SocketDev/socket-lib/releases/tag/v3.1.3) - 2025-11-02 + +### Changed + +- **Dependencies**: Updated `@socketregistry/packageurl-js` to 1.3.5 + +## [3.1.2](https://github.com/SocketDev/socket-lib/releases/tag/v3.1.2) - 2025-11-02 + +### Fixed + +- **External dependencies**: Fixed incorrectly marked external dependencies to use wrapper pattern + - Updated `src/constants/agents.ts` to use `require('../external/which')` instead of direct imports + - Updated `src/zod.ts` to export from `./external/zod'` instead of direct imports + - Maintains zero dependencies policy by ensuring all runtime dependencies go through the external wrapper pattern +- **Spinner**: Fixed undefined properties in setShimmer by handling defaults correctly + +## [3.1.1](https://github.com/SocketDev/socket-lib/releases/tag/v3.1.1) - 2025-11-02 + +### Fixed + +- **Cache TTL**: Fixed flaky test by handling persistent cache write failures gracefully + - Wrapped `cacache.put` in try/catch to prevent failures when persistent cache writes fail or are slow + - In-memory cache is updated synchronously before the persistent write, so immediate reads succeed regardless of persistent cache state + - Improves reliability in test environments and when cache directory has issues + +## [3.1.0](https://github.com/SocketDev/socket-lib/releases/tag/v3.1.0) - 2025-11-01 + +### Changed + +- **File system utilities**: `safeMkdir` and `safeMkdirSync` now default to `recursive: true` + - Nested directories are created by default, simplifying common usage patterns + +## [3.0.6](https://github.com/SocketDev/socket-lib/releases/tag/v3.0.6) - 2025-11-01 + +### Added + +- **Build validation**: Added guard against `link:` protocol dependencies in package.json + - New `validate-no-link-deps.mjs` script automatically runs during `pnpm run check` + - Prevents accidental publication with `link:` dependencies which can cause issues + - Recommends using `workspace:` for monorepos or `catalog:` for centralized version management + - Validates all dependency fields: dependencies, devDependencies, peerDependencies, optionalDependencies + +### Changed + +- **Dependencies**: Updated `@socketregistry/packageurl-js` to 1.3.3 +- **Git hooks**: Committed pre-commit and pre-push hook configurations for version control +- **Scripts**: Removed shebang from `validate-no-link-deps` script (Node.js script, not shell) + +## [3.0.5](https://github.com/SocketDev/socket-lib/releases/tag/v3.0.5) - 2025-11-01 + +### Fixed + +- **Critical: Prompts API breaking changes**: Restored working prompts implementation that was accidentally replaced with non-functional stub in v3.0.0 + - Consolidated all prompts functionality into `src/stdio/prompts.ts` + - Removed unimplemented stub from `src/prompts/` that was throwing "not yet implemented" errors + - Removed `./prompts` package export (use `@socketsecurity/lib/stdio/prompts` instead) + - Restored missing exports: `password`, `search`, `Separator`, and added `createSeparator()` helper + - Fixed `Choice` type to use correct `name` property (matching `@inquirer` API, not erroneous `label`) + +### Added + +- **Theme integration for prompts**: Prompts now automatically use the active theme colors + - Prompt messages styled with `colors.prompt` + - Descriptions and disabled items styled with `colors.textDim` + - Answers and highlights styled with `colors.primary` + - Error messages styled with `colors.error` + - Success indicators styled with `colors.success` + - Exported `createInquirerTheme()` function for converting Socket themes to @inquirer format + - Consistent visual experience with Logger and Spinner theme integration + +- **Theme parameter support**: Logger, Prompts, and text effects now accept optional `theme` parameter + - Pass theme names (`'socket'`, `'sunset'`, `'terracotta'`, `'lush'`, `'ultra'`) or Theme objects + - **Logger**: `new Logger({ theme: 'sunset' })` - uses theme-specific symbol colors + - **Prompts**: `await input({ message: 'Name:', theme: 'ultra' })` - uses theme for prompt styling + - **Text effects**: `applyShimmer(text, state, { theme: 'terracotta' })` - uses theme for shimmer colors + - Instance-specific themes override global theme context when provided + - Falls back to global theme context when no instance theme specified + - **Note**: Spinner already had theme parameter support in v3.0.0 + +### Removed + +- **Unused index entrypoint**: Removed `src/index.ts` and package exports for `"."` and `"./index"` + - This was a leftover from socket-registry and not needed for this library + - Users should import specific modules directly (e.g., `@socketsecurity/lib/logger`) + - Breaking: `import { getDefaultLogger } from '@socketsecurity/lib'` no longer works + - Use: `import { getDefaultLogger } from '@socketsecurity/lib/logger'` instead + +## [3.0.4](https://github.com/SocketDev/socket-lib/releases/tag/v3.0.4) - 2025-11-01 + +### Changed + +- **Sunset theme**: Updated colors from azure blue to warm orange/purple gradient matching Coana branding +- **Terracotta theme**: Renamed from `brick` to `terracotta` for better clarity + +## [3.0.3](https://github.com/SocketDev/socket-lib/releases/tag/v3.0.3) - 2025-11-01 + +### Fixed + +- **Critical: Node.js ESM/CJS interop completely fixed**: Disabled minification to ensure proper ESM named import detection + - Root cause: esbuild minification was breaking Node.js ESM's CJS named export detection + - Solution: Disabled minification entirely (`minify: false` in esbuild config) + - Libraries should not be minified - consumers minify during their own build process + - Unminified esbuild output uses clear `__export` patterns that Node.js ESM natively understands + - Removed `fix-commonjs-exports.mjs` build script - no longer needed with unminified code + - ESM imports now work reliably: `import { getDefaultLogger } from '@socketsecurity/lib/logger'` + - Verified with real-world ESM module testing (`.mjs` files importing from CJS `.js` dist) + +## [3.0.2](https://github.com/SocketDev/socket-lib/releases/tag/v3.0.2) - 2025-11-01 + +### Fixed + +- **Critical: Node.js ESM named imports from CommonJS**: Fixed build output to ensure Node.js ESM can properly detect named exports from CommonJS modules + - Previously, esbuild's minified export pattern placed `module.exports` before variable definitions, causing "Cannot access before initialization" errors + - Build script now uses `@babel/parser` + `magic-string` for safe AST parsing and transformation + - Exports are now correctly placed at end of files after all variable definitions + - Enables proper ESM named imports: `import { getDefaultLogger, Logger } from '@socketsecurity/lib/logger'` + - Fixes socket-cli issue where named imports were failing with obscure initialization errors + +## [3.0.1](https://github.com/SocketDev/socket-lib/releases/tag/v3.0.1) - 2025-11-01 + +### Added + +- **Convenience exports from main index**: Added logger and spinner exports to ease v2→v3 migration + - Logger: `getDefaultLogger()`, `Logger`, `LOG_SYMBOLS` now available from `@socketsecurity/lib` + - Spinner: `getDefaultSpinner()`, `Spinner` now available from `@socketsecurity/lib` + - Both main index (`@socketsecurity/lib`) and subpath (`@socketsecurity/lib/logger`, `@socketsecurity/lib/spinner`) imports now work + - Both import paths return the same singleton instances + +### Fixed + +- **Critical: Spinner crashes when calling logger**: Fixed spinner internal calls to use `getDefaultLogger()` instead of removed `logger` export + - Spinner methods (`start()`, `stop()`, `success()`, `fail()`, etc.) no longer crash with "logger is not defined" errors + - All 5 internal logger access points updated to use the correct v3 API + - Resolves runtime errors when using spinners with hoisted variables + +### Changed + +- **Migration path improvement**: Users can now import logger/spinner from either main index or subpaths, reducing breaking change impact from v3.0.0 + +## [3.0.0](https://github.com/SocketDev/socket-lib/releases/tag/v3.0.0) - 2025-11-01 + +### Added + +- Theme system with 5 built-in themes: `socket`, `sunset`, `terracotta`, `lush`, `ultra` +- `setTheme()`, `getTheme()`, `withTheme()`, `withThemeSync()` for theme management +- `createTheme()`, `extendTheme()`, `resolveColor()` helper functions +- `onThemeChange()` event listener for theme reactivity +- `link()` function for themed terminal hyperlinks in `@socketsecurity/lib/links` +- Logger and spinner now inherit theme colors automatically +- Spinner methods: `enableShimmer()`, `disableShimmer()`, `setShimmer()`, `updateShimmer()` +- DLX cross-platform binary resolution (`.cmd`, `.bat`, `.ps1` on Windows) +- DLX programmatic options aligned with CLI conventions (`force`, `quiet`, `package`) + +### Changed + +- Theme context uses AsyncLocalStorage instead of manual stack management +- Promise retry options renamed: `factor` → `backoffFactor`, `minTimeout` → `baseDelayMs`, `maxTimeout` → `maxDelayMs` + +### Removed + +**BREAKING CHANGES:** + +- `pushTheme()` and `popTheme()` - use `withTheme()` or `withThemeSync()` instead +- `logger` export - use `getDefaultLogger()` instead +- `spinner` export - use `getDefaultSpinner()` instead +- `download-lock.ts` - use `process-lock.ts` instead +- Promise option aliases: `factor`, `minTimeout`, `maxTimeout` + +--- + +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [2.10.3](https://github.com/SocketDev/socket-lib/releases/tag/v2.10.3) - 2025-10-31 + +### Fixed + +- Updated `@socketregistry/packageurl-js` to 1.3.1 to resolve an unintended external dependency +- **Documentation**: Corrected JSDoc `@example` import paths from `@socketsecurity/registry` to `@socketsecurity/lib` across utility modules + - Updated examples in `memoization.ts`, `performance.ts`, `spinner.ts`, `suppress-warnings.ts`, and `tables.ts` + - Ensures documentation reflects correct package name after v1.0.0 rename + +## [2.10.2](https://github.com/SocketDev/socket-lib/releases/tag/v2.10.2) - 2025-10-31 + +### Changed + +- **Package spec parsing**: Refactored to use official `npm-package-arg` library for robust handling of all npm package specification formats (versions, ranges, tags, git URLs) + - Improves reliability when parsing complex package specs + - Better handles edge cases in version ranges and scoped packages + - Falls back to simple parsing if npm-package-arg fails + +### Fixed + +- **Scoped package version parsing**: Fixed critical bug where parsePackageSpec was stripping the `@` prefix from scoped packages with versions + - Example: `@coana-tech/cli@~14.12.51` was incorrectly parsed as `coana-tech/cli@~14.12.51` + - Caused package installation failures for scoped packages in DLX system + +## [2.10.1](https://github.com/SocketDev/socket-lib/releases/tag/v2.10.1) - 2025-10-31 + +### Fixed + +- **Process lock directory creation**: Use recursive mkdir to ensure parent directories exist when creating lock directory +- **Node.js debug flags**: Remove buggy `getNodeDebugFlags()` function that returned debug flags without required argument values + +## [2.10.0](https://github.com/SocketDev/socket-lib/releases/tag/v2.10.0) - 2025-10-30 + +### Added + +- **Unified DLX metadata schema**: Standardized `.dlx-metadata.json` format across TypeScript and C++ implementations + - Exported `DlxMetadata` interface as canonical schema reference + - Core fields: `version`, `cache_key`, `timestamp`, `checksum`, `checksum_algorithm`, `platform`, `arch`, `size`, `source` + - Support for `source` tracking (download vs decompression origin) + - Reserved `extra` field for implementation-specific data + - Comprehensive documentation with examples for both download and decompression use cases + +### Changed + +- **DLX binary metadata structure**: Updated `writeMetadata()` to use unified schema with additional fields + - Now includes `cache_key` (first 16 chars of SHA-512 hash) + - Added `size` field for cached binary size + - Added `checksum_algorithm` field (currently "sha256") + - Restructured to use `source.type` and `source.url` for origin tracking + - Maintains backward compatibility in `listDlxCache()` reader + +## [2.9.1](https://github.com/SocketDev/socket-lib/releases/tag/v2.9.1) - 2025-10-30 + +### Added + +- **Smart binary detection in dlxPackage**: Automatically finds the correct binary even when package name doesn't match binary name + - If package has single binary, uses it automatically regardless of name + - Resolves packages like `@socketsecurity/cli` (binary: `socket`) without manual configuration + - Falls back to intelligent name matching for multi-binary packages +- **Optional binaryName parameter**: Added `binaryName` option to `DlxPackageOptions` for explicit binary selection when auto-detection isn't sufficient + +### Fixed + +- **Binary resolution for scoped packages**: Fixed issue where `dlxPackage` couldn't find binaries when package name didn't match binary name (e.g., `@socketsecurity/cli` with `bin: { socket: '...' }`) + +## [2.9.0](https://github.com/SocketDev/socket-lib/releases/tag/v2.9.0) - 2025-10-30 + +### Added + +- **Socket.dev URL constants**: Added centralized URL constants for Socket.dev services + - `SOCKET_WEBSITE_URL`: Main Socket.dev website + - `SOCKET_CONTACT_URL`: Contact page + - `SOCKET_DASHBOARD_URL`: Dashboard homepage + - `SOCKET_API_TOKENS_URL`: API tokens settings page + - `SOCKET_PRICING_URL`: Pricing information + - `SOCKET_STATUS_URL`: Service status page + - `SOCKET_DOCS_URL`: Documentation site + - Available via `@socketsecurity/lib/constants/socket` + +### Changed + +- **Enhanced error messages across library**: Comprehensive audit and improvement of error handling + - Added actionable error messages with resolution steps throughout modules + - Improved file system operation errors (permissions, read-only filesystems, path issues) + - Enhanced DLX error messages with clear troubleshooting guidance + - Better error context in process locking, binary downloads, and package operations + - Consistent error formatting with helpful user guidance +- **Consolidated process locking**: Standardized on directory-based lock format across all modules + - All locking operations now use `process-lock` module exclusively + - Lock directories provide atomic guarantees across all filesystems including NFS + - Consistent mtime-based stale detection with 5-second timeout (aligned with npm npx) + - Automatic cleanup on process exit with proper signal handling + +## [2.8.4](https://github.com/SocketDev/socket-lib/releases/tag/v2.8.4) - 2025-10-30 + +### Added + +- **DLX binary helper functions mirror dlx-package pattern** + - `downloadBinary`: Download binary with caching (without execution) + - `executeBinary`: Execute cached binary without re-downloading + - Renamed internal `downloadBinary` to `downloadBinaryFile` to avoid naming conflicts + - Maintains feature parity with `downloadPackage`/`executePackage` from dlx-package + +## [2.8.3](https://github.com/SocketDev/socket-lib/releases/tag/v2.8.3) - 2025-10-30 + +### Fixed + +- **Logger now fully defers all console access for Node.js internal bootstrap compatibility**: Completed lazy initialization to prevent ERR_CONSOLE_WRITABLE_STREAM errors + - Deferred `Object.getOwnPropertySymbols(console)` call until first logger use + - Deferred `kGroupIndentationWidth` symbol lookup + - Deferred `Object.entries(console)` and prototype method initialization + - Ensures logger can be safely imported in Node.js internal bootstrap contexts (e.g., `lib/internal/bootstrap/*.js`) before stdout is initialized + - Builds on v2.8.2 console deferring to complete early bootstrap compatibility + +## [2.8.2](https://github.com/SocketDev/socket-lib/releases/tag/v2.8.2) - 2025-10-29 + +### Changed + +- Enhanced Logger class to defer Console creation until first use + - Eliminates early bootstrap errors when importing logger before stdout is ready + - Enables safe logger imports during Node.js early initialization phase + - Simplified internal storage with WeakMap-only pattern for constructor args + +## [2.8.1](https://github.com/SocketDev/socket-lib/releases/tag/v2.8.1) - 2025-10-29 + +### Changed + +- **Consolidated DLX cache key generation**: Extracted `generateCacheKey` function to shared `dlx.ts` module + - Eliminates code duplication between `dlx-binary.ts` and `dlx-package.ts` + - Enables consistent cache key generation across the Socket ecosystem + - Exports function for use in dependent packages (e.g., socket-cli) + - Maintains SHA-512 truncated to 16 chars strategy from v2.8.0 + +## [2.8.0](https://github.com/SocketDev/socket-lib/releases/tag/v2.8.0) - 2025-10-29 + +### Changed + +- **Enhanced DLX cache key generation with npm/npx compatibility**: Updated cache key strategy to align with npm/npx ecosystem patterns + - Changed from SHA-256 (64 chars) to SHA-512 truncated to 16 chars (matching npm/npx) + - Optimized for Windows MAX_PATH compatibility (260 character limit) + - Accepts collision risk for shorter paths (~1 in 18 quintillion with 1000 entries) + - Added support for PURL-style package specifications (e.g., `npm:prettier@3.0.0`, `pypi:requests@2.31.0`) + - Documented Socket's shorthand format (without `pkg:` prefix) handled by `@socketregistry/packageurl-js` + - References npm/cli v11.6.2 implementation for consistency + +## [2.7.0](https://github.com/SocketDev/socket-lib/releases/tag/v2.7.0) - 2025-10-28 + +### Added + +- **DLX cache locking for concurrent installation protection**: Added process-lock protection to dlx-package installation operations + - Lock file created at `~/.socket/_dlx//.lock` (similar to npm npx's `concurrency.lock`) + - Prevents concurrent installations from corrupting the same package cache + - Uses 5-second stale timeout and 2-second periodic touching (aligned with npm npx) + - Double-check pattern verifies installation after acquiring lock to avoid redundant work + - Completes 100% alignment with npm's npx locking strategy + +## [2.6.0](https://github.com/SocketDev/socket-lib/releases/tag/v2.6.0) - 2025-10-28 + +### Changed + +- **Process locking aligned with npm npx**: Enhanced process-lock module to match npm's npx locking strategy + - Reduced stale timeout from 10 seconds to 5 seconds (matches npm npx) + - Added periodic lock touching (2-second interval) to prevent false stale detection during long operations + - Implemented second-level granularity for mtime comparison to avoid APFS floating-point precision issues + - Added automatic touch timer cleanup on process exit + - Timers use `unref()` to prevent keeping process alive + - Aligns with npm's npx implementation per https://github.com/npm/cli/pull/8512 + +## [2.5.0](https://github.com/SocketDev/socket-lib/releases/tag/v2.5.0) - 2025-10-28 + +### Added + +- **Process locking utilities**: Added `ProcessLockManager` class providing cross-platform inter-process synchronization using file-system based locks + - Atomic lock acquisition via `mkdir()` for thread-safe operations + - Stale lock detection with automatic cleanup (default 10 seconds, aligned with npm's npx strategy) + - Exponential backoff with jitter for retry attempts + - Process exit handlers for guaranteed cleanup even on abnormal termination + - Three main APIs: `acquire()`, `release()`, and `withLock()` (recommended) + - Comprehensive test suite with `describe.sequential` for proper isolation + - Export: `@socketsecurity/lib/process-lock` + +### Changed + +- **Script refactoring**: Renamed `spinner.succeed()` to `spinner.success()` for consistency +- **Script cleanup**: Removed redundant spinner cleanup in interactive-runner + +## [2.4.0](https://github.com/SocketDev/socket-lib/releases/tag/v2.4.0) - 2025-10-28 + +### Changed + +- **Download locking aligned with npm**: Reduced default `staleTimeout` in `downloadWithLock()` from 300 seconds to 10 seconds to align with npm's npx locking strategy + - Prevents stale locks from blocking downloads for extended periods + - Matches npm's battle-tested timeout range (5-10 seconds) + - Binary downloads now protected against concurrent corruption +- **Binary download protection**: `dlxBinary.downloadBinary()` now uses `downloadWithLock()` to prevent corruption when multiple processes download the same binary concurrently + - Eliminates race conditions during parallel binary downloads + - Maintains checksum verification and executable permissions + +## [2.3.0](https://github.com/SocketDev/socket-lib/releases/tag/v2.3.0) - 2025-10-28 + +### Added + +- **Binary utility wrapper functions**: Added `which()` and `whichSync()` wrapper functions to `bin` module + - Cross-platform binary lookup that respects PATH environment variable + - Synchronous and asynchronous variants for different use cases + - Integrates with existing binary resolution utilities + +## [2.2.1](https://github.com/SocketDev/socket-lib/releases/tag/v2.2.1) - 2025-10-28 + +### Fixed + +- **Logger write() method**: Fixed `write()` to bypass Console formatting when outputting raw text + - Previously, `write()` used Console's internal `_stdout` stream which applied unintended formatting like group indentation + - Now stores a reference to the original stdout stream in a dedicated private field (`#originalStdout`) during construction + - The `write()` method uses this stored reference to write directly to the raw stream, bypassing all Console formatting layers + - Ensures raw text output without any formatting applied, fixing test failures in CI environments where writes after `indent()` were unexpectedly formatted + +## [2.2.0](https://github.com/SocketDev/socket-lib/releases/tag/v2.2.0) - 2025-10-28 + +### Added + +- **Logger step symbol**: `logger.step()` now displays a cyan arrow symbol (→ or > in ASCII) before step messages for improved visual separation + - New `LOG_SYMBOLS.step` symbol added to the symbol palette + - Automatic stripping of existing symbols from step messages + - Maintains existing blank line behavior for clear step separation + +## [2.1.0](https://github.com/SocketDev/socket-lib/releases/tag/v2.1.0) - 2025-10-28 + +### Added + +- Package manager detection utilities (`detectPackageManager()`, `getPackageManagerInfo()`, `getPackageManagerUserAgent()`) +- `isInSocketDlx()` utility to check if file path is within `~/.socket/_dlx/` +- `downloadPackage()` and `executePackage()` functions for separate download and execution of packages + +## [2.0.0](https://github.com/SocketDev/socket-lib/releases/tag/v2.0.0) - 2025-10-27 + +### Breaking Changes + +**Environment Variable System Refactor** + +This release completely refactors the environment variable system, consolidating 60+ individual env constant files into grouped getter modules with AsyncLocalStorage-based test rewiring. + +**Consolidated env files** - Individual files replaced with grouped modules: +- `env/github.ts` - All GitHub-related env vars (GITHUB_TOKEN, GH_TOKEN, GITHUB_API_URL, etc.) +- `env/socket.ts` - Socket-specific env vars (SOCKET_API_TOKEN, SOCKET_CACACHE_DIR, etc.) +- `env/socket-cli.ts` - Socket CLI env vars (SOCKET_CLI_API_TOKEN, SOCKET_CLI_CONFIG, etc.) +- `env/npm.ts` - NPM-related env vars +- `env/locale.ts` - Locale env vars (LANG, LC_ALL, LC_MESSAGES) +- `env/windows.ts` - Windows-specific env vars (USERPROFILE, LOCALAPPDATA, APPDATA, COMSPEC) +- `env/xdg.ts` - XDG base directory env vars +- `env/temp-dir.ts` - Temp directory env vars (TEMP, TMP, TMPDIR) +- `env/test.ts` - Test framework env vars (VITEST, JEST_WORKER_ID) + +**Constants → Getter functions** - All env constants converted to functions: +```typescript +// Before (v1.x): +import { GITHUB_TOKEN } from '#env/github-token' + +// After (v2.x): +import { getGithubToken } from '#env/github' +``` + +**Deleted files** - Removed 60+ individual env constant files: +- `env/github-token.ts`, `env/socket-api-token.ts`, etc. → Consolidated into grouped files +- `env/getters.ts` → Functions moved to their respective grouped files + +### Added + +**AsyncLocalStorage-Based Test Rewiring** + +New `env/rewire.ts` and `path/rewire.ts` modules provides context-isolated environment variable overrides for testing: + +```typescript +import { withEnv, setEnv, resetEnv, getEnvValue } from '#env/rewire' + +// Option 1: Isolated context with AsyncLocalStorage +await withEnv({ CI: '1', NODE_ENV: 'test' }, async () => { + // CI env var is '1' only within this block + // Concurrent tests don't interfere +}) + +// Option 2: Traditional beforeEach/afterEach pattern +beforeEach(() => { + setEnv('CI', '1') +}) + +afterEach(() => { + resetEnv() +}) +``` + +**Features:** +- Allows toggling between snapshot and live behavior +- Compatible with `vi.stubEnv()` as fallback + +### Changed + +- Updated all dynamic `require()` statements to use path aliases (`#constants/*`, `#packages/*`) +- Improved logger blank line tracking per stream (separate stderr/stdout tracking) +- Exported `getCacache()` function for external use + +## [1.3.6](https://github.com/SocketDev/socket-lib/releases/tag/v1.3.6) - 2025-10-26 + +### Fixed + +- Fixed `debug` module functions being incorrectly tree-shaken as no-ops in bundled output + - Removed incorrect `/*@__NO_SIDE_EFFECTS__*/` annotations from `debug()`, `debugDir()`, `debugLog()`, and their `*Ns` variants + - These functions have side effects (logging output, spinner manipulation) and should not be removed by bundlers + - Fixes issue where `debugLog()` and `debugDir()` were compiled to empty no-op functions + +## [1.3.5](https://github.com/SocketDev/socket-lib/releases/tag/v1.3.5) - 2025-10-26 + +### Added + +- Added `createEnvProxy()` utility function to `env` module for Windows-compatible environment variable access + - Provides case-insensitive environment variable access (e.g., PATH, Path, path all work) + - Smart priority system: overrides > exact match > case-insensitive fallback + - Full Proxy implementation with proper handlers for get, set, has, ownKeys, getOwnPropertyDescriptor + - Opt-in helper for users who need Windows env var compatibility + - Well-documented with usage examples and performance notes +- Added `findCaseInsensitiveEnvKey()` utility function to `env` module + - Searches for environment variable keys using case-insensitive matching + - Optimized with length fast path to minimize expensive `toUpperCase()` calls + - Useful for cross-platform env var access where case may vary (e.g., PATH vs Path vs path) +- Added comprehensive test suite for `env` module with 71 tests + - Covers `envAsBoolean()`, `envAsNumber()`, `envAsString()` conversion utilities + - Tests `createEnvProxy()` with Windows environment variables and edge cases + - Validates `findCaseInsensitiveEnvKey()` optimization and behavior + +### Fixed + +- Fixed `spawn` module to preserve Windows `process.env` Proxy behavior + - When no custom environment variables are provided, use `process.env` directly instead of spreading it + - Preserves Windows case-insensitive environment variable access (PATH vs Path) + - Fixes empty CLI output issue on Windows CI runners + - Only spreads `process.env` when merging custom environment variables + +## [1.3.4](https://github.com/SocketDev/socket-lib/releases/tag/v1.3.4) - 2025-10-26 + +### Added + +- Added Node.js SIGUSR1 signal handler prevention utilities in `constants/node` module + - `supportsNodeDisableSigusr1Flag()`: Detects if Node supports `--disable-sigusr1` flag (v22.14+, v23.7+, v24.8+) + - `getNodeDisableSigusr1Flags()`: Returns appropriate flags to prevent debugger attachment + - Returns `['--disable-sigusr1']` on supported versions (prevents Signal I/O Thread creation) + - Falls back to `['--no-inspect']` on Node 18+ (blocks debugger but still creates thread) + - Enables production CLI environments to prevent SIGUSR1 debugger signal handling for security + +## [1.3.3](https://github.com/SocketDev/socket-lib/releases/tag/v1.3.3) - 2025-10-24 + +### Fixed + +- Fixed lazy getter bug in `objects` module where `defineGetter`, `defineLazyGetter`, and `defineLazyGetters` had incorrect `/*@__NO_SIDE_EFFECTS__*/` annotations + - These functions mutate objects by defining properties, so marking them as side-effect-free caused esbuild to incorrectly tree-shake the calls during bundling + - Lazy getters were returning `undefined` instead of their computed values + - Removed double wrapping in `defineLazyGetters` where `createLazyGetter` was being called unnecessarily + +## [1.3.2](https://github.com/SocketDev/socket-lib/releases/tag/v1.3.2) - 2025-10-24 + +### Fixed + +- Continued fixing of broken external dependency bundling + +## [1.3.1](https://github.com/SocketDev/socket-lib/releases/tag/v1.3.1) - 2025-10-24 + +### Fixed + +- Fixed @inquirer modules (`input`, `password`, `search`) not being properly bundled into `dist/external/` + - Resolves build failures in downstream packages (socket-cli) that depend on socket-lib + - Added missing packages to bundling configuration in `scripts/build-externals.mjs` + - All @inquirer packages now ship as zero-dependency bundles + +### Added + +- Added tests to prevent rogue external stubs in `dist/external/` + - Detects stub re-export patterns that indicate incomplete bundling + - Verifies all @inquirer modules are properly bundled (> 1KB) + - Catches bundling regressions early in CI pipeline + +## [1.3.0](https://github.com/SocketDev/socket-lib/releases/tag/v1.3.0) - 2025-10-23 + +### Added + +- Added `validateFiles()` utility function to `fs` module for defensive file access validation + - Returns `ValidateFilesResult` with `validPaths` and `invalidPaths` arrays + - Filters out unreadable files before processing (common with Yarn Berry PnP virtual filesystem, pnpm symlinks) + - Prevents ENOENT errors when files exist in glob results but are not accessible + - Comprehensive test coverage for all validation scenarios + +## [1.2.0](https://github.com/SocketDev/socket-lib/releases/tag/v1.2.0) - 2025-10-23 + +### Added + +- Added `dlx-package` module for installing and executing npm packages directly + - Content-addressed caching using SHA256 hash (like npm's _npx) + - Auto-force for version ranges (^, ~, >, <) to get latest within range + - Cross-platform support with comprehensive tests (30 tests) + - Parses scoped and unscoped package specs correctly + - Resolves binaries from package.json bin field + +### Changed + +- Unified DLX storage under `~/.socket/_dlx/` directory + - Binary downloads now use `~/.socket/_dlx/` instead of non-existent cache path + - Both npm packages and binaries share parent directory with content-addressed hashing +- Updated paths.ts documentation to clarify unified directory structure + +## [1.1.2] - 2025-10-23 + +### Fixed + +- Fixed broken relative import paths in `packages/isolation.ts` and `packages/provenance.ts` that prevented bundling by external tools + +## [1.1.1] - 2025-10-23 + +### Fixed + +- Fixed shimmer text effects not respecting CI environment detection (now disabled in CI to prevent ANSI escape codes in logs) + +## [1.1.0] - 2025-10-23 + +### Added + +- Added `filterOutput` option to `stdio/mask` for filtering output chunks before display/buffering +- Added `overrideExitCode` option to `stdio/mask` for customizing exit codes based on captured output +- Added comprehensive JSDoc documentation across entire library for enhanced VSCode IntelliSense + - Detailed @param, @returns, @template, @throws tags + - Practical @example blocks with real-world usage patterns + - @default tags showing default values + - Enhanced interface property documentation + +### Changed + +- Improved TypeScript type hints and tooltips throughout library +- Enhanced documentation for all core utilities (arrays, fs, git, github, http-request, json, logger, objects, path, promises, spawn, spinner, strings) +- Enhanced documentation for stdio utilities (clear, divider, footer, header, mask, progress, prompts, stderr, stdout) +- Enhanced documentation for validation utilities (json-parser, types) + +## [1.0.5] - 2025-10-22 + +### Added + +- Added support for custom retry delays from onRetry callback + +## [1.0.4] - 2025-10-21 + +### Fixed + +- Fixed external dependency paths in root-level source files (corrected require paths from `../external/` to `./external/` in bin, cacache, fs, globs, spawn, spinner, and streams modules) + +## [1.0.3] - 2025-10-21 + +### Fixed + +- Fixed external dependency import paths in packages and stdio modules (corrected require paths from `../../external/` to `../external/`) + +## [1.0.2] - 2025-10-21 + +### Fixed + +- Fixed module resolution error in packages/normalize module (corrected require path from `../../constants/socket` to `../constants/socket`) + +## [1.0.1] - 2025-10-21 + +### Fixed + +- Fixed relative import paths in compiled CommonJS output (changed `require("../external/...")` to `require("./external/...")` for root-level dist files) + +## [1.0.0] - 2025-10-20 + +### Changed + +- Consolidated parseArgs into argv/parse module + +--- + +**Historical Entries**: The entries below are from when this package was named `@socketsecurity/registry`. This package was renamed to `@socketsecurity/lib` starting with version 1.0.0. + +--- + +## [1.5.3] - 2025-10-07 + +### Added + +- Fix bad build and add validation to prevent in future + +## [1.5.2] - 2025-10-07 + +### Added + +- Added coverage utilities to parse v8 and type coverage reports + +### Fixed + +- Fixed `isPath` function to exclude URLs with protocols +- Fixed `isolatePackage` to handle file: URLs and npm-package-arg paths correctly + +## [1.5.1] - 2025-10-05 + +### Added + +- Added `isolatePackage` to `lib/packages/isolation` for creating isolated package test environments + +### Changed + +- Removed `dependencies/index` barrel file to prevent eager loading of all dependency modules + +## [1.5.0] - 2025-10-05 + +### Added + +- Added support for testing local development packages in addition to socket-registry packages +- Exposed isolation module as part of public API via `lib/packages` + +### Changed + +- Renamed `setupPackageTest` to `isolatePackage` for clearer intent +- Refactored `installPackageForTesting` to accept explicit `sourcePath` and `packageName` parameters +- Simplified package installation logic by removing path detection from low-level function +- Consolidated `setupPackageTest` and `setupMultiEntryTest` into single `isolatePackage` function with options + +## [1.4.6] - 2025-10-05 + +### Added + +- Added comprehensive package.json exports validation tests + +## [1.4.5] - 2025-10-05 + +### Added + +- Added performance monitoring utilities with timer, measurement, and reporting functions +- Added memoization utilities with LRU, TTL, weak references, and promise deduplication support +- Added table formatting utilities (`formatTable`, `formatSimpleTable`) for CLI output +- Added progress tracking to spinner with `updateProgress()` and `incrementProgress()` methods +- Added `isDir` and `safeStats` async helpers to fs module + +### Changed + +- Removed `platform` and `arch` options from `dlxBinary` function as cross-platform binary execution is not supported + +### Fixed + +- Fixed Windows shell execution in `dlxBinary` by adding cache directory to PATH + +## [1.4.4] - 2025-10-05 + +### Fixed + +- Fixed subpath exports + +## [1.4.3] - 2025-10-04 + +### Added + +- Spinner lifecycle utilities (`withSpinner`, `withSpinnerRestore`, `withSpinnerSync`) for automatic spinner cleanup with try/finally blocks + +## [1.4.2] - 2025-10-04 + +### Added + +- Added `GITHUB_API_BASE_URL` constant for GitHub API endpoint configuration +- Added `SOCKET_API_BASE_URL` constant for Socket API endpoint configuration +- Added generic TTL cache utility (`createTtlCache`) with in-memory memoization and persistent storage support + +### Changed + +- Refactored GitHub caching to use the new `cache-with-ttl` utility for better performance and consistency + +## [1.4.1] - 2025-10-04 + +### Changed + +- Update maintained Node.js versions of `constants.maintainedNodeVersions` + +## [1.4.0] - 2025-10-04 + +### Added + +- Added `PromiseQueue` utility for controlled concurrency operations +- Added lazy dependency loaders and test utilities +- Added HTTP utilities with retry logic and download locking +- Added `.claude` directory for scratch documents +- Added `noUnusedLocals` and `noUnusedParameters` to TypeScript config + +### Changed + +- Refactored all library functions to use options objects for better API consistency + - `lib/strings.ts` - String manipulation functions + - `lib/url.ts` - URL handling functions + - `lib/words.ts` - Word manipulation functions +- Refactored `lib/packages` module into specialized submodules for improved code organization + - `lib/packages/editable.ts` - Package editing functionality + - `lib/packages/exports.ts` - Export resolution utilities + - `lib/packages/licenses.ts` - License handling and validation + - `lib/packages/manifest.ts` - Manifest data operations + - `lib/packages/normalize.ts` - Path normalization utilities + - `lib/packages/operations.ts` - Package installation and modification operations + - `lib/packages/paths.ts` - Package path utilities + - `lib/packages/provenance.ts` - Package provenance verification + - `lib/packages/specs.ts` - Package spec parsing + - `lib/packages/validation.ts` - Package validation utilities +- Moved configuration files (vitest, eslint, knip, oxlint, taze) to `.config` directory +- Replaced `fetch()` with Node.js native `http`/`https` modules for better reliability +- Replaced `any` types with meaningful types across library utilities +- Improved pnpm security with build script allowlist +- Updated vitest coverage thresholds to 80% +- Consolidated test files to reduce duplication +- Note: Public API remains unchanged; these are internal organizational improvements + +### Fixed + +- Fixed resource leaks and race conditions in socket-registry +- Fixed `yarn-cache-path` constant to return string type consistently +- Fixed Yarn Windows temp path detection in `shouldSkipShadow` +- Fixed path normalization for Windows compatibility across all path utilities +- Fixed cache path tests for Windows case sensitivity +- Fixed type errors in promises, parse-args, logger, and specs tests +- Fixed GitHub tests to mock `httpRequest` correctly +- Fixed SEA build tests to mock `httpRequest` +- Decoded URL percent-encoding in `pathLikeToString` fallback + +## [1.3.10] - 2025-10-03 + +### Added + +- New utility modules for DLX, shadow, SEA, cacache, and versions functionality +- getSocketHomePath alias to paths module +- del dependency and external wrapper for safer file deletion +- @fileoverview tags to lib modules +- camelCase expansion for kebab-case arguments in parseArgs +- Coerce and configuration options to parseArgs + +### Changed + +- Updated file removal to use del package for safer deletion +- Normalized path returns in fs and Socket directory utilities +- Removed default exports from git and parse-args modules +- Enhanced test coverage across multiple modules (parse-args, prompts, strings, env, spawn, json) + +## [1.3.9] - 2025-10-03 + +### Changed + +- Internal build and distribution updates + +## [1.3.8] - 2025-10-03 + +### Added + +- Added unified directory structure for Socket ecosystem tools +- New path utilities module for cross-platform directory resolution +- Directory structure constants for Socket CLI, Registry, Firewall, and DLX + +## [1.3.7] - 2025-10-02 + +### Changed + +- Updated manifest.json entries + +## [1.3.6] - 2025-10-01 + +### Fixed + +- Fixed indent-string interoperability with older v1 and v2 versions + +## [1.3.5] - 2025-10-01 + +### Added + +- Added lib/git utilities module + +### Fixed + +- Fixed invalid manifest entries +- Fixed parseArgs strip-aliased bug + +## [1.3.4] - 2025-10-01 + +### Changed + +- Updated various package override versions + +## [1.3.3] - 2025-10-01 + +### Fixed + +- Fixed normalizePath collapsing multiple leading `..` segments incorrectly + +## [1.3.2] - 2025-10-01 + +### Added + +- Added 'sfw' to isBlessedPackageName method check +- Added ENV.DEBUG normalization for debug package compatibility + - `DEBUG='1'` or `DEBUG='true'` automatically expands to `DEBUG='*'` (enables all namespaces) + - `DEBUG='0'` or `DEBUG='false'` automatically converts to empty string (disables all output) + - Namespace patterns like `DEBUG='app:*'` are preserved unchanged + +## [1.3.1] - 2025-09-30 + +### Changed + +- Renamed debug functions from *Complex to *Ns + +### Fixed + +- Fixed regression with lib/prompts module imports + +## [1.3.0] - 2025-09-29 + +### Changed + +- Updated registry subpath exports + +### Fixed + +- Fixed Node.js built-in module imports in CommonJS output + +## [1.2.2] - 2025-09-29 + +### Changed + +- Internal improvements to module structure + +## [1.2.1] - 2025-09-29 + +### Changed + +- Restructured constants module with new architecture +- Updated build configuration and package exports diff --git a/packages/lib/LICENSE b/packages/lib/LICENSE new file mode 100644 index 000000000..602b3ecef --- /dev/null +++ b/packages/lib/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Socket Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/lib/README.md b/packages/lib/README.md new file mode 100644 index 000000000..8bc7bd8b9 --- /dev/null +++ b/packages/lib/README.md @@ -0,0 +1,274 @@ +# @socketsecurity/lib + +[![Socket Badge](https://socket.dev/api/badge/npm/package/@socketsecurity/lib)](https://socket.dev/npm/package/@socketsecurity/lib) +[![CI](https://github.com/SocketDev/socket-lib/actions/workflows/ci.yml/badge.svg)](https://github.com/SocketDev/socket-lib/actions/workflows/ci.yml) +![Test Coverage](https://img.shields.io/badge/test--coverage-100%25-brightgreen) +![Type Coverage](https://img.shields.io/badge/type--coverage-100%25-brightgreen) + +[![Follow @SocketSecurity](https://img.shields.io/twitter/follow/SocketSecurity?style=social)](https://twitter.com/SocketSecurity) + +**Core infrastructure library for Socket.dev security tools** — utilities, constants, and helpers with zero dependencies. + +## Quick Start + +```bash +pnpm add @socketsecurity/lib +``` + +```typescript +// Import what you need - tree-shakeable exports +import { Spinner } from '@socketsecurity/lib/spinner' +import { readJsonFile } from '@socketsecurity/lib/fs' +import { NODE_MODULES } from '@socketsecurity/lib/constants/packages' + +const spinner = Spinner({ text: 'Loading...' }) +spinner.start() +const pkg = await readJsonFile('./package.json') +spinner.stop() +``` + +## 📦 What's Inside + +``` +@socketsecurity/lib +├── Visual Effects → 5 themes, spinners, shimmer, logger +├── File System → fs, paths, globs, temp files +├── Package Management → 11 utilities (npm, pnpm, yarn, dlx) +├── Process & Spawn → Safe process spawning, IPC +├── Environment → 22 modules with 68 typed env getters +├── Constants → 14 modules (Node.js, npm, platform) +├── Utilities → Arrays, objects, strings, promises +└── Types → Full TypeScript definitions +``` + +## 💡 Key Features + +### Visual Effects + +**Themed spinners and text effects:** + +```typescript +import { Spinner, setTheme } from '@socketsecurity/lib' + +setTheme('ultra') // 🌈 Rainbow shimmer! +const spinner = Spinner({ text: 'Processing...' }) +spinner.start() +``` + +**5 Built-in Themes:** `socket` (violet) · `sunset` (twilight) · `terracotta` (warm) · `lush` (steel blue) · `ultra` (rainbow) + +👉 [**Theme System Docs**](./docs/themes.md) + +### File System + +**Safe, typed file operations:** + +```typescript +import { readJsonFile, writeJsonFile } from '@socketsecurity/lib/fs' + +const pkg = await readJsonFile('./package.json') +await writeJsonFile('./output.json', { data: pkg }) +``` + +### Package Management + +**Parse and validate package specs:** + +```typescript +import { parsePackageSpec } from '@socketsecurity/lib/packages' + +const spec = parsePackageSpec('lodash@^4.17.0') +// { name: 'lodash', version: '^4.17.0', type: 'range', ... } +``` + +### Environment Variables + +**68 typed environment getters:** + +```typescript +import { getCI } from '@socketsecurity/lib/env/ci' +import { getHome } from '@socketsecurity/lib/env/home' +import { getNodeEnv } from '@socketsecurity/lib/env/node-env' + +if (getCI()) { + console.log('Running in CI') +} +``` + +### Constants + +**Access platform and Node.js constants:** + +```typescript +import { + NODE_MODULES, + PACKAGE_JSON, + NPM_REGISTRY_URL, +} from '@socketsecurity/lib/constants/packages' + +import { DARWIN, WIN32 } from '@socketsecurity/lib/constants/platform' +``` + +## Common Patterns + +### Spinner with Progress + +```typescript +import { withSpinner, Spinner } from '@socketsecurity/lib/spinner' + +await withSpinner({ + message: 'Installing packages...', + spinner: Spinner({ color: [140, 82, 255] }), + operation: async () => { + await installPackages() + } +}) +``` + +### Safe Process Spawning + +```typescript +import { spawn } from '@socketsecurity/lib/spawn' + +const result = await spawn('npm', ['install'], { + cwd: '/path/to/project', + timeout: 30000 +}) +``` + +### JSON File Operations + +```typescript +import { readJsonFile, writeJsonFile } from '@socketsecurity/lib/fs' + +const data = await readJsonFile('./config.json') +data.version = '2.0.0' +await writeJsonFile('./config.json', data) +``` + +### Promise Utilities + +```typescript +import { timeout, retry } from '@socketsecurity/lib/promises' + +// Timeout after 5 seconds +const result = await timeout(fetchData(), 5000) + +// Retry up to 3 times +const data = await retry(() => fetchData(), { maxAttempts: 3 }) +``` + +## Module Organization + +**120+ granular exports** organized by category: + +``` +/constants/ → Node.js, npm, platform constants + ├─ packages → PACKAGE_JSON, NODE_MODULES, etc. + ├─ platform → DARWIN, WIN32, S_IXUSR, etc. + ├─ node → NODE_VERSION, NODE_PATH, etc. + ├─ time → MILLISECONDS_PER_*, DLX_BINARY_CACHE_TTL + └─ encoding → UTF8, CHAR_* codes + +/env/ → 22 modules providing 68 typed getters + ├─ ci → getCI() - Detect CI environment + ├─ home → getHome() - User home directory + ├─ node-env → getNodeEnv() - NODE_ENV value + └─ ... → And 19 more modules! + +/packages/ → Package management utilities (11 modules) + ├─ validation → Package name/version validation + ├─ operations → Install, extract, manifest, dlx + ├─ registry → npm registry utilities + └─ editable → Editable installs detection + +/effects/ → Visual effects for CLI + ├─ text-shimmer → Animated gradient text + ├─ pulse-frames → Pulsing text effect + └─ ultra → Rainbow gradients + +/stdio/ → Terminal I/O utilities + ├─ stdout → Safe stdout operations + ├─ stderr → Safe stderr operations + ├─ clear → Clear terminal + └─ footer → Terminal footers + +/themes/ → Theme system for consistent branding (5 modules) + ├─ types → Theme type definitions + ├─ themes → 5 themes (socket, sunset, terracotta, lush, ultra) + ├─ context → Global theme management + └─ utils → Color resolution, theme creation +``` + +## Documentation + +| Doc | Description | +|-----|-------------| +| [**Getting Started**](./docs/getting-started.md) | Quick start for contributors (5 min setup) | +| [**Theme System**](./docs/themes.md) | Themed spinners, colors, and effects | +| [**Build Architecture**](./docs/build.md) | Vendored dependencies, build system | +| [**CLAUDE.md**](./CLAUDE.md) | Coding standards and patterns | + +## Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ @socketsecurity/lib │ +│ Zero runtime dependencies │ +├─────────────────────────────────────────────────────┤ +│ src/ │ +│ ├── constants/ 14 modules │ +│ ├── env/ 22 modules (68 getters) │ +│ ├── packages/ 11 utilities │ +│ ├── effects/ 4 visual effects │ +│ ├── stdio/ 9 I/O utilities │ +│ ├── themes/ 5 theme definitions │ +│ ├── external/ 16 vendored deps │ +│ └── ... 62+ more modules │ +├─────────────────────────────────────────────────────┤ +│ Build: esbuild → CommonJS (ES2022) │ +│ Types: tsgo (TypeScript Native Preview) │ +│ Tests: Vitest (4600+ tests, 100% coverage) │ +└─────────────────────────────────────────────────────┘ +``` + +## Development + +**New to the project?** See the [**Getting Started Guide**](./docs/getting-started.md) for setup, workflow, and contribution guidelines. + +**Quick commands:** +```bash +pnpm install # Install dependencies +pnpm run dev # Watch mode +pnpm test # Run tests +pnpm run fix # Auto-fix issues +``` + +## Stats + +- **143** TypeScript modules +- **120+** granular exports +- **68** typed environment getters +- **22** environment modules +- **14** constant modules +- **5** theme definitions +- **4600+** tests passing +- **Zero** runtime dependencies + +## Contributing + +**Ready to contribute?** Start with the [Getting Started Guide](./docs/getting-started.md) for a quick setup walkthrough. + +See [CLAUDE.md](./CLAUDE.md) for: +- Code style and patterns +- Path alias usage +- Testing guidelines +- Build system details + +## License + +MIT + +--- + +**Built by Socket.dev** — [socket.dev](https://socket.dev) | [@SocketSecurity](https://twitter.com/SocketSecurity) diff --git a/packages/lib/biome.json b/packages/lib/biome.json new file mode 100644 index 000000000..665ce4a49 --- /dev/null +++ b/packages/lib/biome.json @@ -0,0 +1,184 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "files": { + "includes": [ + "**", + "!**/.cache", + "!**/.claude", + "!**/.DS_Store", + "!**/._.DS_Store", + "!**/.env", + "!**/.git", + "!**/.github", + "!**/.husky", + "!**/.vscode", + "!**/coverage", + "!**/dist", + "!**/html", + "!**/node_modules", + "!**/package.json", + "!**/pnpm-lock.yaml", + "!packages/npm/**/build", + "!packages/npm/**/package", + "!perf/**/fixtures", + "!scripts/templates", + "!test/**/fixtures", + "!test/**/packages" + ], + "maxSize": 8388608 + }, + "formatter": { + "enabled": true, + "attributePosition": "auto", + "bracketSpacing": true, + "formatWithErrors": false, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 80, + "useEditorconfig": true + }, + "javascript": { + "formatter": { + "arrowParentheses": "asNeeded", + "attributePosition": "auto", + "bracketSameLine": false, + "bracketSpacing": true, + "jsxQuoteStyle": "double", + "quoteProperties": "asNeeded", + "quoteStyle": "single", + "semicolons": "asNeeded", + "trailingCommas": "all" + } + }, + "json": { + "formatter": { + "enabled": true, + "trailingCommas": "none" + }, + "parser": { + "allowComments": true, + "allowTrailingCommas": true + } + }, + "linter": { + "rules": { + "complexity": { + "noBannedTypes": "off", + "useLiteralKeys": "off" + }, + "style": { + "noParameterAssign": "off", + "noNonNullAssertion": "off", + "useAsConstAssertion": "error", + "useDefaultParameterLast": "error", + "useEnumInitializers": "error", + "useNodejsImportProtocol": "off", + "useSelfClosingElements": "error", + "useSingleVarDeclarator": "error", + "noUnusedTemplateLiteral": "error", + "useNumberNamespace": "error", + "noInferrableTypes": "off", + "noUselessElse": "error", + "useNumericSeparators": "error" + }, + "suspicious": { + "noExplicitAny": "off", + "noAsyncPromiseExecutor": "off", + "noAssignInExpressions": "off", + "useIterableCallbackReturn": "off" + } + } + }, + "assist": { + "enabled": false + }, + "overrides": [ + { + "includes": ["packages/npm/**/*.d.ts"], + "linter": { + "rules": { + "suspicious": { + "noExplicitAny": "off" + } + } + } + }, + { + "includes": ["packages/npm/**/*.js", "packages/npm/**/*.cjs"], + "linter": { + "rules": { + "complexity": { + "noArguments": "off", + "noBannedTypes": "off", + "useLiteralKeys": "off" + }, + "correctness": { + "noSelfAssign": "off", + "noUnusedVariables": "off" + }, + "style": { + "noParameterAssign": "off", + "useConst": "off" + }, + "suspicious": { + "noAssignInExpressions": "off", + "noDoubleEquals": "off", + "noExplicitAny": "off", + "noFunctionAssign": "off", + "noRedundantUseStrict": "off", + "noSelfCompare": "off", + "noShadowRestrictedNames": "off", + "useGetterReturn": "off" + } + } + } + }, + { + "includes": ["test/**/*.mts"], + "linter": { + "rules": { + "complexity": { + "noArguments": "off", + "useArrowFunction": "off" + }, + "suspicious": { + "noExplicitAny": "off", + "noImportAssign": "off", + "noShadowRestrictedNames": "off" + } + } + } + }, + { + "includes": ["test/**/*.ts"], + "linter": { + "rules": { + "suspicious": { + "noImportAssign": "off" + } + } + } + }, + { + "includes": ["registry/src/external/**/*.d.ts"], + "linter": { + "rules": { + "suspicious": { + "noExplicitAny": "off" + } + } + } + }, + { + "includes": ["src/external/**/*.js", "src/external/**/*.cjs"], + "linter": { + "rules": { + "suspicious": { + "noRedundantUseStrict": "off" + } + } + } + } + ] +} diff --git a/packages/lib/data/extensions.json b/packages/lib/data/extensions.json new file mode 100644 index 000000000..f9a007dfb --- /dev/null +++ b/packages/lib/data/extensions.json @@ -0,0 +1,28 @@ +{ + "npm": [ + [ + "pkg:npm/%40socketregistry/packageurl-js@latest", + { + "categories": ["levelup"], + "engines": { "node": ">=18" }, + "interop": ["cjs"], + "license": "MIT", + "name": "@socketregistry/packageurl-js", + "package": "packageurl-js", + "version": "latest" + } + ], + [ + "pkg:npm/shell-quote@latest", + { + "categories": ["tuneup"], + "engines": { "node": ">=18" }, + "interop": ["cjs"], + "license": "MIT", + "name": "shell-quote", + "package": "shell-quote", + "version": "latest" + } + ] + ] +} diff --git a/packages/lib/docs/build.md b/packages/lib/docs/build.md new file mode 100644 index 000000000..3460b51ac --- /dev/null +++ b/packages/lib/docs/build.md @@ -0,0 +1,408 @@ +# Build Architecture + +Socket Lib's build system optimizes for zero runtime dependencies while supporting vendored packages. + +## Quick Reference + +| Task | Command | Purpose | +|------|---------|---------| +| **Full build** | `pnpm run build` | Complete production build | +| **JavaScript only** | `pnpm run build:js` | Compile TypeScript → JavaScript | +| **Types only** | `pnpm run build:types` | Generate .d.ts files | +| **Bundle externals** | `pnpm run build:externals` | Bundle vendored dependencies | +| **Fix exports** | `pnpm run fix:exports` | Fix CommonJS exports | +| **Watch mode** | `pnpm run dev` | Auto-rebuild on changes | +| **Clean** | `pnpm run clean` | Remove dist/ | +| **Validate** | `pnpm run validate:externals` | Check dependency rules | + +--- + +## Core Concept + +**Dependency Strategy:** +``` +┌──────────────────────────────────────────────────────────┐ +│ Production Build Strategy │ +├──────────────────────────────────────────────────────────┤ +│ │ +│ Vendored (Bundled in dist/) Runtime (Dependencies) │ +│ ├─ Socket packages @socketregistry/ │ +│ ├─ Small utilities packageurl-js │ +│ ├─ Tight version control (1 runtime dep) │ +│ └─ Type definitions │ +│ │ +│ Goal: Minimize dependencies, maximize control │ +└──────────────────────────────────────────────────────────┘ +``` + +**When to Vendor vs Runtime:** + +| Factor | Vendor (src/external/) | Runtime (dependencies) | +|--------|------------------------|------------------------| +| **Size** | Small utilities | Large/complex packages | +| **Ownership** | Socket-owned packages | Third-party packages | +| **Version control** | Need tight control | Standard semver OK | +| **Updates** | Rarely changes | Frequent updates | +| **Examples** | yoctocolors-cjs, ansi-regex | @socketregistry/packageurl-js | + +--- + +## Build Pipeline + +**Complete Build Flow:** +``` +┌─────────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ +│ Source │ │ Compile │ │ Post-Process │ │ Output │ +│ (src/) │──────→│ (esbuild) │──────→│ (scripts/) │──────→│ (dist/) │ +└─────────────┘ └──────────────┘ └──────────────┘ └─────────────┘ + ↓ ↓ ↓ ↓ + ├─ *.ts - CommonJS - Fix exports ├─ *.js + ├─ external/ - ES2022 - Bundle deps ├─ *.d.ts + ├─ themes/ - Preserve modules - Validate └─ external/ + └─ lib/ - ~1.6s (parallel) - ~300ms + ↓ + tsgo (types) + ~2s (parallel) +``` + +**Build Steps Breakdown:** + +| Step | Tool | Input | Output | Time | +|------|------|-------|--------|------| +| 1️⃣ **Clean** | fs.rm | `dist/` | Empty dir | ~10ms | +| 2️⃣ **Compile JS** | esbuild | `src/**/*.ts` | `dist/**/*.js` | ~1.6s | +| 3️⃣ **Generate Types** | tsgo | `src/**/*.ts` | `dist/**/*.d.ts` | ~2s | +| 4️⃣ **Bundle Externals** | esbuild | `src/external/` | `dist/external/` | ~200ms | +| 5️⃣ **Fix Exports** | Babel AST | `dist/**/*.js` | Fixed CommonJS | ~100ms | +| 6️⃣ **Validate** | Custom | `dist/` | Checks pass | ~50ms | + +**Total: ~4s** (steps 2 & 3 run in parallel) + +**Key Tools:** + +| Tool | Purpose | Why This Tool? | +|------|---------|----------------| +| **esbuild** | TypeScript → JavaScript | Written in Go, extremely fast (~1.6s) | +| **tsgo** | Type definitions (.d.ts) | TypeScript Native Preview, Rust-based | +| **Babel AST** | Post-build transforms | Surgical code changes with source maps | + +--- + +## Dependency Types + +### Runtime Dependencies (package.json) + +```json +{ + "dependencies": { + "@socketregistry/packageurl-js": "1.3.0" + } +} +``` + +| Criteria | Decision | +|----------|----------| +| **Use when** | Package is large or complex | +| | Package needs separate versioning | +| | Package is already a published dependency | +| | Frequent security/bug updates expected | +| **Current Count** | 1 runtime dependency | + +### Vendored Dependencies (src/external/) + +``` +src/external/ +├── @socketregistry/ +│ ├── is-unicode-supported.js # Small utility +│ ├── ansi-regex.js # Tight control needed +│ └── strip-ansi.js # Socket-owned +├── yoctocolors-cjs/ # Color utilities +└── ... (40+ packages) # Bundle in dist/ +``` + +| Criteria | Decision | +|----------|----------| +| **Use when** | Small utility packages (<50 KB) | +| | Socket-owned packages | +| | Tight version control needed | +| | Reducing dependency tree | +| **Current Count** | 40+ vendored packages | + +--- + +## Import Rules + +**Visual Decision Tree:** +``` +Is it in src/external/? + ├─ YES → Vendored code + │ ├─ Inside src/external/ → Implement directly + │ └─ Outside src/external/ → Use relative path + └─ NO → Runtime dependency + └─ Use bare import (must be in ALLOWED list) +``` + +### Import Pattern Reference + +| Location | Dependency Type | Import Style | Example | +|----------|----------------|--------------|---------| +| **Inside src/external/** | Vendored code | Direct implementation | `module.exports = function() { ... }` | +| **Outside src/external/** | Vendored | Relative path | `import foo from '../external/foo'` | +| **Anywhere** | Runtime | Bare import | `import { PackageURL } from '@socketregistry/packageurl-js'` | + +### Examples + +**✅ Correct Patterns:** + +```typescript +// 1. Runtime dependency (in ALLOWED_EXTERNAL_PACKAGES) +import { PackageURL } from '@socketregistry/packageurl-js' + +// 2. Vendored dependency (relative path) +import isUnicodeSupported from '../external/@socketregistry/is-unicode-supported' +import colors from '../external/yoctocolors-cjs' + +// 3. Internal module (path alias) +import { getCI } from '#env/ci' +``` + +**❌ Wrong Patterns:** + +```typescript +// ❌ Bare import for vendored package +import isUnicodeSupported from '@socketregistry/is-unicode-supported' + +// ❌ Relative import for runtime dependency +import { PackageURL } from '../../../node_modules/@socketregistry/packageurl-js' + +// ❌ Unapproved runtime dependency +import axios from 'axios' // Not in ALLOWED list +``` + +--- + +## Build Commands + +| Command | Purpose | When to Use | +|---------|---------|-------------| +| `pnpm run build` | Full production build | Before release/testing | +| `pnpm run build:js` | Compile TypeScript → JavaScript | JS changes only | +| `pnpm run build:types` | Generate .d.ts files | Type changes only | +| `pnpm run build:externals` | Bundle external dependencies | After vendoring new package | +| `pnpm run fix:exports` | Fix CommonJS exports | After build troubleshooting | +| `pnpm run dev` | Watch mode (auto-rebuild) | Active development | +| `pnpm run clean` | Remove dist/ | Clean slate rebuild | +| `pnpm run validate:externals` | Check dependency rules | After adding dependencies | + +--- + +## Build Scripts + +All build scripts are Node.js modules (`.mjs`) for cross-platform compatibility: + +``` +scripts/ +├── build.mjs # Main build orchestrator +├── build-js.mjs # esbuild JavaScript compilation +├── build-externals.mjs # Bundle external dependencies +├── fix-commonjs-exports.mjs # Post-build CommonJS fixes +├── fix-default-imports.mjs # Fix default import patterns +├── validate-external.mjs # Dependency validation +└── babel/ + ├── transform-commonjs-exports.mjs # AST transformations + └── README.md # Transform documentation +``` + +**No shell scripts (`.sh`)** — Node.js only for cross-platform compatibility (Windows support). + +## Post-Build Transformations + +### CommonJS Export Fixes + +TypeScript compiles `export default X` to `exports.default = X`, requiring `.default` accessor. We fix this: + +```javascript +// Before (TypeScript output) +exports.default = WIN32 + +// After (our fix) +module.exports = WIN32 +``` + +This allows cleaner imports: + +```javascript +// ✅ After fix +const WIN32 = require('./WIN32') + +// ❌ Before fix +const WIN32 = require('./WIN32').default +``` + +### External Bundling + +Vendored dependencies are bundled during build: + +```javascript +// Build bundles src/external/* → dist/external/* +// Preserves module structure for selective imports +``` + +## Validation + +`scripts/validate-external.mjs` ensures: + +✅ All `src/external/` packages are properly vendored (no bare imports) +✅ Only allowed runtime dependencies are used +✅ No accidental external package imports + +```bash +pnpm run validate:externals +``` + +--- + +## Adding Dependencies + +### Adding a Runtime Dependency + +| Step | Action | Command/Code | +|------|--------|--------------| +| 1 | Install package | `pnpm add @socketregistry/new-package` | +| 2 | Add to allowed list | Edit `scripts/validate-external.mjs` | +| 3 | Use in code | `import { Thing } from '@socketregistry/new-package'` | +| 4 | Validate | `pnpm run validate:externals` | + +**Allowed list example:** +```javascript +const ALLOWED_EXTERNAL_PACKAGES = [ + '@socketregistry/packageurl-js', + '@socketregistry/new-package', // ← Add here +] +``` + +### Vendoring a Package + +| Step | Action | Command/Code | +|------|--------|--------------| +| 1 | Copy source | `mkdir -p src/external/@socketregistry/my-util`
`cp node_modules/@socketregistry/my-util/index.js src/external/@socketregistry/my-util/` | +| 2 | Import with relative path | `import myUtil from '../external/@socketregistry/my-util'` | +| 3 | Build | `pnpm run build` | +| 4 | Validate | `pnpm run validate:externals` | + +--- + +## Performance + +### Build Times + +| Step | Time | Tool | +|------|------|------| +| Clean | ~10ms | fs.rm | +| JavaScript | ~1.6s | esbuild | +| Types | ~2s | tsgo | +| Externals | ~200ms | esbuild | +| Post-process | ~100ms | Babel AST | +| **Total** | **~4s** | | + +### Optimization Strategies + +| Strategy | Benefit | Impact | +|----------|---------|--------| +| **Parallel builds** | JavaScript and types compile concurrently | ~50% faster | +| **Incremental** | Only rebuild changed files in watch mode | Near-instant rebuilds | +| **Native tools** | esbuild (Go), tsgo (Rust) | 10-100x faster than JS tools | +| **No bundling** | Preserve module structure | Faster than Rollup/Webpack | + +--- + +## Build Output + +``` +dist/ +├── constants/ +│ ├── packages.js +│ ├── packages.d.ts +│ └── ... +├── env/ +│ ├── ci.js +│ ├── ci.d.ts +│ └── ... +├── external/ +│ ├── @socketregistry/ +│ └── ... +├── effects/ +├── stdio/ +├── themes/ +└── ... (matches src/ structure) +``` + +**Format:** CommonJS (`.js`) + TypeScript definitions (`.d.ts`) + +**Target:** ES2022 (Node.js 20+) + +--- + +## Troubleshooting + +### Build Fails + +**Quick fixes:** +```bash +pnpm run build # Check build output +pnpm run clean && pnpm run build # Clean rebuild +``` + +| Issue | Symptom | Solution | +|-------|---------|----------| +| Missing dist/ | `Cannot find module` errors | Run `pnpm run build` | +| TypeScript errors | Build fails with type errors | Run `pnpm run check` | +| Path alias issues | Module not found | Check `tsconfig.json` paths match src/ structure | +| Stale cache | Outdated output | Run `pnpm run clean` first | + +### Validation Errors + +**Check dependencies:** +```bash +pnpm run validate:externals +``` + +| Issue | Symptom | Solution | +|-------|---------|----------| +| Bare import in vendored code | Validation error | Use relative path: `'../external/pkg'` | +| Unapproved dependency | Import not in ALLOWED list | Add to `ALLOWED_EXTERNAL_PACKAGES` in `scripts/validate-external.mjs` | +| Missing vendored file | Module not found | Copy to `src/external/` | + +### Watch Mode Not Working + +| Issue | Solution | +|-------|----------| +| Port already in use | Kill existing process: `pkill -f "pnpm dev"` | +| Changes not detected | Restart: `pnpm run dev` | +| Build errors | Check console output for specific error | + +--- + +## Advanced: Babel Transformations + +Socket Lib uses Babel AST + magic-string for post-build transformations. + +**Pattern:** +1. Parse with Babel → Get AST +2. Walk with Babel traverse → Find nodes +3. Edit with magic-string → Surgical changes +4. Preserve source maps → magic-string maintains mappings + +**Available Transforms:** +- `transform-commonjs-exports.mjs` — Fix `exports.default` +- See `scripts/babel/README.md` for details + +## References + +- [esbuild Documentation](https://esbuild.github.io/) +- [tsgo Repository](https://github.com/microsoft/TypeScript/tree/main/src/tsgo) +- [Babel Parser](https://babeljs.io/docs/babel-parser) + +--- + +**Back to:** [Getting Started](./getting-started.md) · [README](../README.md) diff --git a/packages/lib/docs/getting-started.md b/packages/lib/docs/getting-started.md new file mode 100644 index 000000000..0c0b5aef5 --- /dev/null +++ b/packages/lib/docs/getting-started.md @@ -0,0 +1,307 @@ +# Getting Started + +**Quick start guide** — Get up and running in 5 minutes. + +--- + +## 📋 Prerequisites + +``` +Required: + ✓ Node.js 20+ (LTS recommended) + ✓ pnpm 9+ + ✓ Git + +Optional: + ✓ VS Code (recommended) + ✓ GitHub CLI (gh) +``` + +--- + +## 🚀 Quick Start + +### 1. Clone & Setup + +```bash +# Clone the repository +git clone https://github.com/SocketDev/socket-lib.git +cd socket-lib + +# Install dependencies +pnpm install + +# Verify installation +pnpm test +``` + +**Expected output:** +``` +✓ 4600+ tests passing +✓ 100% code coverage +✓ Build artifacts in dist/ +``` + +--- + +### 2. Project Structure + +``` +socket-lib/ +├── src/ # Source code +│ ├── constants/ # Node.js, npm, package constants +│ ├── env/ # Environment variable getters (68+) +│ ├── effects/ # Visual effects (shimmer, pulse, ultra) +│ ├── lib/ # Core utilities +│ │ ├── fs/ # File system operations +│ │ ├── packages/ # Package management (npm, pnpm, yarn) +│ │ └── spawn/ # Process spawning utilities +│ ├── stdio/ # Terminal I/O utilities +│ ├── themes/ # Theme system (5 themes) +│ └── external/ # Vendored dependencies (41 packages) +│ +├── test/ # Test files (mirrors src/) +├── scripts/ # Build and utility scripts +├── .config/ # Tool configurations +│ └── vitest.config.mts # Test configuration +└── docs/ # Documentation + ├── getting-started.md # Development setup + ├── build.md # Build system details + └── themes.md # Theme system guide +``` + +--- + +### 3. Essential Commands + +```bash +# Development +pnpm run dev # Watch mode (auto-rebuild on changes) +pnpm build # Production build +pnpm run clean # Remove build artifacts + +# Testing +pnpm test # Run all tests +pnpm run cover # Run tests with coverage +pnpm test path/to/file # Run specific test file + +# Quality +pnpm run check # Type check + lint +pnpm run lint # Lint code +pnpm run fix # Auto-fix formatting/lint issues + +# Pre-commit +pnpm run fix && pnpm test # Recommended before committing +``` + +--- + +## 🏗️ Build System + +**Stack:** +- **Compiler**: esbuild (fast TypeScript → CommonJS) +- **Types**: tsgo (TypeScript Native Preview) +- **Target**: ES2022, CommonJS output +- **Watch**: `pnpm run dev` for live rebuilds + +**Build artifacts:** +``` +dist/ +├── *.js # Compiled JavaScript (CommonJS) +├── *.d.ts # TypeScript declarations +└── external/ # Bundled vendored dependencies +``` + +**Build time:** ~1-2 seconds (esbuild is fast!) + +--- + +## 🧪 Testing + +**Framework:** Vitest with v8 coverage + +**Test structure:** +- Tests mirror `src/` structure +- File naming: `*.test.ts` (e.g., `spinner.test.ts` tests `spinner.ts`) +- Coverage requirement: 100% + +**Common patterns:** + +```typescript +// test/spinner.test.ts +import { describe, it, expect } from 'vitest' +import { Spinner } from '../src/spinner' + +describe('Spinner', () => { + it('creates spinner with text', () => { + const spinner = Spinner({ text: 'Loading...' }) + expect(spinner.text).toBe('Loading...') + }) +}) +``` + +**Running tests:** +```bash +pnpm test # All tests +pnpm test spinner # Tests matching "spinner" +pnpm run cover # With coverage report +``` + +--- + +## 💡 Development Workflow + +### Making Changes + +``` +1. Create branch → git checkout -b feature/my-change +2. Make changes → Edit src/ files +3. Watch mode → pnpm run dev (auto-rebuild) +4. Add tests → test/ files with 100% coverage +5. Verify → pnpm run fix && pnpm test +6. Commit → Conventional commits format +7. Push & PR → Open pull request +``` + +### Commit Message Format + +``` +type(scope): description + +Examples: + feat(spinner): add pulse animation support + fix(fs): handle ENOENT in readJsonFile + docs(themes): add usage examples + test(spawn): add timeout test cases +``` + +**Types:** feat, fix, docs, style, refactor, test, chore + +--- + +## 📚 Key Concepts + +### 1. Path Aliases + +Internal imports use path aliases defined in `tsconfig.json`: + +```typescript +// ✓ Use path aliases +import { getCI } from '#env/ci' +import { NODE_MODULES } from '#constants/packages' + +// ✗ Don't use relative imports for internal modules +import { getCI } from '../env/ci' // Wrong! +``` + +### 2. Zero Dependencies + +This library has **zero runtime dependencies**. All external code is vendored in `src/external/`. + +### 3. CommonJS Output + +While source is TypeScript/ESM, build output is CommonJS for maximum compatibility. + +### 4. Environment Module Pattern + +Environment getters are pure functions: + +```typescript +// src/env/ci.ts +export function getCI(): boolean { + return process.env.CI === 'true' || process.env.CI === '1' +} +``` + +For testing, use the rewire module: + +```typescript +import { setEnv, resetEnv } from '#env/rewire' + +setEnv('CI', 'true') +expect(getCI()).toBe(true) +resetEnv() // Clean up in afterEach +``` + +--- + +## 🎨 Visual Effects & Themes + +The library includes 5 themes with visual effects: + +```typescript +import { setTheme, Spinner } from '@socketsecurity/lib' + +setTheme('ultra') // 🌈 Rainbow shimmer +const spinner = Spinner({ text: 'Processing...' }) +spinner.start() +``` + +**Available themes:** none, default, pulse, shimmer, ultra + +See [docs/themes.md](./themes.md) for details. + +--- + +## 🔧 Troubleshooting + +### Tests Failing + +```bash +# Clear cache and rebuild +pnpm run clean +pnpm install +pnpm test +``` + +### Build Errors + +```bash +# Check TypeScript errors +pnpm run check + +# Check for missing dependencies +pnpm install +``` + +### Coverage Not 100% + +```bash +# Run coverage and see report +pnpm run cover + +# Open HTML report +open coverage/index.html +``` + +--- + +## 📖 Additional Resources + +- [Build System](./build.md) - Build architecture and vendored deps +- [Themes Documentation](./themes.md) - Theme system deep dive +- [CLAUDE.md](../CLAUDE.md) - Development standards +- [Main README](../README.md) - API overview + +--- + +## 🆘 Getting Help + +- **Issues:** [GitHub Issues](https://github.com/SocketDev/socket-lib/issues) +- **Discussions:** Ask questions in issue comments +- **Standards:** Check [CLAUDE.md](../CLAUDE.md) for coding conventions + +--- + +## ✅ Checklist for First Contribution + +- [ ] Cloned repo and installed dependencies +- [ ] Ran `pnpm test` successfully (4600+ tests passing) +- [ ] Read [CLAUDE.md](../CLAUDE.md) development standards +- [ ] Understand path aliases (`#env/*`, `#constants/*`, etc.) +- [ ] Know how to run tests (`pnpm test`) +- [ ] Know how to check code (`pnpm run check`) +- [ ] Understand commit message format (conventional commits) +- [ ] Ready to make your first PR! + +**Welcome to socket-lib!** 🎉 diff --git a/packages/lib/docs/socket-lib-structure.md b/packages/lib/docs/socket-lib-structure.md new file mode 100644 index 000000000..971b74bd5 --- /dev/null +++ b/packages/lib/docs/socket-lib-structure.md @@ -0,0 +1,815 @@ +# Socket Lib Complete Structure Analysis + +## Project Overview + +**Socket Lib** (`@socketsecurity/lib`) is a core infrastructure library for Socket.dev security tools. It provides utilities, constants, and helper functions used across Socket projects. + +- **Package Name**: `@socketsecurity/lib` +- **Current Version**: 1.3.5 +- **Node.js Requirement**: >=22 +- **License**: MIT +- **Repository**: https://github.com/SocketDev/socket-lib + +--- + +## 1. TOP-LEVEL DIRECTORY STRUCTURE + +``` +socket-lib/ +├── .claude/ # Claude.com scratch documents (gitignored) +├── .config/ # Build & tooling configuration +├── .git/ # Git repository +├── .git-hooks/ # Custom git hooks +├── .github/workflows/ # CI/CD workflows +├── .husky/ # Husky pre-commit hooks +├── .vscode/ # VS Code settings +├── biome.json # Biome formatter/linter config +├── CHANGELOG.md # Version history +├── CLAUDE.md # Project instructions & standards +├── data/ # Static data files +├── dist/ # Build output (6.7M) +├── docs/ # Documentation +├── LICENSE # MIT license +├── node_modules/ # Dependencies +├── package.json # NPM package metadata +├── pnpm-lock.yaml # Dependency lockfile +├── plugins/ # Custom Babel plugins +├── README.md # Project README +├── scripts/ # Build & development scripts +├── src/ # TypeScript source (1.5M) +└── test/ # Test files (748K, 36 test files) +``` + +**Key sizes**: +- Source code: 1.5MB (183 TypeScript files) +- Tests: 748KB (36 test files) +- Build output: 6.7MB + +--- + +## 2. DOCUMENTATION + +### Existing Documentation + +| File | Purpose | Status | +|------|---------|--------| +| `README.md` | Project overview & installation | Comprehensive | +| `CLAUDE.md` | Project standards & guidelines | Comprehensive | +| `CHANGELOG.md` | Version history & release notes | Current | +| `docs/getting-started.md` | Quick start guide for contributors | Comprehensive | +| `docs/build.md` | Build architecture deep-dive | Comprehensive | +| `docs/themes.md` | Theme system documentation | Comprehensive | +| `LICENSE` | MIT license text | Present | + +### Documentation Status + +**What's COMPLETE**: +- Getting started guide for new contributors (5-min setup) +- Project standards and patterns (CLAUDE.md) +- Build architecture documentation +- Theme system guide +- Development workflow and commands +- Testing patterns and setup + +**What could be ENHANCED**: +- Architecture overview diagram +- Expanded API reference documentation +- Video tutorials or screencasts + +**Current state**: Comprehensive for both maintainers and new developers + +--- + +## 3. AVAILABLE SCRIPTS + +Located in `package.json` and `scripts/` directory (Node.js `.mjs` files): + +### Main Commands + +| Command | Script | Purpose | +|---------|--------|---------| +| `pnpm build` | `scripts/build.mjs` | Production build (esbuild + type generation) | +| `pnpm run build:watch` or `pnpm dev` | `scripts/build-js.mjs` | Watch mode for development | +| `pnpm test` | `scripts/test.mjs` | Run tests with vitest | +| `pnpm run test-ci` | vitest run | CI test execution | +| `pnpm run check` | `scripts/check.mjs` | TypeScript type checking | +| `pnpm lint` | `scripts/lint.mjs` | Biome linting | +| `pnpm run fix` | `scripts/lint.mjs --fix` | Auto-fix linting issues | +| `pnpm run lint-ci` | Same as lint | CI linting | +| `pnpm run type-ci` | `pnpm run check` | CI type checking | +| `pnpm run cover` | `scripts/cover.mjs` | Test coverage report | +| `pnpm run clean` | `scripts/clean.mjs` | Remove build artifacts | +| `pnpm run update` | `scripts/update.mjs` | Update dependencies | +| `pnpm run claude` | `scripts/claude.mjs` | Claude-related tooling | + +### Support Scripts (in `scripts/` directory) + +- `build-externals.mjs` - Bundle external/vendored dependencies +- `build-js.mjs` - JavaScript compilation wrapper +- `fix-build.mjs` - Post-build fixes +- `fix-commonjs-exports.mjs` - Fix CommonJS export format +- `fix-external-imports.mjs` - Fix external import paths +- `generate-package-exports.mjs` - Auto-generate package.json exports +- `utils/` - Shared script utilities + +### Utility Directory (`scripts/utils/`) + +Helper functions and utilities for scripts: +- `changed-test-mapper.mjs` - Map changed files to tests +- `flags.mjs` - Argument parsing +- `helpers.mjs` - Formatting & output +- `logger.mjs` - Script logging +- `parse-args.mjs` - CLI argument parsing +- `run-command.mjs` - Command execution +- `signal-exit.mjs` - Process exit handling +- `spinner.mjs` - Progress spinner + +--- + +## 4. SOURCE CODE ORGANIZATION (`src/`) + +### Directory Layout + +``` +src/ +├── abort.ts # AbortController utilities +├── agent.ts # npm/yarn/pnpm package manager detection +├── ansi.ts # ANSI code utilities +├── argv/ # Argument parsing +│ ├── flags.ts +│ └── parse.ts +├── arrays.ts # Array utilities +├── bin.ts # Binary/executable utilities +├── cacache.ts # Cache utilities +├── cache-with-ttl.ts # TTL-based caching +├── constants/ # 14 constant modules +│ ├── agents.ts +│ ├── core.ts +│ ├── encoding.ts +│ ├── github.ts +│ ├── licenses.ts +│ ├── node.ts +│ ├── packages.ts +│ ├── paths.ts +│ ├── platform.ts +│ ├── process.ts +│ ├── socket.ts +│ ├── testing.ts +│ ├── time.ts +│ └── typescript.ts +├── cover/ # Type coverage utilities +├── debug.ts # Debug logging +├── dlx.ts # npm dlx-like execution +├── dlx-binary.ts # Binary installation +├── dlx-package.ts # Package downloading +├── download-lock.ts # Download synchronization +├── effects/ # CLI visual effects +│ ├── pulse-frames.ts +│ ├── text-shimmer.ts +│ ├── types.ts +│ └── ultra.ts +├── env/ # 68 typed env variable getters +│ ├── ci.ts +│ ├── getters.ts +│ ├── helpers.ts +│ ├── appdata.ts +│ ├── github-*.ts (multiple) +│ ├── home.ts +│ ├── node-env.ts +│ ├── npm-*.ts (multiple) +│ ├── path.ts +│ ├── socket-*.ts (multiple Socket-specific vars) +│ ├── temp.ts +│ ├── tmp.ts +│ ├── xdg-*.ts (multiple) +│ └── ... +├── env.ts # Main env module +├── external/ # Vendored/bundled dependencies +│ ├── @npmcli/ +│ ├── @socketregistry/ +│ ├── @yarnpkg/ +│ ├── @inquirer/ +│ ├── cacache.d.ts / .js +│ ├── debug.d.ts / .js +│ ├── fast-glob.js +│ ├── fast-sort.js +│ ├── make-fetch-happen.js +│ ├── pacote.d.ts / .js +│ ├── semver.d.ts / .js +│ ├── which.js +│ ├── yargs-parser.d.ts / .js +│ ├── yoctocolors-cjs.d.ts / .js +│ ├── zod.js +│ └── ... +├── fs.ts # File system utilities +├── functions.ts # Function utilities +├── git.ts # Git operations +├── github.ts # GitHub API utilities +├── globs.ts # Glob pattern utilities +├── http-request.ts # HTTP client +├── index.ts # Main barrel export +├── ipc.ts # Inter-process communication +├── json.ts # JSON utilities +├── logger.ts # Logging with colors/symbols +├── maintained-node-versions.ts # Node.js version info +├── memoization.ts # Function memoization +├── objects.ts # Object utilities +├── packages/ # Package-related utilities +│ ├── editable.ts +│ ├── exports.ts +│ ├── isolation.ts +│ ├── licenses.ts +│ ├── manifest.ts +│ ├── normalize.ts +│ ├── operations.ts +│ ├── paths.ts +│ ├── provenance.ts +│ ├── registry.ts +│ ├── specs.ts +│ └── validation.ts +├── packages.ts # Main packages export +├── path.ts # Path utilities +├── paths.ts # Path constants +├── performance.ts # Performance measurement +├── promise-queue.ts # Promise queue utility +├── promises.ts # Promise utilities +├── prompts.ts # CLI prompts +├── regexps.ts # Regular expression utilities +├── sea.ts # SEA (Single Executable Application) support +├── shadow.ts # Shadow executable support +├── signal-exit.ts # Process exit handling +├── sorts.ts # Sorting utilities +├── spawn.ts # Child process spawning wrapper +├── spinner.ts # CLI spinner/progress +├── ssri.ts # Subresource Integrity utilities +├── stdio/ # Standard I/O utilities +│ ├── clear.ts +│ ├── divider.ts +│ ├── footer.ts +│ ├── header.ts +│ ├── mask.ts +│ ├── progress.ts +│ ├── prompts.ts +│ ├── stderr.ts +│ └── stdout.ts +├── streams.ts # Stream utilities +├── strings.ts # String utilities +├── suppress-warnings.ts # Warning suppression +├── tables.ts # Table formatting +├── temporary-executor.ts # Temporary execution context +├── types/ # TypeScript type definitions +├── types.ts # Type exports +├── url.ts # URL utilities +├── utils/ # Shared utilities +│ └── get-ipc.ts +├── validation/ # Data validation +│ ├── json-parser.ts +│ └── types.ts +├── versions.ts # Version utilities +├── words.ts # Word/text utilities +└── zod.ts # Zod validation re-export + +**Total**: 64 top-level modules + subdirectories +``` + +### Key Module Categories + +| Category | Modules | Purpose | +|----------|---------|---------| +| **Environment** | `env/*.ts` (68 files) | Typed access to environment variables | +| **Constants** | `constants/*.ts` (14 files) | System & npm constants | +| **Packages** | `packages/*.ts` (12 files) | NPM package utilities | +| **File System** | `fs.ts`, `path.ts`, `paths.ts` | File operations | +| **Utilities** | `strings.ts`, `arrays.ts`, `objects.ts`, etc. | Common helpers | +| **CLI** | `logger.ts`, `spinner.ts`, `stdio/*.ts` | Terminal output | +| **Process** | `spawn.ts`, `signal-exit.ts`, `agent.ts` | Process management | +| **External** | `external/` (40+ files) | Vendored dependencies | + +--- + +## 5. TEST ORGANIZATION (`test/`) + +### Test Structure + +``` +test/ +├── agent.test.ts # Package manager detection tests +├── argv/ +│ ├── flags.test.ts +│ └── parse.test.ts +├── arrays.test.ts +├── bin.test.ts +├── build-externals.test.ts +├── cache-with-ttl.test.ts +├── debug.test.ts +├── dlx-binary.test.ts +├── dlx-package.test.ts +├── effects/ +│ └── pulse-frames.test.ts +├── env.test.ts # Environment variable tests +├── fs.test.ts # File system tests (large) +├── fs-additional.test.ts # Additional FS tests +├── functions.test.ts +├── git.test.ts +├── git-extended.test.ts # Extended git tests +├── github.test.ts +├── http-request.test.ts # HTTP client tests +├── ipc.test.ts +├── json.test.ts +├── logger.test.ts +├── memoization.test.ts +├── objects.test.ts +├── path.test.ts # Path utilities (large) +├── paths.test.ts +├── promises.test.ts +├── packages/ +│ ├── exports.test.ts +│ ├── isolation.test.ts +│ ├── licenses.test.ts +│ ├── manifest.test.ts +│ ├── normalize.test.ts +│ ├── operations.test.ts +│ ├── registry.test.ts +│ ├── specs.test.ts +│ └── validation.test.ts +├── regexps.test.ts +├── signal-exit.test.ts +├── sorts.test.ts +├── spawn.test.ts +├── streams.test.ts +├── strings.test.ts +├── tables.test.ts +├── url.test.ts +├── validation/ +│ └── json-parser.test.ts +├── versions.test.ts +└── words.test.ts + +**Total**: 36 test files +``` + +### Test Coverage + +- Tests cover all major modules +- Mix of unit tests and integration tests +- Large tests: `fs.test.ts`, `bin.test.ts`, `path.test.ts`, `http-request.test.ts` +- Comprehensive coverage for critical modules + +--- + +## 6. CONFIGURATION FILES + +### Root-Level Config Files + +| File | Purpose | Type | +|------|---------|------| +| `package.json` | NPM metadata, scripts, dependencies | JSON | +| `pnpm-lock.yaml` | Dependency lockfile | YAML | +| `biome.json` | Formatter/linter configuration | JSON | +| `tsconfig.json` | TypeScript compilation settings | JSON | +| `tsconfig.dts.json` | Type declaration generation settings | JSON | +| `tsconfig.test.json` | Test-specific TypeScript settings | JSON | +| `.editorconfig` | Editor settings | INI | +| `.gitattributes` | Git file attributes | Text | +| `.gitignore` | Ignored files | Text | +| `.npmrc` | NPM configuration | INI | +| `.node-version` | Node.js version | Text | + +### `.config/` Directory Configuration + +``` +.config/ +├── esbuild.config.mjs # esbuild build configuration +├── eslint.config.mjs # ESLint rules (unified config) +├── isolated-tests.json # Vitest isolated test config +├── knip.json # Dead code finder config +├── taze.config.mts # Dependency updater config +├── tsconfig.check.json # Type checking config +├── vitest-global-setup.mts # Global test setup +├── vitest.config.mts # Vitest test configuration +├── vitest.setup.mts # Test setup hooks +└── vitest-plugins/ # Custom vitest plugins + ├── import-transform.mts # Import transformation + ├── require-transform.mts # Require transformation + └── transform-utils.mts # Transformation utilities +``` + +### `.github/` Workflows + +``` +.github/workflows/ +├── ci.yml # Main CI/CD pipeline +├── claude-auto-review.yml # Claude code review +├── claude.yml # Claude-related workflow +├── provenance.yml # Build provenance +└── socket-auto-pr.yml # Socket auto-PR workflow +``` + +### `.husky/` Git Hooks + +``` +.husky/ +├── pre-commit # Pre-commit hook script +└── security-checks.sh # Security checks script +``` + +Pre-commit hook runs: +1. `pnpm lint --staged` (Biome linting on staged files) +2. `dotenvx -q run -f .env.precommit -- pnpm test --staged` (Tests on changed files) + +--- + +## 7. BUILD SYSTEM + +### Architecture + +The build system is sophisticated with multiple stages: + +``` +TypeScript Source (src/) + ↓ +[1] Compilation with esbuild → CommonJS (dist/) + ↓ +[2] Type Generation with tsgo → .d.ts files (dist/) + ↓ +[3] External Bundling → vendored code (dist/external/) + ↓ +[4] CommonJS Export Fixes → exports.default → module.exports + ↓ +[5] Import Path Fixes → relative vs external paths + ↓ +Final Distribution (dist/) +``` + +### Key Build Features + +**esbuild Configuration** (`.config/esbuild.config.mjs`): +- Bundle name: `socket-lib` +- Format: CommonJS +- Target: ES2022 +- Externalizes: Node.js built-ins, dependencies, `/external/` paths +- Bundled internally: Custom code + +**TypeScript Compilation**: +- Tool: tsgo (TypeScript Native Preview, not tsc) +- Declarations: Generated to `dist/*.d.ts` +- Config: `tsconfig.dts.json` + +**External Dependencies Architecture**: +- **Runtime dependencies**: Listed in `package.json` deps + - Example: `@socketregistry/packageurl-js` +- **Vendored dependencies**: Code copied to `src/external/` + - Examples: `@npmcli/*`, `@socketregistry/*`, `@inquirer/*` + - NOT in package.json dependencies + - Bundled during build +- **Validation**: `scripts/validate-external.mjs` checks for re-exports + +### Build Output + +**dist/** directory (6.7M): +- JavaScript files (CommonJS format) +- TypeScript declaration files (.d.ts) +- External bundled code +- Data files (extensions.json) +- Biome configuration (exported) + +### Build Scripts + +| Script | Purpose | +|--------|---------| +| `scripts/build.mjs` | Master build orchestrator | +| `scripts/build-js.mjs` | esbuild wrapper with watch mode | +| `scripts/build-externals.mjs` | Bundle external dependencies | +| `scripts/fix-commonjs-exports.mjs` | Post-build CommonJS fixes | +| `scripts/fix-external-imports.mjs` | Fix import paths | +| `scripts/clean.mjs` | Clean build artifacts | + +### Build Configuration + +**Development mode**: `pnpm run dev` or `pnpm run build:watch` +- Live rebuilding +- Fast feedback loop +- Useful for development + +**Production mode**: `pnpm build` +- Full build with type generation +- All validations run +- Optimized output + +--- + +## 8. DEPENDENCIES + +### Dependency Statistics + +**Runtime Dependencies**: 1 +- `@socketregistry/packageurl-js@1.3.0` + +**Development Dependencies**: 65+ + +### Key DevDependencies (by category) + +#### Build & Compilation +- `esbuild@0.25.11` - Fast JavaScript bundler +- `@typescript/native-preview@7.0.0-dev.20250920.1` - tsgo (TypeScript Native Preview) +- `typescript@5.7.3` - TypeScript compiler +- `vite-tsconfig-paths@5.1.4` - Vite TypeScript path support + +#### Linting & Formatting +- `@biomejs/biome@2.2.4` - Fast formatter/linter +- `eslint@9.35.0` - JavaScript linter +- `typescript-eslint@8.44.1` - TypeScript ESLint support +- `eslint-plugin-*@*` - ESLint plugins (import-x, n, unicorn, sort-destructure-keys) +- `eslint-import-resolver-typescript@4.4.4` + +#### Testing +- `vitest@4.0.3` - Vitest test runner +- `@vitest/coverage-v8@4.0.3` - V8 coverage provider +- `@vitest/ui@4.0.3` - Vitest UI + +#### Package Management & Tools +- `@npmcli/package-json@7.0.0` - npm package.json utilities +- `@npmcli/promise-spawn@8.0.3` - Promise-based spawning +- `npm-package-arg@13.0.0` - Parse npm package strings +- `pacote@21.0.1` - npm package extraction +- `libnpmpack@9.0.9` - npm pack utilities +- `normalize-package-data@8.0.0` - Normalize package.json +- `validate-npm-package-name@6.0.2` - Validate npm names +- `semver@7.7.2` - Semantic versioning + +#### CLI & Utilities +- `yoctocolors-cjs@2.1.3` - Terminal colors (CommonJS) +- `yargs-parser@22.0.0` - Argument parsing +- `@inquirer/confirm@5.1.16` - Prompts +- `@inquirer/input@4.2.2` +- `@inquirer/password@4.0.18` +- `@inquirer/search@3.1.1` +- `@inquirer/select@4.3.2` +- `@socketregistry/yocto-spinner@1.0.19` - Progress spinner +- `@socketregistry/is-unicode-supported@1.0.5` - Unicode detection +- `@socketregistry/packageurl-js@1.3.0` - Package URL parsing + +#### Development Tools +- `pnpm` (built-in) - Package manager +- `husky@9.1.7` - Git hooks +- `lint-staged@15.2.11` - Lint staged files +- `trash@10.0.0` - Safe file deletion +- `del@8.0.1` - Delete files/folders +- `taze@19.6.0` - Dependency updater +- `type-coverage@2.29.7` - Type coverage reporter +- `knip@19.6.0` - Dead code finder +- `npm-run-all2@8.0.4` - Run multiple npm scripts +- `dotenvx@1.49.0` - dotenv utility + +#### Other +- `@babel/*@7.28.4` - Babel packages for plugins +- `magic-string@0.30.17` - String manipulation +- `make-fetch-happen@15.0.2` - Fetch implementation +- `fast-glob@3.3.3` - Fast globbing +- `fast-sort@3.4.1` - Fast sorting +- `get-east-asian-width@1.3.0` - Character width +- `debug@4.4.3` - Debugging +- `which@5.0.0` - Find executable paths +- `globals@16.4.0` - Global variable names +- `picomatch@2.3.1` - Pattern matching +- `spdx-*@*` - SPDX license utilities +- `streaming-iterables@8.0.1` - Async iterables +- `zod@4.1.12` - Schema validation +- `@yarnpkg/extensions@2.0.6` - Yarn extensions + +### Dependency Notes + +- **No backend dependencies**: Pure Node.js utilities +- **Tree-shakeable**: `sideEffects: false` +- **CommonJS focused**: Scripts use `.mjs` modules +- **Platform independent**: Works on Windows, macOS, Linux +- **Browser fallbacks**: Has extensive browser field mappings + +--- + +## 9. PACKAGE EXPORTS + +The package uses granular exports (from `package.json` exports): + +**Export Categories**: + +1. **Main entry**: `./`, `./index` +2. **Modules**: `./abort`, `./ansi`, `./agent`, `./arrays`, `./bin`, etc. (~120+ exports) +3. **Nested paths**: + - `./argv/*` (flags, parse) + - `./constants/*` (14 constant modules) + - `./cover/*` (code, formatters, type, types) + - `./effects/*` (pulse-frames, text-shimmer, types, ultra) + - `./env/*` (68 specific env vars) + - `./packages/*` (12 package utilities) + - `./packages/licenses`, `./packages/paths`, etc. + - `./stdio/*` (clear, divider, footer, header, mask, progress, prompts, stderr, stdout) + - `./utils/*` (get-ipc) + - `./validation/*` (json-parser, types) + +4. **Plugins**: `./plugins/babel-plugin-inline-require-calls` +5. **Configuration**: `./biome.json`, `./data/extensions.json`, `./package.json`, `./tsconfig*.json` +6. **Import aliases**: + - `#constants/*` → `dist/constants/*.js` + - `#env/*` → `dist/env/*.js` + - `#lib/*` → `dist/*.js` + - `#packages/*` → `dist/packages/*.js` + - `#types` → `dist/types.js` + - `#utils/*` → `dist/utils/*.js` + +--- + +## 10. PLUGINS + +Located in `plugins/` directory: + +### Babel Plugins + +- `babel-plugin-inline-require-calls.js` - Inline require() calls +- `babel-plugin-inline-const-enum.mjs` - Inline const enums +- `babel-plugin-inline-process-env.mjs` - Inline process.env values +- `babel-plugin-strip-debug.mjs` - Strip debug statements +- `README.md` - Plugin documentation + +### Vitest Plugins + +Located in `.config/vitest-plugins/`: + +- `import-transform.mts` - Transform imports in tests +- `require-transform.mts` - Transform requires in tests +- `transform-utils.mts` - Shared transformation utilities + +--- + +## 11. STANDARDS & PRACTICES + +### Code Organization + +**Follows socket-registry CLAUDE.md standards**: +- ESM imports with `node:` prefix required +- Kebab-case file names +- Mandatory `@fileoverview` headers +- No `any` types (use `unknown`) +- Null-prototype objects with `{ __proto__: null, ...props }` +- Alphabetical sorting (imports, exports, properties) +- Semicolons omitted (trailing commas: all) + +### Build Standards + +- **No shell scripts**: Only Node.js `.mjs` files +- **CommonJS output**: esbuild compiles to CommonJS +- **Type-safe**: Full TypeScript with declarations +- **Cross-platform**: Works on Windows/Unix + +### Testing Standards + +- **Framework**: Vitest 4.0.3 +- **Environment**: Node.js only +- **Pool**: Threads (faster than forks) +- **Coverage**: V8-based +- **Staged testing**: Pre-commit runs affected tests + +### Performance Optimization + +- **Fast build**: esbuild for speed +- **Minimal externals**: Only 1 runtime dependency +- **Tree-shaking**: Full support +- **Code splitting**: Granular exports + +--- + +## 12. ENVIRONMENT + +### Git + +- **Repository**: SocketDev/socket-lib +- **Current branch**: main (from context) +- **Hooks**: Pre-commit lint + test validation + +### Node.js + +- **Required version**: >=22 +- **Tested in CI**: Multiple versions +- **Platform support**: Windows, macOS, Linux + +### Package Manager + +- **Primary**: pnpm +- **Lockfile**: pnpm-lock.yaml +- **Install**: `pnpm install` + +--- + +## 13. DEVELOPMENT WORKFLOW + +### Quick Start + +```bash +# Install dependencies +pnpm install + +# Watch mode (auto-rebuild) +pnpm run dev + +# Run tests +pnpm test + +# Type checking +pnpm run check + +# Linting +pnpm run lint + +# Format + fix +pnpm run fix + +# Production build +pnpm build + +# Coverage report +pnpm run cover +``` + +### Pre-commit Flow + +1. **Lint staged files**: `pnpm lint --staged` +2. **Test staged files**: `pnpm test --staged` +3. **Skip options**: Set env vars: + - `DISABLE_PRECOMMIT_LINT=1` - Skip linting + - `DISABLE_PRECOMMIT_TEST=1` - Skip testing + +### CI/CD Pipeline + +Located in `.github/workflows/`: +- **ci.yml**: Main CI with build, lint, test, coverage +- **provenance.yml**: Build provenance tracking +- **claude.yml**: Claude integration +- **socket-auto-pr.yml**: Socket auto-PR workflow + +--- + +## 14. KEY INSIGHTS + +### Strengths + +1. **Comprehensive utility library** - 60+ modules covering common needs +2. **Well-documented standards** - CLAUDE.md provides clear guidelines +3. **Type-safe** - Full TypeScript with declarations +4. **Granular exports** - Consumers can import specific utilities +5. **Fast build** - esbuild compilation is quick +6. **No production dependencies** - Single optional peer dependency +7. **Cross-platform** - Windows/Unix support built-in +8. **Modern tooling** - Vitest, Biome, TypeScript modern setup + +### Documentation Strengths + +1. **Getting started guide** — Quick 5-minute setup for new contributors +2. **Development standards** — Comprehensive CLAUDE.md with patterns +3. **Build documentation** — Detailed build system architecture +4. **Theme system guide** — Visual effects and theming documentation +5. **Development workflow** — Commands, testing, and contribution flow +6. **Testing patterns** — Vitest setup and common patterns +7. **Code standards** — Clear formatting, naming, and organization rules + +### Code Patterns + +**Consistent patterns across modules**: +- Options objects with null-prototype +- Async/promise-based APIs +- Error handling with descriptive messages +- Cross-platform path handling +- Terminal color support (yoctocolors-cjs) +- Logging with symbols (✓ ✗ ⚠ ℹ) + +--- + +## 15. STATISTICS + +| Metric | Value | +|--------|-------| +| TypeScript files (src/) | 183 | +| Test files | 36 | +| Total src size | 1.5M | +| Total test size | 748K | +| Build output size | 6.7M | +| Package.json exports | 120+ | +| Environment vars (env/) | 68 | +| Constants modules | 14 | +| Package utilities (packages/) | 12 | +| Build scripts | 7 main scripts | +| Dev dependencies | 65+ | +| Runtime dependencies | 1 | + +--- + +## Summary + +Socket Lib is a mature, well-organized infrastructure library with: +- **Clear structure**: Source organized by functionality +- **Comprehensive features**: 180+ utility modules +- **Strong standards**: Adheres to socket-registry guidelines +- **Good automation**: Build system, testing, linting +- **Modern tooling**: esbuild, Vitest, TypeScript, Biome +- **Production-ready**: Used across Socket projects +- **Well-documented**: Getting started guide, build docs, and contributor resources + diff --git a/packages/lib/docs/themes.md b/packages/lib/docs/themes.md new file mode 100644 index 000000000..b664cf6cd --- /dev/null +++ b/packages/lib/docs/themes.md @@ -0,0 +1,651 @@ +# Theme System + +Socket Lib provides a comprehensive theming system for consistent branding across spinners, text effects, links, prompts, and logger output. + +## Quick Reference + +| Theme | Use Case | Primary Color | Special Effects | +|-------|----------|---------------|-----------------| +| **`lush`** | Steel elegance | Steel blue `#4682B4` + Gold `#FFD700` | Elegant harmony | +| **`socket`** (default) | Socket Security | Violet `#8C52FF` | Subtle shimmer | +| **`sunset`** | Vibrant twilight | Orange `#FF8C64` + Pink `#C864B4` | Purple-to-orange gradient | +| **`terracotta`** | Solid warmth | Terracotta `#FF6432` | Rich shimmer | +| **`ultra`** | Premium intensity | 🌈 Prismatic | Rainbow shimmer | + +### Quick Start + +```typescript +import { setTheme, Spinner } from '@socketsecurity/lib' + +setTheme('terracotta') // Set once at startup +const spinner = Spinner({ text: 'Scanning...' }) +spinner.start() // Uses terracotta theme automatically +``` + +--- + +## Core Concepts + +### 🎨 What's a Theme? + +A theme defines the visual identity for all CLI components: + +``` +Theme +├── Colors → Brand & semantic colors +│ ├── Brand → primary, secondary +│ ├── Semantic → success, error, warning, info +│ └── UI → text, links, prompts +│ +└── Effects → Visual enhancements + ├── Spinner → Style & animation + ├── Shimmer → Gradient text effects + └── Pulse → Breathing animations +``` + +### 🔄 Theme Lifecycle + +``` +┌─────────────┐ +│ App Startup │ +└──────┬──────┘ + │ + ↓ +┌─────────────────┐ ┌──────────────┐ +│ setTheme('xxx') │────────→│ Global Theme │ +└─────────────────┘ └──────┬───────┘ + │ + ┌──────────────┼──────────────┐ + ↓ ↓ ↓ + ┌─────────┐ ┌─────────┐ ┌────────┐ + │ Spinner │ │ Logger │ │ Links │ + └─────────┘ └─────────┘ └────────┘ +``` + +### 📚 Theme Stack + +Themes use a stack model for temporary changes: + +```typescript +// Stack visualization: +// ┌──────────┐ +// │ ultra │ ← popTheme() removes this +// ├──────────┤ +// │ sunset │ ← pushTheme() adds here +// ├──────────┤ +// │ socket │ ← Base theme +// └──────────┘ + +pushTheme('sunset') // Add to stack +pushTheme('ultra') // Add another +popTheme() // Remove ultra → back to sunset +popTheme() // Remove sunset → back to socket +``` + +### 🎯 Scoped Themes (Recommended) + +Use `withTheme()` for automatic cleanup: + +```typescript +import { withTheme } from '@socketsecurity/lib/themes' + +// Before: sunset theme +await withTheme('ultra', async () => { + // Inside: ultra theme 🌈 + const spinner = Spinner({ text: 'MAXIMUM POWER!' }) + await epicOperation() +}) +// After: sunset theme (auto-restored) +``` + +**Visual Flow:** +``` +Normal Flow → [sunset] → [sunset] → [sunset] + ↓ +withTheme() → [sunset] → [ultra] → [sunset] + ↑─────────↑ + Auto-restore +``` + +--- + +## Built-in Themes + +### 🔷 Lush + +```typescript +setTheme('lush') +``` + +| Attribute | Value | +|-----------|-------| +| **Primary Color** | `#4682B4` (Steel blue) | +| **Secondary Color** | `#FFD700` (Gold) | +| **Best For** | Elegant interfaces, harmonious design | +| **Spinner** | Dots style, clean animations | +| **Visual Style** | Elegant theme with steel blue and golden harmony | + +### 🟣 Socket Security (Default) + +```typescript +setTheme('socket') +``` + +| Attribute | Value | +|-----------|-------| +| **Primary Color** | `#8C52FF` (Refined violet) | +| **Best For** | Socket.dev tools, security scanning | +| **Spinner** | Socket style with subtle shimmer | +| **Visual Style** | Signature theme designed for focus and elegance | + +**Preview:** +``` +✓ Package scan complete # Green +✗ Vulnerability detected # Red +⚠ 3 issues require attention # Yellow +→ Installing dependencies... # Cyan +``` + +### 🌅 Sunset + +```typescript +setTheme('sunset') +``` + +| Attribute | Value | +|-----------|-------| +| **Primary Color** | `#FF8C64` (Warm orange) | +| **Secondary Color** | `#C864B4` (Purple/pink) | +| **Best For** | Coana branding, warm interfaces, twilight aesthetics | +| **Spinner** | Dots style, clean animations | +| **Effects** | Purple-to-orange gradient shimmer | +| **Visual Style** | Vibrant twilight gradient with warm sunset tones | + +### 🟠 Terracotta + +```typescript +setTheme('terracotta') +``` + +| Attribute | Value | +|-----------|-------| +| **Primary Color** | `#FF6432` (Terracotta) | +| **Secondary Color** | `#FF9664` (Light terracotta) | +| **Best For** | Grounded confidence, warm interfaces | +| **Spinner** | Socket style with rich shimmer | +| **Visual Style** | Solid theme with rich terracotta and ember warmth | + +### 🌈 Ultra + +```typescript +setTheme('ultra') +``` + +| Attribute | Value | +|-----------|-------| +| **Primary Color** | `#8C52FF` with rainbow shimmer | +| **Best For** | Deep analysis, premium experiences | +| **Effects** | Prismatic shimmer, bidirectional rainbow | +| **Spinner** | Socket style with rainbow effects | +| **Visual Style** | Premium intensity where complexity meets elegance | + +**When to use Ultra:** +- Complex analysis operations +- Premium feature demonstrations +- Deep diagnostic sessions +- Maximum visual impact needed + +--- + +## API Reference + +### Core Functions + +#### `setTheme(theme)` +Set global theme (use at app startup) + +```typescript +import { setTheme } from '@socketsecurity/lib/themes' + +// By name +setTheme('terracotta') + +// By custom object +setTheme(myCustomTheme) +``` + +#### `getTheme()` +Get current active theme + +```typescript +import { getTheme } from '@socketsecurity/lib/themes' + +const theme = getTheme() +console.log(theme.displayName) // "Socket Security" +console.log(theme.colors.primary) // [140, 82, 255] +``` + +### Stack Management + +#### `pushTheme(theme)` / `popTheme()` +Manual stack operations + +```typescript +import { pushTheme, popTheme } from '@socketsecurity/lib/themes' + +pushTheme('ultra') // Switch to ultra +// ... operations ... +popTheme() // Restore previous +``` + +⚠️ **Warning:** Always match `push` with `pop` to avoid theme leaks! + +#### `withTheme(theme, fn)` ✨ Recommended +Auto-managed theme scope (async) + +```typescript +import { withTheme } from '@socketsecurity/lib/themes' + +await withTheme('sunset', async () => { + await doAnalysis() +}) +// Theme auto-restored +``` + +#### `withThemeSync(theme, fn)` +Auto-managed theme scope (sync) + +```typescript +import { withThemeSync } from '@socketsecurity/lib/themes' + +const result = withThemeSync('terracotta', () => { + return processSecurity() +}) +``` + +### Theme Creation + +#### `createTheme(config)` +Build custom theme from scratch + +```typescript +import { createTheme } from '@socketsecurity/lib/themes' + +const myTheme = createTheme({ + name: 'my-theme', + displayName: 'My Theme', + colors: { + primary: [255, 100, 200], + success: 'greenBright', + error: 'redBright', + warning: 'yellowBright', + info: 'blueBright', + step: 'cyanBright', + text: 'white', + textDim: 'gray', + link: 'cyanBright', + prompt: 'primary' + } +}) +``` + +#### `extendTheme(base, overrides)` +Customize existing theme + +```typescript +import { extendTheme, SOCKET_THEME } from '@socketsecurity/lib/themes' + +const customTheme = extendTheme(SOCKET_THEME, { + name: 'custom-socket', + colors: { + primary: [200, 100, 255] // Different purple + } +}) +``` + +### Event Handling + +#### `onThemeChange(listener)` +React to theme changes + +```typescript +import { onThemeChange } from '@socketsecurity/lib/themes' + +const unsubscribe = onThemeChange((theme) => { + console.log('Theme changed to:', theme.displayName) + updateMyUI(theme) +}) + +// Stop listening +unsubscribe() +``` + +--- + +## Integration + +### Spinners 🔄 + +Spinners inherit theme colors and styles automatically: + +```typescript +import { Spinner, setTheme } from '@socketsecurity/lib' + +setTheme('ultra') +const spinner = Spinner({ text: 'Processing...' }) +spinner.start() // 🌈 Rainbow spinner! +``` + +**Override for specific spinner:** +```typescript +const spinner = Spinner({ + text: 'Custom color', + color: [255, 100, 50] // Ignores theme +}) +``` + +### Logger 📝 + +Logger symbols use theme colors: + +```typescript +import { logger, setTheme } from '@socketsecurity/lib' + +setTheme('terracotta') + +logger.success('Build complete') // ✓ in green +logger.error('Build failed') // ✗ in red +logger.warn('Update available') // ⚠ in yellow +logger.info('System status: OK') // ℹ in blue +``` + +**Output Preview:** +``` +✓ Build complete # Theme success color +✗ Build failed # Theme error color +⚠ Update available # Theme warning color +ℹ System status: OK # Theme info color +``` + +### Links 🔗 + +Create themed terminal hyperlinks: + +```typescript +import { link } from '@socketsecurity/lib/links' + +// Uses current theme +console.log(link('Documentation', 'https://socket.dev')) + +// Override theme +console.log(link('API', 'https://api.socket.dev', { + theme: 'sunset' +})) + +// Show URL fallback +console.log(link('GitHub', 'https://github.com', { + fallback: true +})) +// Output: "GitHub (https://github.com)" +``` + +--- + +## Color System + +### Color Types + +| Type | Example | Description | +|------|---------|-------------| +| **Named colors** | `'red'`, `'greenBright'` | Standard terminal colors | +| **RGB tuples** | `[255, 100, 50]` | Custom RGB (0-255 each) | +| **Theme refs** | `'primary'`, `'secondary'` | Reference theme colors | +| **Special** | `'rainbow'`, `'inherit'` | Dynamic colors | + +### Color Reference Table + +```typescript +colors: { + // Brand colors + primary: [140, 82, 255], // Main brand color + secondary: [100, 200, 255], // Optional accent + + // Semantic colors (status indicators) + success: 'greenBright', // ✓ Success messages + error: 'redBright', // ✗ Error messages + warning: 'yellowBright', // ⚠ Warning messages + info: 'blueBright', // ℹ Info messages + step: 'cyanBright', // → Progress steps + + // UI colors + text: 'white', // Regular text + textDim: 'gray', // Secondary/dim text + link: 'cyanBright', // Hyperlinks + prompt: 'primary' // Interactive prompts +} +``` + +### Named Colors Reference + +| Basic | Bright | Use Case | +|-------|--------|----------| +| `'red'` | `'redBright'` | Errors, critical issues | +| `'green'` | `'greenBright'` | Success, completion | +| `'yellow'` | `'yellowBright'` | Warnings, caution | +| `'blue'` | `'blueBright'` | Info, general status | +| `'cyan'` | `'cyanBright'` | Links, progress | +| `'magenta'` | `'magentaBright'` | Highlights, special | +| `'white'` | `'whiteBright'` | Text, content | +| `'gray'` | `'blackBright'` | Dim text, disabled | + +💡 **Tip:** Use `Bright` variants for better terminal visibility! + +--- + +## Common Patterns + +### Pattern 1: Product Branding + +```typescript +import { setTheme, Spinner } from '@socketsecurity/lib' + +// Set theme once at startup +setTheme('terracotta') + +// All components inherit theme +const spinner = Spinner({ text: 'Building project...' }) +spinner.start() +``` + +### Pattern 2: Temporary Theme Switch + +```typescript +import { withTheme, logger } from '@socketsecurity/lib' + +// Normal operations +logger.info('Starting scan...') + +// Switch to ultra for celebration +await withTheme('ultra', async () => { + logger.success('🎉 All packages safe!') +}) + +// Back to normal +logger.info('Scan complete') +``` + +### Pattern 3: Custom Product Theme + +```typescript +import { createTheme, setTheme } from '@socketsecurity/lib/themes' + +const myProductTheme = createTheme({ + name: 'my-product', + displayName: 'My Product', + colors: { + primary: [50, 150, 250], + secondary: [255, 200, 0], + success: 'greenBright', + error: 'redBright', + warning: 'yellowBright', + info: 'cyanBright', + step: 'blueBright', + text: 'white', + textDim: 'gray', + link: 'secondary', + prompt: 'primary' + }, + meta: { + description: 'Custom theme for My Product CLI', + version: '1.0.0' + } +}) + +setTheme(myProductTheme) +``` + +--- + +## Best Practices + +### ✅ Do's + +1. **Set theme early** — Call `setTheme()` at application startup +2. **Use scoped themes** — Prefer `withTheme()` over manual push/pop +3. **Use color references** — Use `'primary'` instead of hard-coded RGB +4. **Test all themes** — Verify output looks good with each theme +5. **Document custom themes** — Add `meta` with description + +### ❌ Don'ts + +1. **Don't forget to pop** — Always match `pushTheme()` with `popTheme()` +2. **Don't hard-code colors** — Use theme system for consistency +3. **Don't nest excessively** — Keep theme nesting shallow (< 3 levels) +4. **Don't ignore terminal support** — Test in different terminals +5. **Don't overuse ultra** — Save rainbow mode for special moments! 🌈 + +--- + + +## Troubleshooting + +### Q: Theme changes not taking effect? + +**A:** Rebuild the project after theme changes: +```bash +pnpm run build +``` + +### Q: How do I know which theme is active? + +**A:** Use `getTheme()`: +```typescript +const theme = getTheme() +console.log(theme.name, theme.displayName) +// "socket" "Socket Security" +``` + +### Q: Can I use custom RGB colors? + +**A:** Yes! Specify as `[R, G, B]` tuples (0-255): +```typescript +colors: { + primary: [255, 100, 200] // Custom pink +} +``` + +### Q: Why use references like 'primary'? + +**A:** References adapt when themes change: +```typescript +colors: { + link: 'primary' // Follows theme primary color +} + +// Changes automatically when theme changes! +setTheme('sunset') // Links become warm orange +setTheme('terracotta') // Links become warm peachy coral (secondary) +``` + +### Q: Theme not restoring after crash? + +**A:** Use `withTheme()` for automatic cleanup: +```typescript +// ✅ Safe - auto-restores even on error +await withTheme('ultra', async () => { + await riskyOperation() +}) + +// ❌ Risky - theme stuck if error +pushTheme('ultra') +await riskyOperation() // If this throws, theme stuck! +popTheme() +``` + +--- + +## Theme Type Reference + +Complete TypeScript definition: + +```typescript +type Theme = { + name: string + displayName: string + + colors: { + // Brand + primary: ColorValue + secondary?: ColorValue + + // Semantic + success: ColorValue + error: ColorValue + warning: ColorValue + info: ColorValue + step: ColorValue + + // UI + text: ColorValue + textDim: ColorValue + link: ColorReference + prompt: ColorReference + } + + effects?: { + spinner?: { + color?: ColorReference + style?: SpinnerStyle | string + } + shimmer?: { + enabled?: boolean + color?: ColorReference | ColorValue[] + direction?: 'ltr' | 'rtl' | 'ttb' | 'btt' + speed?: number + } + pulse?: { + speed?: number + } + } + + meta?: { + description?: string + author?: string + version?: string + } +} + +type ColorValue = string | [number, number, number] +type ColorReference = ColorValue | 'primary' | 'secondary' | 'inherit' | 'rainbow' +``` + +--- + +## Next Steps + +| Resource | Description | +|----------|-------------| +| [**Getting Started**](./getting-started.md) | Development workflow, commands | +| [**Build Architecture**](./build.md) | How the build system works | +| [**CLAUDE.md**](../CLAUDE.md) | Coding standards & patterns | + diff --git a/packages/lib/package.json b/packages/lib/package.json new file mode 100644 index 000000000..091a1f070 --- /dev/null +++ b/packages/lib/package.json @@ -0,0 +1,740 @@ +{ + "name": "@socketsecurity/lib", + "version": "3.2.8", + "private": true, + "license": "MIT", + "description": "Core utilities and infrastructure for Socket.dev security tools", + "keywords": [ + "Socket.dev", + "security", + "utilities", + "infrastructure" + ], + "homepage": "https://github.com/SocketDev/socket-cli", + "repository": { + "type": "git", + "url": "git+https://github.com/SocketDev/socket-cli.git", + "directory": "packages/lib" + }, + "author": { + "name": "Socket Inc", + "email": "eng@socket.dev", + "url": "https://socket.dev" + }, + "exports": { + "./abort": { + "types": "./dist/abort.d.ts", + "default": "./dist/abort.js" + }, + "./agent": { + "types": "./dist/agent.d.ts", + "default": "./dist/agent.js" + }, + "./ansi": { + "types": "./dist/ansi.d.ts", + "default": "./dist/ansi.js" + }, + "./argv/flags": { + "types": "./dist/argv/flags.d.ts", + "default": "./dist/argv/flags.js" + }, + "./argv/parse": { + "types": "./dist/argv/parse.d.ts", + "default": "./dist/argv/parse.js" + }, + "./arrays": { + "types": "./dist/arrays.d.ts", + "default": "./dist/arrays.js" + }, + "./bin": { + "types": "./dist/bin.d.ts", + "default": "./dist/bin.js" + }, + "./cacache": { + "types": "./dist/cacache.d.ts", + "default": "./dist/cacache.js" + }, + "./cache-with-ttl": { + "types": "./dist/cache-with-ttl.d.ts", + "default": "./dist/cache-with-ttl.js" + }, + "./constants/agents": { + "types": "./dist/constants/agents.d.ts", + "default": "./dist/constants/agents.js" + }, + "./constants/core": { + "types": "./dist/constants/core.d.ts", + "default": "./dist/constants/core.js" + }, + "./constants/encoding": { + "types": "./dist/constants/encoding.d.ts", + "default": "./dist/constants/encoding.js" + }, + "./constants/github": { + "types": "./dist/constants/github.d.ts", + "default": "./dist/constants/github.js" + }, + "./constants/licenses": { + "types": "./dist/constants/licenses.d.ts", + "default": "./dist/constants/licenses.js" + }, + "./constants/node": { + "types": "./dist/constants/node.d.ts", + "default": "./dist/constants/node.js" + }, + "./constants/packages": { + "types": "./dist/constants/packages.d.ts", + "default": "./dist/constants/packages.js" + }, + "./constants/paths": { + "types": "./dist/constants/paths.d.ts", + "default": "./dist/constants/paths.js" + }, + "./constants/platform": { + "types": "./dist/constants/platform.d.ts", + "default": "./dist/constants/platform.js" + }, + "./constants/process": { + "types": "./dist/constants/process.d.ts", + "default": "./dist/constants/process.js" + }, + "./constants/socket": { + "types": "./dist/constants/socket.d.ts", + "default": "./dist/constants/socket.js" + }, + "./constants/testing": { + "types": "./dist/constants/testing.d.ts", + "default": "./dist/constants/testing.js" + }, + "./constants/time": { + "types": "./dist/constants/time.d.ts", + "default": "./dist/constants/time.js" + }, + "./constants/typescript": { + "types": "./dist/constants/typescript.d.ts", + "default": "./dist/constants/typescript.js" + }, + "./cover/code": { + "types": "./dist/cover/code.d.ts", + "default": "./dist/cover/code.js" + }, + "./cover/formatters": { + "types": "./dist/cover/formatters.d.ts", + "default": "./dist/cover/formatters.js" + }, + "./cover/type": { + "types": "./dist/cover/type.d.ts", + "default": "./dist/cover/type.js" + }, + "./cover/types": { + "types": "./dist/cover/types.d.ts", + "default": "./dist/cover/types.js" + }, + "./debug": { + "types": "./dist/debug.d.ts", + "default": "./dist/debug.js" + }, + "./dlx": { + "types": "./dist/dlx.d.ts", + "default": "./dist/dlx.js" + }, + "./dlx-binary": { + "types": "./dist/dlx-binary.d.ts", + "default": "./dist/dlx-binary.js" + }, + "./dlx-manifest": { + "types": "./dist/dlx-manifest.d.ts", + "default": "./dist/dlx-manifest.js" + }, + "./dlx-package": { + "types": "./dist/dlx-package.d.ts", + "default": "./dist/dlx-package.js" + }, + "./effects/pulse-frames": { + "types": "./dist/effects/pulse-frames.d.ts", + "default": "./dist/effects/pulse-frames.js" + }, + "./effects/text-shimmer": { + "types": "./dist/effects/text-shimmer.d.ts", + "default": "./dist/effects/text-shimmer.js" + }, + "./effects/types": { + "types": "./dist/effects/types.d.ts", + "default": "./dist/effects/types.js" + }, + "./effects/ultra": { + "types": "./dist/effects/ultra.d.ts", + "default": "./dist/effects/ultra.js" + }, + "./env": { + "types": "./dist/env.d.ts", + "default": "./dist/env.js" + }, + "./env/ci": { + "types": "./dist/env/ci.d.ts", + "default": "./dist/env/ci.js" + }, + "./env/debug": { + "types": "./dist/env/debug.d.ts", + "default": "./dist/env/debug.js" + }, + "./env/github": { + "types": "./dist/env/github.d.ts", + "default": "./dist/env/github.js" + }, + "./env/helpers": { + "types": "./dist/env/helpers.d.ts", + "default": "./dist/env/helpers.js" + }, + "./env/home": { + "types": "./dist/env/home.d.ts", + "default": "./dist/env/home.js" + }, + "./env/locale": { + "types": "./dist/env/locale.d.ts", + "default": "./dist/env/locale.js" + }, + "./env/node-auth-token": { + "types": "./dist/env/node-auth-token.d.ts", + "default": "./dist/env/node-auth-token.js" + }, + "./env/node-env": { + "types": "./dist/env/node-env.d.ts", + "default": "./dist/env/node-env.js" + }, + "./env/npm": { + "types": "./dist/env/npm.d.ts", + "default": "./dist/env/npm.js" + }, + "./env/package-manager": { + "types": "./dist/env/package-manager.d.ts", + "default": "./dist/env/package-manager.js" + }, + "./env/path": { + "types": "./dist/env/path.d.ts", + "default": "./dist/env/path.js" + }, + "./env/pre-commit": { + "types": "./dist/env/pre-commit.d.ts", + "default": "./dist/env/pre-commit.js" + }, + "./env/rewire": { + "types": "./dist/env/rewire.d.ts", + "default": "./dist/env/rewire.js" + }, + "./env/shell": { + "types": "./dist/env/shell.d.ts", + "default": "./dist/env/shell.js" + }, + "./env/socket": { + "types": "./dist/env/socket.d.ts", + "default": "./dist/env/socket.js" + }, + "./env/socket-cli": { + "types": "./dist/env/socket-cli.d.ts", + "default": "./dist/env/socket-cli.js" + }, + "./env/socket-cli-shadow": { + "types": "./dist/env/socket-cli-shadow.d.ts", + "default": "./dist/env/socket-cli-shadow.js" + }, + "./env/temp-dir": { + "types": "./dist/env/temp-dir.d.ts", + "default": "./dist/env/temp-dir.js" + }, + "./env/term": { + "types": "./dist/env/term.d.ts", + "default": "./dist/env/term.js" + }, + "./env/test": { + "types": "./dist/env/test.d.ts", + "default": "./dist/env/test.js" + }, + "./env/windows": { + "types": "./dist/env/windows.d.ts", + "default": "./dist/env/windows.js" + }, + "./env/xdg": { + "types": "./dist/env/xdg.d.ts", + "default": "./dist/env/xdg.js" + }, + "./fs": { + "types": "./dist/fs.d.ts", + "default": "./dist/fs.js" + }, + "./functions": { + "types": "./dist/functions.d.ts", + "default": "./dist/functions.js" + }, + "./git": { + "types": "./dist/git.d.ts", + "default": "./dist/git.js" + }, + "./github": { + "types": "./dist/github.d.ts", + "default": "./dist/github.js" + }, + "./globs": { + "types": "./dist/globs.d.ts", + "default": "./dist/globs.js" + }, + "./http-request": { + "types": "./dist/http-request.d.ts", + "default": "./dist/http-request.js" + }, + "./ipc": { + "types": "./dist/ipc.d.ts", + "default": "./dist/ipc.js" + }, + "./json": { + "types": "./dist/json.d.ts", + "default": "./dist/json.js" + }, + "./lifecycle-script-names": { + "types": "./dist/lifecycle-script-names.d.ts", + "default": "./dist/lifecycle-script-names.js" + }, + "./links": { + "types": "./dist/links/index.d.ts", + "default": "./dist/links/index.js" + }, + "./links/index": { + "types": "./dist/links/index.d.ts", + "default": "./dist/links/index.js" + }, + "./logger": { + "types": "./dist/logger.d.ts", + "default": "./dist/logger.js" + }, + "./maintained-node-versions": { + "types": "./dist/maintained-node-versions.d.ts", + "default": "./dist/maintained-node-versions.js" + }, + "./memoization": { + "types": "./dist/memoization.d.ts", + "default": "./dist/memoization.js" + }, + "./objects": { + "types": "./dist/objects.d.ts", + "default": "./dist/objects.js" + }, + "./package-default-node-range": { + "types": "./dist/package-default-node-range.d.ts", + "default": "./dist/package-default-node-range.js" + }, + "./package-default-socket-categories": { + "types": "./dist/package-default-socket-categories.d.ts", + "default": "./dist/package-default-socket-categories.js" + }, + "./package-extensions": { + "types": "./dist/package-extensions.d.ts", + "default": "./dist/package-extensions.js" + }, + "./packages": { + "types": "./dist/packages.d.ts", + "default": "./dist/packages.js" + }, + "./packages/editable": { + "types": "./dist/packages/editable.d.ts", + "default": "./dist/packages/editable.js" + }, + "./packages/exports": { + "types": "./dist/packages/exports.d.ts", + "default": "./dist/packages/exports.js" + }, + "./packages/isolation": { + "types": "./dist/packages/isolation.d.ts", + "default": "./dist/packages/isolation.js" + }, + "./packages/licenses": { + "types": "./dist/packages/licenses.d.ts", + "default": "./dist/packages/licenses.js" + }, + "./packages/manifest": { + "types": "./dist/packages/manifest.d.ts", + "default": "./dist/packages/manifest.js" + }, + "./packages/normalize": { + "types": "./dist/packages/normalize.d.ts", + "default": "./dist/packages/normalize.js" + }, + "./packages/operations": { + "types": "./dist/packages/operations.d.ts", + "default": "./dist/packages/operations.js" + }, + "./packages/paths": { + "types": "./dist/packages/paths.d.ts", + "default": "./dist/packages/paths.js" + }, + "./packages/provenance": { + "types": "./dist/packages/provenance.d.ts", + "default": "./dist/packages/provenance.js" + }, + "./packages/specs": { + "types": "./dist/packages/specs.d.ts", + "default": "./dist/packages/specs.js" + }, + "./packages/validation": { + "types": "./dist/packages/validation.d.ts", + "default": "./dist/packages/validation.js" + }, + "./path": { + "types": "./dist/path.d.ts", + "default": "./dist/path.js" + }, + "./paths": { + "types": "./dist/paths.d.ts", + "default": "./dist/paths.js" + }, + "./paths/rewire": { + "types": "./dist/paths/rewire.d.ts", + "default": "./dist/paths/rewire.js" + }, + "./performance": { + "types": "./dist/performance.d.ts", + "default": "./dist/performance.js" + }, + "./plugins/babel-plugin-inline-require-calls": { + "types": "./plugins/babel-plugin-inline-require-calls.d.ts", + "default": "./plugins/babel-plugin-inline-require-calls.js" + }, + "./process-lock": { + "types": "./dist/process-lock.d.ts", + "default": "./dist/process-lock.js" + }, + "./promise-queue": { + "types": "./dist/promise-queue.d.ts", + "default": "./dist/promise-queue.js" + }, + "./promises": { + "types": "./dist/promises.d.ts", + "default": "./dist/promises.js" + }, + "./regexps": { + "types": "./dist/regexps.d.ts", + "default": "./dist/regexps.js" + }, + "./sea": { + "types": "./dist/sea.d.ts", + "default": "./dist/sea.js" + }, + "./shadow": { + "types": "./dist/shadow.d.ts", + "default": "./dist/shadow.js" + }, + "./signal-exit": { + "types": "./dist/signal-exit.d.ts", + "default": "./dist/signal-exit.js" + }, + "./sorts": { + "types": "./dist/sorts.d.ts", + "default": "./dist/sorts.js" + }, + "./spawn": { + "types": "./dist/spawn.d.ts", + "default": "./dist/spawn.js" + }, + "./spinner": { + "types": "./dist/spinner.d.ts", + "default": "./dist/spinner.js" + }, + "./ssri": { + "types": "./dist/ssri.d.ts", + "default": "./dist/ssri.js" + }, + "./stdio/clear": { + "types": "./dist/stdio/clear.d.ts", + "default": "./dist/stdio/clear.js" + }, + "./stdio/divider": { + "types": "./dist/stdio/divider.d.ts", + "default": "./dist/stdio/divider.js" + }, + "./stdio/footer": { + "types": "./dist/stdio/footer.d.ts", + "default": "./dist/stdio/footer.js" + }, + "./stdio/header": { + "types": "./dist/stdio/header.d.ts", + "default": "./dist/stdio/header.js" + }, + "./stdio/mask": { + "types": "./dist/stdio/mask.d.ts", + "default": "./dist/stdio/mask.js" + }, + "./stdio/progress": { + "types": "./dist/stdio/progress.d.ts", + "default": "./dist/stdio/progress.js" + }, + "./stdio/prompts": { + "types": "./dist/stdio/prompts.d.ts", + "default": "./dist/stdio/prompts.js" + }, + "./stdio/stderr": { + "types": "./dist/stdio/stderr.d.ts", + "default": "./dist/stdio/stderr.js" + }, + "./stdio/stdout": { + "types": "./dist/stdio/stdout.d.ts", + "default": "./dist/stdio/stdout.js" + }, + "./streams": { + "types": "./dist/streams.d.ts", + "default": "./dist/streams.js" + }, + "./strings": { + "types": "./dist/strings.d.ts", + "default": "./dist/strings.js" + }, + "./suppress-warnings": { + "types": "./dist/suppress-warnings.d.ts", + "default": "./dist/suppress-warnings.js" + }, + "./tables": { + "types": "./dist/tables.d.ts", + "default": "./dist/tables.js" + }, + "./temporary-executor": { + "types": "./dist/temporary-executor.d.ts", + "default": "./dist/temporary-executor.js" + }, + "./themes": { + "types": "./dist/themes/index.d.ts", + "default": "./dist/themes/index.js" + }, + "./themes/context": { + "types": "./dist/themes/context.d.ts", + "default": "./dist/themes/context.js" + }, + "./themes/index": { + "types": "./dist/themes/index.d.ts", + "default": "./dist/themes/index.js" + }, + "./themes/themes": { + "types": "./dist/themes/themes.d.ts", + "default": "./dist/themes/themes.js" + }, + "./themes/types": { + "types": "./dist/themes/types.d.ts", + "default": "./dist/themes/types.js" + }, + "./themes/utils": { + "types": "./dist/themes/utils.d.ts", + "default": "./dist/themes/utils.js" + }, + "./types": { + "types": "./dist/types.d.ts", + "default": "./dist/types.js" + }, + "./url": { + "types": "./dist/url.d.ts", + "default": "./dist/url.js" + }, + "./utils/get-ipc": { + "types": "./dist/utils/get-ipc.d.ts", + "default": "./dist/utils/get-ipc.js" + }, + "./validation/json-parser": { + "types": "./dist/validation/json-parser.d.ts", + "default": "./dist/validation/json-parser.js" + }, + "./validation/types": { + "types": "./dist/validation/types.d.ts", + "default": "./dist/validation/types.js" + }, + "./versions": { + "types": "./dist/versions.d.ts", + "default": "./dist/versions.js" + }, + "./words": { + "types": "./dist/words.d.ts", + "default": "./dist/words.js" + }, + "./zod": { + "types": "./dist/zod.d.ts", + "default": "./dist/zod.js" + }, + "./biome.json": "./biome.json", + "./data/extensions.json": "./data/extensions.json", + "./package.json": "./package.json", + "./tsconfig.dts.json": "./tsconfig.dts.json", + "./tsconfig.json": "./tsconfig.json", + "./tsconfig.test.json": "./tsconfig.test.json" + }, + "imports": { + "#constants/*": "./dist/constants/*.js", + "#env/*": "./dist/env/*.js", + "#lib/*": "./dist/*.js", + "#packages/*": "./dist/packages/*.js", + "#socketsecurity/lib/*": "@socketsecurity/lib-stable/*", + "#types": "./dist/types.js", + "#utils/*": "./dist/utils/*.js" + }, + "files": [ + "dist", + "data/extensions.json", + "CHANGELOG.md" + ], + "engines": { + "node": ">=22" + }, + "sideEffects": false, + "scripts": { + "build": "node scripts/build.mjs", + "build:watch": "node scripts/build-js.mjs --watch", + "check": "node scripts/check.mjs", + "claude": "node scripts/claude.mjs", + "clean": "node scripts/clean.mjs", + "cover": "node scripts/cover.mjs", + "dev": "pnpm run build:watch", + "fix": "node scripts/lint.mjs --fix", + "fix:build": "node scripts/fix-build.mjs", + "lint": "node scripts/lint.mjs", + "test": "node scripts/test.mjs", + "update": "node scripts/update.mjs" + }, + "dependencies": { + "@babel/core": "catalog:", + "@babel/parser": "catalog:", + "@babel/traverse": "catalog:", + "@babel/types": "catalog:", + "@biomejs/biome": "catalog:", + "@eslint/compat": "catalog:", + "@eslint/js": "catalog:", + "@inquirer/confirm": "catalog:", + "@inquirer/input": "catalog:", + "@inquirer/password": "catalog:", + "@inquirer/search": "catalog:", + "@inquirer/select": "catalog:", + "@npmcli/package-json": "catalog:", + "@npmcli/promise-spawn": "catalog:", + "@socketregistry/is-unicode-supported": "catalog:", + "@socketregistry/packageurl-js": "catalog:", + "@socketregistry/yocto-spinner": "catalog:", + "@socketsecurity/lib-stable": "https://registry.npmjs.org/@socketsecurity/lib/-/lib-3.2.4.tgz", + "@types/node": "catalog:", + "@typescript/native-preview": "catalog:", + "@vitest/coverage-v8": "catalog:", + "@vitest/ui": "catalog:", + "@yarnpkg/extensions": "catalog:", + "cacache": "catalog:", + "debug": "catalog:", + "del": "catalog:", + "del-cli": "catalog:", + "esbuild": "catalog:", + "eslint": "catalog:", + "eslint-import-resolver-typescript": "catalog:", + "eslint-plugin-import-x": "catalog:", + "eslint-plugin-n": "catalog:", + "eslint-plugin-sort-destructure-keys": "catalog:", + "eslint-plugin-unicorn": "catalog:", + "fast-glob": "catalog:", + "fast-sort": "catalog:", + "get-east-asian-width": "catalog:", + "globals": "catalog:", + "husky": "catalog:", + "libnpmexec": "catalog:", + "libnpmpack": "catalog:", + "lint-staged": "catalog:", + "magic-string": "catalog:", + "make-fetch-happen": "catalog:", + "normalize-package-data": "catalog:", + "npm-package-arg": "catalog:", + "pacote": "catalog:", + "picomatch": "catalog:", + "semver": "catalog:", + "spdx-correct": "catalog:", + "spdx-expression-parse": "catalog:", + "streaming-iterables": "catalog:", + "taze": "catalog:", + "trash": "catalog:", + "type-coverage": "catalog:", + "typescript": "catalog:", + "typescript-eslint": "catalog:", + "validate-npm-package-name": "catalog:", + "vite-tsconfig-paths": "catalog:", + "vitest": "catalog:", + "which": "catalog:", + "yargs-parser": "catalog:", + "yoctocolors-cjs": "catalog:", + "zod": "catalog:" + }, + "peerDependencies": { + "typescript": "catalog:" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + }, + "browser": { + "_http_agent": false, + "_http_client": false, + "_http_common": false, + "_http_incoming": false, + "_http_outgoing": false, + "_http_server": false, + "_stream_duplex": false, + "_stream_passthrough": false, + "_stream_readable": false, + "_stream_transform": false, + "_stream_wrap": false, + "_stream_writable": false, + "_tls_common": false, + "_tls_wrap": false, + "assert": false, + "assert/strict": false, + "async_hooks": false, + "buffer": false, + "child_process": false, + "cluster": false, + "console": false, + "constants": false, + "crypto": false, + "dgram": false, + "diagnostics_channel": false, + "dns": false, + "dns/promises": false, + "domain": false, + "events": false, + "fs": false, + "fs/promises": false, + "http": false, + "http2": false, + "https": false, + "inspector": false, + "inspector/promises": false, + "module": false, + "net": false, + "os": false, + "path": false, + "path/posix": false, + "path/win32": false, + "perf_hooks": false, + "process": false, + "punycode": false, + "querystring": false, + "readline": false, + "readline/promises": false, + "repl": false, + "stream": false, + "stream/consumers": false, + "stream/promises": false, + "stream/web": false, + "string_decoder": false, + "sys": false, + "timers": false, + "timers/promises": false, + "tls": false, + "trace_events": false, + "tty": false, + "url": false, + "util": false, + "util/types": false, + "v8": false, + "vm": false, + "wasi": false, + "worker_threads": false, + "zlib": false + } +} diff --git a/packages/lib/plugins/README.md b/packages/lib/plugins/README.md new file mode 100644 index 000000000..e1b91fd2a --- /dev/null +++ b/packages/lib/plugins/README.md @@ -0,0 +1,80 @@ +# Babel Plugins + +Babel transformation plugins exported by `@socketsecurity/lib`. + +## `babel-plugin-inline-require-calls` + +**Replaces `/*@__INLINE__*/` marked require() calls with literal values.** + +```javascript +// Before +const CHAR_FORWARD_SLASH = /*@__INLINE__*/ require('./constants/CHAR_FORWARD_SLASH') + +// After +const CHAR_FORWARD_SLASH = 47 /* was: require('./constants/CHAR_FORWARD_SLASH') */ +``` + +Supports: string, number, boolean, null, undefined + +## `babel-plugin-strip-debug` + +**Removes DEBUG code blocks.** + +```javascript +// Before +if (DEBUG) { console.log('debug') } +DEBUG && expression +const x = DEBUG ? dev : prod + +// After +// Removed +// Removed +const x = prod +``` + +Patterns: `if (DEBUG)`, `DEBUG &&`, `DEBUG ? a : b` + +## `babel-plugin-inline-process-env` + +**Replaces process.env values with literals for dead code elimination.** + +```javascript +// Before +if (process.env.NODE_ENV === 'production') { prodCode() } else { devCode() } + +// After (with env: { NODE_ENV: 'production' }) +if ('production' === 'production') { prodCode() } else { devCode() } + +// After Rollup tree-shaking +prodCode() +``` + +Coerces types: 'true' → boolean, '42' → number + +## `babel-plugin-inline-const-enum` + +**Inlines TypeScript enum member access.** + +```javascript +// Before +if (response.status === HttpStatus.OK) { return StatusCode.Success } + +// After +if (response.status === 200) { return 0 } +``` + +Note: Use TypeScript's `const enum` instead when possible. + +## Configuration + +```javascript +// .config/babel.config.js +const path = require('node:path') +const pluginsPath = path.join(__dirname, '..', 'plugins') + +module.exports = { + plugins: [ + path.join(pluginsPath, 'babel-plugin-inline-require-calls.js'), + ] +} +``` diff --git a/packages/lib/plugins/babel-plugin-inline-const-enum.mjs b/packages/lib/plugins/babel-plugin-inline-const-enum.mjs new file mode 100644 index 000000000..d0c813fa8 --- /dev/null +++ b/packages/lib/plugins/babel-plugin-inline-const-enum.mjs @@ -0,0 +1,153 @@ +/** + * @fileoverview Babel plugin to inline TypeScript const enum member access. + * Replaces enum member access with literal values when enum definition is available. + * + * Note: TypeScript normally handles this during compilation. This plugin is useful + * for post-processing compiled code or handling external modules. + */ + +/** + * Babel plugin to inline const enum values. + * + * Transforms: + * MyEnum.Value → 42 (if MyEnum.Value = 42 in the enum definition) + * + * Usage: + * Pass enum definitions via options.enums: + * { + * enums: { + * MyEnum: { Value1: 0, Value2: 1 } + * } + * } + * + * Or let the plugin scan the code for enum declarations (limited support). + * + * @param {object} babel - Babel API object + * @param {object} options - Plugin options + * @param {Record>} [options.enums] - Enum definitions + * @param {boolean} [options.scanDeclarations=false] - Auto-detect enum declarations + * @returns {object} Babel plugin object + */ +export default function inlineConstEnum(babel, options = {}) { + const { types: t } = babel + const { enums = {}, scanDeclarations = false } = options + + // Map of enum name to member values. + const enumMap = new Map(Object.entries(enums)) + + return { + name: 'inline-const-enum', + + visitor: { + // Scan for enum declarations if enabled. + // Note: This has limited support and may not catch all cases. + VariableDeclaration(path) { + if (!scanDeclarations) { + return + } + + // Look for: const MyEnum = { Value: 0, ... } + const { declarations } = path.node + + for (const decl of declarations) { + if ( + !t.isVariableDeclarator(decl) || + !t.isIdentifier(decl.id) || + !t.isObjectExpression(decl.init) + ) { + continue + } + + const enumName = decl.id.name + const enumValues = {} + + // Extract property values. + for (const prop of decl.init.properties) { + if ( + !t.isObjectProperty(prop) || + !t.isIdentifier(prop.key) || + !isLiteralValue(t, prop.value) + ) { + continue + } + + enumValues[prop.key.name] = getLiteralValue(t, prop.value) + } + + if (Object.keys(enumValues).length > 0) { + enumMap.set(enumName, enumValues) + } + } + }, + + // Inline enum member access: MyEnum.Value + MemberExpression(path) { + const { object, property } = path.node + + // Match: EnumName.MemberName + if (!t.isIdentifier(object) || !t.isIdentifier(property)) { + return + } + + const enumName = object.name + const memberName = property.name + + // Check if we have this enum. + const enumDef = enumMap.get(enumName) + if (!enumDef || !(memberName in enumDef)) { + return + } + + const value = enumDef[memberName] + + // Replace with literal value. + const replacement = valueToLiteral(t, value) + path.replaceWith(replacement) + }, + }, + } +} + +/** + * Check if a node is a literal value. + */ +function isLiteralValue(t, node) { + return ( + t.isNumericLiteral(node) || + t.isStringLiteral(node) || + t.isBooleanLiteral(node) || + t.isNullLiteral(node) + ) +} + +/** + * Get the value from a literal node. + */ +function getLiteralValue(t, node) { + if (t.isNullLiteral(node)) { + return null + } + return node.value +} + +/** + * Convert a value to a Babel AST literal node. + */ +function valueToLiteral(t, value) { + if (value === null) { + return t.nullLiteral() + } + if (value === undefined) { + return t.identifier('undefined') + } + if (typeof value === 'string') { + return t.stringLiteral(value) + } + if (typeof value === 'number') { + return t.numericLiteral(value) + } + if (typeof value === 'boolean') { + return t.booleanLiteral(value) + } + throw new Error(`Unsupported enum value type: ${typeof value}`) +} diff --git a/packages/lib/plugins/babel-plugin-inline-process-env.mjs b/packages/lib/plugins/babel-plugin-inline-process-env.mjs new file mode 100644 index 000000000..d3c38a04f --- /dev/null +++ b/packages/lib/plugins/babel-plugin-inline-process-env.mjs @@ -0,0 +1,110 @@ +/** + * @fileoverview Babel plugin to inline process.env values. + * Replaces process.env.X with literal values, enabling dead code elimination. + * + * After this plugin runs, Rollup's tree-shaking can eliminate unreachable branches: + * if (process.env.NODE_ENV === 'production') { prodCode() } + * else { devCode() } + * + * Becomes: + * if ('production' === 'production') { prodCode() } + * else { devCode() } + * + * Then Rollup removes the dead else branch, leaving just: prodCode() + */ + +/** + * Babel plugin to inline process.env. + * + * Replaces process.env.VAR_NAME with the actual value from process.env. + * Use options.env to provide custom environment values. + * + * @param {object} babel - Babel API object + * @param {object} options - Plugin options + * @param {Record} [options.env] - Environment variables to inline + * @param {string[]} [options.include] - Only inline these env vars (whitelist) + * @param {string[]} [options.exclude] - Never inline these env vars (blacklist) + * @returns {object} Babel plugin object + * + * @example + * // With options: { env: { NODE_ENV: 'production' } } + * process.env.NODE_ENV // → 'production' + * process.env.DEBUG // → unchanged (not in env) + */ +export default function inlineProcessEnv(babel, options = {}) { + const { types: t } = babel + const { env = process.env, exclude = [], include = [] } = options + + const excludeSet = new Set(exclude) + const includeSet = new Set(include) + + return { + name: 'inline-process-env', + + visitor: { + MemberExpression(path) { + const { object, property } = path.node + + // Match: process.env.VAR_NAME + if ( + !t.isMemberExpression(object) || + !t.isIdentifier(object.object, { name: 'process' }) || + !t.isIdentifier(object.property, { name: 'env' }) || + !t.isIdentifier(property) + ) { + return + } + + const envKey = property.name + + // Check whitelist/blacklist. + if (includeSet.size > 0 && !includeSet.has(envKey)) { + return + } + if (excludeSet.has(envKey)) { + return + } + + // Get the value from env. + const value = env[envKey] + + // Only inline if value exists. + if (value === undefined) { + return + } + + // Replace with literal value. + const replacement = valueToLiteral(t, value) + path.replaceWith(replacement) + }, + }, + } +} + +/** + * Convert a value to a Babel AST literal node. + */ +function valueToLiteral(t, value) { + // Handle common types. + if (value === null) { + return t.nullLiteral() + } + if (value === undefined) { + return t.identifier('undefined') + } + if (value === 'true') { + return t.booleanLiteral(true) + } + if (value === 'false') { + return t.booleanLiteral(false) + } + + // Check if it's a number. + const num = Number(value) + if (!Number.isNaN(num) && String(num) === value) { + return t.numericLiteral(num) + } + + // Default to string. + return t.stringLiteral(value) +} diff --git a/packages/lib/plugins/babel-plugin-inline-require-calls.js b/packages/lib/plugins/babel-plugin-inline-require-calls.js new file mode 100644 index 000000000..5001a7cbf --- /dev/null +++ b/packages/lib/plugins/babel-plugin-inline-require-calls.js @@ -0,0 +1,256 @@ +const { createRequire } = require('node:module') +const fs = require('node:fs') +const path = require('node:path') + +/** + * Babel plugin to inline require calls. + * + * @param {object} babel - Babel API object + * @returns {object} Babel plugin object + */ +module.exports = function inlineRequireCalls(babel) { + const { types: t } = babel + + return { + name: 'inline-require-calls', + + visitor: { + CallExpression(nodePath, state) { + const { node } = nodePath + + // Check if this is a require() call. + if ( + !t.isIdentifier(node.callee, { name: 'require' }) || + node.arguments.length !== 1 + ) { + return + } + + // Check if the first argument is a string literal. + const arg = node.arguments[0] + if (!t.isStringLiteral(arg)) { + return + } + + // Check for /*@__INLINE__*/ comment in leading comments. + const leadingComments = node.leadingComments || [] + const hasInlineDirective = leadingComments.some(comment => { + return ( + comment.type === 'CommentBlock' && + comment.value.trim() === '@__INLINE__' + ) + }) + + if (!hasInlineDirective) { + return + } + + // Resolve the require path relative to the current file. + const currentFilePath = state.filename || state.file.opts.filename + if (!currentFilePath) { + throw nodePath.buildCodeFrameError( + 'Cannot inline require: unable to determine current file path', + ) + } + + const requirePath = arg.value + const currentDir = path.dirname(currentFilePath) + let absolutePath + + try { + // Resolve the path relative to the current file. + // Try both with and without .ts extension for TypeScript files. + absolutePath = path.resolve(currentDir, requirePath) + const possiblePaths = [ + absolutePath, + `${absolutePath}.ts`, + `${absolutePath}.js`, + `${absolutePath}/index.ts`, + `${absolutePath}/index.js`, + ] + + // Find the first path that exists. + let resolvedPath = absolutePath + for (const testPath of possiblePaths) { + try { + if (fs.existsSync(testPath)) { + resolvedPath = testPath + break + } + } catch { + // Ignore errors, continue checking. + } + } + + // Check if the source exports a primitive value that is safe to inline. + const source = fs.readFileSync(resolvedPath, 'utf8') + if (!isInlinablePrimitive(source, babel)) { + throw new Error( + 'Cannot inline require: module must export a literal primitive value (string, number, boolean, null)', + ) + } + + // Create a require function relative to the current file. + const requireFunc = createRequire(currentFilePath) + + // Load the module. + const module = requireFunc(resolvedPath) + + // Get the default export (supports both ESM default and CJS module.exports). + const value = module.default ?? module + + // Verify the value is serializable (primitive or simple object). + if (!isSerializable(value)) { + throw new Error( + 'Cannot inline require: value is not serializable (got ' + + typeof value + + ')', + ) + } + + // Replace the require call with the actual value. + const replacement = valueToASTNode(t, value) + + // Add a comment to indicate what was inlined. + t.addComment( + replacement, + 'trailing', + ` was: require('${requirePath}') `, + false, + ) + + nodePath.replaceWith(replacement) + } catch (e) { + throw nodePath.buildCodeFrameError( + `Cannot inline require('${requirePath}'): ${e.message}`, + ) + } + }, + }, + } +} + +/** + * Check if source code exports a primitive literal value that is safe to inline. + * + * @param {string} source - Source code to check + * @param {object} babel - Babel API object + * @returns {boolean} True if the module exports only a primitive literal + */ +function isInlinablePrimitive(source, babel) { + const { parseSync } = require('@babel/core') + const { types: t } = babel + + try { + // Parse the source code. + const ast = parseSync(source, { + filename: 'inline-check.ts', + presets: ['@babel/preset-typescript'], + sourceType: 'module', + }) + + if (!ast || !ast.program || !ast.program.body) { + return false + } + + // Find export default declaration. + const exportDefault = ast.program.body.find(node => + t.isExportDefaultDeclaration(node), + ) + + if (!exportDefault) { + return false + } + + const declaration = exportDefault.declaration + + // Check if it's a literal primitive value or constant expression. + // Allow: string, number, boolean, null, undefined, and constant math expressions + // Disallow: runtime-dependent expressions, identifiers, function calls, etc. + return isPrimitiveOrConstantExpression(declaration, t) + } catch { + // If parsing fails, don't inline. + return false + } +} + +/** + * Check if a node is a primitive literal or a constant expression. + * + * @param {object} node - Babel AST node + * @param {object} t - Babel types + * @returns {boolean} True if safe to inline + */ +function isPrimitiveOrConstantExpression(node, t) { + // Literal primitives. + if ( + t.isStringLiteral(node) || + t.isNumericLiteral(node) || + t.isBooleanLiteral(node) || + t.isNullLiteral(node) || + t.isIdentifier(node, { name: 'undefined' }) + ) { + return true + } + + // Unary expressions: -5, +5, !true, ~0x1 + if (t.isUnaryExpression(node)) { + return isPrimitiveOrConstantExpression(node.argument, t) + } + + // Binary expressions: 7 * 24 * 60 * 60 * 1000 + if (t.isBinaryExpression(node) || t.isLogicalExpression(node)) { + return ( + isPrimitiveOrConstantExpression(node.left, t) && + isPrimitiveOrConstantExpression(node.right, t) + ) + } + + // Template literals with no expressions: `hello` + if (t.isTemplateLiteral(node)) { + return ( + !node.expressions || + node.expressions.length === 0 || + node.expressions.every(expr => isPrimitiveOrConstantExpression(expr, t)) + ) + } + + // Disallow: member expressions (process.platform), call expressions, etc. + return false +} + +/** + * Check if a value can be serialized to an AST node. + */ +function isSerializable(value) { + const type = typeof value + return ( + value === null || + value === undefined || + type === 'string' || + type === 'number' || + type === 'boolean' + ) +} + +/** + * Convert a JavaScript value to a Babel AST node. + */ +function valueToASTNode(t, value) { + if (value === null) { + return t.nullLiteral() + } + if (value === undefined) { + return t.identifier('undefined') + } + if (typeof value === 'string') { + return t.stringLiteral(value) + } + if (typeof value === 'number') { + return t.numericLiteral(value) + } + if (typeof value === 'boolean') { + return t.booleanLiteral(value) + } + throw new Error(`Unsupported value type: ${typeof value}`) +} diff --git a/packages/lib/plugins/babel-plugin-strip-debug.mjs b/packages/lib/plugins/babel-plugin-strip-debug.mjs new file mode 100644 index 000000000..67b03d2de --- /dev/null +++ b/packages/lib/plugins/babel-plugin-strip-debug.mjs @@ -0,0 +1,108 @@ +/** + * @fileoverview Babel plugin to strip debug code blocks. + * Removes code wrapped in DEBUG checks: if (DEBUG) { ... } + */ + +/** + * Babel plugin to strip debug code. + * + * Removes: + * - if (DEBUG) { ... } + * - if (DEBUG && condition) { ... } + * - DEBUG && expression + * - DEBUG ? trueExpr : falseExpr (keeps falseExpr) + * + * Usage in code: + * if (DEBUG) { + * console.log('debug info') + * } + * // In production build: entire block removed + * + * @param {object} babel - Babel API object + * @param {object} options - Plugin options + * @param {string[]} [options.identifiers=['DEBUG']] - Debug identifiers to strip + * @returns {object} Babel plugin object + */ +export default function stripDebug(babel, options = {}) { + const { types: t } = babel + const { identifiers = ['DEBUG'] } = options + const debugIds = new Set(identifiers) + + return { + name: 'strip-debug', + + visitor: { + // Remove: if (DEBUG) { ... } + IfStatement(path) { + const { test } = path.node + + // Check if test is DEBUG identifier or logical expression containing DEBUG. + if (isDebugTest(t, test, debugIds)) { + path.remove() + return + } + + // Handle: if (DEBUG && condition) { ... } + if ( + t.isLogicalExpression(test, { operator: '&&' }) && + isDebugIdentifier(t, test.left, debugIds) + ) { + path.remove() + return + } + }, + + // Remove: DEBUG && expression + LogicalExpression(path) { + const { left, operator } = path.node + + if (operator === '&&' && isDebugIdentifier(t, left, debugIds)) { + // Remove entire expression. + if (path.parentPath.isExpressionStatement()) { + path.parentPath.remove() + } else { + // Replace with undefined in other contexts. + path.replaceWith(t.identifier('undefined')) + } + } + }, + + // Handle: DEBUG ? trueExpr : falseExpr → falseExpr + ConditionalExpression(path) { + const { alternate, test } = path.node + + if (isDebugIdentifier(t, test, debugIds)) { + // Replace with alternate (false branch). + path.replaceWith(alternate) + } + }, + }, + } +} + +/** + * Check if a node is a DEBUG identifier. + */ +function isDebugIdentifier(t, node, debugIds) { + return t.isIdentifier(node) && debugIds.has(node.name) +} + +/** + * Check if test expression is a debug check. + */ +function isDebugTest(t, test, debugIds) { + // Simple: if (DEBUG) + if (isDebugIdentifier(t, test, debugIds)) { + return true + } + + // Logical: if (DEBUG && x) or if (x && DEBUG) + if (t.isLogicalExpression(test, { operator: '&&' })) { + return ( + isDebugIdentifier(t, test.left, debugIds) || + isDebugIdentifier(t, test.right, debugIds) + ) + } + + return false +} diff --git a/packages/lib/pnpm-lock.yaml b/packages/lib/pnpm-lock.yaml new file mode 100644 index 000000000..4c779af89 --- /dev/null +++ b/packages/lib/pnpm-lock.yaml @@ -0,0 +1,6078 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@babel/core': + specifier: 7.28.4 + version: 7.28.4 + '@babel/parser': + specifier: 7.28.4 + version: 7.28.4 + '@babel/traverse': + specifier: 7.28.4 + version: 7.28.4 + '@babel/types': + specifier: 7.28.4 + version: 7.28.4 + '@biomejs/biome': + specifier: 2.2.4 + version: 2.2.4 + '@eslint/compat': + specifier: 1.4.0 + version: 1.4.0(eslint@9.35.0(jiti@2.6.1)) + '@eslint/js': + specifier: 9.38.0 + version: 9.38.0 + '@inquirer/confirm': + specifier: 5.1.16 + version: 5.1.16(@types/node@24.9.2) + '@inquirer/input': + specifier: 4.2.2 + version: 4.2.2(@types/node@24.9.2) + '@inquirer/password': + specifier: 4.0.18 + version: 4.0.18(@types/node@24.9.2) + '@inquirer/search': + specifier: 3.1.1 + version: 3.1.1(@types/node@24.9.2) + '@inquirer/select': + specifier: 4.3.2 + version: 4.3.2(@types/node@24.9.2) + '@npmcli/package-json': + specifier: 7.0.0 + version: 7.0.0 + '@npmcli/promise-spawn': + specifier: 8.0.3 + version: 8.0.3 + '@socketregistry/is-unicode-supported': + specifier: 1.0.5 + version: 1.0.5 + '@socketregistry/packageurl-js': + specifier: 1.3.5 + version: 1.3.5 + '@socketregistry/yocto-spinner': + specifier: 1.0.25 + version: 1.0.25 + '@socketsecurity/lib-stable': + specifier: https://registry.npmjs.org/@socketsecurity/lib/-/lib-3.2.4.tgz + version: '@socketsecurity/lib@3.2.4(typescript@5.9.2)' + '@types/node': + specifier: 24.9.2 + version: 24.9.2 + '@typescript/native-preview': + specifier: 7.0.0-dev.20250920.1 + version: 7.0.0-dev.20250920.1 + '@vitest/coverage-v8': + specifier: 4.0.3 + version: 4.0.3(vitest@4.0.3) + '@vitest/ui': + specifier: 4.0.3 + version: 4.0.3(vitest@4.0.3) + '@yarnpkg/extensions': + specifier: 2.0.6 + version: 2.0.6(@yarnpkg/core@4.4.4(typanion@3.14.0)) + cacache: + specifier: 20.0.1 + version: 20.0.1 + debug: + specifier: 4.4.3 + version: 4.4.3 + del: + specifier: 8.0.1 + version: 8.0.1 + del-cli: + specifier: 6.0.0 + version: 6.0.0 + esbuild: + specifier: 0.25.11 + version: 0.25.11 + eslint: + specifier: 9.35.0 + version: 9.35.0(jiti@2.6.1) + eslint-import-resolver-typescript: + specifier: 4.4.4 + version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.44.1(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.6.1)))(eslint@9.35.0(jiti@2.6.1)) + eslint-plugin-import-x: + specifier: 4.16.1 + version: 4.16.1(@typescript-eslint/utils@8.44.1(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.6.1)) + eslint-plugin-n: + specifier: 17.23.1 + version: 17.23.1(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.2) + eslint-plugin-sort-destructure-keys: + specifier: 2.0.0 + version: 2.0.0(eslint@9.35.0(jiti@2.6.1)) + eslint-plugin-unicorn: + specifier: 61.0.2 + version: 61.0.2(eslint@9.35.0(jiti@2.6.1)) + fast-glob: + specifier: 3.3.3 + version: 3.3.3 + fast-sort: + specifier: 3.4.1 + version: 3.4.1 + get-east-asian-width: + specifier: 1.3.0 + version: 1.3.0 + globals: + specifier: 16.4.0 + version: 16.4.0 + husky: + specifier: 9.1.7 + version: 9.1.7 + libnpmexec: + specifier: ^10.1.8 + version: 10.1.8 + libnpmpack: + specifier: 9.0.9 + version: 9.0.9 + lint-staged: + specifier: 15.2.11 + version: 15.2.11 + magic-string: + specifier: 0.30.17 + version: 0.30.17 + make-fetch-happen: + specifier: 15.0.2 + version: 15.0.2 + normalize-package-data: + specifier: 8.0.0 + version: 8.0.0 + npm-package-arg: + specifier: 13.0.0 + version: 13.0.0 + pacote: + specifier: 21.0.1 + version: 21.0.1 + picomatch: + specifier: 2.3.1 + version: 2.3.1 + semver: + specifier: 7.7.2 + version: 7.7.2 + spdx-correct: + specifier: 3.2.0 + version: 3.2.0 + spdx-expression-parse: + specifier: 4.0.0 + version: 4.0.0 + streaming-iterables: + specifier: 8.0.1 + version: 8.0.1 + taze: + specifier: 19.6.0 + version: 19.6.0 + trash: + specifier: 10.0.0 + version: 10.0.0 + type-coverage: + specifier: 2.29.7 + version: 2.29.7(typescript@5.9.2) + typescript: + specifier: 5.9.2 + version: 5.9.2 + typescript-eslint: + specifier: 8.44.1 + version: 8.44.1(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.2) + validate-npm-package-name: + specifier: 6.0.2 + version: 6.0.2 + vite-tsconfig-paths: + specifier: 5.1.4 + version: 5.1.4(typescript@5.9.2)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(yaml@2.8.1)) + vitest: + specifier: 4.0.3 + version: 4.0.3(@types/node@24.9.2)(@vitest/ui@4.0.3)(jiti@2.6.1)(yaml@2.8.1) + which: + specifier: 5.0.0 + version: 5.0.0 + yargs-parser: + specifier: 22.0.0 + version: 22.0.0 + yoctocolors-cjs: + specifier: 2.1.3 + version: 2.1.3 + zod: + specifier: 4.1.12 + version: 4.1.12 + +packages: + + '@antfu/ni@25.0.0': + resolution: {integrity: sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA==} + hasBin: true + + '@arcanis/slice-ansi@1.1.1': + resolution: {integrity: sha512-xguP2WR2Dv0gQ7Ykbdb7BNCnPnIPB94uTi0Z2NvkRBEnhbwjOQ7QyQKJXrVQg4qDpiD9hA5l5cCwy/z2OXgc3w==} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.4': + resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.4': + resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.4': + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@biomejs/biome@2.2.4': + resolution: {integrity: sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.2.4': + resolution: {integrity: sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.2.4': + resolution: {integrity: sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.2.4': + resolution: {integrity: sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@2.2.4': + resolution: {integrity: sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@2.2.4': + resolution: {integrity: sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@2.2.4': + resolution: {integrity: sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@2.2.4': + resolution: {integrity: sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.2.4': + resolution: {integrity: sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@emnapi/core@1.6.0': + resolution: {integrity: sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg==} + + '@emnapi/runtime@1.6.0': + resolution: {integrity: sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@esbuild/aix-ppc64@0.25.11': + resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.11': + resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.11': + resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.11': + resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.11': + resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.11': + resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.11': + resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.11': + resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.11': + resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.11': + resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.11': + resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.11': + resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.11': + resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.11': + resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.11': + resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.11': + resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.11': + resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.11': + resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.11': + resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.11': + resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.11': + resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.11': + resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.11': + resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.11': + resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.11': + resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.11': + resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/compat@1.4.0': + resolution: {integrity: sha512-DEzm5dKeDBPm3r08Ixli/0cmxr8LkRdwxMRUIJBlSCpAwSrvFEJpVBzV+66JhDxiaqKxnRzCXhtiMiczF7Hglg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.40 || 9 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.3.1': + resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.16.0': + resolution: {integrity: sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.35.0': + resolution: {integrity: sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.38.0': + resolution: {integrity: sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@inquirer/ansi@1.0.1': + resolution: {integrity: sha512-yqq0aJW/5XPhi5xOAL1xRCpe1eh8UFVgYFpFsjEqmIR8rKLyP+HINvFXwUaxYICflJrVlxnp7lLN6As735kVpw==} + engines: {node: '>=18'} + + '@inquirer/confirm@5.1.16': + resolution: {integrity: sha512-j1a5VstaK5KQy8Mu8cHmuQvN1Zc62TbLhjJxwHvKPPKEoowSF6h/0UdOpA9DNdWZ+9Inq73+puRq1df6OJ8Sag==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.0': + resolution: {integrity: sha512-Uv2aPPPSK5jeCplQmQ9xadnFx2Zhj9b5Dj7bU6ZeCdDNNY11nhYy4btcSdtDguHqCT2h5oNeQTcUNSGGLA7NTA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.14': + resolution: {integrity: sha512-DbFgdt+9/OZYFM+19dbpXOSeAstPy884FPy1KjDu4anWwymZeOYhMY1mdFri172htv6mvc/uvIAAi7b7tvjJBQ==} + engines: {node: '>=18'} + + '@inquirer/input@4.2.2': + resolution: {integrity: sha512-hqOvBZj/MhQCpHUuD3MVq18SSoDNHy7wEnQ8mtvs71K8OPZVXJinOzcvQna33dNYLYE4LkA9BlhAhK6MJcsVbw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@4.0.18': + resolution: {integrity: sha512-zXvzAGxPQTNk/SbT3carAD4Iqi6A2JS2qtcqQjsL22uvD+JfQzUrDEtPjLL7PLn8zlSNyPdY02IiQjzoL9TStA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@3.1.1': + resolution: {integrity: sha512-TkMUY+A2p2EYVY3GCTItYGvqT6LiLzHBnqsU1rJbrpXUijFfM6zvUx0R4civofVwFCmJZcKqOVwwWAjplKkhxA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@4.3.2': + resolution: {integrity: sha512-nwous24r31M+WyDEHV+qckXkepvihxhnyIaod2MG7eCE6G0Zm/HUF6jgN8GXgf4U7AU6SLseKdanY195cwvU6w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@3.0.9': + resolution: {integrity: sha512-QPaNt/nmE2bLGQa9b7wwyRJoLZ7pN6rcyXvzU0YCmivmJyq1BVo94G98tStRWkoD1RgDX5C+dPlhhHzNdu/W/w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@isaacs/string-locale-compare@1.1.0': + resolution: {integrity: sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@npmcli/agent@3.0.0': + resolution: {integrity: sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@npmcli/agent@4.0.0': + resolution: {integrity: sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/arborist@9.1.6': + resolution: {integrity: sha512-c5Pr3EG8UP5ollkJy2x+UdEQC5sEHe3H9whYn6hb2HJimAKS4zmoJkx5acCiR/g4P38RnCSMlsYQyyHnKYeLvQ==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + '@npmcli/fs@4.0.0': + resolution: {integrity: sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@npmcli/git@6.0.3': + resolution: {integrity: sha512-GUYESQlxZRAdhs3UhbB6pVRNUELQOHXwK9ruDkwmCv2aZ5y0SApQzUJCg02p3A7Ue2J5hxvlk1YI53c00NmRyQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@npmcli/git@7.0.0': + resolution: {integrity: sha512-vnz7BVGtOctJAIHouCJdvWBhsTVSICMeUgZo2c7XAi5d5Rrl80S1H7oPym7K03cRuinK5Q6s2dw36+PgXQTcMA==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/installed-package-contents@3.0.0': + resolution: {integrity: sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + + '@npmcli/map-workspaces@5.0.0': + resolution: {integrity: sha512-+YJN6+BIQEC5QL4EqffJ2I1S9ySspwn7GP7uQINtZhf3uy7P0KnnIg+Ab5WeSUTZYpg+jn3GSfMme2FutB7qEQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/metavuln-calculator@9.0.2': + resolution: {integrity: sha512-eESzlCRLuD30qYefT2jYZTUepgu9DNJQdXABGGxjkir055x2UtnpNfDZCA6OJxButQNgxNKc9AeTchYxSgbMCw==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/name-from-folder@3.0.0': + resolution: {integrity: sha512-61cDL8LUc9y80fXn+lir+iVt8IS0xHqEKwPu/5jCjxQTVoSCmkXvw4vbMrzAMtmghz3/AkiBjhHkDKUH+kf7kA==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@npmcli/node-gyp@4.0.0': + resolution: {integrity: sha512-+t5DZ6mO/QFh78PByMq1fGSAub/agLJZDRfJRMeOSNCt8s9YVlTjmGpIPwPhvXTGUIJk+WszlT0rQa1W33yzNA==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@npmcli/package-json@7.0.0': + resolution: {integrity: sha512-wy5os0g17akBCVScLyDsDFFf4qC/MmUgIGAFw2pmBGJ/yAQfFbTR9gEaofy4HGm9Jf2MQBnKZICfNds2h3WpEg==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/promise-spawn@8.0.3': + resolution: {integrity: sha512-Yb00SWaL4F8w+K8YGhQ55+xE4RUNdMHV43WZGsiTM92gS+lC0mGsn7I4hLug7pbao035S6bj3Y3w0cUNGLfmkg==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@npmcli/query@4.0.1': + resolution: {integrity: sha512-4OIPFb4weUUwkDXJf4Hh1inAn8neBGq3xsH4ZsAaN6FK3ldrFkH7jSpCc7N9xesi0Sp+EBXJ9eGMDrEww2Ztqw==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@npmcli/redact@3.2.2': + resolution: {integrity: sha512-7VmYAmk4csGv08QzrDKScdzn11jHPFGyqJW39FyPgPuAp3zIaUmuCo1yxw9aGs+NEJuTGQ9Gwqpt93vtJubucg==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@npmcli/run-script@10.0.0': + resolution: {integrity: sha512-vaQj4nccJbAslopIvd49pQH2NhUp7G9pY4byUtmwhe37ZZuubGrx0eB9hW2F37uVNRuDDK6byFGXF+7JCuMSZg==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@quansync/fs@0.1.5': + resolution: {integrity: sha512-lNS9hL2aS2NZgNW7BBj+6EBl4rOf8l+tQ0eRY6JWCI8jI2kc53gSoqbjojU0OnAWhzoXiOjFyGsHcDGePB3lhA==} + + '@rollup/rollup-android-arm-eabi@4.52.5': + resolution: {integrity: sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.52.5': + resolution: {integrity: sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.52.5': + resolution: {integrity: sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.52.5': + resolution: {integrity: sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.52.5': + resolution: {integrity: sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.52.5': + resolution: {integrity: sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.52.5': + resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.52.5': + resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.52.5': + resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.52.5': + resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.52.5': + resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.52.5': + resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.52.5': + resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.52.5': + resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.52.5': + resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.52.5': + resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.52.5': + resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.52.5': + resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.52.5': + resolution: {integrity: sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.52.5': + resolution: {integrity: sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.52.5': + resolution: {integrity: sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.52.5': + resolution: {integrity: sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==} + cpu: [x64] + os: [win32] + + '@sigstore/bundle@4.0.0': + resolution: {integrity: sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@sigstore/core@3.0.0': + resolution: {integrity: sha512-NgbJ+aW9gQl/25+GIEGYcCyi8M+ng2/5X04BMuIgoDfgvp18vDcoNHOQjQsG9418HGNYRxG3vfEXaR1ayD37gg==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@sigstore/protobuf-specs@0.5.0': + resolution: {integrity: sha512-MM8XIwUjN2bwvCg1QvrMtbBmpcSHrkhFSCu1D11NyPvDQ25HEc4oG5/OcQfd/Tlf/OxmKWERDj0zGE23jQaMwA==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@sigstore/sign@4.0.1': + resolution: {integrity: sha512-KFNGy01gx9Y3IBPG/CergxR9RZpN43N+lt3EozEfeoyqm8vEiLxwRl3ZO5sPx3Obv1ix/p7FWOlPc2Jgwfp9PA==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@sigstore/tuf@4.0.0': + resolution: {integrity: sha512-0QFuWDHOQmz7t66gfpfNO6aEjoFrdhkJaej/AOqb4kqWZVbPWFZifXZzkxyQBB1OwTbkhdT3LNpMFxwkTvf+2w==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@sigstore/verify@3.0.0': + resolution: {integrity: sha512-moXtHH33AobOhTZF8xcX1MpOFqdvfCk7v6+teJL8zymBiDXwEsQH6XG9HGx2VIxnJZNm4cNSzflTLDnQLmIdmw==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@sindresorhus/chunkify@2.0.0': + resolution: {integrity: sha512-srajPSoMTC98FETCJIeXJhJqB77IRPJSu8g907jLuuioLORHZJ3YAOY2DsP5ebrZrjOrAwjqf+Cgkg/I8TGPpw==} + engines: {node: '>=18'} + deprecated: 'Renamed to chunkify: https://www.npmjs.com/package/chunkify' + + '@sindresorhus/df@1.0.1': + resolution: {integrity: sha512-1Hyp7NQnD/u4DSxR2DGW78TF9k7R0wZ8ev0BpMAIzA6yTQSHqNb5wTuvtcPYf4FWbVse2rW7RgDsyL8ua2vXHw==} + engines: {node: '>=0.10.0'} + + '@sindresorhus/df@3.1.1': + resolution: {integrity: sha512-SME/vtXaJcnQ/HpeV6P82Egy+jThn11IKfwW8+/XVoRD0rmPHVTeKMtww1oWdVnMykzVPjmrDN9S8NBndPEHCQ==} + engines: {node: '>=8'} + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + + '@sindresorhus/merge-streams@2.3.0': + resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} + engines: {node: '>=18'} + + '@socketregistry/is-unicode-supported@1.0.5': + resolution: {integrity: sha512-l3wz0cknjyGlI2iCyZxp50FJhtUFXkdZR6CfUU7OfNxE7I4CRBdsvORLgV+JPwqQQErRO/CZgKsbDHefd3puYA==} + engines: {node: '>=18'} + + '@socketregistry/packageurl-js@1.3.5': + resolution: {integrity: sha512-Fl4GNUJ/z3IBJBGj4IsJfuRGUBCRMgX0df0mb5x5buaCPDKC+NhMhAFuxpc3viLSHV12CO2rGaNCf4fBYWI0FA==} + engines: {node: '>=18', pnpm: '>=10.16.0'} + + '@socketregistry/yocto-spinner@1.0.25': + resolution: {integrity: sha512-f8AqJMH1+BL15G6bHDzb1jyY+wW4gOYQs5JumSxmnE/H/+KgqbIZgaPwDdRwoeciDGojoSVrRHiTZjbe7n7dJA==} + engines: {node: '>=18'} + + '@socketsecurity/lib@3.2.4': + resolution: {integrity: sha512-zB/yzF+X9TlJDAANTRppioNf6ZfnD7Iysr2FnynWMGcSfO3v9SarxtOduC4XmnpegEZpo24A+w1lk7+F2A6B6g==} + engines: {node: '>=22'} + peerDependencies: + typescript: '>=5.0.0' + peerDependenciesMeta: + typescript: + optional: true + + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + + '@stroncium/procfs@1.2.1': + resolution: {integrity: sha512-X1Iui3FUNZP18EUvysTHxt+Avu2nlVzyf90YM8OYgP6SGzTzzX/0JgObfO1AQQDzuZtNNz29bVh8h5R97JrjxA==} + engines: {node: '>=8'} + + '@szmarczak/http-timer@4.0.6': + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + + '@tufjs/canonical-json@2.0.0': + resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==} + engines: {node: ^16.14.0 || >=18.0.0} + + '@tufjs/models@4.0.0': + resolution: {integrity: sha512-h5x5ga/hh82COe+GoD4+gKUeV4T3iaYOxqLt41GRKApinPI7DMidhCmNVTjKfhCWFJIGXaFJee07XczdT4jdZQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/cacheable-request@6.0.3': + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/emscripten@1.41.5': + resolution: {integrity: sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/http-cache-semantics@4.0.4': + resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + + '@types/node@24.9.2': + resolution: {integrity: sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==} + + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + + '@types/treeify@1.0.3': + resolution: {integrity: sha512-hx0o7zWEUU4R2Amn+pjCBQQt23Khy/Dk56gQU5xi5jtPL1h83ACJCeFaB2M/+WO1AntvWrSoVnnCAfI1AQH4Cg==} + + '@typescript-eslint/eslint-plugin@8.44.1': + resolution: {integrity: sha512-molgphGqOBT7t4YKCSkbasmu1tb1MgrZ2szGzHbclF7PNmOkSTQVHy+2jXOSnxvR3+Xe1yySHFZoqMpz3TfQsw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.44.1 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.44.1': + resolution: {integrity: sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.44.1': + resolution: {integrity: sha512-ycSa60eGg8GWAkVsKV4E6Nz33h+HjTXbsDT4FILyL8Obk5/mx4tbvCNsLf9zret3ipSumAOG89UcCs/KRaKYrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.44.1': + resolution: {integrity: sha512-NdhWHgmynpSvyhchGLXh+w12OMT308Gm25JoRIyTZqEbApiBiQHD/8xgb6LqCWCFcxFtWwaVdFsLPQI3jvhywg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.44.1': + resolution: {integrity: sha512-B5OyACouEjuIvof3o86lRMvyDsFwZm+4fBOqFHccIctYgBjqR3qT39FBYGN87khcgf0ExpdCBeGKpKRhSFTjKQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.44.1': + resolution: {integrity: sha512-KdEerZqHWXsRNKjF9NYswNISnFzXfXNDfPxoTh7tqohU/PRIbwTmsjGK6V9/RTYWau7NZvfo52lgVk+sJh0K3g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.44.1': + resolution: {integrity: sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/types@8.46.2': + resolution: {integrity: sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.44.1': + resolution: {integrity: sha512-qnQJ+mVa7szevdEyvfItbO5Vo+GfZ4/GZWWDRRLjrxYPkhM+6zYB2vRYwCsoJLzqFCdZT4mEqyJoyzkunsZ96A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.44.1': + resolution: {integrity: sha512-DpX5Fp6edTlocMCwA+mHY8Mra+pPjRZ0TfHkXI8QFelIKcbADQz1LUPNtzOFUriBB2UYqw4Pi9+xV4w9ZczHFg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.44.1': + resolution: {integrity: sha512-576+u0QD+Jp3tZzvfRfxon0EA2lzcDt3lhUbsC6Lgzy9x2VR4E+JUiNyGHi5T8vk0TV+fpJ5GLG1JsJuWCaKhw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20250920.1': + resolution: {integrity: sha512-nmb5PLzkJNZgsPMfKh1WYnNpTTBDE/fHSPmkYnOT+mhM++vaBCESJ7wbUWKjUN4YMfA7LJT/8hiVaIzjjUS3Bg==} + cpu: [arm64] + os: [darwin] + + '@typescript/native-preview-darwin-x64@7.0.0-dev.20250920.1': + resolution: {integrity: sha512-6/lj1L2MH/j2CKflTV1JZKX8DG9gCotBBxxTOHnjbaElbYk9lI9rvhPWG4oFmsaIyTCVb3fqYOyuabDDdXZZUQ==} + cpu: [x64] + os: [darwin] + + '@typescript/native-preview-linux-arm64@7.0.0-dev.20250920.1': + resolution: {integrity: sha512-NrW1mq3SkacBpFEGqmTakSFbCD0F9fdcmRXHb6nIt16Yy6YG4ulEws4hxzE8WdQAN+pggQhs6oLGUQtD5YvrsQ==} + cpu: [arm64] + os: [linux] + + '@typescript/native-preview-linux-arm@7.0.0-dev.20250920.1': + resolution: {integrity: sha512-Q2XXvYDXYCLYE+vjQHheExZKlJlQcNe2tAeJfimplEiNl+HNBqXiQ5NFGalwd52eN/qB+8p98KiqCLz7etRaVA==} + cpu: [arm] + os: [linux] + + '@typescript/native-preview-linux-x64@7.0.0-dev.20250920.1': + resolution: {integrity: sha512-6slYt8MKVAOZDbs2GXznPJ7KVmMLssqvmTJOmmNNyNJt/0MBJqwgL1b/TlxS9UgUTuBsNyKJbLtJgjvBVixIAA==} + cpu: [x64] + os: [linux] + + '@typescript/native-preview-win32-arm64@7.0.0-dev.20250920.1': + resolution: {integrity: sha512-RIgBHD13gmlN3srdwKRP90nhHB1SRqYKJzdZuU3lS9M11RuKcw58lyznfUhdbHnQTaaUvGTs4cSGZGfrdshVHw==} + cpu: [arm64] + os: [win32] + + '@typescript/native-preview-win32-x64@7.0.0-dev.20250920.1': + resolution: {integrity: sha512-arxNEOP4LMr+rxCxbitG9ejEkmvvj7pkjMFTAFHfWJC0mKuT+1cQu79iZBhY16oHB4HLCziJRBRdMQhIB2Wf+w==} + cpu: [x64] + os: [win32] + + '@typescript/native-preview@7.0.0-dev.20250920.1': + resolution: {integrity: sha512-iuMeX3RUMysVWGvzXzJrPdMBWDo0LlLDk6FU8PbYgCA1MUlguHePK5wqS/GlOrRLBQ0BjXM1W8q81J37vIW63g==} + hasBin: true + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + + '@vitest/coverage-v8@4.0.3': + resolution: {integrity: sha512-I+MlLwyJRBjmJr1kFYSxoseINbIdpxIAeK10jmXgB0FUtIfdYsvM3lGAvBu5yk8WPyhefzdmbCHCc1idFbNRcg==} + peerDependencies: + '@vitest/browser': 4.0.3 + vitest: 4.0.3 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@4.0.3': + resolution: {integrity: sha512-v3eSDx/bF25pzar6aEJrrdTXJduEBU3uSGXHslIdGIpJVP8tQQHV6x1ZfzbFQ/bLIomLSbR/2ZCfnaEGkWkiVQ==} + + '@vitest/mocker@4.0.3': + resolution: {integrity: sha512-evZcRspIPbbiJEe748zI2BRu94ThCBE+RkjCpVF8yoVYuTV7hMe+4wLF/7K86r8GwJHSmAPnPbZhpXWWrg1qbA==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.0.3': + resolution: {integrity: sha512-N7gly/DRXzxa9w9sbDXwD9QNFYP2hw90LLLGDobPNwiWgyW95GMxsCt29/COIKKh3P7XJICR38PSDePenMBtsw==} + + '@vitest/runner@4.0.3': + resolution: {integrity: sha512-1/aK6fPM0lYXWyGKwop2Gbvz1plyTps/HDbIIJXYtJtspHjpXIeB3If07eWpVH4HW7Rmd3Rl+IS/+zEAXrRtXA==} + + '@vitest/snapshot@4.0.3': + resolution: {integrity: sha512-amnYmvZ5MTjNCP1HZmdeczAPLRD6iOm9+2nMRUGxbe/6sQ0Ymur0NnR9LIrWS8JA3wKE71X25D6ya/3LN9YytA==} + + '@vitest/spy@4.0.3': + resolution: {integrity: sha512-82vVL8Cqz7rbXaNUl35V2G7xeNMAjBdNOVaHbrzznT9BmiCiPOzhf0FhU3eP41nP1bLDm/5wWKZqkG4nyU95DQ==} + + '@vitest/ui@4.0.3': + resolution: {integrity: sha512-HURRrgGVzz2GQ2Imurp55FA+majHXgCXMzcwtojUZeRsAXyHNgEvxGRJf4QQY4kJeVakiugusGYeUqBgZ/xylg==} + peerDependencies: + vitest: 4.0.3 + + '@vitest/utils@4.0.3': + resolution: {integrity: sha512-qV6KJkq8W3piW6MDIbGOmn1xhvcW4DuA07alqaQ+vdx7YA49J85pnwnxigZVQFQw3tWnQNRKWwhz5wbP6iv/GQ==} + + '@yarnpkg/core@4.4.4': + resolution: {integrity: sha512-0bcUFx4wzq0szvInY0PkzqjsAlM69lgzOsEbltbiyE6q/h0hRb1oOHWSBvq7rUGA+Ob5vuyhoDYWyyXY/1W4VQ==} + engines: {node: '>=18.12.0'} + + '@yarnpkg/extensions@2.0.6': + resolution: {integrity: sha512-3LciOqpKIuoc9MmYoX3k+NmCdcrvw7HqZT4N/AW3sYkujxfbFA9Ml01JNqu4InzdV9V9NcyFkAKAorCjhY8w6Q==} + engines: {node: '>=18.12.0'} + peerDependencies: + '@yarnpkg/core': ^4.4.2 + + '@yarnpkg/fslib@3.1.3': + resolution: {integrity: sha512-LqfyD3r/8SJm8rPPfmGVHfp4Ag2xTKscDwihOJt+QNrtOeaLykikqKWoBVRBw1cCIbtU7kjT7l1JcWW26hAKtA==} + engines: {node: '>=18.12.0'} + + '@yarnpkg/libzip@3.2.2': + resolution: {integrity: sha512-Kqxgjfy6SwwC4tTGQYToIWtUhIORTpkowqgd9kkMiBixor0eourHZZAggt/7N4WQKt9iCyPSkO3Xvr44vXUBAw==} + engines: {node: '>=18.12.0'} + peerDependencies: + '@yarnpkg/fslib': ^3.1.3 + + '@yarnpkg/parsers@3.0.3': + resolution: {integrity: sha512-mQZgUSgFurUtA07ceMjxrWkYz8QtDuYkvPlu0ZqncgjopQ0t6CNEo/OSealkmnagSUx8ZD5ewvezUwUuMqutQg==} + engines: {node: '>=18.12.0'} + + '@yarnpkg/shell@4.1.3': + resolution: {integrity: sha512-5igwsHbPtSAlLdmMdKqU3atXjwhtLFQXsYAG0sn1XcPb3yF8WxxtWxN6fycBoUvFyIHFz1G0KeRefnAy8n6gdw==} + engines: {node: '>=18.12.0'} + hasBin: true + + abbrev@3.0.1: + resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} + engines: {node: ^18.17.0 || >=20.5.0} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-escapes@7.1.1: + resolution: {integrity: sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==} + engines: {node: '>=18'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-v8-to-istanbul@0.3.7: + resolution: {integrity: sha512-kr1Hy6YRZBkGQSb6puP+D6FQ59Cx4m0siYhAxygMCAgadiWQ6oxAxQXHOMvJx67SJ63jRoVIIg5eXzUbbct1ww==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + baseline-browser-mapping@2.8.19: + resolution: {integrity: sha512-zoKGUdu6vb2jd3YOq0nnhEDQVbPcHhco3UImJrv5dSkvxTc2pl2WjOPsjZXDwPDSl5eghIMuY3R6J9NDKF3KcQ==} + hasBin: true + + bin-links@5.0.0: + resolution: {integrity: sha512-sdleLVfCjBtgO5cNjA2HVRvWBJAHs4zwenaCPMNJAJU0yNxpzj80IpjOIimkpkr+mhlA+how5poQtt53PygbHA==} + engines: {node: ^18.17.0 || >=20.5.0} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.27.0: + resolution: {integrity: sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + builtin-modules@5.0.0: + resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} + engines: {node: '>=18.20'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + cacache@19.0.1: + resolution: {integrity: sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + cacache@20.0.1: + resolution: {integrity: sha512-+7LYcYGBYoNqTp1Rv7Ny1YjUo5E0/ftkQtraH3vkfAGgVHc+ouWdC8okAwQgQR7EVIdW6JTzTmhKFwzb+4okAQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + + cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001751: + resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==} + + chai@6.2.0: + resolution: {integrity: sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + ci-info@4.3.1: + resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} + engines: {node: '>=8'} + + clean-regexp@1.0.0: + resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} + engines: {node: '>=4'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-truncate@4.0.0: + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + clipanion@4.0.0-rc.4: + resolution: {integrity: sha512-CXkMQxU6s9GklO/1f714dkKBMu1lopS1WFF0B8o4AxPykR1hpozxSiUZ5ZUeBjfPgCWqbcNOtZVFhB8Lkfp1+Q==} + peerDependencies: + typanion: '*' + + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + + cmd-shim@7.0.0: + resolution: {integrity: sha512-rtpaCbr164TPPh+zFdkWpCyZuKkjpAzODfaZCf/SVJZzJN+4bHQb/LP3Jzq5/+84um3XXY8r548XiWKSborwVw==} + engines: {node: ^18.17.0 || >=20.5.0} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + comment-parser@1.4.1: + resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} + engines: {node: '>= 12.0.0'} + + common-ancestor-path@1.0.1: + resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + core-js-compat@3.46.0: + resolution: {integrity: sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + del-cli@6.0.0: + resolution: {integrity: sha512-9nitGV2W6KLFyya4qYt4+9AKQFL+c0Ehj5K7V7IwlxTc6RMCfQUGY9E9pLG6e8TQjtwXpuiWIGGZb3mfVxyZkw==} + engines: {node: '>=18'} + hasBin: true + + del@8.0.1: + resolution: {integrity: sha512-gPqh0mKTPvaUZGAuHbrBUYKZWBNAeHG7TU3QH5EhVwPMyKvmfJaNXhcD2jTcXsJRRcffuho4vaYweu80dRrMGA==} + engines: {node: '>=18'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + electron-to-chromium@1.5.239: + resolution: {integrity: sha512-1y5w0Zsq39MSPmEjHjbizvhYoTaulVtivpxkp5q5kaPmQtsK6/2nvAzGRxNMS9DoYySp9PkW0MAQDwU1m764mg==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-toolkit@1.41.0: + resolution: {integrity: sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA==} + + esbuild@0.25.11: + resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-compat-utils@0.5.1: + resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=6.0.0' + + eslint-import-context@0.1.9: + resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + peerDependencies: + unrs-resolver: ^1.0.0 + peerDependenciesMeta: + unrs-resolver: + optional: true + + eslint-import-resolver-typescript@4.4.4: + resolution: {integrity: sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==} + engines: {node: ^16.17.0 || >=18.6.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-plugin-es-x@7.8.0: + resolution: {integrity: sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '>=8' + + eslint-plugin-import-x@4.16.1: + resolution: {integrity: sha512-vPZZsiOKaBAIATpFE2uMI4w5IRwdv/FpQ+qZZMR4E+PeOcM4OeoEbqxRMnywdxP19TyB/3h6QBB0EWon7letSQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/utils': ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 + eslint-import-resolver-node: '*' + peerDependenciesMeta: + '@typescript-eslint/utils': + optional: true + eslint-import-resolver-node: + optional: true + + eslint-plugin-n@17.23.1: + resolution: {integrity: sha512-68PealUpYoHOBh332JLLD9Sj7OQUDkFpmcfqt8R9sySfFSeuGJjMTJQvCRRB96zO3A/PELRLkPrzsHmzEFQQ5A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=8.23.0' + + eslint-plugin-sort-destructure-keys@2.0.0: + resolution: {integrity: sha512-4w1UQCa3o/YdfWaLr9jY8LfGowwjwjmwClyFLxIsToiyIdZMq3x9Ti44nDn34DtTPP7PWg96tUONKVmATKhYGQ==} + engines: {node: '>=12'} + peerDependencies: + eslint: 5 - 9 + + eslint-plugin-unicorn@61.0.2: + resolution: {integrity: sha512-zLihukvneYT7f74GNbVJXfWIiNQmkc/a9vYBTE4qPkQZswolWNdu+Wsp9sIXno1JOzdn6OUwLPd19ekXVkahRA==} + engines: {node: ^20.10.0 || >=21.0.0} + peerDependencies: + eslint: '>=9.29.0' + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.35.0: + resolution: {integrity: sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + execa@2.1.0: + resolution: {integrity: sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==} + engines: {node: ^8.12.0 || >=9.7.0} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + engines: {node: '>=12.0.0'} + + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-sort@3.4.1: + resolution: {integrity: sha512-76uvGPsF6So53sZAqenP9UVT3p5l7cyTHkLWVCMinh41Y8NDrK1IYXJgaBMfc1gk7nJiSRZp676kddFG2Aa5+A==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up-simple@1.0.1: + resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} + engines: {node: '>=18'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs-minipass@3.0.3: + resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fzf@0.5.2: + resolution: {integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} + + get-east-asian-width@1.4.0: + resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} + engines: {node: '>=18'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + glob@11.0.3: + resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} + engines: {node: 20 || >=22} + hasBin: true + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + + globals@16.4.0: + resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + engines: {node: '>=18'} + + globby@14.1.0: + resolution: {integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==} + engines: {node: '>=18'} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + grapheme-splitter@1.0.4: + resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hosted-git-info@8.1.0: + resolution: {integrity: sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==} + engines: {node: ^18.17.0 || >=20.5.0} + + hosted-git-info@9.0.2: + resolution: {integrity: sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==} + engines: {node: ^20.17.0 || >=22.9.0} + + hpagent@1.2.0: + resolution: {integrity: sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==} + engines: {node: '>=14'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ignore-walk@8.0.0: + resolution: {integrity: sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==} + engines: {node: ^20.17.0 || >=22.9.0} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + + ini@5.0.0: + resolution: {integrity: sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==} + engines: {node: ^18.17.0 || >=20.5.0} + + ip-address@10.0.1: + resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} + engines: {node: '>= 12'} + + is-builtin-module@5.0.0: + resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==} + engines: {node: '>=18.20'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-cwd@3.0.0: + resolution: {integrity: sha512-kyiNFFLU0Ampr6SDZitD/DwUo4Zs1nSdnygUBqsu3LooL00Qvb5j+UnvApUn/TTj1J3OuE6BTdQ5rudKmU2ZaA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-path-inside@4.0.0: + resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} + engines: {node: '>=12'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isexe@3.1.1: + resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} + engines: {node: '>=16'} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jackspeak@4.1.1: + resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} + engines: {node: 20 || >=22} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@4.0.0: + resolution: {integrity: sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==} + engines: {node: ^18.17.0 || >=20.5.0} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json-stringify-nice@1.1.4: + resolution: {integrity: sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + + just-diff-apply@5.5.0: + resolution: {integrity: sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==} + + just-diff@6.0.2: + resolution: {integrity: sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + libnpmexec@10.1.8: + resolution: {integrity: sha512-VS4/zL1ZV73tNZbsh/UXyumYP/NMN0vCENigiaWtwq1zJqe/y9bhgaK74QzTb8K50po6jMJQhD8V96F0/yDajg==} + engines: {node: ^20.17.0 || >=22.9.0} + + libnpmpack@9.0.9: + resolution: {integrity: sha512-0UNr2ULi2QOo82EbOCIkn/tQJqD+AAa9iY3kd0kJN23HuwFmCQKba2A9Mep377uSc9VpcHIbUBRW8ROvkMkNlw==} + engines: {node: ^20.17.0 || >=22.9.0} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lint-staged@15.2.11: + resolution: {integrity: sha512-Ev6ivCTYRTGs9ychvpVw35m/bcNDuBN+mnTeObCL5h+boS5WzBEC6LHI4I9F/++sZm1m+J2LEiy0gxL/R9TBqQ==} + engines: {node: '>=18.12.0'} + hasBin: true + + listr2@8.2.5: + resolution: {integrity: sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==} + engines: {node: '>=18.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.2.2: + resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-fetch-happen@14.0.3: + resolution: {integrity: sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + make-fetch-happen@15.0.2: + resolution: {integrity: sha512-sI1NY4lWlXBAfjmCtVWIIpBypbBdhHtcjnwnv+gtCnsaOffyFil3aidszGC8hgzJe+fT1qix05sWxmD/Bmf/oQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@10.0.3: + resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} + engines: {node: 20 || >=22} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass-collect@2.0.1: + resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass-fetch@4.0.1: + resolution: {integrity: sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + minipass-flush@1.0.5: + resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + mount-point@3.0.0: + resolution: {integrity: sha512-jAhfD7ZCG+dbESZjcY1SdFVFqSJkh/yGbdsifHcPkvuLRO5ugK0Ssmd9jdATu29BTd4JiN+vkpMzVvsUgP3SZA==} + engines: {node: '>=0.10.0'} + + move-file@3.1.0: + resolution: {integrity: sha512-4aE3U7CCBWgrQlQDMq8da4woBWDGHioJFiOZ8Ie6Yq2uwYQ9V2kGhTz4x3u6Wc+OU17nw0yc3rJ/lQ4jIiPe3A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare-lite@1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-gyp@11.5.0: + resolution: {integrity: sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + + node-releases@2.0.26: + resolution: {integrity: sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==} + + nopt@8.1.0: + resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + + normalize-package-data@8.0.0: + resolution: {integrity: sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + + npm-bundled@4.0.0: + resolution: {integrity: sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==} + engines: {node: ^18.17.0 || >=20.5.0} + + npm-install-checks@7.1.2: + resolution: {integrity: sha512-z9HJBCYw9Zr8BqXcllKIs5nI+QggAImbBdHphOzVYrz2CB4iQ6FzWyKmlqDZua+51nAu7FcemlbTc9VgQN5XDQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + npm-normalize-package-bin@4.0.0: + resolution: {integrity: sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==} + engines: {node: ^18.17.0 || >=20.5.0} + + npm-package-arg@12.0.2: + resolution: {integrity: sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + npm-package-arg@13.0.0: + resolution: {integrity: sha512-+t2etZAGcB7TbbLHfDwooV9ppB2LhhcT6A+L9cahsf9mEUAoQ6CktLEVvEnpD0N5CkX7zJqnPGaFtoQDy9EkHQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + npm-packlist@10.0.2: + resolution: {integrity: sha512-DrIWNiWT0FTdDRjGOYfEEZUNe1IzaSZ+up7qBTKnrQDySpdmuOQvytrqQlpK5QrCA4IThMvL4wTumqaa1ZvVIQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + npm-pick-manifest@10.0.0: + resolution: {integrity: sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + npm-pick-manifest@11.0.1: + resolution: {integrity: sha512-HnU7FYSWbo7dTVHtK0G+BXbZ0aIfxz/aUCVLN0979Ec6rGUX5cJ6RbgVx5fqb5G31ufz+BVFA7y1SkRTPVNoVQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + npm-registry-fetch@19.0.0: + resolution: {integrity: sha512-DFxSAemHUwT/POaXAOY4NJmEWBPB0oKbwD6FFDE9hnt1nORkt/FXvgjD4hQjoKoHw9u0Ezws9SPXwV7xE/Gyww==} + engines: {node: ^20.17.0 || >=22.9.0} + + npm-run-path@3.1.0: + resolution: {integrity: sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==} + engines: {node: '>=8'} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + ofetch@1.4.1: + resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + os-homedir@1.0.2: + resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==} + engines: {node: '>=0.10.0'} + + p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + + p-finally@2.0.1: + resolution: {integrity: sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==} + engines: {node: '>=8'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-map@7.0.3: + resolution: {integrity: sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==} + engines: {node: '>=18'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + package-manager-detector@1.5.0: + resolution: {integrity: sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==} + + pacote@21.0.1: + resolution: {integrity: sha512-LHGIUQUrcDIJUej53KJz1BPvUuHrItrR2yrnN0Kl9657cJ0ZT6QJHk9wWPBnQZhYT5KLyZWrk9jaYc2aKDu4yw==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + pacote@21.0.3: + resolution: {integrity: sha512-itdFlanxO0nmQv4ORsvA9K1wv40IPfB9OmWqfaJWvoJ30VKyHsqNgDVeG+TVhI7Gk7XW8slUy7cA9r6dF5qohw==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-conflict-json@4.0.0: + resolution: {integrity: sha512-37CN2VtcuvKgHUs8+0b1uJeEsbGn61GRHz469C94P5xiOoqpDYJYwjg4RY9Vmz39WyZAVkR5++nbJwLMIgOCnQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-scurry@2.0.0: + resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + engines: {node: 20 || >=22} + + path-type@6.0.0: + resolution: {integrity: sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==} + engines: {node: '>=18'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pinkie-promise@2.0.1: + resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==} + engines: {node: '>=0.10.0'} + + pinkie@2.0.4: + resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==} + engines: {node: '>=0.10.0'} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + pnpm-workspace-yaml@1.3.0: + resolution: {integrity: sha512-Krb5q8Totd5mVuLx7we+EFHq/AfxA75nbfTm25Q1pIf606+RlaKUG+PXH8SDihfe5b5k4H09gE+sL47L1t5lbw==} + + postcss-selector-parser@7.1.0: + resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} + engines: {node: '>=4'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + presentable-error@0.0.1: + resolution: {integrity: sha512-E6rsNU1QNJgB3sjj7OANinGncFKuK+164sLXw1/CqBjj/EkXSoSdHCtWQGBNlREIGLnL7IEUEGa08YFVUbrhVg==} + engines: {node: '>=16'} + + proc-log@5.0.0: + resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + proggy@3.0.0: + resolution: {integrity: sha512-QE8RApCM3IaRRxVzxrjbgNMpQEX6Wu0p0KBeoSiSEw5/bsGwZHsshF4LCxH2jp/r6BU+bqA3LrMDEYNfJnpD8Q==} + engines: {node: ^18.17.0 || >=20.5.0} + + promise-all-reject-late@1.0.1: + resolution: {integrity: sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw==} + + promise-call-limit@3.0.2: + resolution: {integrity: sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw==} + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + read-cmd-shim@5.0.0: + resolution: {integrity: sha512-SEbJV7tohp3DAAILbEMPXavBjAnMN0tVnh4+9G8ihV4Pq3HYF9h8QNez9zkJ1ILkv9G2BjdzwctznGZXgu/HGw==} + engines: {node: ^18.17.0 || >=20.5.0} + + read@4.1.0: + resolution: {integrity: sha512-uRfX6K+f+R8OOrYScaM3ixPY4erg69f8DN6pgTvMcA9iRc8iDhwrA4m3Yu8YYKsXJgVvum+m8PkRboZwwuLzYA==} + engines: {node: ^18.17.0 || >=20.5.0} + + regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + + regjsparser@0.12.0: + resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} + hasBin: true + + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.52.5: + resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sigstore@4.0.0: + resolution: {integrity: sha512-Gw/FgHtrLM9WP8P5lLcSGh9OQcrTruWCELAiS48ik1QbL0cH+dfjomiRTUE9zzz+D1N6rOLkwXUvVmXZAsNE0Q==} + engines: {node: ^20.17.0 || >=22.9.0} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + + slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} + engines: {node: '>=18'} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-expression-parse@4.0.0: + resolution: {integrity: sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==} + + spdx-license-ids@3.0.22: + resolution: {integrity: sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + ssri@12.0.0: + resolution: {integrity: sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + stable-hash-x@0.2.0: + resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} + engines: {node: '>=12.0.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + streaming-iterables@8.0.1: + resolution: {integrity: sha512-yfQdmUB1b+rGLZkD/r6YisT/eNOjZxBAckXKlzYNmRJnwSzHaiScykD8gsQceFcShtK09qAbLhOqvzIpnBPoDQ==} + engines: {node: '>=18'} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-indent@4.1.1: + resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + + tar@7.5.1: + resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==} + engines: {node: '>=18'} + + taze@19.6.0: + resolution: {integrity: sha512-hQGQH4WVtV9BqsZbrGzOmOP4NdWqie948BnqtH+NPwdVt5mI+qALVRDvgzgdf+neN7bcrVVpV4ToyFkxg0U0xQ==} + hasBin: true + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyexec@1.0.1: + resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinylogic@2.0.0: + resolution: {integrity: sha512-dljTkiLLITtsjqBvTA1MRZQK/sGP4kI3UJKc3yA9fMzYbMF2RhcN04SeROVqJBIYYOoJMM8u0WDnhFwMSFQotw==} + + tinyrainbow@3.0.3: + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + trash@10.0.0: + resolution: {integrity: sha512-nyHQPJ7F4dYCfj1xN95DAkLkf9qlyRLDpT9yYwcR5SH16q+f7VA1L5VwsdEqWFUuGNpKwgLnbOS1QBvXMYnLfA==} + engines: {node: '>=20'} + + treeify@1.1.0: + resolution: {integrity: sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==} + engines: {node: '>=0.6'} + + treeverse@3.0.0: + resolution: {integrity: sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-declaration-location@1.0.7: + resolution: {integrity: sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==} + peerDependencies: + typescript: '>=4.0.0' + + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsutils@3.21.0: + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + + tuf-js@4.0.0: + resolution: {integrity: sha512-Lq7ieeGvXDXwpoSmOSgLWVdsGGV9J4a77oDTAPe/Ltrqnnm/ETaRlBAQTH5JatEh8KXuE6sddf9qAv1Q2282Hg==} + engines: {node: ^20.17.0 || >=22.9.0} + + typanion@3.14.0: + resolution: {integrity: sha512-ZW/lVMRabETuYCd9O9ZvMhAh8GslSqaUjxmK/JLPCh6l73CvLBiuXswj/+7LdnWOgYsQ130FqLzFz5aGT4I3Ug==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-coverage-core@2.29.7: + resolution: {integrity: sha512-bt+bnXekw3p5NnqiZpNupOOxfUKGw2Z/YJedfGHkxpeyGLK7DZ59a6Wds8eq1oKjJc5Wulp2xL207z8FjFO14Q==} + peerDependencies: + typescript: 2 || 3 || 4 || 5 + + type-coverage@2.29.7: + resolution: {integrity: sha512-E67Chw7SxFe++uotisxt/xzB1UxxvLztzzQqVyUZ/jKujsejVqvoO5vn25oMvqJydqYrASBVBCQCy082E2qQYQ==} + hasBin: true + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + typescript-eslint@8.44.1: + resolution: {integrity: sha512-0ws8uWGrUVTjEeN2OM4K1pLKHK/4NiNP/vz6ns+LjT/6sqpaYzIVFajZb1fj/IDwpsrrHb3Jy0Qm5u9CPcKaeg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + unconfig@7.3.3: + resolution: {integrity: sha512-QCkQoOnJF8L107gxfHL0uavn7WD9b3dpBcFX6HtfQYmjw2YzWxGuFQ0N0J6tE9oguCBJn9KOvfqYDCMPHIZrBA==} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + unique-filename@4.0.0: + resolution: {integrity: sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + unique-slug@5.0.0: + resolution: {integrity: sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==} + engines: {node: ^18.17.0 || >=20.5.0} + + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + + update-browserslist-db@1.1.4: + resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + user-home@2.0.0: + resolution: {integrity: sha512-KMWqdlOcjCYdtIJpicDSFBQ8nFwS2i9sslAd6f4+CBGcU4gist2REnr2fxj2YocvJFxSF3ZOHLYLVZnUxv4BZQ==} + engines: {node: '>=0.10.0'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + validate-npm-package-name@6.0.2: + resolution: {integrity: sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + vite-tsconfig-paths@5.1.4: + resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + vite@7.1.12: + resolution: {integrity: sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.0.3: + resolution: {integrity: sha512-IUSop8jgaT7w0g1yOM/35qVtKjr/8Va4PrjzH1OUb0YH4c3OXB2lCZDkMAB6glA8T5w8S164oJGsbcmAecr4sA==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.3 + '@vitest/browser-preview': 4.0.3 + '@vitest/browser-webdriverio': 4.0.3 + '@vitest/ui': 4.0.3 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + which@5.0.0: + resolution: {integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@6.0.0: + resolution: {integrity: sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + xdg-basedir@4.0.0: + resolution: {integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==} + engines: {node: '>=8'} + + xdg-trashdir@3.1.0: + resolution: {integrity: sha512-N1XQngeqMBoj9wM4ZFadVV2MymImeiFfYD+fJrNlcVcOHsJFFQe7n3b+aBoTPwARuq2HQxukfzVpQmAk1gN4sQ==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yaml@2.6.1: + resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==} + engines: {node: '>= 14'} + hasBin: true + + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + + zod@4.1.12: + resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} + +snapshots: + + '@antfu/ni@25.0.0': + dependencies: + ansis: 4.2.0 + fzf: 0.5.2 + package-manager-detector: 1.5.0 + tinyexec: 1.0.1 + + '@arcanis/slice-ansi@1.1.1': + dependencies: + grapheme-splitter: 1.0.4 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.4': {} + + '@babel/core@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.3': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.4 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.27.0 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + + '@babel/parser@7.28.4': + dependencies: + '@babel/types': 7.28.4 + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@babel/traverse@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.4': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@bcoe/v8-coverage@1.0.2': {} + + '@biomejs/biome@2.2.4': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.2.4 + '@biomejs/cli-darwin-x64': 2.2.4 + '@biomejs/cli-linux-arm64': 2.2.4 + '@biomejs/cli-linux-arm64-musl': 2.2.4 + '@biomejs/cli-linux-x64': 2.2.4 + '@biomejs/cli-linux-x64-musl': 2.2.4 + '@biomejs/cli-win32-arm64': 2.2.4 + '@biomejs/cli-win32-x64': 2.2.4 + + '@biomejs/cli-darwin-arm64@2.2.4': + optional: true + + '@biomejs/cli-darwin-x64@2.2.4': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.2.4': + optional: true + + '@biomejs/cli-linux-arm64@2.2.4': + optional: true + + '@biomejs/cli-linux-x64-musl@2.2.4': + optional: true + + '@biomejs/cli-linux-x64@2.2.4': + optional: true + + '@biomejs/cli-win32-arm64@2.2.4': + optional: true + + '@biomejs/cli-win32-x64@2.2.4': + optional: true + + '@emnapi/core@1.6.0': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.6.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.25.11': + optional: true + + '@esbuild/android-arm64@0.25.11': + optional: true + + '@esbuild/android-arm@0.25.11': + optional: true + + '@esbuild/android-x64@0.25.11': + optional: true + + '@esbuild/darwin-arm64@0.25.11': + optional: true + + '@esbuild/darwin-x64@0.25.11': + optional: true + + '@esbuild/freebsd-arm64@0.25.11': + optional: true + + '@esbuild/freebsd-x64@0.25.11': + optional: true + + '@esbuild/linux-arm64@0.25.11': + optional: true + + '@esbuild/linux-arm@0.25.11': + optional: true + + '@esbuild/linux-ia32@0.25.11': + optional: true + + '@esbuild/linux-loong64@0.25.11': + optional: true + + '@esbuild/linux-mips64el@0.25.11': + optional: true + + '@esbuild/linux-ppc64@0.25.11': + optional: true + + '@esbuild/linux-riscv64@0.25.11': + optional: true + + '@esbuild/linux-s390x@0.25.11': + optional: true + + '@esbuild/linux-x64@0.25.11': + optional: true + + '@esbuild/netbsd-arm64@0.25.11': + optional: true + + '@esbuild/netbsd-x64@0.25.11': + optional: true + + '@esbuild/openbsd-arm64@0.25.11': + optional: true + + '@esbuild/openbsd-x64@0.25.11': + optional: true + + '@esbuild/openharmony-arm64@0.25.11': + optional: true + + '@esbuild/sunos-x64@0.25.11': + optional: true + + '@esbuild/win32-arm64@0.25.11': + optional: true + + '@esbuild/win32-ia32@0.25.11': + optional: true + + '@esbuild/win32-x64@0.25.11': + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@9.35.0(jiti@2.6.1))': + dependencies: + eslint: 9.35.0(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/compat@1.4.0(eslint@9.35.0(jiti@2.6.1))': + dependencies: + '@eslint/core': 0.16.0 + optionalDependencies: + eslint: 9.35.0(jiti@2.6.1) + + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.3.1': {} + + '@eslint/core@0.15.2': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/core@0.16.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.35.0': {} + + '@eslint/js@9.38.0': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.3.5': + dependencies: + '@eslint/core': 0.15.2 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@inquirer/ansi@1.0.1': {} + + '@inquirer/confirm@5.1.16(@types/node@24.9.2)': + dependencies: + '@inquirer/core': 10.3.0(@types/node@24.9.2) + '@inquirer/type': 3.0.9(@types/node@24.9.2) + optionalDependencies: + '@types/node': 24.9.2 + + '@inquirer/core@10.3.0(@types/node@24.9.2)': + dependencies: + '@inquirer/ansi': 1.0.1 + '@inquirer/figures': 1.0.14 + '@inquirer/type': 3.0.9(@types/node@24.9.2) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.9.2 + + '@inquirer/figures@1.0.14': {} + + '@inquirer/input@4.2.2(@types/node@24.9.2)': + dependencies: + '@inquirer/core': 10.3.0(@types/node@24.9.2) + '@inquirer/type': 3.0.9(@types/node@24.9.2) + optionalDependencies: + '@types/node': 24.9.2 + + '@inquirer/password@4.0.18(@types/node@24.9.2)': + dependencies: + '@inquirer/core': 10.3.0(@types/node@24.9.2) + '@inquirer/type': 3.0.9(@types/node@24.9.2) + ansi-escapes: 4.3.2 + optionalDependencies: + '@types/node': 24.9.2 + + '@inquirer/search@3.1.1(@types/node@24.9.2)': + dependencies: + '@inquirer/core': 10.3.0(@types/node@24.9.2) + '@inquirer/figures': 1.0.14 + '@inquirer/type': 3.0.9(@types/node@24.9.2) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.9.2 + + '@inquirer/select@4.3.2(@types/node@24.9.2)': + dependencies: + '@inquirer/core': 10.3.0(@types/node@24.9.2) + '@inquirer/figures': 1.0.14 + '@inquirer/type': 3.0.9(@types/node@24.9.2) + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.9.2 + + '@inquirer/type@3.0.9(@types/node@24.9.2)': + optionalDependencies: + '@types/node': 24.9.2 + + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@isaacs/string-locale-compare@1.1.0': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.6.0 + '@emnapi/runtime': 1.6.0 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@npmcli/agent@3.0.0': + dependencies: + agent-base: 7.1.4 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 10.4.3 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + '@npmcli/agent@4.0.0': + dependencies: + agent-base: 7.1.4 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 11.2.2 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + '@npmcli/arborist@9.1.6': + dependencies: + '@isaacs/string-locale-compare': 1.1.0 + '@npmcli/fs': 4.0.0 + '@npmcli/installed-package-contents': 3.0.0 + '@npmcli/map-workspaces': 5.0.0 + '@npmcli/metavuln-calculator': 9.0.2 + '@npmcli/name-from-folder': 3.0.0 + '@npmcli/node-gyp': 4.0.0 + '@npmcli/package-json': 7.0.0 + '@npmcli/query': 4.0.1 + '@npmcli/redact': 3.2.2 + '@npmcli/run-script': 10.0.0 + bin-links: 5.0.0 + cacache: 20.0.1 + common-ancestor-path: 1.0.1 + hosted-git-info: 9.0.2 + json-stringify-nice: 1.1.4 + lru-cache: 11.2.2 + minimatch: 10.0.3 + nopt: 8.1.0 + npm-install-checks: 7.1.2 + npm-package-arg: 13.0.0 + npm-pick-manifest: 11.0.1 + npm-registry-fetch: 19.0.0 + pacote: 21.0.3 + parse-conflict-json: 4.0.0 + proc-log: 5.0.0 + proggy: 3.0.0 + promise-all-reject-late: 1.0.1 + promise-call-limit: 3.0.2 + semver: 7.7.2 + ssri: 12.0.0 + treeverse: 3.0.0 + walk-up-path: 4.0.0 + transitivePeerDependencies: + - supports-color + + '@npmcli/fs@4.0.0': + dependencies: + semver: 7.7.2 + + '@npmcli/git@6.0.3': + dependencies: + '@npmcli/promise-spawn': 8.0.3 + ini: 5.0.0 + lru-cache: 10.4.3 + npm-pick-manifest: 10.0.0 + proc-log: 5.0.0 + promise-retry: 2.0.1 + semver: 7.7.2 + which: 5.0.0 + + '@npmcli/git@7.0.0': + dependencies: + '@npmcli/promise-spawn': 8.0.3 + ini: 5.0.0 + lru-cache: 11.2.2 + npm-pick-manifest: 11.0.1 + proc-log: 5.0.0 + promise-retry: 2.0.1 + semver: 7.7.2 + which: 5.0.0 + + '@npmcli/installed-package-contents@3.0.0': + dependencies: + npm-bundled: 4.0.0 + npm-normalize-package-bin: 4.0.0 + + '@npmcli/map-workspaces@5.0.0': + dependencies: + '@npmcli/name-from-folder': 3.0.0 + '@npmcli/package-json': 7.0.0 + glob: 11.0.3 + minimatch: 10.0.3 + + '@npmcli/metavuln-calculator@9.0.2': + dependencies: + cacache: 20.0.1 + json-parse-even-better-errors: 4.0.0 + pacote: 21.0.3 + proc-log: 5.0.0 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + + '@npmcli/name-from-folder@3.0.0': {} + + '@npmcli/node-gyp@4.0.0': {} + + '@npmcli/package-json@7.0.0': + dependencies: + '@npmcli/git': 6.0.3 + glob: 11.0.3 + hosted-git-info: 9.0.2 + json-parse-even-better-errors: 4.0.0 + proc-log: 5.0.0 + semver: 7.7.2 + validate-npm-package-license: 3.0.4 + + '@npmcli/promise-spawn@8.0.3': + dependencies: + which: 5.0.0 + + '@npmcli/query@4.0.1': + dependencies: + postcss-selector-parser: 7.1.0 + + '@npmcli/redact@3.2.2': {} + + '@npmcli/run-script@10.0.0': + dependencies: + '@npmcli/node-gyp': 4.0.0 + '@npmcli/package-json': 7.0.0 + '@npmcli/promise-spawn': 8.0.3 + node-gyp: 11.5.0 + proc-log: 5.0.0 + which: 5.0.0 + transitivePeerDependencies: + - supports-color + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@polka/url@1.0.0-next.29': {} + + '@quansync/fs@0.1.5': + dependencies: + quansync: 0.2.11 + + '@rollup/rollup-android-arm-eabi@4.52.5': + optional: true + + '@rollup/rollup-android-arm64@4.52.5': + optional: true + + '@rollup/rollup-darwin-arm64@4.52.5': + optional: true + + '@rollup/rollup-darwin-x64@4.52.5': + optional: true + + '@rollup/rollup-freebsd-arm64@4.52.5': + optional: true + + '@rollup/rollup-freebsd-x64@4.52.5': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.52.5': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.52.5': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.52.5': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.52.5': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-x64-musl@4.52.5': + optional: true + + '@rollup/rollup-openharmony-arm64@4.52.5': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.52.5': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.52.5': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.52.5': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.52.5': + optional: true + + '@sigstore/bundle@4.0.0': + dependencies: + '@sigstore/protobuf-specs': 0.5.0 + + '@sigstore/core@3.0.0': {} + + '@sigstore/protobuf-specs@0.5.0': {} + + '@sigstore/sign@4.0.1': + dependencies: + '@sigstore/bundle': 4.0.0 + '@sigstore/core': 3.0.0 + '@sigstore/protobuf-specs': 0.5.0 + make-fetch-happen: 15.0.2 + proc-log: 5.0.0 + promise-retry: 2.0.1 + transitivePeerDependencies: + - supports-color + + '@sigstore/tuf@4.0.0': + dependencies: + '@sigstore/protobuf-specs': 0.5.0 + tuf-js: 4.0.0 + transitivePeerDependencies: + - supports-color + + '@sigstore/verify@3.0.0': + dependencies: + '@sigstore/bundle': 4.0.0 + '@sigstore/core': 3.0.0 + '@sigstore/protobuf-specs': 0.5.0 + + '@sindresorhus/chunkify@2.0.0': {} + + '@sindresorhus/df@1.0.1': {} + + '@sindresorhus/df@3.1.1': + dependencies: + execa: 2.1.0 + + '@sindresorhus/is@4.6.0': {} + + '@sindresorhus/merge-streams@2.3.0': {} + + '@socketregistry/is-unicode-supported@1.0.5': {} + + '@socketregistry/packageurl-js@1.3.5': {} + + '@socketregistry/yocto-spinner@1.0.25': + dependencies: + yoctocolors-cjs: 2.1.3 + + '@socketsecurity/lib@3.2.4(typescript@5.9.2)': + optionalDependencies: + typescript: 5.9.2 + + '@standard-schema/spec@1.0.0': {} + + '@stroncium/procfs@1.2.1': {} + + '@szmarczak/http-timer@4.0.6': + dependencies: + defer-to-connect: 2.0.1 + + '@tufjs/canonical-json@2.0.0': {} + + '@tufjs/models@4.0.0': + dependencies: + '@tufjs/canonical-json': 2.0.0 + minimatch: 9.0.5 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/cacheable-request@6.0.3': + dependencies: + '@types/http-cache-semantics': 4.0.4 + '@types/keyv': 3.1.4 + '@types/node': 24.9.2 + '@types/responselike': 1.0.3 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/emscripten@1.41.5': {} + + '@types/estree@1.0.8': {} + + '@types/http-cache-semantics@4.0.4': {} + + '@types/json-schema@7.0.15': {} + + '@types/keyv@3.1.4': + dependencies: + '@types/node': 24.9.2 + + '@types/node@24.9.2': + dependencies: + undici-types: 7.16.0 + + '@types/responselike@1.0.3': + dependencies: + '@types/node': 24.9.2 + + '@types/semver@7.7.1': {} + + '@types/treeify@1.0.3': {} + + '@typescript-eslint/eslint-plugin@8.44.1(@typescript-eslint/parser@8.44.1(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.2)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.44.1(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/scope-manager': 8.44.1 + '@typescript-eslint/type-utils': 8.44.1(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/utils': 8.44.1(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.44.1 + eslint: 9.35.0(jiti@2.6.1) + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.44.1(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.2)': + dependencies: + '@typescript-eslint/scope-manager': 8.44.1 + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.44.1 + debug: 4.4.3 + eslint: 9.35.0(jiti@2.6.1) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.44.1(typescript@5.9.2)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.44.1(typescript@5.9.2) + '@typescript-eslint/types': 8.44.1 + debug: 4.4.3 + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.44.1': + dependencies: + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/visitor-keys': 8.44.1 + + '@typescript-eslint/tsconfig-utils@8.44.1(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@typescript-eslint/type-utils@8.44.1(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.2)': + dependencies: + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.9.2) + '@typescript-eslint/utils': 8.44.1(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.2) + debug: 4.4.3 + eslint: 9.35.0(jiti@2.6.1) + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.44.1': {} + + '@typescript-eslint/types@8.46.2': {} + + '@typescript-eslint/typescript-estree@8.44.1(typescript@5.9.2)': + dependencies: + '@typescript-eslint/project-service': 8.44.1(typescript@5.9.2) + '@typescript-eslint/tsconfig-utils': 8.44.1(typescript@5.9.2) + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/visitor-keys': 8.44.1 + debug: 4.4.3 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.44.1(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.2)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.44.1 + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.9.2) + eslint: 9.35.0(jiti@2.6.1) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.44.1': + dependencies: + '@typescript-eslint/types': 8.44.1 + eslint-visitor-keys: 4.2.1 + + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20250920.1': + optional: true + + '@typescript/native-preview-darwin-x64@7.0.0-dev.20250920.1': + optional: true + + '@typescript/native-preview-linux-arm64@7.0.0-dev.20250920.1': + optional: true + + '@typescript/native-preview-linux-arm@7.0.0-dev.20250920.1': + optional: true + + '@typescript/native-preview-linux-x64@7.0.0-dev.20250920.1': + optional: true + + '@typescript/native-preview-win32-arm64@7.0.0-dev.20250920.1': + optional: true + + '@typescript/native-preview-win32-x64@7.0.0-dev.20250920.1': + optional: true + + '@typescript/native-preview@7.0.0-dev.20250920.1': + optionalDependencies: + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20250920.1 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20250920.1 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20250920.1 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20250920.1 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20250920.1 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20250920.1 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20250920.1 + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + optional: true + + '@vitest/coverage-v8@4.0.3(vitest@4.0.3)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.0.3 + ast-v8-to-istanbul: 0.3.7 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magicast: 0.3.5 + std-env: 3.10.0 + tinyrainbow: 3.0.3 + vitest: 4.0.3(@types/node@24.9.2)(@vitest/ui@4.0.3)(jiti@2.6.1)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@4.0.3': + dependencies: + '@standard-schema/spec': 1.0.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.3 + '@vitest/utils': 4.0.3 + chai: 6.2.0 + tinyrainbow: 3.0.3 + + '@vitest/mocker@4.0.3(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 4.0.3 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(yaml@2.8.1) + + '@vitest/pretty-format@4.0.3': + dependencies: + tinyrainbow: 3.0.3 + + '@vitest/runner@4.0.3': + dependencies: + '@vitest/utils': 4.0.3 + pathe: 2.0.3 + + '@vitest/snapshot@4.0.3': + dependencies: + '@vitest/pretty-format': 4.0.3 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.0.3': {} + + '@vitest/ui@4.0.3(vitest@4.0.3)': + dependencies: + '@vitest/utils': 4.0.3 + fflate: 0.8.2 + flatted: 3.3.3 + pathe: 2.0.3 + sirv: 3.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vitest: 4.0.3(@types/node@24.9.2)(@vitest/ui@4.0.3)(jiti@2.6.1)(yaml@2.8.1) + + '@vitest/utils@4.0.3': + dependencies: + '@vitest/pretty-format': 4.0.3 + tinyrainbow: 3.0.3 + + '@yarnpkg/core@4.4.4(typanion@3.14.0)': + dependencies: + '@arcanis/slice-ansi': 1.1.1 + '@types/semver': 7.7.1 + '@types/treeify': 1.0.3 + '@yarnpkg/fslib': 3.1.3 + '@yarnpkg/libzip': 3.2.2(@yarnpkg/fslib@3.1.3) + '@yarnpkg/parsers': 3.0.3 + '@yarnpkg/shell': 4.1.3(typanion@3.14.0) + camelcase: 5.3.1 + chalk: 4.1.2 + ci-info: 4.3.1 + clipanion: 4.0.0-rc.4(typanion@3.14.0) + cross-spawn: 7.0.6 + diff: 5.2.0 + dotenv: 16.6.1 + es-toolkit: 1.41.0 + fast-glob: 3.3.3 + got: 11.8.6 + hpagent: 1.2.0 + micromatch: 4.0.8 + p-limit: 2.3.0 + semver: 7.7.2 + strip-ansi: 6.0.1 + tar: 6.2.1 + tinylogic: 2.0.0 + treeify: 1.1.0 + tslib: 2.8.1 + transitivePeerDependencies: + - typanion + + '@yarnpkg/extensions@2.0.6(@yarnpkg/core@4.4.4(typanion@3.14.0))': + dependencies: + '@yarnpkg/core': 4.4.4(typanion@3.14.0) + + '@yarnpkg/fslib@3.1.3': + dependencies: + tslib: 2.8.1 + + '@yarnpkg/libzip@3.2.2(@yarnpkg/fslib@3.1.3)': + dependencies: + '@types/emscripten': 1.41.5 + '@yarnpkg/fslib': 3.1.3 + tslib: 2.8.1 + + '@yarnpkg/parsers@3.0.3': + dependencies: + js-yaml: 3.14.1 + tslib: 2.8.1 + + '@yarnpkg/shell@4.1.3(typanion@3.14.0)': + dependencies: + '@yarnpkg/fslib': 3.1.3 + '@yarnpkg/parsers': 3.0.3 + chalk: 4.1.2 + clipanion: 4.0.0-rc.4(typanion@3.14.0) + cross-spawn: 7.0.6 + fast-glob: 3.3.3 + micromatch: 4.0.8 + tslib: 2.8.1 + transitivePeerDependencies: + - typanion + + abbrev@3.0.1: {} + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + agent-base@7.1.4: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-escapes@7.1.1: + dependencies: + environment: 1.1.0 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + ansis@4.2.0: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + assertion-error@2.0.1: {} + + ast-v8-to-istanbul@0.3.7: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 9.0.1 + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.8.19: {} + + bin-links@5.0.0: + dependencies: + cmd-shim: 7.0.0 + npm-normalize-package-bin: 4.0.0 + proc-log: 5.0.0 + read-cmd-shim: 5.0.0 + write-file-atomic: 6.0.0 + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.27.0: + dependencies: + baseline-browser-mapping: 2.8.19 + caniuse-lite: 1.0.30001751 + electron-to-chromium: 1.5.239 + node-releases: 2.0.26 + update-browserslist-db: 1.1.4(browserslist@4.27.0) + + builtin-modules@5.0.0: {} + + cac@6.7.14: {} + + cacache@19.0.1: + dependencies: + '@npmcli/fs': 4.0.0 + fs-minipass: 3.0.3 + glob: 10.4.5 + lru-cache: 10.4.3 + minipass: 7.1.2 + minipass-collect: 2.0.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + p-map: 7.0.3 + ssri: 12.0.0 + tar: 7.5.1 + unique-filename: 4.0.0 + + cacache@20.0.1: + dependencies: + '@npmcli/fs': 4.0.0 + fs-minipass: 3.0.3 + glob: 11.0.3 + lru-cache: 11.2.2 + minipass: 7.1.2 + minipass-collect: 2.0.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + p-map: 7.0.3 + ssri: 12.0.0 + unique-filename: 4.0.0 + + cacheable-lookup@5.0.4: {} + + cacheable-request@7.0.4: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.2.0 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + + callsites@3.1.0: {} + + camelcase@5.3.1: {} + + caniuse-lite@1.0.30001751: {} + + chai@6.2.0: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.3.0: {} + + change-case@5.4.4: {} + + chownr@2.0.0: {} + + chownr@3.0.0: {} + + ci-info@4.3.1: {} + + clean-regexp@1.0.0: + dependencies: + escape-string-regexp: 1.0.5 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-truncate@4.0.0: + dependencies: + slice-ansi: 5.0.0 + string-width: 7.2.0 + + cli-width@4.1.0: {} + + clipanion@4.0.0-rc.4(typanion@3.14.0): + dependencies: + typanion: 3.14.0 + + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + + cmd-shim@7.0.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + commander@12.1.0: {} + + comment-parser@1.4.1: {} + + common-ancestor-path@1.0.1: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + core-js-compat@3.46.0: + dependencies: + browserslist: 4.27.0 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-is@0.1.4: {} + + defer-to-connect@2.0.1: {} + + defu@6.1.4: {} + + del-cli@6.0.0: + dependencies: + del: 8.0.1 + meow: 13.2.0 + + del@8.0.1: + dependencies: + globby: 14.1.0 + is-glob: 4.0.3 + is-path-cwd: 3.0.0 + is-path-inside: 4.0.0 + p-map: 7.0.3 + presentable-error: 0.0.1 + slash: 5.1.0 + + destr@2.0.5: {} + + diff@5.2.0: {} + + dotenv@16.6.1: {} + + eastasianwidth@0.2.0: {} + + electron-to-chromium@1.5.239: {} + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + env-paths@2.2.1: {} + + environment@1.1.0: {} + + err-code@2.0.3: {} + + es-module-lexer@1.7.0: {} + + es-toolkit@1.41.0: {} + + esbuild@0.25.11: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.11 + '@esbuild/android-arm': 0.25.11 + '@esbuild/android-arm64': 0.25.11 + '@esbuild/android-x64': 0.25.11 + '@esbuild/darwin-arm64': 0.25.11 + '@esbuild/darwin-x64': 0.25.11 + '@esbuild/freebsd-arm64': 0.25.11 + '@esbuild/freebsd-x64': 0.25.11 + '@esbuild/linux-arm': 0.25.11 + '@esbuild/linux-arm64': 0.25.11 + '@esbuild/linux-ia32': 0.25.11 + '@esbuild/linux-loong64': 0.25.11 + '@esbuild/linux-mips64el': 0.25.11 + '@esbuild/linux-ppc64': 0.25.11 + '@esbuild/linux-riscv64': 0.25.11 + '@esbuild/linux-s390x': 0.25.11 + '@esbuild/linux-x64': 0.25.11 + '@esbuild/netbsd-arm64': 0.25.11 + '@esbuild/netbsd-x64': 0.25.11 + '@esbuild/openbsd-arm64': 0.25.11 + '@esbuild/openbsd-x64': 0.25.11 + '@esbuild/openharmony-arm64': 0.25.11 + '@esbuild/sunos-x64': 0.25.11 + '@esbuild/win32-arm64': 0.25.11 + '@esbuild/win32-ia32': 0.25.11 + '@esbuild/win32-x64': 0.25.11 + + escalade@3.2.0: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + eslint-compat-utils@0.5.1(eslint@9.35.0(jiti@2.6.1)): + dependencies: + eslint: 9.35.0(jiti@2.6.1) + semver: 7.7.2 + + eslint-import-context@0.1.9(unrs-resolver@1.11.1): + dependencies: + get-tsconfig: 4.13.0 + stable-hash-x: 0.2.0 + optionalDependencies: + unrs-resolver: 1.11.1 + + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.44.1(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.6.1)))(eslint@9.35.0(jiti@2.6.1)): + dependencies: + debug: 4.4.3 + eslint: 9.35.0(jiti@2.6.1) + eslint-import-context: 0.1.9(unrs-resolver@1.11.1) + get-tsconfig: 4.13.0 + is-bun-module: 2.0.0 + stable-hash-x: 0.2.0 + tinyglobby: 0.2.15 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.44.1(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-plugin-es-x@7.8.0(eslint@9.35.0(jiti@2.6.1)): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + eslint: 9.35.0(jiti@2.6.1) + eslint-compat-utils: 0.5.1(eslint@9.35.0(jiti@2.6.1)) + + eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.44.1(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.6.1)): + dependencies: + '@typescript-eslint/types': 8.46.2 + comment-parser: 1.4.1 + debug: 4.4.3 + eslint: 9.35.0(jiti@2.6.1) + eslint-import-context: 0.1.9(unrs-resolver@1.11.1) + is-glob: 4.0.3 + minimatch: 10.0.3 + semver: 7.7.2 + stable-hash-x: 0.2.0 + unrs-resolver: 1.11.1 + optionalDependencies: + '@typescript-eslint/utils': 8.44.1(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.2) + transitivePeerDependencies: + - supports-color + + eslint-plugin-n@17.23.1(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.2): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.6.1)) + enhanced-resolve: 5.18.3 + eslint: 9.35.0(jiti@2.6.1) + eslint-plugin-es-x: 7.8.0(eslint@9.35.0(jiti@2.6.1)) + get-tsconfig: 4.13.0 + globals: 15.15.0 + globrex: 0.1.2 + ignore: 5.3.2 + semver: 7.7.2 + ts-declaration-location: 1.0.7(typescript@5.9.2) + transitivePeerDependencies: + - typescript + + eslint-plugin-sort-destructure-keys@2.0.0(eslint@9.35.0(jiti@2.6.1)): + dependencies: + eslint: 9.35.0(jiti@2.6.1) + natural-compare-lite: 1.4.0 + + eslint-plugin-unicorn@61.0.2(eslint@9.35.0(jiti@2.6.1)): + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.6.1)) + '@eslint/plugin-kit': 0.3.5 + change-case: 5.4.4 + ci-info: 4.3.1 + clean-regexp: 1.0.0 + core-js-compat: 3.46.0 + eslint: 9.35.0(jiti@2.6.1) + esquery: 1.6.0 + find-up-simple: 1.0.1 + globals: 16.4.0 + indent-string: 5.0.0 + is-builtin-module: 5.0.0 + jsesc: 3.1.0 + pluralize: 8.0.0 + regexp-tree: 0.1.27 + regjsparser: 0.12.0 + semver: 7.7.2 + strip-indent: 4.1.1 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.35.0(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.3.1 + '@eslint/core': 0.15.2 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.35.0 + '@eslint/plugin-kit': 0.3.5 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esprima@4.0.1: {} + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + eventemitter3@5.0.1: {} + + execa@2.1.0: + dependencies: + cross-spawn: 7.0.6 + get-stream: 5.2.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 3.1.0 + onetime: 5.1.2 + p-finally: 2.0.1 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@8.0.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + + expect-type@1.2.2: {} + + exponential-backoff@3.1.3: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-sort@3.4.1: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fflate@0.8.2: {} + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up-simple@1.0.1: {} + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs-minipass@3.0.3: + dependencies: + minipass: 7.1.2 + + fsevents@2.3.3: + optional: true + + fzf@0.5.2: {} + + gensync@1.0.0-beta.2: {} + + get-east-asian-width@1.3.0: {} + + get-east-asian-width@1.4.0: {} + + get-stream@5.2.0: + dependencies: + pump: 3.0.3 + + get-stream@8.0.1: {} + + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@11.0.3: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.1.1 + minimatch: 10.0.3 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.0 + + globals@14.0.0: {} + + globals@15.15.0: {} + + globals@16.4.0: {} + + globby@14.1.0: + dependencies: + '@sindresorhus/merge-streams': 2.3.0 + fast-glob: 3.3.3 + ignore: 7.0.5 + path-type: 6.0.0 + slash: 5.1.0 + unicorn-magic: 0.3.0 + + globrex@0.1.2: {} + + got@11.8.6: + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + + graceful-fs@4.2.11: {} + + grapheme-splitter@1.0.4: {} + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + hosted-git-info@8.1.0: + dependencies: + lru-cache: 10.4.3 + + hosted-git-info@9.0.2: + dependencies: + lru-cache: 11.2.2 + + hpagent@1.2.0: {} + + html-escaper@2.0.2: {} + + http-cache-semantics@4.2.0: {} + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + http2-wrapper@1.0.3: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + human-signals@5.0.0: {} + + husky@9.1.7: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + optional: true + + ignore-walk@8.0.0: + dependencies: + minimatch: 10.0.3 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + indent-string@5.0.0: {} + + ini@5.0.0: {} + + ip-address@10.0.1: {} + + is-builtin-module@5.0.0: + dependencies: + builtin-modules: 5.0.0 + + is-bun-module@2.0.0: + dependencies: + semver: 7.7.2 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-fullwidth-code-point@4.0.0: {} + + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.4.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-path-cwd@3.0.0: {} + + is-path-inside@4.0.0: {} + + is-stream@2.0.1: {} + + is-stream@3.0.0: {} + + isexe@2.0.0: {} + + isexe@3.1.1: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jackspeak@4.1.1: + dependencies: + '@isaacs/cliui': 8.0.2 + + jiti@2.6.1: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.0.2: {} + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@4.0.0: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json-stringify-nice@1.1.4: {} + + json5@2.2.3: {} + + jsonparse@1.3.1: {} + + just-diff-apply@5.5.0: {} + + just-diff@6.0.2: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + libnpmexec@10.1.8: + dependencies: + '@npmcli/arborist': 9.1.6 + '@npmcli/package-json': 7.0.0 + '@npmcli/run-script': 10.0.0 + ci-info: 4.3.1 + npm-package-arg: 13.0.0 + pacote: 21.0.3 + proc-log: 5.0.0 + promise-retry: 2.0.1 + read: 4.1.0 + semver: 7.7.2 + signal-exit: 4.1.0 + walk-up-path: 4.0.0 + transitivePeerDependencies: + - supports-color + + libnpmpack@9.0.9: + dependencies: + '@npmcli/arborist': 9.1.6 + '@npmcli/run-script': 10.0.0 + npm-package-arg: 13.0.0 + pacote: 21.0.3 + transitivePeerDependencies: + - supports-color + + lilconfig@3.1.3: {} + + lint-staged@15.2.11: + dependencies: + chalk: 5.3.0 + commander: 12.1.0 + debug: 4.4.3 + execa: 8.0.1 + lilconfig: 3.1.3 + listr2: 8.2.5 + micromatch: 4.0.8 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.6.1 + transitivePeerDependencies: + - supports-color + + listr2@8.2.5: + dependencies: + cli-truncate: 4.0.0 + colorette: 2.0.20 + eventemitter3: 5.0.1 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.2 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + log-update@6.1.0: + dependencies: + ansi-escapes: 7.1.1 + cli-cursor: 5.0.0 + slice-ansi: 7.1.2 + strip-ansi: 7.1.2 + wrap-ansi: 9.0.2 + + lowercase-keys@2.0.0: {} + + lru-cache@10.4.3: {} + + lru-cache@11.2.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.3.5: + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.2 + + make-fetch-happen@14.0.3: + dependencies: + '@npmcli/agent': 3.0.0 + cacache: 19.0.1 + http-cache-semantics: 4.2.0 + minipass: 7.1.2 + minipass-fetch: 4.0.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 1.0.0 + proc-log: 5.0.0 + promise-retry: 2.0.1 + ssri: 12.0.0 + transitivePeerDependencies: + - supports-color + + make-fetch-happen@15.0.2: + dependencies: + '@npmcli/agent': 4.0.0 + cacache: 20.0.1 + http-cache-semantics: 4.2.0 + minipass: 7.1.2 + minipass-fetch: 4.0.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 1.0.0 + proc-log: 5.0.0 + promise-retry: 2.0.1 + ssri: 12.0.0 + transitivePeerDependencies: + - supports-color + + meow@13.2.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mimic-fn@2.1.0: {} + + mimic-fn@4.0.0: {} + + mimic-function@5.0.1: {} + + mimic-response@1.0.1: {} + + mimic-response@3.1.0: {} + + minimatch@10.0.3: + dependencies: + '@isaacs/brace-expansion': 5.0.0 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + minipass-collect@2.0.1: + dependencies: + minipass: 7.1.2 + + minipass-fetch@4.0.1: + dependencies: + minipass: 7.1.2 + minipass-sized: 1.0.3 + minizlib: 3.1.0 + optionalDependencies: + encoding: 0.1.13 + + minipass-flush@1.0.5: + dependencies: + minipass: 3.3.6 + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + + minipass-sized@1.0.3: + dependencies: + minipass: 3.3.6 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minipass@7.1.2: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + minizlib@3.1.0: + dependencies: + minipass: 7.1.2 + + mkdirp@1.0.4: {} + + mount-point@3.0.0: + dependencies: + '@sindresorhus/df': 1.0.1 + pify: 2.3.0 + pinkie-promise: 2.0.1 + + move-file@3.1.0: + dependencies: + path-exists: 5.0.0 + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + mute-stream@2.0.0: {} + + nanoid@3.3.11: {} + + napi-postinstall@0.3.4: {} + + natural-compare-lite@1.4.0: {} + + natural-compare@1.4.0: {} + + negotiator@1.0.0: {} + + node-fetch-native@1.6.7: {} + + node-gyp@11.5.0: + dependencies: + env-paths: 2.2.1 + exponential-backoff: 3.1.3 + graceful-fs: 4.2.11 + make-fetch-happen: 14.0.3 + nopt: 8.1.0 + proc-log: 5.0.0 + semver: 7.7.2 + tar: 7.5.1 + tinyglobby: 0.2.15 + which: 5.0.0 + transitivePeerDependencies: + - supports-color + + node-releases@2.0.26: {} + + nopt@8.1.0: + dependencies: + abbrev: 3.0.1 + + normalize-package-data@8.0.0: + dependencies: + hosted-git-info: 9.0.2 + semver: 7.7.2 + validate-npm-package-license: 3.0.4 + + normalize-path@3.0.0: {} + + normalize-url@6.1.0: {} + + npm-bundled@4.0.0: + dependencies: + npm-normalize-package-bin: 4.0.0 + + npm-install-checks@7.1.2: + dependencies: + semver: 7.7.2 + + npm-normalize-package-bin@4.0.0: {} + + npm-package-arg@12.0.2: + dependencies: + hosted-git-info: 8.1.0 + proc-log: 5.0.0 + semver: 7.7.2 + validate-npm-package-name: 6.0.2 + + npm-package-arg@13.0.0: + dependencies: + hosted-git-info: 9.0.2 + proc-log: 5.0.0 + semver: 7.7.2 + validate-npm-package-name: 6.0.2 + + npm-packlist@10.0.2: + dependencies: + ignore-walk: 8.0.0 + proc-log: 5.0.0 + + npm-pick-manifest@10.0.0: + dependencies: + npm-install-checks: 7.1.2 + npm-normalize-package-bin: 4.0.0 + npm-package-arg: 12.0.2 + semver: 7.7.2 + + npm-pick-manifest@11.0.1: + dependencies: + npm-install-checks: 7.1.2 + npm-normalize-package-bin: 4.0.0 + npm-package-arg: 13.0.0 + semver: 7.7.2 + + npm-registry-fetch@19.0.0: + dependencies: + '@npmcli/redact': 3.2.2 + jsonparse: 1.3.1 + make-fetch-happen: 15.0.2 + minipass: 7.1.2 + minipass-fetch: 4.0.1 + minizlib: 3.1.0 + npm-package-arg: 13.0.0 + proc-log: 5.0.0 + transitivePeerDependencies: + - supports-color + + npm-run-path@3.1.0: + dependencies: + path-key: 3.1.1 + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + ofetch@1.4.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + os-homedir@1.0.2: {} + + p-cancelable@2.1.1: {} + + p-finally@2.0.1: {} + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-map@7.0.3: {} + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + package-manager-detector@1.5.0: {} + + pacote@21.0.1: + dependencies: + '@npmcli/git': 6.0.3 + '@npmcli/installed-package-contents': 3.0.0 + '@npmcli/package-json': 7.0.0 + '@npmcli/promise-spawn': 8.0.3 + '@npmcli/run-script': 10.0.0 + cacache: 20.0.1 + fs-minipass: 3.0.3 + minipass: 7.1.2 + npm-package-arg: 13.0.0 + npm-packlist: 10.0.2 + npm-pick-manifest: 10.0.0 + npm-registry-fetch: 19.0.0 + proc-log: 5.0.0 + promise-retry: 2.0.1 + sigstore: 4.0.0 + ssri: 12.0.0 + tar: 7.5.1 + transitivePeerDependencies: + - supports-color + + pacote@21.0.3: + dependencies: + '@npmcli/git': 7.0.0 + '@npmcli/installed-package-contents': 3.0.0 + '@npmcli/package-json': 7.0.0 + '@npmcli/promise-spawn': 8.0.3 + '@npmcli/run-script': 10.0.0 + cacache: 20.0.1 + fs-minipass: 3.0.3 + minipass: 7.1.2 + npm-package-arg: 13.0.0 + npm-packlist: 10.0.2 + npm-pick-manifest: 11.0.1 + npm-registry-fetch: 19.0.0 + proc-log: 5.0.0 + promise-retry: 2.0.1 + sigstore: 4.0.0 + ssri: 12.0.0 + tar: 7.5.1 + transitivePeerDependencies: + - supports-color + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-conflict-json@4.0.0: + dependencies: + json-parse-even-better-errors: 4.0.0 + just-diff: 6.0.2 + just-diff-apply: 5.5.0 + + path-exists@4.0.0: {} + + path-exists@5.0.0: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-scurry@2.0.0: + dependencies: + lru-cache: 11.2.2 + minipass: 7.1.2 + + path-type@6.0.0: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pidtree@0.6.0: {} + + pify@2.3.0: {} + + pinkie-promise@2.0.1: + dependencies: + pinkie: 2.0.4 + + pinkie@2.0.4: {} + + pluralize@8.0.0: {} + + pnpm-workspace-yaml@1.3.0: + dependencies: + yaml: 2.8.1 + + postcss-selector-parser@7.1.0: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + presentable-error@0.0.1: {} + + proc-log@5.0.0: {} + + proggy@3.0.0: {} + + promise-all-reject-late@1.0.1: {} + + promise-call-limit@3.0.2: {} + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode@2.3.1: {} + + quansync@0.2.11: {} + + queue-microtask@1.2.3: {} + + quick-lru@5.1.1: {} + + read-cmd-shim@5.0.0: {} + + read@4.1.0: + dependencies: + mute-stream: 2.0.0 + + regexp-tree@0.1.27: {} + + regjsparser@0.12.0: + dependencies: + jsesc: 3.0.2 + + resolve-alpn@1.2.1: {} + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + responselike@2.0.1: + dependencies: + lowercase-keys: 2.0.0 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + retry@0.12.0: {} + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rollup@4.52.5: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.52.5 + '@rollup/rollup-android-arm64': 4.52.5 + '@rollup/rollup-darwin-arm64': 4.52.5 + '@rollup/rollup-darwin-x64': 4.52.5 + '@rollup/rollup-freebsd-arm64': 4.52.5 + '@rollup/rollup-freebsd-x64': 4.52.5 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.5 + '@rollup/rollup-linux-arm-musleabihf': 4.52.5 + '@rollup/rollup-linux-arm64-gnu': 4.52.5 + '@rollup/rollup-linux-arm64-musl': 4.52.5 + '@rollup/rollup-linux-loong64-gnu': 4.52.5 + '@rollup/rollup-linux-ppc64-gnu': 4.52.5 + '@rollup/rollup-linux-riscv64-gnu': 4.52.5 + '@rollup/rollup-linux-riscv64-musl': 4.52.5 + '@rollup/rollup-linux-s390x-gnu': 4.52.5 + '@rollup/rollup-linux-x64-gnu': 4.52.5 + '@rollup/rollup-linux-x64-musl': 4.52.5 + '@rollup/rollup-openharmony-arm64': 4.52.5 + '@rollup/rollup-win32-arm64-msvc': 4.52.5 + '@rollup/rollup-win32-ia32-msvc': 4.52.5 + '@rollup/rollup-win32-x64-gnu': 4.52.5 + '@rollup/rollup-win32-x64-msvc': 4.52.5 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safer-buffer@2.1.2: + optional: true + + semver@6.3.1: {} + + semver@7.7.2: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + sigstore@4.0.0: + dependencies: + '@sigstore/bundle': 4.0.0 + '@sigstore/core': 3.0.0 + '@sigstore/protobuf-specs': 0.5.0 + '@sigstore/sign': 4.0.1 + '@sigstore/tuf': 4.0.0 + '@sigstore/verify': 3.0.0 + transitivePeerDependencies: + - supports-color + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + slash@5.1.0: {} + + slice-ansi@5.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 4.0.0 + + slice-ansi@7.1.2: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + smart-buffer@4.2.0: {} + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + + socks@2.8.7: + dependencies: + ip-address: 10.0.1 + smart-buffer: 4.2.0 + + source-map-js@1.2.1: {} + + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.22 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.22 + + spdx-expression-parse@4.0.0: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.22 + + spdx-license-ids@3.0.22: {} + + sprintf-js@1.0.3: {} + + ssri@12.0.0: + dependencies: + minipass: 7.1.2 + + stable-hash-x@0.2.0: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + streaming-iterables@8.0.1: {} + + string-argv@0.3.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + strip-final-newline@2.0.0: {} + + strip-final-newline@3.0.0: {} + + strip-indent@4.1.1: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tapable@2.3.0: {} + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + tar@7.5.1: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 + + taze@19.6.0: + dependencies: + '@antfu/ni': 25.0.0 + cac: 6.7.14 + find-up-simple: 1.0.1 + ofetch: 1.4.1 + package-manager-detector: 1.5.0 + pathe: 2.0.3 + pnpm-workspace-yaml: 1.3.0 + restore-cursor: 5.1.0 + tinyexec: 1.0.1 + tinyglobby: 0.2.15 + unconfig: 7.3.3 + yaml: 2.8.1 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyexec@1.0.1: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinylogic@2.0.0: {} + + tinyrainbow@3.0.3: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + totalist@3.0.1: {} + + trash@10.0.0: + dependencies: + '@sindresorhus/chunkify': 2.0.0 + '@stroncium/procfs': 1.2.1 + globby: 14.1.0 + is-path-inside: 4.0.0 + move-file: 3.1.0 + p-map: 7.0.3 + xdg-trashdir: 3.1.0 + + treeify@1.1.0: {} + + treeverse@3.0.0: {} + + ts-api-utils@2.1.0(typescript@5.9.2): + dependencies: + typescript: 5.9.2 + + ts-declaration-location@1.0.7(typescript@5.9.2): + dependencies: + picomatch: 4.0.3 + typescript: 5.9.2 + + tsconfck@3.1.6(typescript@5.9.2): + optionalDependencies: + typescript: 5.9.2 + + tslib@1.14.1: {} + + tslib@2.8.1: {} + + tsutils@3.21.0(typescript@5.9.2): + dependencies: + tslib: 1.14.1 + typescript: 5.9.2 + + tuf-js@4.0.0: + dependencies: + '@tufjs/models': 4.0.0 + debug: 4.4.3 + make-fetch-happen: 15.0.2 + transitivePeerDependencies: + - supports-color + + typanion@3.14.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-coverage-core@2.29.7(typescript@5.9.2): + dependencies: + fast-glob: 3.3.3 + minimatch: 10.0.3 + normalize-path: 3.0.0 + tslib: 2.8.1 + tsutils: 3.21.0(typescript@5.9.2) + typescript: 5.9.2 + + type-coverage@2.29.7(typescript@5.9.2): + dependencies: + chalk: 4.1.2 + minimist: 1.2.8 + type-coverage-core: 2.29.7(typescript@5.9.2) + transitivePeerDependencies: + - typescript + + type-fest@0.21.3: {} + + typescript-eslint@8.44.1(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.2): + dependencies: + '@typescript-eslint/eslint-plugin': 8.44.1(@typescript-eslint/parser@8.44.1(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/parser': 8.44.1(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.9.2) + '@typescript-eslint/utils': 8.44.1(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.2) + eslint: 9.35.0(jiti@2.6.1) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + typescript@5.9.2: {} + + ufo@1.6.1: {} + + unconfig@7.3.3: + dependencies: + '@quansync/fs': 0.1.5 + defu: 6.1.4 + jiti: 2.6.1 + quansync: 0.2.11 + + undici-types@7.16.0: {} + + unicorn-magic@0.3.0: {} + + unique-filename@4.0.0: + dependencies: + unique-slug: 5.0.0 + + unique-slug@5.0.0: + dependencies: + imurmurhash: 0.1.4 + + unrs-resolver@1.11.1: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + + update-browserslist-db@1.1.4(browserslist@4.27.0): + dependencies: + browserslist: 4.27.0 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + user-home@2.0.0: + dependencies: + os-homedir: 1.0.2 + + util-deprecate@1.0.2: {} + + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + validate-npm-package-name@6.0.2: {} + + vite-tsconfig-paths@5.1.4(typescript@5.9.2)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(yaml@2.8.1)): + dependencies: + debug: 4.4.3 + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@5.9.2) + optionalDependencies: + vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + - typescript + + vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(yaml@2.8.1): + dependencies: + esbuild: 0.25.11 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.5 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.9.2 + fsevents: 2.3.3 + jiti: 2.6.1 + yaml: 2.8.1 + + vitest@4.0.3(@types/node@24.9.2)(@vitest/ui@4.0.3)(jiti@2.6.1)(yaml@2.8.1): + dependencies: + '@vitest/expect': 4.0.3 + '@vitest/mocker': 4.0.3(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(yaml@2.8.1)) + '@vitest/pretty-format': 4.0.3 + '@vitest/runner': 4.0.3 + '@vitest/snapshot': 4.0.3 + '@vitest/spy': 4.0.3 + '@vitest/utils': 4.0.3 + debug: 4.4.3 + es-module-lexer: 1.7.0 + expect-type: 1.2.2 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.9.2 + '@vitest/ui': 4.0.3(vitest@4.0.3) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + walk-up-path@4.0.0: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@5.0.0: + dependencies: + isexe: 3.1.1 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.1.2 + + wrappy@1.0.2: {} + + write-file-atomic@6.0.0: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + xdg-basedir@4.0.0: {} + + xdg-trashdir@3.1.0: + dependencies: + '@sindresorhus/df': 3.1.1 + mount-point: 3.0.0 + user-home: 2.0.0 + xdg-basedir: 4.0.0 + + yallist@3.1.1: {} + + yallist@4.0.0: {} + + yallist@5.0.0: {} + + yaml@2.6.1: {} + + yaml@2.8.1: {} + + yargs-parser@22.0.0: {} + + yocto-queue@0.1.0: {} + + yoctocolors-cjs@2.1.3: {} + + zod@4.1.12: {} diff --git a/packages/lib/scripts/babel/README.md b/packages/lib/scripts/babel/README.md new file mode 100644 index 000000000..e33e89bd0 --- /dev/null +++ b/packages/lib/scripts/babel/README.md @@ -0,0 +1,298 @@ +# Babel AST Transforms + +Source code transformations using Babel AST walkers + magic-string. + +## Pattern: Babel AST + magic-string + +**All transforms in this directory follow this pattern:** + +1. **Parse with Babel** - Get AST for analysis +2. **Walk with Babel traverse** - Find nodes to transform +3. **Edit with magic-string** - Surgical source modifications +4. **Preserve source maps** - magic-string maintains mappings + +**Rationale:** + +- Babel AST for parsing and semantic analysis +- magic-string for precise edits without re-printing +- Combines AST analysis with source-level modifications + +## Example Transform + +```javascript +import MagicString from 'magic-string' +const { parse } = await import('@babel/parser') +const traverse = (await import('@babel/traverse')).default +const t = await import('@babel/types') + +async function transform(filePath) { + const content = await fs.readFile(filePath, 'utf8') + const magicString = new MagicString(content) + + // 1. Parse + const ast = parse(content, { sourceType: 'module' }) + + // 2. Walk + traverse(ast, { + Identifier(path) { + if (path.node.name === 'oldName') { + // 3. Edit with magic-string (not Babel transform) + magicString.overwrite( + path.node.start, + path.node.end, + 'newName' + ) + } + } + }) + + // 4. Write + await fs.writeFile(filePath, magicString.toString(), 'utf8') +} +``` + +## Required Dependencies (Pinned) + +```json +{ + "@babel/parser": "7.28.4", + "@babel/traverse": "7.28.4", + "@babel/types": "7.28.4", + "magic-string": "0.30.19" +} +``` + +**Always pin versions for source transforms** to ensure consistent behavior across builds. + +## Available Transforms + +### `transform-commonjs-exports.mjs` + +Fixes TypeScript-compiled CommonJS exports for better compatibility. + +**Transforms:** +- `exports.default = value` → `module.exports = value` +- Removes `__esModule` markers +- Fixes `.default` accessor in imports + +**Usage:** + +```javascript +import { transformFile, fixImports } from './transform-commonjs-exports.mjs' + +// Transform exports +const result = await transformFile('dist/lib/constants/WIN32.js') + +// Fix imports +await fixImports('dist/lib/path.js', fixedModules) +``` + +**Why needed:** + +TypeScript compiles `export default X` to `exports.default = X`, requiring `.default` accessor in CommonJS. This transform makes it work without `.default`: + +```javascript +// Before: require('./WIN32').default +// After: require('./WIN32') +``` + +## Creating New Transforms + +1. **Create `transform-.mjs`** in this directory +2. **Follow the pattern**: Babel AST + magic-string +3. **Export functions**: `transformFile()`, etc. +4. **Document**: Add section to this README +5. **Pin versions**: Use exact dependency versions + +### Transform Template + +```javascript +/** + * @fileoverview Transform description. + * Uses Babel AST walkers + magic-string for surgical transformations. + */ + +import { promises as fs } from 'node:fs' +import MagicString from 'magic-string' + +// Pinned versions required: +// - @babel/parser@7.28.4 +// - @babel/traverse@7.28.4 +// - @babel/types@7.28.4 +// - magic-string@0.30.19 + +const { parse } = await import('@babel/parser') +const traverse = (await import('@babel/traverse')).default +const t = await import('@babel/types') + +function parseCode(code) { + return parse(code, { + sourceType: 'module', + // Add parser plugins as needed + }) +} + +export async function transformFile(filePath, options = {}) { + const content = await fs.readFile(filePath, 'utf8') + const magicString = new MagicString(content) + let modified = false + + try { + const ast = parseCode(content) + + traverse(ast, { + // Add visitors + Identifier(path) { + // Check conditions + if (shouldTransform(path.node)) { + // Use magic-string for edits + magicString.overwrite( + path.node.start, + path.node.end, + 'newValue' + ) + modified = true + } + } + }) + + if (modified) { + await fs.writeFile(filePath, magicString.toString(), 'utf8') + return { modified: true } + } + } catch (e) { + // Handle parse errors + } + + return { modified: false } +} +``` + +## Babel Plugins vs Transforms + +**Babel Plugins** (`registry/plugins/`): +- Run **during** Babel's transformation pipeline +- Use Babel's transformation API +- Return AST nodes +- Example: `babel-plugin-inline-require-calls.mjs` + +**Standalone Transforms** (`scripts/babel/`): +- Run **after** compilation as post-processing +- Use Babel AST for analysis only +- Use magic-string for source edits +- Example: `transform-commonjs-exports.mjs` + +**When to use each:** + +| Use Case | Tool | +|----------|------| +| Babel pipeline | Babel Plugin | +| Post-build fixes | Standalone Transform | +| Rollup integration | Babel Plugin | +| Script automation | Standalone Transform | + +## Integration with Build + +### Rollup `writeBundle` Hook (Recommended) + +Integrate transforms directly into Rollup's build pipeline using the `writeBundle` hook: + +```javascript +// .config/rollup.dist.config.mjs +import fastGlob from 'fast-glob' +import { + fixImports, + transformFile, +} from '../scripts/babel/transform-commonjs-exports.mjs' + +export default { + // ... other config + plugins: [ + // ... other plugins + { + name: 'transform-commonjs-exports', + async writeBundle() { + const files = await fastGlob('**/*.js', { + absolute: true, + cwd: distPath, + }) + + const fixedModules = new Set() + + // First pass: transform exports.default to module.exports + for (const file of files) { + const result = await transformFile(file) + if (result.modified && result.moduleName) { + fixedModules.add(result.moduleName) + } + } + + // Second pass: fix .default accessors in imports + for (const file of files) { + await fixImports(file, fixedModules) + } + }, + }, + ], +} +``` + +### Standalone Script (Alternative) + +For projects not using Rollup, run as a standalone script: + +```javascript +// scripts/fix-commonjs-exports.mjs +import { transformFile, fixImports } from './babel/transform-commonjs-exports.mjs' +import fastGlob from 'fast-glob' + +const files = await fastGlob('dist/**/*.js') +const fixedModules = new Set() + +// First pass: transform exports +for (const file of files) { + const result = await transformFile(file) + if (result.modified) { + fixedModules.add(result.moduleName) + } +} + +// Second pass: fix imports +for (const file of files) { + await fixImports(file, fixedModules) +} +``` + +```json +// package.json +{ + "scripts": { + "build": "tsgo && node scripts/fix-commonjs-exports.mjs" + } +} +``` + +## Best Practices + +1. **Always use magic-string** - Don't use Babel's code generator for transforms +2. **Pin dependency versions** - Source transforms need stability +3. **Parse once** - Cache AST if walking multiple times +4. **Handle errors gracefully** - Skip unparseable files +5. **Test thoroughly** - Verify source maps still work +6. **Document transformations** - Explain why each transform is needed + +## Performance + +- Babel parsing (optimized C++ parser) +- AST analysis (JavaScript object traversal) +- magic-string edits (string slicing, no re-parsing) +- No code generation (skips Babel's printer) + +< 10ms per file for most transforms. + +## References + +- [Babel Parser](https://babeljs.io/docs/babel-parser) +- [Babel Traverse](https://babeljs.io/docs/babel-traverse) +- [Babel Types](https://babeljs.io/docs/babel-types) +- [magic-string](https://github.com/rich-harris/magic-string) diff --git a/packages/lib/scripts/babel/transform-commonjs-exports.mjs b/packages/lib/scripts/babel/transform-commonjs-exports.mjs new file mode 100644 index 000000000..28edd1359 --- /dev/null +++ b/packages/lib/scripts/babel/transform-commonjs-exports.mjs @@ -0,0 +1,304 @@ +/** + * @fileoverview Babel transform to fix CommonJS exports compatibility. + * Uses Babel AST walkers + magic-string for surgical transformations. + * + * Transforms: + * - exports.default = value → module.exports = value + * - Removes __esModule markers + * - Fixes .default accessor in imports + * + * Pattern: Babel AST for analysis + magic-string for source manipulation. + */ + +import { promises as fs } from 'node:fs' +import { isBuiltin } from 'node:module' +import path from 'node:path' + +import { parse } from '@babel/parser' +import traverseModule from '@babel/traverse' +import * as t from '@babel/types' +import MagicString from 'magic-string' + +// Pinned versions required: +// - @babel/parser@7.28.4 +// - @babel/traverse@7.28.4 +// - @babel/types@7.28.4 +// - magic-string@0.30.19 + +const traverse = traverseModule.default + +/** + * Parse JavaScript code into AST. + */ +function parseCode(code) { + return parse(code, { + allowImportExportEverywhere: true, + allowReturnOutsideFunction: true, + sourceType: 'module', + }) +} + +/** + * Check if AST node is exports.default assignment. + */ +function isExportsDefaultAssignment(node) { + return ( + t.isAssignmentExpression(node) && + t.isMemberExpression(node.left) && + t.isIdentifier(node.left.object, { name: 'exports' }) && + t.isIdentifier(node.left.property, { name: 'default' }) + ) +} + +/** + * Check if AST node is __esModule marker. + */ +function isESModuleMarker(node) { + return ( + t.isCallExpression(node) && + t.isMemberExpression(node.callee) && + t.isIdentifier(node.callee.object, { name: 'Object' }) && + t.isIdentifier(node.callee.property, { name: 'defineProperty' }) && + node.arguments.length >= 2 && + t.isIdentifier(node.arguments[0], { name: 'exports' }) && + t.isStringLiteral(node.arguments[1], { value: '__esModule' }) + ) +} + +/** + * Transform a file to fix CommonJS exports. + */ +export async function transformFile(filePath, options = {}) { + const { keepDefaultExport = new Set(), logger } = options + + const filename = path.basename(filePath) + const dirname = path.basename(path.dirname(filePath)) + + // Skip files that need to keep default export. + if (keepDefaultExport.has(filename)) { + return { modified: false } + } + + // Skip non-js files. + if (!filename.endsWith('.js')) { + return { modified: false } + } + + const content = await fs.readFile(filePath, 'utf8') + const magicString = new MagicString(content) + + // Special handling for constants/index.js aggregation file. + if (filename === 'index.js' && dirname === 'constants') { + const searchStr = 'exports.default = (0, objects_1.createConstantsObject)' + const replaceStr = 'module.exports = (0, objects_1.createConstantsObject)' + const index = content.indexOf(searchStr) + if (index !== -1) { + magicString.overwrite(index, index + searchStr.length, replaceStr) + await fs.writeFile(filePath, magicString.toString(), 'utf8') + return { dirname, filename, modified: true, moduleName: 'index' } + } + } + + try { + const ast = parseCode(content) + let hasDefaultExport = false + let hasOtherExports = false + let modified = false + const nodesToRemove = [] + + // First pass: analyze exports. + traverse(ast, { + AssignmentExpression(path) { + const { node } = path + if (isExportsDefaultAssignment(node)) { + hasDefaultExport = true + } else if ( + t.isMemberExpression(node.left) && + t.isIdentifier(node.left.object, { name: 'exports' }) && + !t.isIdentifier(node.left.property, { name: 'default' }) + ) { + hasOtherExports = true + } + }, + }) + + // Second pass: transform if safe (only default export). + if (hasDefaultExport && !hasOtherExports) { + traverse(ast, { + ExpressionStatement(path) { + const { node } = path + + // Replace exports.default = value with module.exports = value. + if ( + t.isAssignmentExpression(node.expression) && + isExportsDefaultAssignment(node.expression) + ) { + const start = node.start + const exportsDefaultStr = 'exports.default' + const index = content.indexOf(exportsDefaultStr, start) + if (index !== -1) { + magicString.overwrite( + index, + index + exportsDefaultStr.length, + 'module.exports', + ) + modified = true + } + } + // Mark __esModule markers for removal. + else if ( + t.isCallExpression(node.expression) && + isESModuleMarker(node.expression) + ) { + nodesToRemove.push(node) + } + }, + }) + } + + // Remove __esModule markers if we made modifications. + if (modified) { + for (const node of nodesToRemove) { + if (node.start !== null && node.end !== null) { + let end = node.end + // Include trailing semicolon and newline. + if (content[end] === ';') { + end += 1 + } + if (content[end] === '\n') { + end += 1 + } + magicString.remove(node.start, end) + } + } + + await fs.writeFile(filePath, magicString.toString(), 'utf8') + + const moduleName = filename.replace('.js', '') + return { dirname, filename, modified: true, moduleName } + } + } catch (e) { + if (logger) { + logger.warn(`Could not parse ${filePath}: ${e.message}`) + } + } + + return { modified: false } +} + +/** + * Fix .default accessors in imports. + */ +export async function fixImports(filePath, fixedModules, options = {}) { + const { logger } = options + + const content = await fs.readFile(filePath, 'utf8') + const magicString = new MagicString(content) + let modified = false + + try { + const ast = parseCode(content) + + traverse(ast, { + // Fix: require('./constants/X').default → require('./constants/X') + CallExpression(path) { + const { node } = path + if ( + !t.isIdentifier(node.callee, { name: 'require' }) || + !t.isStringLiteral(node.arguments[0]) + ) { + return + } + + const modulePath = node.arguments[0].value + const moduleName = extractModuleName(modulePath) + + if (moduleName && fixedModules.has(moduleName)) { + const parent = path.parent + if ( + t.isMemberExpression(parent) && + parent.object === node && + t.isIdentifier(parent.property, { name: 'default' }) + ) { + // Remove .default accessor (include the dot). + const start = parent.property.start - 1 + const end = parent.property.end + magicString.remove(start, end) + modified = true + } + } + }, + + // Fix: variableName_1.default → variableName_1 + MemberExpression(path) { + const { node } = path + if ( + !t.isIdentifier(node.object) || + !t.isIdentifier(node.property, { name: 'default' }) || + !/_\d+$/.test(node.object.name) + ) { + return + } + + const varName = node.object.name + let requirePath = null + + // Find the require statement for this variable. + traverse(ast, { + VariableDeclarator(declPath) { + if ( + t.isIdentifier(declPath.node.id, { name: varName }) && + t.isCallExpression(declPath.node.init) && + t.isIdentifier(declPath.node.init.callee, { name: 'require' }) && + t.isStringLiteral(declPath.node.init.arguments[0]) + ) { + requirePath = declPath.node.init.arguments[0].value + declPath.stop() + } + }, + }) + + if (requirePath) { + const isNodeBuiltin = isBuiltin(requirePath) + const moduleName = extractModuleName(requirePath) + const shouldFix = isNodeBuiltin || fixedModules.has(moduleName) + + if (shouldFix) { + // Remove .default accessor (include the dot). + const start = node.property.start - 1 + const end = node.property.end + magicString.remove(start, end) + modified = true + } + } + }, + }) + + if (modified) { + await fs.writeFile(filePath, magicString.toString(), 'utf8') + return { filename: path.basename(filePath), modified: true } + } + } catch (e) { + if (logger) { + logger.warn(`Could not parse ${filePath}: ${e.message}`) + } + } + + return { modified: false } +} + +/** + * Extract module name from require path. + */ +function extractModuleName(modulePath) { + if (modulePath.includes('/constants/')) { + const match = modulePath.match(/\/constants\/([^/]+)$/) + return match ? match[1] : null + } + if (modulePath.includes('/external/')) { + const match = modulePath.match(/\/external\/(.+)$/) + return match ? match[1] : null + } + const match = modulePath.match(/\/([^/]+)$/) + return match ? match[1].replace(/-/g, '-') : null +} diff --git a/packages/lib/scripts/babel/transform-set-proto-plugin.mjs b/packages/lib/scripts/babel/transform-set-proto-plugin.mjs new file mode 100644 index 000000000..864ac0388 --- /dev/null +++ b/packages/lib/scripts/babel/transform-set-proto-plugin.mjs @@ -0,0 +1,45 @@ +// Helper to check if something is a .__proto__ access. +function isProtoAccess(node, t) { + return ( + t.isMemberExpression(node) && + t.isIdentifier(node.property, { name: '__proto__' }) + ) +} + +// Unwraps A.__proto__ or A.prototype.__proto__. +function unwrapProto(node, t) { + const { object } = node + return { + object, + isPrototype: + t.isMemberExpression(object) && + t.isIdentifier(object.property, { name: 'prototype' }), + } +} + +export default function ({ types: t }) { + return { + name: 'transform-set-proto', + visitor: { + ExpressionStatement(path) { + const { expression: expr } = path.node + // Handle: Xyz.prototype.__proto__ = foo + if (t.isAssignmentExpression(expr) && isProtoAccess(expr.left, t)) { + const { object } = unwrapProto(expr.left, t) + const { right } = expr + path.replaceWith( + t.expressionStatement( + t.callExpression( + t.memberExpression( + t.identifier('Object'), + t.identifier('setPrototypeOf'), + ), + [object, right], + ), + ), + ) + } + }, + }, + } +} diff --git a/packages/lib/scripts/babel/transform-url-parse-plugin.mjs b/packages/lib/scripts/babel/transform-url-parse-plugin.mjs new file mode 100644 index 000000000..473414741 --- /dev/null +++ b/packages/lib/scripts/babel/transform-url-parse-plugin.mjs @@ -0,0 +1,39 @@ +export default function ({ types: t }) { + return { + name: 'transform-url-parse', + visitor: { + CallExpression(path) { + const { node } = path + // Match `url.parse(...)` calls with exactly one argument. + if ( + node.callee.type === 'MemberExpression' && + node.callee.object.name === 'url' && + node.callee.property.name === 'parse' && + node.arguments.length === 1 + ) { + const { parent } = path + // Create an AST node for `new URL()`. + const newUrl = t.newExpression(t.identifier('URL'), [ + node.arguments[0], + ]) + // Check if the result of `url.parse()` is immediately accessed, e.g. + // `url.parse(x).protocol`. + if (parent.type === 'MemberExpression' && parent.object === node) { + // Replace the full `url.parse(x).protocol` with `(new URL(x)).protocol`. + path.parentPath.replaceWith( + t.memberExpression( + newUrl, + parent.property, + // Handle dynamic props like `['protocol']`. + parent.computed, + ), + ) + } else { + // Otherwise, replace `url.parse(x)` with `new URL(x)`. + path.replaceWith(newUrl) + } + } + }, + }, + } +} diff --git a/packages/lib/scripts/build-externals.mjs b/packages/lib/scripts/build-externals.mjs new file mode 100644 index 000000000..fad4f137a --- /dev/null +++ b/packages/lib/scripts/build-externals.mjs @@ -0,0 +1,43 @@ +/** + * @fileoverview Bundle external dependencies into standalone zero-dependency modules. + * This bundles packages like cacache, pacote, make-fetch-happen into dist/external. + * + * Entry point that wraps the modular build-externals system. + */ + +import colors from 'yoctocolors-cjs' + +import { isQuiet } from '#socketsecurity/lib/argv/flags' +import { getDefaultLogger } from '#socketsecurity/lib/logger' +import { pluralize } from '#socketsecurity/lib/words' + +import { buildExternals } from './build-externals/orchestrator.mjs' + +const logger = getDefaultLogger() +const printCompletedHeader = title => console.log(colors.green(`✓ ${title}`)) + +async function main() { + // Check for verbose mode via isVerbose or manual check + const verbose = process.argv.includes('--verbose') + const quiet = isQuiet() + + try { + const { bundledCount } = await buildExternals({ verbose, quiet }) + + if (!quiet) { + const title = + bundledCount > 0 + ? `External Bundles (${bundledCount} ${pluralize('package', { count: bundledCount })})` + : 'External Bundles (no packages)' + printCompletedHeader(title) + } + } catch (error) { + logger.error(`Build failed: ${error.message || error}`) + process.exitCode = 1 + } +} + +main().catch(error => { + logger.error(`Build failed: ${error.message || error}`) + process.exitCode = 1 +}) diff --git a/packages/lib/scripts/build-externals/bundler.mjs b/packages/lib/scripts/build-externals/bundler.mjs new file mode 100644 index 000000000..b8aafe0b5 --- /dev/null +++ b/packages/lib/scripts/build-externals/bundler.mjs @@ -0,0 +1,121 @@ +/** + * @fileoverview Package bundling logic using esbuild. + */ + +import { promises as fs } from 'node:fs' +import { createRequire } from 'node:module' +import path from 'node:path' + +import esbuild from 'esbuild' +import { + getEsbuildConfig, + getPackageSpecificOptions, +} from './esbuild-config.mjs' +import { + getLocalPackagePath, + resolveLocalEntryPoint, +} from './local-packages.mjs' + +const require = createRequire(import.meta.url) + +/** + * Bundle a single package with esbuild. + * + * @param {string} packageName - Name of the package to bundle + * @param {string} outputPath - Output file path + * @param {object} options - Bundling options + * @param {boolean} options.quiet - Suppress output + * @param {string} options.rootDir - Root directory + * @returns {Promise} Size in KB or undefined on error + */ +export async function bundlePackage(packageName, outputPath, options = {}) { + const { quiet = false, rootDir } = options + + if (!quiet) { + console.log(` Bundling ${packageName}...`) + } + + try { + // Check if package is installed. + let packagePath + + // First, check if src/external/{packageName}.js exists - use as entry point. + // Preserve scope for scoped packages like @socketregistry/yocto-spinner + const srcExternalPath = path.join( + rootDir, + 'src', + 'external', + `${packageName}.js`, + ) + try { + await fs.access(srcExternalPath) + packagePath = srcExternalPath + if (!quiet) { + console.log( + ` Using entry point ${path.relative(rootDir, srcExternalPath)}`, + ) + } + } catch { + // No src/external file, check for local workspace/sibling versions (dev mode). + const localPath = await getLocalPackagePath(packageName, rootDir) + if (localPath) { + if (!quiet) { + console.log( + ` Using local version from ${path.relative(rootDir, localPath)}`, + ) + } + packagePath = await resolveLocalEntryPoint(localPath) + } else { + // Fall back to installed version. + try { + packagePath = require.resolve(packageName) + } catch { + // Package must be installed for bundling - no fallbacks. + throw new Error( + `Package "${packageName}" is not installed. Please install it with: pnpm add -D ${packageName}`, + ) + } + } + } + + // Get package-specific optimizations. + const packageOpts = getPackageSpecificOptions(packageName) + + // Get esbuild configuration. + const config = getEsbuildConfig(packagePath, outputPath, packageOpts) + + // Bundle the package with esbuild. + await esbuild.build(config) + + // Add a header comment to the bundled file. + const bundleContent = await fs.readFile(outputPath, 'utf8') + // Strip 'use strict' from bundle content if present (will be re-added at top) + const contentWithoutStrict = bundleContent.replace(/^"use strict";\n/, '') + const finalContent = `"use strict"; +/** + * Bundled from ${packageName} + * This is a zero-dependency bundle created by esbuild. + */ +${contentWithoutStrict}` + await fs.writeFile(outputPath, finalContent) + + // Get file size for logging. + const stats = await fs.stat(outputPath) + const sizeKB = Math.round(stats.size / 1024) + if (!quiet) { + console.log(` ✓ Bundled ${packageName} (${sizeKB}KB)`) + } + return sizeKB + } catch (error) { + if (!quiet) { + console.error(` ✗ Failed to bundle ${packageName}:`, error.message) + } + // Create error stub. + const stubContent = `'use strict' + +// Failed to bundle ${packageName}: ${error.message} +throw new Error('Failed to bundle ${packageName}') +` + await fs.writeFile(outputPath, stubContent) + } +} diff --git a/packages/lib/scripts/build-externals/config.mjs b/packages/lib/scripts/build-externals/config.mjs new file mode 100644 index 000000000..ffff96686 --- /dev/null +++ b/packages/lib/scripts/build-externals/config.mjs @@ -0,0 +1,63 @@ +/** + * @fileoverview External package configuration. + * Defines which packages need bundling and their scopes. + */ + +// Define which packages need bundling (ones that are actual npm packages). +export const externalPackages = [ + // NPM internals + { name: 'cacache', bundle: true }, + { name: 'pacote', bundle: true }, + { name: 'make-fetch-happen', bundle: true }, + { name: 'libnpmexec', bundle: true }, + { name: 'libnpmpack', bundle: true }, + { name: 'npm-package-arg', bundle: true }, + { name: 'normalize-package-data', bundle: true }, + // Utilities + { name: 'debug', bundle: true }, + { name: 'del', bundle: true }, + { name: 'fast-glob', bundle: true }, + { name: 'fast-sort', bundle: true }, + { name: 'get-east-asian-width', bundle: true }, + { name: 'picomatch', bundle: true }, + { name: 'semver', bundle: true }, + { name: 'spdx-correct', bundle: true }, + { name: 'spdx-expression-parse', bundle: true }, + { name: 'streaming-iterables', bundle: true }, + { name: 'validate-npm-package-name', bundle: true }, + { name: 'which', bundle: true }, + { name: 'yargs-parser', bundle: true }, + { name: 'yoctocolors-cjs', bundle: true }, + // Used by socket-cli (dist/cli.js has minified zod). + { name: 'zod', bundle: true }, +] + +// Scoped packages need special handling. +export const scopedPackages = [ + { + scope: '@npmcli', + packages: ['package-json', 'promise-spawn'], + bundle: true, + subpaths: ['package-json/lib/read-package.js', 'package-json/lib/sort.js'], + }, + { + scope: '@inquirer', + packages: [ + 'checkbox', + 'confirm', + 'core', + 'input', + 'password', + 'prompts', + 'search', + 'select', + ], + optional: true, + }, + { + scope: '@socketregistry', + packages: ['packageurl-js', 'is-unicode-supported', 'yocto-spinner'], + optional: true, + }, + { scope: '@yarnpkg', name: 'extensions', bundle: true }, +] diff --git a/packages/lib/scripts/build-externals/copy-files.mjs b/packages/lib/scripts/build-externals/copy-files.mjs new file mode 100644 index 000000000..00f53091e --- /dev/null +++ b/packages/lib/scripts/build-externals/copy-files.mjs @@ -0,0 +1,124 @@ +/** + * @fileoverview File copying utilities for external dependencies. + */ + +import { promises as fs } from 'node:fs' +import path from 'node:path' + +/** + * Ensure directory exists. + * + * @param {string} dir - Directory path + * @returns {Promise} + */ +export async function ensureDir(dir) { + await fs.mkdir(dir, { recursive: true }) +} + +/** + * Copy local TypeScript declaration files only. + * JavaScript files are either bundled by esbuild or manually vendored (handled separately). + * + * @param {string} srcDir - Source directory + * @param {string} destDir - Destination directory + * @param {boolean} quiet - Suppress output + * @returns {Promise} Number of files copied + */ +export async function copyLocalFiles(srcDir, destDir, quiet = false) { + const files = await fs.readdir(srcDir) + let count = 0 + + for (const file of files) { + // Only copy .d.ts files (hand-written type definitions) + // .js files are either bundled by esbuild or don't need to be in dist + if (file.endsWith('.d.ts')) { + const srcPath = path.join(srcDir, file) + const destPath = path.join(destDir, file) + + await fs.copyFile(srcPath, destPath) + if (!quiet) { + console.log(` Copied ${file}`) + } + count++ + } + } + + return count +} + +/** + * Recursively copy a directory. + * + * @param {string} srcPath - Source path + * @param {string} destPath - Destination path + * @param {string} relativePath - Relative path for logging + * @param {boolean} quiet - Suppress output + * @returns {Promise} Number of files copied + */ +export async function copyRecursive( + srcPath, + destPath, + relativePath = '', + quiet = false, +) { + await ensureDir(destPath) + const entries = await fs.readdir(srcPath, { withFileTypes: true }) + let count = 0 + + for (const entry of entries) { + const srcEntry = path.join(srcPath, entry.name) + const destEntry = path.join(destPath, entry.name) + const relPath = path.join(relativePath, entry.name) + + if (entry.isDirectory()) { + // Recursively copy directory + count += await copyRecursive(srcEntry, destEntry, relPath, quiet) + } else { + // Only copy if the file doesn't already exist (i.e., wasn't bundled). + try { + await fs.access(destEntry) + // File exists (was bundled), skip copying. + } catch { + // File doesn't exist, copy it. + await fs.copyFile(srcEntry, destEntry) + if (!quiet) { + console.log(` Copied ${relPath}`) + } + count++ + } + } + } + + return count +} + +/** + * Copy scoped package directories. + * + * @param {string} srcDir - Source directory + * @param {string} destDir - Destination directory + * @param {Array} scopedPackages - List of scoped packages + * @param {boolean} quiet - Suppress output + * @returns {Promise} Number of files copied + */ +export async function copyScopedFiles( + srcDir, + destDir, + scopedPackages, + quiet = false, +) { + let count = 0 + + for (const { scope } of scopedPackages) { + const scopeSrcDir = path.join(srcDir, scope) + const scopeDistDir = path.join(destDir, scope) + + try { + count += await copyRecursive(scopeSrcDir, scopeDistDir, scope, quiet) + } catch { + // Scope directory doesn't exist. + } + } + + return count +} diff --git a/packages/lib/scripts/build-externals/esbuild-config.mjs b/packages/lib/scripts/build-externals/esbuild-config.mjs new file mode 100644 index 000000000..fbea0a661 --- /dev/null +++ b/packages/lib/scripts/build-externals/esbuild-config.mjs @@ -0,0 +1,275 @@ +/** + * @fileoverview esbuild configuration for external package bundling. + */ + +import { readFileSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const stubsDir = path.join(__dirname, 'stubs') + +/** + * Stub configuration - maps module patterns to stub files. + * Only includes conservative stubs that are safe to use. + */ +const STUB_MAP = { + // Character encoding - we only use UTF-8 + '^(encoding|iconv-lite)$': 'encoding.cjs', + + // Debug logging - already disabled via process.env.DEBUG = undefined + '^debug$': 'debug.cjs', +} + +// Import createRequire at top level +import { createRequire } from 'node:module' + +const requireResolve = createRequire(import.meta.url) + +/** + * Create esbuild plugin to force npm packages to resolve from node_modules. + * This prevents tsconfig.json path mappings from creating circular dependencies. + * + * @returns {import('esbuild').Plugin} + */ +function createForceNodeModulesPlugin() { + /** + * Packages that must be resolved from node_modules to prevent circular dependencies. + * + * THE PROBLEM: + * ──────────── + * Some packages have tsconfig.json path mappings like: + * "cacache": ["./src/external/cacache"] + * + * This creates a circular dependency during bundling: + * + * ┌─────────────────────────────────────────────────┐ + * │ │ + * │ esbuild bundles: src/external/cacache.js │ + * │ ↓ │ + * │ File contains: require('cacache') │ + * │ ↓ │ + * │ tsconfig redirects: 'cacache' → src/external/ │ ← LOOP! + * │ ↓ │ + * │ esbuild tries to bundle: src/external/cacache │ + * │ ↓ │ + * │ Circular reference! ⚠️ │ + * └─────────────────────────────────────────────────┘ + * + * THE SOLUTION: + * ───────────── + * This plugin intercepts resolution and forces these packages to resolve + * from node_modules, bypassing the tsconfig path mappings: + * + * src/external/cacache.js + * ↓ + * require('cacache') + * ↓ + * Plugin intercepts → node_modules/cacache ✓ + * + * PACKAGES WITH ACTUAL TSCONFIG MAPPINGS (as of now): + * ──────────────────────────────────────────────────── + * ✓ cacache - line 37 in tsconfig.json + * ✓ make-fetch-happen - line 38 in tsconfig.json + * ✓ fast-sort - line 39 in tsconfig.json + * ✓ pacote - line 40 in tsconfig.json + * + * ADDITIONAL PACKAGES (defensive): + * ──────────────────────────────── + * · libnpmexec - Related to pacote, included for consistency + * · libnpmpack - Related to pacote, included for consistency + * · npm-package-arg - Related to pacote, included for consistency + * · normalize-package-data - Related to npm packages, included for consistency + * + * NOTE: Other external packages (debug, del, semver, etc.) don't have + * tsconfig mappings, so they naturally resolve from node_modules without + * needing to be listed here. + */ + const packagesWithPathMappings = [ + 'cacache', + 'make-fetch-happen', + 'fast-sort', + 'pacote', + 'libnpmexec', + 'libnpmpack', + 'npm-package-arg', + 'normalize-package-data', + ] + + return { + name: 'force-node-modules', + setup(build) { + for (const pkg of packagesWithPathMappings) { + build.onResolve({ filter: new RegExp(`^${pkg}$`) }, args => { + // Only intercept if not already in node_modules + if (!args.importer.includes('node_modules')) { + try { + return { path: requireResolve.resolve(pkg), external: false } + } catch { + // Package not found, let esbuild handle the error + return null + } + } + return null + }) + } + }, + } +} + +/** + * Create esbuild plugin to stub modules using files from stubs/ directory. + * + * @param {Record} stubMap - Map of regex patterns to stub filenames + * @returns {import('esbuild').Plugin} + */ +function createStubPlugin(stubMap = STUB_MAP) { + // Pre-compile regex patterns and load stub contents + const stubs = Object.entries(stubMap).map(([pattern, filename]) => ({ + filter: new RegExp(pattern), + contents: readFileSync(path.join(stubsDir, filename), 'utf8'), + stubFile: filename, + })) + + return { + name: 'stub-modules', + setup(build) { + for (const { contents, filter, stubFile } of stubs) { + // Resolve: mark modules as stubbed + build.onResolve({ filter }, args => ({ + path: args.path, + namespace: `stub:${stubFile}`, + })) + + // Load: return stub file contents + build.onLoad({ filter: /.*/, namespace: `stub:${stubFile}` }, () => ({ + contents, + loader: 'js', + })) + } + }, + } +} + +/** + * Get package-specific esbuild options. + * + * @param {string} packageName - The package name + * @returns {object} Package-specific esbuild options + */ +export function getPackageSpecificOptions(packageName) { + const opts = {} + + if (packageName === 'browserslist') { + // Browserslist's data updates frequently - we can exclude some update checking. + opts.define = { + 'process.versions.node': '"18.0.0"', + } + } else if (packageName === 'zod') { + // Zod has localization files we don't need. + opts.external = [...(opts.external || []), './locales/*'] + } else if (packageName.startsWith('@inquirer/')) { + // Inquirer packages have heavy dependencies we might not need. + opts.external = [...(opts.external || []), 'rxjs/operators'] + } else if (packageName === '@socketregistry/packageurl-js') { + // packageurl-js imports from socket-lib, creating a circular dependency. + // Mark socket-lib imports as external to avoid bundling issues. + opts.external = [...(opts.external || []), '@socketsecurity/lib/*'] + } else if (packageName === 'yargs-parser') { + // yargs-parser uses import.meta.url which isn't available in CommonJS. + // Replace import.meta.url with __filename wrapped in pathToFileURL. + opts.define = { + ...opts.define, + 'import.meta.url': '__filename', + } + } + + return opts +} + +/** + * Get base esbuild configuration for bundling. + * + * @param {string} entryPoint - Entry point path + * @param {string} outfile - Output file path + * @param {object} packageOpts - Package-specific options + * @returns {object} esbuild configuration + */ +export function getEsbuildConfig(entryPoint, outfile, packageOpts = {}) { + return { + entryPoints: [entryPoint], + bundle: true, + platform: 'node', + target: 'node18', + format: 'cjs', + outfile, + external: [ + 'node:*', + 'fs', + 'path', + 'os', + 'crypto', + 'stream', + 'util', + 'events', + 'child_process', + 'http', + 'https', + 'net', + 'url', + 'zlib', + 'buffer', + 'querystring', + 'string_decoder', + 'tty', + 'assert', + 'perf_hooks', + 'worker_threads', + 'v8', + 'vm', + '@socketsecurity/registry', + ...(packageOpts.external || []), + ], + plugins: [createForceNodeModulesPlugin(), createStubPlugin()], + minify: false, + sourcemap: false, + metafile: true, + logLevel: 'error', + treeShaking: true, + // Keep function names for better error messages. + keepNames: true, + // Additional optimizations: + pure: ['console.log', 'console.debug', 'console.warn'], + drop: ['debugger', 'console'], + ignoreAnnotations: false, + // Define compile-time constants for dead code elimination. + define: { + 'process.env.NODE_ENV': '"production"', + __DEV__: 'false', + 'global.GENTLY': 'false', + 'process.env.DEBUG': 'undefined', + 'process.browser': 'false', + 'process.env.VERBOSE': 'false', + window: 'undefined', + document: 'undefined', + navigator: 'undefined', + HTMLElement: 'undefined', + localStorage: 'undefined', + sessionStorage: 'undefined', + XMLHttpRequest: 'undefined', + WebSocket: 'undefined', + __TEST__: 'false', + 'process.env.CI': 'false', + __JEST__: 'false', + __MOCHA__: 'false', + 'process.env.JEST_WORKER_ID': 'undefined', + 'process.env.NODE_TEST': 'undefined', + ...packageOpts.define, + }, + charset: 'utf8', + // Banner for generated code + banner: { + js: '"use strict";', + }, + } +} diff --git a/packages/lib/scripts/build-externals/local-packages.mjs b/packages/lib/scripts/build-externals/local-packages.mjs new file mode 100644 index 000000000..363db6d17 --- /dev/null +++ b/packages/lib/scripts/build-externals/local-packages.mjs @@ -0,0 +1,87 @@ +/** + * @fileoverview Local package resolution for development. + * Checks for local workspace or sibling project versions. + */ + +import { promises as fs } from 'node:fs' +import path from 'node:path' + +/** + * Check if local workspace or sibling project versions exist. + * Used for development to use local changes instead of published packages. + * + * @param {string} packageName - The package name to search for + * @param {string} rootDir - The root directory of the project + * @returns {Promise} Path to local package or null + */ +export async function getLocalPackagePath(packageName, rootDir) { + const checks = [] + + // Check workspace packages (e.g. @socketregistry/yocto-spinner). + if (packageName.startsWith('@socketregistry/')) { + const pkgName = packageName.replace('@socketregistry/', '') + const workspacePath = path.resolve( + rootDir, + '..', + 'packages', + 'npm', + pkgName, + ) + checks.push(workspacePath) + } + + // Check sibling projects (e.g. socket-packageurl-js). + if (packageName === '@socketregistry/packageurl-js') { + const siblingPath = path.resolve( + rootDir, + '..', + '..', + 'socket-packageurl-js', + ) + checks.push(siblingPath) + } + + // Return first existing path. + for (const checkPath of checks) { + try { + await fs.access(path.join(checkPath, 'package.json')) + return checkPath + } catch { + // Path doesn't exist, continue. + } + } + + return null +} + +/** + * Resolve the entry point for a local package. + * + * @param {string} localPath - Path to the local package + * @returns {Promise} Entry point path + */ +export async function resolveLocalEntryPoint(localPath) { + const localPkgJson = JSON.parse( + await fs.readFile(path.join(localPath, 'package.json'), 'utf8'), + ) + + // Resolve the main export - handle nested exports structure. + let mainExport = localPkgJson.main || 'index.js' + const exportsField = localPkgJson.exports?.['.'] + + if (exportsField) { + if (typeof exportsField === 'string') { + mainExport = exportsField + } else if (typeof exportsField === 'object') { + // Try to find default export in nested structure. + mainExport = + exportsField.node?.default?.default || + exportsField.node?.default || + exportsField.default?.default || + exportsField.default || + mainExport + } + } + + return path.join(localPath, mainExport) +} diff --git a/packages/lib/scripts/build-externals/orchestrator.mjs b/packages/lib/scripts/build-externals/orchestrator.mjs new file mode 100644 index 000000000..24f8bc4ba --- /dev/null +++ b/packages/lib/scripts/build-externals/orchestrator.mjs @@ -0,0 +1,156 @@ +/** + * @fileoverview Main entry point for bundling external dependencies. + * Orchestrates bundling and reporting. + */ + +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import { bundlePackage } from './bundler.mjs' +import { externalPackages, scopedPackages } from './config.mjs' +import { ensureDir } from './copy-files.mjs' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const rootDir = path.resolve(__dirname, '..', '..') +const distExternalDir = path.join(rootDir, 'dist', 'external') + +/** + * Bundle all external packages. + * + * @param {object} options - Options + * @param {boolean} options.quiet - Suppress individual package output + * @returns {Promise<{bundledCount: number, totalSize: number}>} + */ +async function bundleAllPackages(options = {}) { + const { quiet = false } = options + let bundledCount = 0 + let totalSize = 0 + + // Bundle each external package. + for (const { bundle, name } of externalPackages) { + if (bundle) { + const outputPath = path.join(distExternalDir, `${name}.js`) + const size = await bundlePackage(name, outputPath, { + quiet, + rootDir, + }) + if (size) { + bundledCount++ + totalSize += size + } + } + } + + // Bundle scoped packages. + for (const { name, optional, packages, scope, subpaths } of scopedPackages) { + const scopeDir = path.join(distExternalDir, scope) + await ensureDir(scopeDir) + + if (name) { + // Single package in scope. + const outputPath = path.join(scopeDir, `${name}.js`) + if (optional) { + try { + const size = await bundlePackage(`${scope}/${name}`, outputPath, { + quiet, + rootDir, + }) + if (size) { + bundledCount++ + totalSize += size + } + } catch { + if (!quiet) { + console.log(` Skipping optional package ${scope}/${name}`) + } + } + } else { + const size = await bundlePackage(`${scope}/${name}`, outputPath, { + quiet, + rootDir, + }) + if (size) { + bundledCount++ + totalSize += size + } + } + } else if (packages) { + // Multiple packages in scope. + for (const pkg of packages) { + const outputPath = path.join(scopeDir, `${pkg}.js`) + if (optional) { + try { + const size = await bundlePackage(`${scope}/${pkg}`, outputPath, { + quiet, + rootDir, + }) + if (size) { + bundledCount++ + totalSize += size + } + } catch { + if (!quiet) { + console.log(` Skipping optional package ${scope}/${pkg}`) + } + } + } else { + const size = await bundlePackage(`${scope}/${pkg}`, outputPath, { + quiet, + rootDir, + }) + if (size) { + bundledCount++ + totalSize += size + } + } + } + } + + // Bundle subpath exports (e.g., @npmcli/package-json/lib/read-package) + if (subpaths) { + for (const subpath of subpaths) { + const outputPath = path.join(distExternalDir, scope, subpath) + const packageName = `${scope}/${subpath}` + // Ensure parent directory exists + await ensureDir(path.dirname(outputPath)) + const size = await bundlePackage(packageName, outputPath, { + quiet, + rootDir, + }) + if (size) { + bundledCount++ + totalSize += size + } + } + } + } + + return { bundledCount, totalSize } +} + +/** + * Main build function. + * + * @param {object} options - Build options + * @param {boolean} options.verbose - Show detailed output + * @param {boolean} options.quiet - Suppress all output + * @returns {Promise} + */ +export async function buildExternals(options = {}) { + const { quiet = false, verbose = false } = options + + // Default behavior: show header but not individual packages (concise) + // --verbose: show all package details + // --quiet: show nothing + const showDetails = verbose && !quiet + + // Ensure dist/external directory exists. + await ensureDir(distExternalDir) + + // Bundle all packages + const { bundledCount, totalSize } = await bundleAllPackages({ + quiet: quiet || !showDetails, + }) + + return { bundledCount, totalSize } +} diff --git a/packages/lib/scripts/build-externals/stubs/README.md b/packages/lib/scripts/build-externals/stubs/README.md new file mode 100644 index 000000000..6d98fbfcd --- /dev/null +++ b/packages/lib/scripts/build-externals/stubs/README.md @@ -0,0 +1,72 @@ +# External Dependency Stubs + +This directory contains stub modules used during the external package bundling process to replace unused dependencies and reduce bundle size. + +**Philosophy:** Be conservative. Only stub dependencies that are provably unused or already disabled. + +## How It Works + +The build-externals system bundles external npm dependencies (like pacote, cacache, make-fetch-happen) into standalone modules in `dist/external/`. During bundling, esbuild uses the stubs in this directory to replace dependencies we don't need. + +The stub configuration lives in `../esbuild-config.mjs`, which maps module patterns to stub files: + +```javascript +const STUB_MAP = { + '^(encoding|iconv-lite)$': 'encoding.cjs', + '^debug$': 'debug.cjs', +} +``` + +When esbuild encounters `require('encoding')` during bundling, it replaces it with the contents of `encoding.cjs` instead of bundling the entire encoding package. + +## Stub Types + +This directory provides both active stubs (currently in use) and utility stubs (available for future use): + +### Utility Stubs (Available for Use) + +**`empty.cjs`** - Empty object for unused modules +- Exports: `{}` +- Use case: Dependencies referenced but never executed + +**`noop.cjs`** - No-op function for optional features +- Exports: Function that does nothing +- Use case: Logging, debugging, optional callbacks + +**`throw.cjs`** - Error-throwing for unexpected usage +- Exports: Function that throws descriptive error +- Use case: Code paths that should never execute + +### Active Stubs (Currently in Use) + +**`encoding.cjs`** - Character encoding stub +- Replaces: `encoding`, `iconv-lite` +- Reason: We only use UTF-8, don't need legacy encoding support +- Size impact: ~9KB saved (pacote, make-fetch-happen) + +**`debug.cjs`** - Debug logging stub +- Replaces: `debug` module +- Reason: Already compiled out via `process.env.DEBUG = undefined` +- Size impact: ~9KB saved + +## Adding New Stubs + +**Before adding a stub:** +1. Verify the dependency is truly unused via code analysis +2. Check if it's already disabled via esbuild `define` constants +3. Consider the risk - conservative only! + +**To add a stub:** +1. Create stub file in this directory +2. Document what it replaces and why it's safe +3. Add entry to `STUB_MAP` in `../esbuild-config.mjs` +4. Test: `pnpm build && pnpm test` +5. Verify size savings: `du -sh dist/external` + +## Testing Stubs + +After adding stubs, verify: +1. Build succeeds: `pnpm build` +2. Tests pass: `pnpm test` +3. No runtime errors in dependent packages +4. Bundle size decreased as expected diff --git a/packages/lib/scripts/build-externals/stubs/debug.cjs b/packages/lib/scripts/build-externals/stubs/debug.cjs new file mode 100644 index 000000000..687303fcb --- /dev/null +++ b/packages/lib/scripts/build-externals/stubs/debug.cjs @@ -0,0 +1,24 @@ +/** + * Debug stub - stubs out debug logging. + * + * Many npm packages include debug() calls for verbose logging. + * In production, these are disabled via process.env.DEBUG. + * This stub removes the debug module entirely. + * + * Used by: Various npm packages + * Savings: ~9KB + removes debug dependency checks + */ +'use strict' + +// Return a no-op function that accepts any arguments +function debug() { + return function noop() {} +} + +// Common debug properties +debug.enabled = false +debug.names = [] +debug.skips = [] +debug.formatters = {} + +module.exports = debug diff --git a/packages/lib/scripts/build-externals/stubs/empty.cjs b/packages/lib/scripts/build-externals/stubs/empty.cjs new file mode 100644 index 000000000..cb0c90cbf --- /dev/null +++ b/packages/lib/scripts/build-externals/stubs/empty.cjs @@ -0,0 +1,7 @@ +/** + * Empty stub - provides no functionality. + * Used for dependencies that are never actually called in our code paths. + */ +'use strict' + +module.exports = {} diff --git a/packages/lib/scripts/build-externals/stubs/encoding.cjs b/packages/lib/scripts/build-externals/stubs/encoding.cjs new file mode 100644 index 000000000..0bc4de68c --- /dev/null +++ b/packages/lib/scripts/build-externals/stubs/encoding.cjs @@ -0,0 +1,11 @@ +/** + * Encoding/iconv-lite stub. + * + * These packages provide character encoding conversion (e.g., UTF-8 to Latin1). + * We only work with UTF-8, so we stub them out to save ~100KB. + * + * Used by: make-fetch-happen, pacote (for legacy content-encoding) + */ +'use strict' + +module.exports = {} diff --git a/packages/lib/scripts/build-externals/stubs/noop.cjs b/packages/lib/scripts/build-externals/stubs/noop.cjs new file mode 100644 index 000000000..47a07ad34 --- /dev/null +++ b/packages/lib/scripts/build-externals/stubs/noop.cjs @@ -0,0 +1,10 @@ +/** + * No-op stub - provides functions that do nothing. + * Used for optional features we don't need (logging, debugging, etc). + */ +'use strict' + +const noop = () => {} + +module.exports = noop +module.exports.default = noop diff --git a/packages/lib/scripts/build-externals/stubs/throw.cjs b/packages/lib/scripts/build-externals/stubs/throw.cjs new file mode 100644 index 000000000..93d6a42ec --- /dev/null +++ b/packages/lib/scripts/build-externals/stubs/throw.cjs @@ -0,0 +1,15 @@ +/** + * Throw stub - errors if called. + * Used for dependencies that should never be reached in production. + * Helps catch bugs if accidentally called. + */ +'use strict' + +function throwStub(moduleName) { + throw new Error( + `Module '${moduleName}' is stubbed and should not be called. ` + + 'This is likely a bundling error or unexpected code path.', + ) +} + +module.exports = throwStub diff --git a/packages/lib/scripts/build-js.mjs b/packages/lib/scripts/build-js.mjs new file mode 100644 index 000000000..069a4c265 --- /dev/null +++ b/packages/lib/scripts/build-js.mjs @@ -0,0 +1,133 @@ +/** + * @fileoverview JavaScript compilation using esbuild (10x faster than tsgo) + * This replaces tsgo for JS compilation while keeping tsgo for declarations + */ + +import { build, context } from 'esbuild' + +import { + analyzeMetafile, + buildConfig, + watchConfig, +} from '../.config/esbuild.config.mjs' +import { getDefaultLogger } from '#socketsecurity/lib/logger' + +const logger = getDefaultLogger() + +const isQuiet = process.argv.includes('--quiet') +const isVerbose = process.argv.includes('--verbose') +const isWatch = process.argv.includes('--watch') + +/** + * Standard build for production + */ +async function buildJS() { + try { + if (!isQuiet) { + console.log('→ Building JavaScript with esbuild') + } + + const startTime = Date.now() + const result = await build({ + ...buildConfig, + logLevel: isQuiet ? 'silent' : isVerbose ? 'debug' : 'info', + }) + + const buildTime = Date.now() - startTime + + if (!isQuiet) { + console.log(` JavaScript built in ${buildTime}ms`) + + if (result?.metafile && isVerbose) { + const analysis = analyzeMetafile(result.metafile) + console.log(` Total size: ${analysis.totalSize}`) + } + } + + return 0 + } catch (error) { + if (!isQuiet) { + logger.error('JavaScript build failed') + console.error(error) + } + return 1 + } +} + +/** + * Watch mode with incremental builds (68% faster rebuilds) + */ +async function watchJS() { + try { + if (!isQuiet) { + console.log('→ Starting watch mode with incremental builds') + console.log(' Watching for file changes...') + } + + const ctx = await context({ + ...watchConfig, + logLevel: isQuiet ? 'silent' : isVerbose ? 'debug' : 'warning', + plugins: [ + ...(watchConfig.plugins || []), + { + name: 'rebuild-logger', + setup(build) { + build.onEnd(result => { + if (result.errors.length > 0) { + if (!isQuiet) { + logger.error('Rebuild failed') + } + } else { + if (!isQuiet) { + logger.success('Rebuild succeeded') + + if (result?.metafile && isVerbose) { + const analysis = analyzeMetafile(result.metafile) + console.log(` Total size: ${analysis.totalSize}`) + } + } + } + }) + }, + }, + ], + }) + + await ctx.watch() + + // Keep process alive + process.on('SIGINT', async () => { + if (!isQuiet) { + console.log('\nStopping watch mode...') + } + await ctx.dispose() + process.exit(0) + }) + + // Wait indefinitely + await new Promise(() => {}) + } catch (error) { + if (!isQuiet) { + logger.error('Watch mode failed') + console.error(error) + } + return 1 + } +} + +// Main +if (isWatch) { + watchJS().catch(error => { + console.error(error) + process.exit(1) + }) +} else { + buildJS() + .then(code => { + process.exitCode = code + }) + .catch(error => { + console.error(error) + process.exitCode = 1 + }) +} diff --git a/packages/lib/scripts/build.mjs b/packages/lib/scripts/build.mjs new file mode 100644 index 000000000..a6a44bd3e --- /dev/null +++ b/packages/lib/scripts/build.mjs @@ -0,0 +1,462 @@ +/** + * @fileoverview Fast build runner using esbuild for smaller bundles and faster builds. + */ + +import { existsSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import { build, context } from 'esbuild' +import colors from 'yoctocolors-cjs' + +import { + analyzeMetafile, + buildConfig, + watchConfig, +} from '../.config/esbuild.config.mjs' +import { isQuiet } from '#socketsecurity/lib/argv/flags' +import { getDefaultLogger } from '#socketsecurity/lib/logger' +import { printFooter, printHeader } from '#socketsecurity/lib/stdio/header' + +import { parseArgs } from './utils/parse-args.mjs' +import { runSequence } from './utils/run-command.mjs' + +const logger = getDefaultLogger() + +// Helper for completed headers (simple wrapper) +const printCompletedHeader = title => console.log(colors.green(`✓ ${title}`)) + +const rootPath = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '..', +) + +/** + * Build source code with esbuild. + * Returns { exitCode, buildTime, result } for external logging. + */ +async function buildSource(options = {}) { + const { quiet = false, skipClean = false, verbose = false } = options + + // Clean dist directory if needed + if (!skipClean) { + const exitCode = await runSequence([ + { + args: ['scripts/clean.mjs', '--dist', '--quiet'], + command: 'node', + }, + ]) + if (exitCode !== 0) { + if (!quiet) { + logger.error('Clean failed') + } + return { exitCode, buildTime: 0, result: null } + } + } + + try { + const startTime = Date.now() + // Determine log level based on verbosity + const logLevel = quiet ? 'silent' : verbose ? 'info' : 'warning' + const result = await build({ + ...buildConfig, + logLevel, + }) + const buildTime = Date.now() - startTime + + return { exitCode: 0, buildTime, result } + } catch (error) { + if (!quiet) { + logger.error('Source build failed') + console.error(error) + } + return { exitCode: 1, buildTime: 0, result: null } + } +} + +/** + * Build TypeScript declarations. + * Returns exitCode for external logging. + */ +async function buildTypes(options = {}) { + const { + quiet = false, + skipClean = false, + verbose: _verbose = false, + } = options + + const commands = [] + + if (!skipClean) { + commands.push({ + args: ['scripts/clean.mjs', '--types', '--quiet'], + command: 'node', + }) + } + + commands.push({ + args: ['exec', 'tsgo', '--project', 'tsconfig.dts.json'], + command: 'pnpm', + options: { + ...(process.platform === 'win32' && { shell: true }), + }, + }) + + const exitCode = await runSequence(commands) + + if (exitCode !== 0) { + if (!quiet) { + logger.error('Type declarations build failed') + } + } + + return exitCode +} + +/** + * Build external dependencies. + * Returns exitCode for external logging. + */ +async function buildExternals(options = {}) { + const { quiet = false, verbose = false } = options + + const args = ['scripts/build-externals.mjs'] + if (quiet) { + args.push('--quiet') + } + if (verbose) { + args.push('--verbose') + } + + const exitCode = await runSequence([ + { + args, + command: 'node', + }, + ]) + + if (exitCode !== 0) { + if (!quiet) { + logger.error('External dependencies build failed') + } + } + + return exitCode +} + +/** + * Fix exports after build. + * Returns exitCode for external logging. + */ +async function fixExports(options = {}) { + const { quiet = false, verbose = false } = options + + const fixArgs = ['scripts/fix-build.mjs'] + if (quiet) { + fixArgs.push('--quiet') + } + if (verbose) { + fixArgs.push('--verbose') + } + + const exitCode = await runSequence([ + { + args: fixArgs, + command: 'node', + }, + ]) + + if (exitCode !== 0) { + if (!quiet) { + logger.error('Build fixing failed') + } + } + + return exitCode +} + +/** + * Watch mode for development with incremental builds (68% faster rebuilds). + */ +async function watchBuild(options = {}) { + const { quiet = false, verbose = false } = options + + if (!quiet) { + logger.step('Starting watch mode with incremental builds') + logger.substep('Watching for file changes...') + } + + try { + // Determine log level based on verbosity + const logLevel = quiet ? 'silent' : verbose ? 'debug' : 'warning' + + // Use context API for incremental builds (68% faster rebuilds) + // Extract watch option from watchConfig as it's not valid for context() + const { watch: _watchOpts, ...contextConfig } = watchConfig + const ctx = await context({ + ...contextConfig, + logLevel, + plugins: [ + ...(contextConfig.plugins || []), + { + name: 'rebuild-logger', + setup(build) { + build.onEnd(result => { + if (result.errors.length > 0) { + if (!quiet) { + logger.error('Rebuild failed') + } + } else { + if (!quiet) { + logger.success('Rebuild succeeded') + if (result?.metafile && verbose) { + const analysis = analyzeMetafile(result.metafile) + logger.info(`Bundle size: ${analysis.totalSize}`) + } + } + } + }) + }, + }, + ], + }) + + // Enable watch mode + await ctx.watch() + + // Keep the process alive + process.on('SIGINT', async () => { + await ctx.dispose() + process.exitCode = 0 + throw new Error('Watch mode interrupted') + }) + + // Wait indefinitely + await new Promise(() => {}) + } catch (error) { + if (!quiet) { + logger.error('Watch mode failed:', error) + } + return 1 + } +} + +/** + * Check if build is needed. + */ +function isBuildNeeded() { + const distPath = path.join(rootPath, 'dist', 'index.js') + const distTypesPath = path.join(rootPath, 'dist', 'types', 'index.d.ts') + + return !existsSync(distPath) || !existsSync(distTypesPath) +} + +async function main() { + try { + // Parse arguments + const { values } = parseArgs({ + options: { + help: { + type: 'boolean', + default: false, + }, + src: { + type: 'boolean', + default: false, + }, + types: { + type: 'boolean', + default: false, + }, + watch: { + type: 'boolean', + default: false, + }, + needed: { + type: 'boolean', + default: false, + }, + analyze: { + type: 'boolean', + default: false, + }, + silent: { + type: 'boolean', + default: false, + }, + quiet: { + type: 'boolean', + default: false, + }, + verbose: { + type: 'boolean', + default: false, + }, + }, + allowPositionals: false, + strict: false, + }) + + // Show help if requested + if (values.help) { + console.log('Build Runner') + console.log('\nUsage: pnpm build [options]') + console.log('\nOptions:') + console.log(' --help Show this help message') + console.log(' --src Build source code only') + console.log(' --types Build TypeScript declarations only') + console.log( + ' --watch Watch mode with incremental builds (68% faster rebuilds)', + ) + console.log(' --needed Only build if dist files are missing') + console.log(' --analyze Show bundle size analysis') + console.log(' --quiet, --silent Suppress progress messages') + console.log(' --verbose Show detailed build output') + console.log('\nExamples:') + console.log(' pnpm build # Full build (source + types)') + console.log(' pnpm build --src # Build source only') + console.log(' pnpm build --types # Build types only') + console.log( + ' pnpm build --watch # Watch mode with incremental builds', + ) + console.log(' pnpm build --analyze # Build with size analysis') + console.log( + '\nNote: Watch mode uses esbuild context API for 68% faster rebuilds', + ) + process.exitCode = 0 + return + } + + const quiet = isQuiet(values) + const verbose = values.verbose + + // Check if build is needed + if (values.needed && !isBuildNeeded()) { + if (!quiet) { + logger.info('Build artifacts exist, skipping build') + } + process.exitCode = 0 + return + } + + let exitCode = 0 + + // Handle watch mode + if (values.watch) { + if (!quiet) { + printHeader('Build Runner (Watch Mode)') + } + exitCode = await watchBuild({ quiet, verbose }) + } + // Build types only + else if (values.types && !values.src) { + if (!quiet) { + printHeader('Building TypeScript Declarations') + } + exitCode = await buildTypes({ quiet, verbose }) + if (exitCode === 0 && !quiet) { + logger.substep('Type declarations built') + } + } + // Build source only + else if (values.src && !values.types) { + if (!quiet) { + printHeader('Building Source') + } + const { + buildTime, + exitCode: srcExitCode, + result, + } = await buildSource({ quiet, verbose, analyze: values.analyze }) + exitCode = srcExitCode + if (exitCode === 0 && !quiet) { + logger.substep(`Source build complete in ${buildTime}ms`) + + if (values.analyze && result?.metafile) { + const analysis = analyzeMetafile(result.metafile) + logger.info('Build output:') + for (const file of analysis.files) { + logger.substep(`${file.name}: ${file.size}`) + } + logger.step(`Total bundle size: ${analysis.totalSize}`) + } + } + } + // Build everything (default) + else { + if (!quiet) { + printHeader('Building Package') + } + + exitCode = await runSequence([ + { + args: ['scripts/clean.mjs', '--dist', '--types', '--quiet'], + command: 'node', + }, + ]) + if (exitCode !== 0) { + if (!quiet) { + logger.error('Clean failed') + } + process.exitCode = exitCode + return + } + + if (!quiet) { + printCompletedHeader('Build Cleaned') + } + + // Run source and types builds in parallel (externals not needed in monorepo) + const [srcResult, typesExitCode] = await Promise.all([ + buildSource({ + quiet, + verbose, + skipClean: true, + analyze: values.analyze, + }), + buildTypes({ quiet, verbose, skipClean: true }), + ]) + + // Log completion messages if analyze flag is set + if (!quiet && values.analyze && srcResult.result?.metafile) { + const analysis = analyzeMetafile(srcResult.result.metafile) + logger.info('Build output:') + for (const file of analysis.files) { + logger.substep(`${file.name}: ${file.size}`) + } + logger.step(`Total bundle size: ${analysis.totalSize}`) + } + + // Check if any of the parallel builds failed + exitCode = + srcResult.exitCode !== 0 + ? srcResult.exitCode + : typesExitCode + + // If all parallel builds succeeded, fix exports + if (exitCode === 0) { + const fixExitCode = await fixExports({ quiet, verbose }) + exitCode = fixExitCode + } + } + + // Print final status and footer + if (!quiet) { + if (exitCode === 0) { + console.log(colors.green('✓ Build completed successfully!')) + } else { + console.error(colors.red('✗ Build failed')) + } + printFooter() + } + + if (exitCode !== 0) { + process.exitCode = exitCode + } + } catch (error) { + logger.error(`Build runner failed: ${error.message}`) + process.exitCode = 1 + } +} + +main().catch(console.error) diff --git a/packages/lib/scripts/check.mjs b/packages/lib/scripts/check.mjs new file mode 100644 index 000000000..c424c9221 --- /dev/null +++ b/packages/lib/scripts/check.mjs @@ -0,0 +1,107 @@ +/** + * @fileoverview Check script for the lib. + * Runs all quality checks in parallel: + * - Biome (linting) + * - TypeScript type checking + * + * Usage: + * node scripts/check.mjs + */ + +import { getDefaultLogger } from '#socketsecurity/lib/logger' +import { printFooter, printHeader } from '#socketsecurity/lib/stdio/header' + +import { runParallel } from './utils/run-command.mjs' + +const logger = getDefaultLogger() + +async function main() { + try { + printHeader('Code Checks') + + const checks = [ + { + args: ['exec', 'biome', 'check', '.'], + command: 'pnpm', + options: { + ...(process.platform === 'win32' && { shell: true }), + }, + }, + { + args: ['exec', 'tsgo', '--noEmit'], + command: 'pnpm', + options: { + ...(process.platform === 'win32' && { shell: true }), + }, + }, + { + args: ['scripts/validate-no-link-deps.mjs'], + command: 'node', + options: { + ...(process.platform === 'win32' && { shell: true }), + }, + }, + { + args: ['scripts/validate-no-extraneous-dependencies.mjs'], + command: 'node', + options: { + ...(process.platform === 'win32' && { shell: true }), + }, + }, + { + args: ['scripts/validate-esbuild-minify.mjs'], + command: 'node', + options: { + ...(process.platform === 'win32' && { shell: true }), + }, + }, + { + args: ['scripts/validate-no-cdn-refs.mjs'], + command: 'node', + options: { + ...(process.platform === 'win32' && { shell: true }), + }, + }, + { + args: ['scripts/validate-markdown-filenames.mjs'], + command: 'node', + options: { + ...(process.platform === 'win32' && { shell: true }), + }, + }, + { + args: ['scripts/validate-file-size.mjs'], + command: 'node', + options: { + ...(process.platform === 'win32' && { shell: true }), + }, + }, + { + args: ['scripts/validate-file-count.mjs'], + command: 'node', + options: { + ...(process.platform === 'win32' && { shell: true }), + }, + }, + ] + + const exitCodes = await runParallel(checks) + const failed = exitCodes.some(code => code !== 0) + + if (failed) { + logger.error('Some checks failed') + process.exitCode = 1 + } else { + logger.success('All checks passed') + printFooter() + } + } catch (error) { + logger.error(`Check failed: ${error.message}`) + process.exitCode = 1 + } +} + +main().catch(e => { + logger.error(e) + process.exitCode = 1 +}) diff --git a/packages/lib/scripts/claude.mjs b/packages/lib/scripts/claude.mjs new file mode 100644 index 000000000..c3efbd699 --- /dev/null +++ b/packages/lib/scripts/claude.mjs @@ -0,0 +1,5720 @@ +/** + * @fileoverview Claude Code-powered utilities for Socket projects. + * Provides various AI-assisted development tools and automations using Claude Code CLI. + * Requires Claude Code (claude) CLI to be installed. + */ + +import { spawn } from 'node:child_process' +import crypto from 'node:crypto' +import { + existsSync, + readFileSync, + writeFileSync, + promises as fs, +} from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import { deleteAsync as del } from 'del' +import colors from 'yoctocolors-cjs' + +import { parseArgs } from './utils/parse-args.mjs' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const rootPath = path.join(__dirname, '..') +const parentPath = path.join(rootPath, '..') +const claudeDir = path.join(rootPath, '.claude') +const WIN32 = process.platform === 'win32' + +// Socket project names. +const SOCKET_PROJECTS = [ + 'socket-cli', + 'socket-lib', + 'socket-sdk-js', + 'socket-packageurl-js', + 'socket-registry', +] + +// Storage paths. +// User-level (cross-repo, persistent) +const CLAUDE_HOME = path.join(os.homedir(), '.claude') +const STORAGE_PATHS = { + fixMemory: path.join(CLAUDE_HOME, 'fix-memory.db'), + stats: path.join(CLAUDE_HOME, 'stats.json'), + history: path.join(CLAUDE_HOME, 'history.json'), + config: path.join(CLAUDE_HOME, 'config.json'), + cache: path.join(CLAUDE_HOME, 'cache'), +} + +// Repo-level (per-project, temporary) +const REPO_STORAGE = { + snapshots: path.join(claudeDir, 'snapshots'), + session: path.join(claudeDir, 'session.json'), + scratch: path.join(claudeDir, 'scratch'), +} + +// Retention periods (milliseconds). +const RETENTION = { + // 7 days + snapshots: 7 * 24 * 60 * 60 * 1000, + // 30 days + cache: 30 * 24 * 60 * 60 * 1000, + // 1 day + sessions: 24 * 60 * 60 * 1000, +} + +// Claude API pricing (USD per token). +// https://www.anthropic.com/pricing +const PRICING = { + 'claude-sonnet-4-5': { + // $3 per 1M input tokens + input: 3.0 / 1_000_000, + // $15 per 1M output tokens + output: 15.0 / 1_000_000, + // $3.75 per 1M cache write tokens + cache_write: 3.75 / 1_000_000, + // $0.30 per 1M cache read tokens + cache_read: 0.3 / 1_000_000, + }, + 'claude-sonnet-3-7': { + // $3 per 1M input tokens + input: 3.0 / 1_000_000, + // $15 per 1M output tokens + output: 15.0 / 1_000_000, + // $3.75 per 1M cache write tokens + cache_write: 3.75 / 1_000_000, + // $0.30 per 1M cache read tokens + cache_read: 0.3 / 1_000_000, + }, +} + +// Simple inline logger. +const log = { + info: msg => console.log(msg), + error: msg => console.error(`${colors.red('✗')} ${msg}`), + success: msg => console.log(`${colors.green('✓')} ${msg}`), + step: msg => console.log(`\n${msg}`), + substep: msg => console.log(` ${msg}`), + progress: msg => { + process.stdout.write('\r\x1b[K') + process.stdout.write(` ∴ ${msg}`) + }, + done: msg => { + process.stdout.write('\r\x1b[K') + console.log(` ${colors.green('✓')} ${msg}`) + }, + failed: msg => { + process.stdout.write('\r\x1b[K') + console.log(` ${colors.red('✗')} ${msg}`) + }, + warn: msg => console.log(`${colors.yellow('⚠')} ${msg}`), +} + +function printHeader(title) { + console.log(`\n${'─'.repeat(60)}`) + console.log(` ${title}`) + console.log(`${'─'.repeat(60)}`) +} + +function printFooter(message) { + console.log(`\n${'─'.repeat(60)}`) + if (message) { + console.log(` ${colors.green('✓')} ${message}`) + } +} + +/** + * Initialize storage directories. + */ +async function initStorage() { + await fs.mkdir(CLAUDE_HOME, { recursive: true }) + await fs.mkdir(STORAGE_PATHS.cache, { recursive: true }) + await fs.mkdir(REPO_STORAGE.snapshots, { recursive: true }) + await fs.mkdir(REPO_STORAGE.scratch, { recursive: true }) +} + +/** + * Clean up old data using del package. + */ +async function cleanupOldData() { + const now = Date.now() + + // Clean old snapshots in current repo. + try { + const snapshots = await fs.readdir(REPO_STORAGE.snapshots) + const toDelete = [] + for (const snap of snapshots) { + const snapPath = path.join(REPO_STORAGE.snapshots, snap) + const stats = await fs.stat(snapPath) + if (now - stats.mtime.getTime() > RETENTION.snapshots) { + toDelete.push(snapPath) + } + } + if (toDelete.length > 0) { + // Force delete temp directories outside CWD. + await del(toDelete, { force: true }) + } + } catch { + // Ignore errors if directory doesn't exist. + } + + // Clean old cache entries in ~/.claude/cache/. + try { + const cached = await fs.readdir(STORAGE_PATHS.cache) + const toDelete = [] + for (const file of cached) { + const filePath = path.join(STORAGE_PATHS.cache, file) + const stats = await fs.stat(filePath) + if (now - stats.mtime.getTime() > RETENTION.cache) { + toDelete.push(filePath) + } + } + if (toDelete.length > 0) { + // Force delete temp directories outside CWD. + await del(toDelete, { force: true }) + } + } catch { + // Ignore errors if directory doesn't exist. + } +} + +/** + * Cost tracking with budget controls. + */ +class CostTracker { + constructor(model = 'claude-sonnet-4-5') { + this.model = model + this.session = { input: 0, output: 0, cacheWrite: 0, cacheRead: 0, cost: 0 } + this.monthly = this.loadMonthlyStats() + this.startTime = Date.now() + } + + loadMonthlyStats() { + try { + if (existsSync(STORAGE_PATHS.stats)) { + const data = JSON.parse(readFileSync(STORAGE_PATHS.stats, 'utf8')) + // YYYY-MM + const currentMonth = new Date().toISOString().slice(0, 7) + if (data.month === currentMonth) { + return data + } + } + } catch { + // Ignore errors, start fresh. + } + return { + month: new Date().toISOString().slice(0, 7), + cost: 0, + fixes: 0, + sessions: 0, + } + } + + saveMonthlyStats() { + try { + writeFileSync(STORAGE_PATHS.stats, JSON.stringify(this.monthly, null, 2)) + } catch { + // Ignore errors. + } + } + + track(usage) { + const pricing = PRICING[this.model] + if (!pricing) { + return + } + + const inputTokens = usage.input_tokens || 0 + const outputTokens = usage.output_tokens || 0 + const cacheWriteTokens = usage.cache_creation_input_tokens || 0 + const cacheReadTokens = usage.cache_read_input_tokens || 0 + + const cost = + inputTokens * pricing.input + + outputTokens * pricing.output + + cacheWriteTokens * pricing.cache_write + + cacheReadTokens * pricing.cache_read + + this.session.input += inputTokens + this.session.output += outputTokens + this.session.cacheWrite += cacheWriteTokens + this.session.cacheRead += cacheReadTokens + this.session.cost += cost + + this.monthly.cost += cost + this.saveMonthlyStats() + } + + showSessionSummary() { + const duration = Date.now() - this.startTime + console.log(colors.cyan('\n💰 Cost Summary:')) + console.log(` Input tokens: ${this.session.input.toLocaleString()}`) + console.log(` Output tokens: ${this.session.output.toLocaleString()}`) + if (this.session.cacheWrite > 0) { + console.log(` Cache write: ${this.session.cacheWrite.toLocaleString()}`) + } + if (this.session.cacheRead > 0) { + console.log(` Cache read: ${this.session.cacheRead.toLocaleString()}`) + } + console.log( + ` Session cost: ${colors.green(`$${this.session.cost.toFixed(4)}`)}`, + ) + console.log( + ` Monthly total: ${colors.yellow(`$${this.monthly.cost.toFixed(2)}`)}`, + ) + console.log(` Duration: ${colors.gray(formatDuration(duration))}`) + } +} + +/** + * Format duration in human-readable form. + */ +function formatDuration(ms) { + const seconds = Math.floor(ms / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + + if (hours > 0) { + return `${hours}h ${minutes % 60}m ${seconds % 60}s` + } + if (minutes > 0) { + return `${minutes}m ${seconds % 60}s` + } + return `${seconds}s` +} + +/** + * Progress tracking with ETA estimation. + */ +class ProgressTracker { + constructor() { + this.phases = [] + this.currentPhase = null + this.startTime = Date.now() + this.history = this.loadHistory() + } + + loadHistory() { + try { + if (existsSync(STORAGE_PATHS.history)) { + const data = JSON.parse(readFileSync(STORAGE_PATHS.history, 'utf8')) + // Keep only last 50 sessions. + return data.sessions.slice(-50) + } + } catch { + // Ignore errors. + } + return [] + } + + saveHistory() { + try { + const data = { + sessions: [ + ...this.history, + { phases: this.phases, timestamp: Date.now() }, + ], + } + // Keep only last 50 sessions. + if (data.sessions.length > 50) { + data.sessions = data.sessions.slice(-50) + } + writeFileSync(STORAGE_PATHS.history, JSON.stringify(data, null, 2)) + } catch { + // Ignore errors. + } + } + + startPhase(name) { + if (this.currentPhase) { + this.endPhase() + } + this.currentPhase = { name, start: Date.now() } + } + + endPhase() { + if (this.currentPhase) { + this.currentPhase.duration = Date.now() - this.currentPhase.start + this.phases.push(this.currentPhase) + this.currentPhase = null + } + } + + estimateETA(phaseName) { + // Find similar past sessions. + const similar = this.history.filter(s => + s.phases.some(p => p.name === phaseName), + ) + if (similar.length === 0) { + return null + } + + // Get median duration for this phase. + const durations = similar + .map(s => s.phases.find(p => p.name === phaseName)?.duration) + .filter(d => d) + .sort((a, b) => a - b) + + if (durations.length === 0) { + return null + } + + const median = durations[Math.floor(durations.length / 2)] + return median + } + + getTotalETA() { + // Sum up remaining phases based on historical data. + const remaining = ['local-checks', 'commit', 'ci-monitor'].filter( + p => !this.phases.some(ph => ph.name === p), + ) + + let total = 0 + for (const phase of remaining) { + const eta = this.estimateETA(phase) + if (eta) { + total += eta + } + } + + // Add current phase remaining time. + if (this.currentPhase) { + const eta = this.estimateETA(this.currentPhase.name) + if (eta) { + const elapsed = Date.now() - this.currentPhase.start + total += Math.max(0, eta - elapsed) + } + } + + return total > 0 ? total : null + } + + showProgress() { + const totalElapsed = Date.now() - this.startTime + const eta = this.getTotalETA() + + console.log(colors.cyan('\n⏱️ Progress:')) + console.log(` Elapsed: ${formatDuration(totalElapsed)}`) + if (eta) { + console.log(` ETA: ${formatDuration(eta)}`) + } + + if (this.currentPhase) { + const phaseElapsed = Date.now() - this.currentPhase.start + console.log( + colors.gray( + ` Current: ${this.currentPhase.name} (${formatDuration(phaseElapsed)})`, + ), + ) + } + + // Show completed phases. + if (this.phases.length > 0) { + console.log(colors.gray(' Completed:')) + this.phases.forEach(p => { + console.log( + colors.gray( + ` ${colors.green('✓')} ${p.name} (${formatDuration(p.duration)})`, + ), + ) + }) + } + } + + complete() { + this.endPhase() + this.saveHistory() + } +} + +/** + * Snapshot system for smart rollback. + */ +class SnapshotManager { + constructor() { + this.snapshots = [] + } + + async createSnapshot(label) { + const sha = await runCommandWithOutput('git', ['rev-parse', 'HEAD'], { + cwd: rootPath, + }) + const diff = await runCommandWithOutput('git', ['diff', 'HEAD'], { + cwd: rootPath, + }) + + const snapshot = { + label, + sha: sha.stdout.trim(), + diff: diff.stdout, + timestamp: Date.now(), + } + + this.snapshots.push(snapshot) + + // Save snapshot to disk. + const snapshotPath = path.join( + REPO_STORAGE.snapshots, + `snapshot-${Date.now()}.json`, + ) + await fs.writeFile(snapshotPath, JSON.stringify(snapshot, null, 2)) + + return snapshot + } + + async rollback(steps = 1) { + if (this.snapshots.length < steps) { + log.warn(`Only ${this.snapshots.length} snapshot(s) available`) + return false + } + + const target = this.snapshots[this.snapshots.length - steps] + log.warn(`Rolling back ${steps} fix(es) to: ${target.label}`) + + await runCommand('git', ['reset', '--hard', target.sha], { cwd: rootPath }) + + // Re-apply diff if there was one. + if (target.diff) { + await runCommand('git', ['apply'], { + cwd: rootPath, + input: target.diff, + }) + } + + log.done('Rollback complete') + return true + } + + listSnapshots() { + console.log(colors.cyan('\n📸 Available Snapshots:')) + this.snapshots.forEach((snap, i) => { + const age = formatDuration(Date.now() - snap.timestamp) + console.log( + ` ${i + 1}. ${snap.label} ${colors.gray(`(${age} ago, ${snap.sha.substring(0, 7)})`)}`, + ) + }) + } +} + +/** + * Proactive pre-commit detection. + */ +async function runPreCommitScan(claudeCmd) { + log.step('Running proactive pre-commit scan') + + const staged = await runCommandWithOutput( + 'git', + ['diff', '--cached', '--name-only'], + { + cwd: rootPath, + }, + ) + + if (!staged.stdout.trim()) { + log.substep('No staged files to scan') + return { issues: [], safe: true } + } + + const files = staged.stdout.trim().split('\n') + log.substep(`Scanning ${files.length} staged file(s)`) + + const diff = await runCommandWithOutput('git', ['diff', '--cached'], { + cwd: rootPath, + }) + + const prompt = `You are performing a quick pre-commit scan to catch likely CI failures. + +**Staged Changes:** +\`\`\`diff +${diff.stdout} +\`\`\` + +**Task:** Analyze these changes for potential CI failures. + +**Check for:** +- Type errors +- Lint violations (missing semicolons, unused vars, etc.) +- Breaking API changes +- Missing tests for new functionality +- console.log statements +- debugger statements +- .only() or .skip() in tests + +**Output Format (JSON):** +{ + "issues": [ + { + "severity": "high|medium|low", + "type": "type-error|lint|test|other", + "description": "Brief description of the issue", + "file": "path/to/file.ts", + "confidence": 85 + } + ], + "safe": false +} + +**Rules:** +- Only report issues with >60% confidence +- Be specific about file and line if possible +- Mark safe=true if no issues found +- Don't report style issues that auto-fix will handle` + + try { + const result = await runCommandWithOutput( + claudeCmd, + [ + 'code', + '--non-interactive', + '--output-format', + 'text', + '--prompt', + prompt, + ], + { cwd: rootPath, timeout: 30_000 }, + ) + + if (result.exitCode !== 0) { + log.substep('Scan completed (no issues detected)') + return { issues: [], safe: true } + } + + // Parse JSON response. + const jsonMatch = result.stdout.match(/\{[\s\S]*\}/) + if (!jsonMatch) { + return { issues: [], safe: true } + } + + const scan = JSON.parse(jsonMatch[0]) + return scan + } catch (e) { + log.warn(`Scan error: ${e.message}`) + return { issues: [], safe: true } + } +} + +/** + * Success celebration with stats. + */ +async function celebrateSuccess(costTracker, stats = {}) { + const messages = [ + "🎉 CI is green! You're a legend!", + "✨ All tests passed! Claude's got your back!", + '🚀 Ship it! CI is happy!', + '💚 Green as a well-tested cucumber!', + '🏆 Victory! All checks passed!', + '⚡ Flawless execution! CI approved!', + ] + + const message = messages[Math.floor(Math.random() * messages.length)] + log.success(message) + + // Show session stats. + if (costTracker) { + costTracker.showSessionSummary() + } + + // Show fix details if available. + if (stats.fixCount > 0) { + console.log(colors.cyan('\n📊 Session Stats:')) + console.log(` Fixes applied: ${stats.fixCount}`) + console.log(` Retries: ${stats.retries || 0}`) + } + + // Update success streak. + try { + const streakPath = path.join(CLAUDE_HOME, 'streak.json') + let streak = { current: 0, best: 0, lastSuccess: null } + if (existsSync(streakPath)) { + streak = JSON.parse(await fs.readFile(streakPath, 'utf8')) + } + + const now = Date.now() + const oneDayAgo = now - 24 * 60 * 60 * 1000 + + // Reset streak if last success was more than 24h ago. + if (streak.lastSuccess && streak.lastSuccess < oneDayAgo) { + streak.current = 1 + } else { + streak.current += 1 + } + + streak.best = Math.max(streak.best, streak.current) + streak.lastSuccess = now + + await fs.writeFile(streakPath, JSON.stringify(streak, null, 2)) + + console.log(colors.cyan('\n🔥 Success Streak:')) + console.log(` Current: ${streak.current}`) + console.log(` Best: ${streak.best}`) + } catch { + // Ignore errors. + } +} + +/** + * Analyze error to identify root cause and suggest fix strategies. + */ +async function analyzeRootCause(claudeCmd, error, context = {}) { + const ctx = { __proto__: null, ...context } + const errorHash = hashError(error) + + // Check cache first. + const cachePath = path.join(STORAGE_PATHS.cache, `analysis-${errorHash}.json`) + try { + if (existsSync(cachePath)) { + const cached = JSON.parse(await fs.readFile(cachePath, 'utf8')) + const age = Date.now() - cached.timestamp + // Cache valid for 1 hour. + if (age < 60 * 60 * 1000) { + log.substep(colors.gray('Using cached analysis')) + return cached.analysis + } + } + } catch { + // Ignore cache errors. + } + + // Load error history for learning. + const history = await loadErrorHistory() + const similarErrors = findSimilarErrors(errorHash, history) + + log.progress('Analyzing root cause with Claude') + + const prompt = `You are an expert software engineer analyzing a CI/test failure. + +**Error Output:** +\`\`\` +${error} +\`\`\` + +**Context:** +- Check name: ${ctx.checkName || 'Unknown'} +- Repository: ${ctx.repoName || 'Unknown'} +- Previous attempts: ${ctx.attempts || 0} + +${similarErrors.length > 0 ? `**Similar Past Errors:**\n${similarErrors.map(e => `- ${e.description}: ${e.outcome} (${e.strategy})`).join('\n')}\n` : ''} + +**Task:** Analyze this error and provide a structured diagnosis. + +**Output Format (JSON):** +{ + "rootCause": "Brief description of the actual problem (not symptoms)", + "confidence": 85, // 0-100% how certain you are + "category": "type-error|lint|test-failure|build-error|env-issue|other", + "isEnvironmental": false, // true if likely GitHub runner/network/rate-limit issue + "strategies": [ + { + "name": "Fix type assertion", + "probability": 90, // 0-100% estimated success probability + "description": "Add type assertion to resolve type mismatch", + "reasoning": "Error shows TypeScript expecting string but got number" + }, + { + "name": "Update import", + "probability": 60, + "description": "Update import path or module resolution", + "reasoning": "Might be module resolution issue" + } + ], + "environmentalFactors": [ + "Check if GitHub runner has sufficient memory", + "Verify network connectivity for package downloads" + ], + "explanation": "Detailed explanation of what's happening and why" +} + +**Rules:** +- Be specific about the root cause, not just symptoms +- Rank strategies by success probability (highest first) +- Include 1-3 strategies maximum +- Mark as environmental if it's likely a runner/network/external issue +- Use confidence scores honestly (50-70% = uncertain, 80-95% = confident, 95-100% = very confident)` + + try { + const result = await runCommandWithOutput( + claudeCmd, + [ + 'code', + '--non-interactive', + '--output-format', + 'text', + '--prompt', + prompt, + ], + { cwd: rootPath }, + ) + + if (result.exitCode !== 0) { + log.warn('Analysis failed, proceeding without root cause info') + return null + } + + // Parse JSON response. + const jsonMatch = result.stdout.match(/\{[\s\S]*\}/) + if (!jsonMatch) { + log.warn('Could not parse analysis, proceeding without root cause info') + return null + } + + const analysis = JSON.parse(jsonMatch[0]) + + // Cache the analysis. + try { + await fs.writeFile( + cachePath, + JSON.stringify( + { + analysis, + errorHash, + timestamp: Date.now(), + }, + null, + 2, + ), + ) + } catch { + // Ignore cache write errors. + } + + return analysis + } catch (e) { + log.warn(`Analysis error: ${e.message}`) + return null + } +} + +/** + * Load error history from storage. + */ +async function loadErrorHistory() { + const historyPath = path.join(CLAUDE_HOME, 'error-history.json') + try { + if (existsSync(historyPath)) { + const data = JSON.parse(await fs.readFile(historyPath, 'utf8')) + // Only return recent history (last 100 errors). + return data.errors.slice(-100) + } + } catch { + // Ignore errors. + } + return [] +} + +/** + * Save error outcome to history for learning. + */ +async function saveErrorHistory(errorHash, outcome, strategy, description) { + const historyPath = path.join(CLAUDE_HOME, 'error-history.json') + try { + let data = { errors: [] } + if (existsSync(historyPath)) { + data = JSON.parse(await fs.readFile(historyPath, 'utf8')) + } + + // 'success' | 'failed' + data.errors.push({ + errorHash, + outcome, + strategy, + description, + timestamp: Date.now(), + }) + + // Keep only last 200 errors. + if (data.errors.length > 200) { + data.errors = data.errors.slice(-200) + } + + await fs.writeFile(historyPath, JSON.stringify(data, null, 2)) + } catch { + // Ignore errors. + } +} + +/** + * Find similar errors from history. + */ +function findSimilarErrors(errorHash, history) { + return history + .filter(e => e.errorHash === errorHash && e.outcome === 'success') + .slice(-3) +} + +/** + * Display root cause analysis to user. + */ +function displayAnalysis(analysis) { + if (!analysis) { + return + } + + console.log(colors.cyan('\n🔍 Root Cause Analysis:')) + console.log( + ` Cause: ${analysis.rootCause} ${colors.gray(`(${analysis.confidence}% confident)`)}`, + ) + console.log(` Category: ${analysis.category}`) + + if (analysis.isEnvironmental) { + console.log( + colors.yellow( + '\n ⚠ This appears to be an environmental issue (runner/network/external)', + ), + ) + if (analysis.environmentalFactors.length > 0) { + console.log(colors.yellow(' Factors to check:')) + analysis.environmentalFactors.forEach(factor => { + console.log(colors.yellow(` - ${factor}`)) + }) + } + } + + if (analysis.strategies.length > 0) { + console.log( + colors.cyan('\n💡 Fix Strategies (ranked by success probability):'), + ) + analysis.strategies.forEach((strategy, i) => { + console.log( + ` ${i + 1}. ${colors.bold(strategy.name)} ${colors.gray(`(${strategy.probability}%)`)}`, + ) + console.log(` ${strategy.description}`) + console.log(colors.gray(` ${strategy.reasoning}`)) + }) + } + + if (analysis.explanation) { + console.log(colors.cyan('\n📖 Explanation:')) + console.log(colors.gray(` ${analysis.explanation}`)) + } +} + +async function runCommand(command, args = [], options = {}) { + const opts = { __proto__: null, ...options } + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + stdio: 'inherit', + cwd: rootPath, + ...(WIN32 && { shell: true }), + ...opts, + }) + + child.on('exit', code => { + resolve(code || 0) + }) + + child.on('error', error => { + reject(error) + }) + }) +} + +async function runCommandWithOutput(command, args = [], options = {}) { + const opts = { __proto__: null, ...options } + const { input, ...spawnOpts } = opts + + return new Promise((resolve, reject) => { + let stdout = '' + let stderr = '' + + const child = spawn(command, args, { + cwd: rootPath, + ...(WIN32 && { shell: true }), + ...spawnOpts, + }) + + // Write input to stdin if provided. + if (input && child.stdin) { + child.stdin.write(input) + child.stdin.end() + } + + if (child.stdout) { + child.stdout.on('data', data => { + stdout += data + }) + } + + if (child.stderr) { + child.stderr.on('data', data => { + stderr += data + }) + } + + child.on('exit', code => { + resolve({ exitCode: code || 0, stdout, stderr }) + }) + + child.on('error', error => { + reject(error) + }) + }) +} + +// Simple cache for Claude responses with automatic cleanup +const claudeCache = new Map() +// 5 minutes +const CACHE_TTL = 5 * 60 * 1000 + +// Clean up expired cache entries periodically +setInterval(() => { + const now = Date.now() + for (const [key, value] of claudeCache.entries()) { + if (now - value.timestamp > CACHE_TTL) { + claudeCache.delete(key) + } + } + // unref() allows process to exit if this is the only timer. +}, CACHE_TTL).unref() + +/** + * Run Claude Code with a prompt. + * Handles caching, model tracking, and retry logic. + */ +async function runClaude(claudeCmd, prompt, options = {}) { + const opts = { __proto__: null, ...options } + const args = prepareClaudeArgs([], opts) + + // Determine mode for ultrathink decision. + const task = prompt.slice(0, 100) + const forceModel = opts['the-brain'] + ? 'the-brain' + : opts.pinky + ? 'pinky' + : null + const mode = modelStrategy.selectMode(task, { + forceModel, + lastError: opts.lastError, + }) + + // Prepend ultrathink directive when using The Brain mode. + // Ultrathink is Claude's most intensive thinking mode, providing maximum + // thinking budget for deep analysis and complex problem-solving. + // Learn more: https://www.anthropic.com/engineering/claude-code-best-practices + let enhancedPrompt = prompt + if (mode === 'the-brain') { + enhancedPrompt = `ultrathink\n\n${prompt}` + log.substep('🧠 The Brain activated with ultrathink mode') + } + + // Check cache for non-interactive requests + if (opts.interactive === false && opts.cache !== false) { + const cacheKey = `${enhancedPrompt.slice(0, 100)}_${mode}` + const cached = claudeCache.get(cacheKey) + + if (cached && Date.now() - cached.timestamp < CACHE_TTL) { + log.substep('📦 Using cached response') + return cached.result + } + } + + let result + + // Default timeout: 3 minutes for non-interactive, 10 minutes for interactive + const timeout = + opts.timeout || (opts.interactive === false ? 180_000 : 600_000) + const showProgress = opts.showProgress !== false && opts.interactive === false + const startTime = Date.now() + let progressInterval = null + let timedOut = false + + try { + if (opts.interactive !== false) { + // Interactive mode - spawn with inherited stdio and pipe prompt + result = await new Promise((resolve, _reject) => { + const child = spawn(claudeCmd, args, { + stdio: ['pipe', 'inherit', 'inherit'], + cwd: opts.cwd || rootPath, + ...(WIN32 && { shell: true }), + }) + + // Set up timeout for interactive mode + const timeoutId = setTimeout(() => { + timedOut = true + log.warn( + `Claude interactive session timed out after ${Math.round(timeout / 1000)}s`, + ) + child.kill() + resolve(1) + }, timeout) + + // Write the prompt to stdin + if (enhancedPrompt) { + child.stdin.write(enhancedPrompt) + child.stdin.end() + } + + child.on('exit', code => { + clearTimeout(timeoutId) + resolve(code || 0) + }) + + child.on('error', () => { + clearTimeout(timeoutId) + resolve(1) + }) + }) + } else { + // Non-interactive mode - capture output with progress + + // Show initial progress if enabled + if (showProgress && !opts.silent) { + log.progress('Claude analyzing...') + + // Set up progress interval + progressInterval = setInterval(() => { + const elapsed = Date.now() - startTime + if (elapsed > timeout) { + timedOut = true + log.warn(`Claude timed out after ${Math.round(elapsed / 1000)}s`) + if (progressInterval) { + clearInterval(progressInterval) + progressInterval = null + } + } else { + log.progress( + `Claude processing... (${Math.round(elapsed / 1000)}s)`, + ) + } + // Update every 10 seconds. + }, 10_000) + } + + // Run command with timeout + result = await Promise.race([ + runCommandWithOutput(claudeCmd, args, { + ...opts, + input: enhancedPrompt, + stdio: ['pipe', 'pipe', 'pipe'], + }), + new Promise(resolve => { + setTimeout(() => { + if (!timedOut) { + timedOut = true + resolve({ + exitCode: 1, + stdout: '', + stderr: 'Operation timed out', + }) + } + }, timeout) + }), + ]) + + // Clear progress interval + if (progressInterval) { + clearInterval(progressInterval) + progressInterval = null + if (!opts.silent && !timedOut) { + const elapsed = Date.now() - startTime + log.done(`Claude completed in ${Math.round(elapsed / 1000)}s`) + } + } + + // Cache the result + if (opts.cache !== false && result.exitCode === 0 && !timedOut) { + const cacheKey = `${prompt.slice(0, 100)}_${opts._selectedModel || 'default'}` + claudeCache.set(cacheKey, { + result, + timestamp: Date.now(), + }) + } + } + + // Record success for model strategy + modelStrategy.recordAttempt(task, true) + + return result + } catch (error) { + // Record failure for potential escalation + modelStrategy.recordAttempt(task, false) + + // Check if we should retry with Brain + const attempts = modelStrategy.attempts.get(modelStrategy.getTaskKey(task)) + if (attempts === modelStrategy.escalationThreshold && !opts['the-brain']) { + log.warn('🧠 Pinky failed, escalating to The Brain...') + opts['the-brain'] = true + return runClaude(claudeCmd, prompt, opts) + } + + throw error + } +} + +/** + * Check if Claude Code CLI is available. + */ +async function checkClaude() { + const checkCommand = WIN32 ? 'where' : 'which' + + log.progress('Checking for Claude Code CLI') + + // Check for 'claude' command (Claude Code) + const result = await runCommandWithOutput(checkCommand, ['claude']) + if (result.exitCode === 0) { + log.done('Found Claude Code CLI (claude)') + return 'claude' + } + + // Check for 'ccp' as alternative + log.progress('Checking for alternative CLI (ccp)') + const ccpResult = await runCommandWithOutput(checkCommand, ['ccp']) + if (ccpResult.exitCode === 0) { + log.done('Found Claude Code CLI (ccp)') + return 'ccp' + } + + log.failed('Claude Code CLI not found') + return false +} + +/** + * Ensure Claude Code is authenticated, prompting for authentication if needed. + * Returns true if authenticated, false if unable to authenticate. + */ +async function ensureClaudeAuthenticated(claudeCmd) { + let attempts = 0 + const maxAttempts = 3 + + while (attempts < maxAttempts) { + // Check if Claude is working by checking version + log.progress('Checking Claude Code status') + const versionCheck = await runCommandWithOutput(claudeCmd, ['--version']) + + if (versionCheck.exitCode === 0) { + // Claude Code is installed and working + // Check if we need to login by testing actual Claude functionality + log.progress( + 'Testing Claude authentication (this may take up to 15 seconds)', + ) + + const testPrompt = + 'Respond with only the word "AUTHENTICATED" if you receive this message.' + const startTime = Date.now() + + // Set up progress interval for the 15-second test + const progressInterval = setInterval(() => { + const elapsed = Date.now() - startTime + log.progress( + `Testing authentication... (${Math.round(elapsed / 1000)}s/15s)`, + ) + // Update every 3 seconds. + }, 3000) + + const testResult = await runCommandWithOutput(claudeCmd, ['--print'], { + input: testPrompt, + stdio: ['pipe', 'pipe', 'pipe'], + env: { ...process.env, CLAUDE_OUTPUT_MODE: 'text' }, + timeout: 15_000, + }) + + clearInterval(progressInterval) + + // Check for authentication errors + const output = (testResult.stdout + testResult.stderr).toLowerCase() + const authErrors = [ + 'not logged in', + 'authentication', + 'unauthorized', + 'login required', + 'please login', + 'api key', + ] + + const needsAuth = authErrors.some(error => output.includes(error)) + const authenticated = output.includes('authenticated') + + if (!needsAuth && (authenticated || testResult.exitCode === 0)) { + log.done('Claude Code ready') + return true + } + + if (!needsAuth && testResult.stdout.length > 10) { + // Claude responded with something, likely working + log.done('Claude Code ready') + return true + } + } + + attempts++ + + if (attempts >= maxAttempts) { + log.error(`Failed to setup Claude Code after ${maxAttempts} attempts`) + return false + } + + // Not authenticated, provide instructions for manual authentication + log.warn('Claude Code login required') + console.log(colors.yellow('\nClaude Code needs to be authenticated.')) + console.log('\nTo authenticate:') + console.log(' 1. Open a new terminal') + console.log(` 2. Run: ${colors.green('claude')}`) + console.log(' 3. Follow the browser authentication prompts') + console.log( + ' 4. Once authenticated, return here and press Enter to continue', + ) + + // Wait for user to press Enter + await new Promise(resolve => { + process.stdin.once('data', () => { + resolve() + }) + }) + + // Give it a moment for the auth to register + await new Promise(resolve => setTimeout(resolve, 1000)) + } + + return false +} + +/** + * Ensure GitHub CLI is authenticated, prompting for login if needed. + * Returns true if authenticated, false if unable to authenticate. + */ +async function ensureGitHubAuthenticated() { + let attempts = 0 + const maxAttempts = 3 + + while (attempts < maxAttempts) { + log.progress('Checking GitHub authentication') + const authCheck = await runCommandWithOutput('gh', ['auth', 'status']) + + if (authCheck.exitCode === 0) { + log.done('GitHub CLI authenticated') + return true + } + + attempts++ + + if (attempts >= maxAttempts) { + log.error( + `Failed to authenticate with GitHub after ${maxAttempts} attempts`, + ) + return false + } + + // Not authenticated, prompt for login + log.warn('GitHub authentication required') + console.log(colors.yellow('\nYou need to authenticate with GitHub.')) + console.log('Follow the prompts to complete authentication.\n') + + // Run gh auth login interactively + log.progress('Starting GitHub login process') + const loginResult = await runCommand('gh', ['auth', 'login'], { + stdio: 'inherit', + }) + + if (loginResult === 0) { + log.done('Login process completed') + // Give it a moment for the auth to register + await new Promise(resolve => setTimeout(resolve, 2000)) + } else { + log.failed('Login process failed') + console.log(colors.red('\nLogin failed. Please try again.')) + + if (attempts < maxAttempts) { + console.log( + colors.yellow(`\nAttempt ${attempts + 1} of ${maxAttempts}`), + ) + await new Promise(resolve => setTimeout(resolve, 1000)) + } + } + } + + return false +} + +/** + * Check if a commit SHA is part of a pull request. + * @param {string} sha - The commit SHA to check + * @param {string} owner - The repository owner + * @param {string} repo - The repository name + * @returns {Promise<{isPR: boolean, prNumber?: number, prTitle?: string}>} + */ +async function checkIfCommitIsPartOfPR(sha, owner, repo) { + try { + const result = await runCommandWithOutput('gh', [ + 'pr', + 'list', + '--repo', + `${owner}/${repo}`, + '--state', + 'all', + '--search', + sha, + '--json', + 'number,title,state', + '--limit', + '1', + ]) + + if (result.exitCode === 0 && result.stdout) { + const prs = JSON.parse(result.stdout) + if (prs.length > 0) { + const pr = prs[0] + return { + isPR: true, + prNumber: pr.number, + prTitle: pr.title, + prState: pr.state, + } + } + } + } catch (e) { + log.warn(`Failed to check if commit is part of PR: ${e.message}`) + } + + return { isPR: false } +} + +/** + * Create a semantic hash of error output for tracking duplicate errors. + * Normalizes errors to catch semantically identical issues with different line numbers. + * @param {string} errorOutput - The error output to hash + * @returns {string} A hex hash of the normalized error + */ +function hashError(errorOutput) { + // Normalize error for semantic comparison + const normalized = errorOutput + .trim() + // Remove timestamps + .replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^Z\s]*/g, 'TIMESTAMP') + .replace(/\d{2}:\d{2}:\d{2}/g, 'TIME') + // Remove line:column numbers (but keep file paths) + .replace(/:\d+:\d+/g, ':*:*') + .replace(/line \d+/gi, 'line *') + .replace(/column \d+/gi, 'column *') + // Remove specific SHAs and commit hashes + .replace(/\b[0-9a-f]{7,40}\b/g, 'SHA') + // Remove absolute file system paths (keep relative paths) + .replace(/\/[^\s]*?\/([^/\s]+)/g, '$1') + // Normalize whitespace + .replace(/\s+/g, ' ') + // Take first 500 chars (increased from 200 for better matching) + .slice(0, 500) + + // Use proper cryptographic hashing for consistent results + return crypto + .createHash('sha256') + .update(normalized) + .digest('hex') + .slice(0, 16) +} + +/** + * Model strategy for intelligent Pinky/Brain switching. + * "Gee, Brain, what do you want to do tonight?" + * "The same thing we do every night, Pinky - try to take over the world!" + */ +class ModelStrategy { + constructor() { + this.attempts = new Map() + this.escalationThreshold = 2 + // 5 minutes + this.brainTimeout = 5 * 60 * 1000 + this.brainActivatedAt = null + this.lastTaskComplexity = new Map() + } + + selectMode(task, options = {}) { + const { forceModel = null } = options + + // Honor explicit flags. + if (forceModel === 'the-brain') { + log.substep('🧠 The Brain activated (user requested)') + return 'the-brain' + } + if (forceModel === 'pinky') { + return 'pinky' + } + + // Check if in temporary Brain mode. + if (this.brainActivatedAt) { + const elapsed = Date.now() - this.brainActivatedAt + if (elapsed < this.brainTimeout) { + const remaining = Math.round((this.brainTimeout - elapsed) / 1000) + log.substep(`🧠 Brain mode active (${remaining}s remaining)`) + return 'the-brain' + } + this.brainActivatedAt = null + log.substep('🐭 Reverting to Pinky mode') + } + + // Auto-escalate based on failures. + const taskKey = this.getTaskKey(task) + const attempts = this.attempts.get(taskKey) || 0 + + if (attempts >= this.escalationThreshold) { + log.warn(`🧠 Escalating to The Brain after ${attempts} Pinky attempts`) + this.activateBrain() + return 'the-brain' + } + + // Check task complexity. + if (this.assessComplexity(task) > 0.8) { + log.substep('🧠 Complex task detected, using The Brain') + return 'the-brain' + } + + // Default to efficient Pinky. + return 'pinky' + } + + selectModel(task, options = {}) { + const mode = this.selectMode(task, options) + + // Map mode to model. + // Currently both use the same model, but this allows for future differentiation. + if (mode === 'the-brain') { + return 'claude-3-5-sonnet-20241022' + } + + return 'claude-3-5-sonnet-20241022' + } + + recordAttempt(task, success) { + const taskKey = this.getTaskKey(task) + if (success) { + this.attempts.delete(taskKey) + if (this.brainActivatedAt) { + log.substep('📝 The Brain solved it - noting pattern for future') + } + } else { + const current = this.attempts.get(taskKey) || 0 + this.attempts.set(taskKey, current + 1) + } + } + + activateBrain(duration = this.brainTimeout) { + this.brainActivatedAt = Date.now() + log.substep(`🧠 The Brain activated for ${duration / 1000} seconds`) + } + + assessComplexity(task) { + const taskLower = task.toLowerCase() + const complexPatterns = { + architecture: 0.9, + 'memory leak': 0.85, + 'race condition': 0.85, + security: 0.8, + 'complex refactor': 0.85, + performance: 0.75, + 'production issue': 0.9, + } + + let maxScore = 0.3 + for (const [pattern, score] of Object.entries(complexPatterns)) { + if (taskLower.includes(pattern)) { + maxScore = Math.max(maxScore, score) + } + } + return maxScore + } + + getTaskKey(task) { + return task.slice(0, 100).replace(/\s+/g, '_').toLowerCase() + } +} + +const modelStrategy = new ModelStrategy() + +/** + * Smart context loading - focus on recently changed files for efficiency. + * Reduces context by 90% while catching 95% of issues. + */ +async function getSmartContext(options = {}) { + const { + commits = 5, + fileTypes = null, + includeUncommitted = true, + maxFiles = 30, + } = options + + const context = { + recent: [], + uncommitted: [], + hotspots: [], + priority: [], + commitMessages: [], + } + + // Get uncommitted changes (highest priority) + if (includeUncommitted) { + const stagedResult = await runCommandWithOutput( + 'git', + ['diff', '--cached', '--name-only'], + { + cwd: rootPath, + }, + ) + const unstagedResult = await runCommandWithOutput( + 'git', + ['diff', '--name-only'], + { + cwd: rootPath, + }, + ) + + context.uncommitted = [ + ...new Set([ + ...stagedResult.stdout.trim().split('\n').filter(Boolean), + ...unstagedResult.stdout.trim().split('\n').filter(Boolean), + ]), + ] + } + + // Get files changed in recent commits + const recentResult = await runCommandWithOutput( + 'git', + ['diff', '--name-only', `HEAD~${commits}..HEAD`], + { cwd: rootPath }, + ) + + context.recent = recentResult.stdout.trim().split('\n').filter(Boolean) + + // Find hotspots (files that change frequently) + const frequency = {} + context.recent.forEach(file => { + frequency[file] = (frequency[file] || 0) + 1 + }) + + context.hotspots = Object.entries(frequency) + .filter(([_, count]) => count > 1) + .sort(([_, a], [__, b]) => b - a) + .map(([file]) => file) + + // Get recent commit messages for intent inference + const logResult = await runCommandWithOutput( + 'git', + ['log', '--oneline', '-n', commits.toString()], + { cwd: rootPath }, + ) + + context.commitMessages = logResult.stdout.trim().split('\n') + + // Build priority list + context.priority = [ + ...context.uncommitted, + ...context.hotspots, + ...context.recent.filter(f => !context.hotspots.includes(f)), + ] + + // Remove duplicates and apply filters + context.priority = [...new Set(context.priority)] + + if (fileTypes) { + context.priority = context.priority.filter(file => + fileTypes.some(ext => file.endsWith(ext)), + ) + } + + // Limit to maxFiles + context.priority = context.priority.slice(0, maxFiles) + + // Infer developer intent from commits + context.intent = inferIntent(context.commitMessages) + + return context +} + +/** + * Infer what the developer is working on from commit messages. + */ +function inferIntent(messages) { + const patterns = { + bugfix: /fix|bug|issue|error|crash/i, + feature: /add|implement|feature|new/i, + refactor: /refactor|clean|improve|optimize/i, + security: /security|vulnerability|cve/i, + performance: /perf|speed|optimize|faster/i, + test: /test|spec|coverage/i, + } + + const intents = new Set() + messages.forEach(msg => { + Object.entries(patterns).forEach(([intent, pattern]) => { + if (pattern.test(msg)) { + intents.add(intent) + } + }) + }) + + return Array.from(intents) +} + +/** + * Enhanced prompt templates with rich context. + */ +const PROMPT_TEMPLATES = { + review: context => `Role: Senior Principal Engineer at Socket.dev +Expertise: Security, Performance, Node.js, TypeScript + +Project Context: +- Name: ${context.projectName || 'Socket project'} +- Type: ${context.projectType || 'Node.js/TypeScript'} +- Recent work: ${context.intent?.join(', ') || 'general development'} +- Files changed: ${context.uncommitted?.length || 0} uncommitted, ${context.hotspots?.length || 0} hotspots + +Review Criteria (in priority order): +1. Security vulnerabilities (especially supply chain) +2. Performance bottlenecks and memory leaks +3. Race conditions and async issues +4. Error handling gaps +5. Code maintainability + +Recent commits context: +${context.commitMessages?.slice(0, 5).join('\n') || 'No recent commits'} + +Provide: +- Severity level for each issue +- Specific line numbers +- Concrete fix examples +- Performance impact estimates`, + + fix: context => `Role: Principal Security Engineer +Focus: Socket.dev supply chain security + +Scan Context: +- Priority files: ${context.priority?.slice(0, 10).join(', ') || 'all files'} +- Intent: ${context.intent?.join(', ') || 'general fixes'} + +Focus Areas: +1. PRIORITY 1 - Security vulnerabilities +2. PRIORITY 2 - Memory leaks and performance +3. PRIORITY 3 - Error handling + +Auto-fix Capabilities: +- Apply ESLint fixes +- Update TypeScript types +- Add error boundaries +- Implement retry logic +- Add input validation`, + + green: context => `Role: Principal DevOps Engineer +Mission: Achieve green CI build + +Current Issues: +${context.ciErrors?.map(e => `- ${e}`).join('\n') || 'Unknown CI failures'} + +Available Actions: +1. Update test snapshots +2. Fix lint issues +3. Resolve type errors +4. Install missing pinned dependencies +5. Update configurations + +Constraints: +- Do NOT modify business logic +- Do NOT delete tests +- DO fix root causes`, + + test: context => `Role: Principal Test Engineer +Framework: ${context.testFramework || 'Vitest'} + +Generate comprehensive tests for: +${context.targetFiles?.join('\n') || 'specified files'} + +Requirements: +- Achieve 100% code coverage +- Include edge cases +- Add error scenarios +- Test async operations +- Mock external dependencies`, + + refactor: context => `Role: Principal Software Architect +Focus: Code quality and maintainability + +Files to refactor: +${context.priority?.slice(0, 20).join('\n') || 'specified files'} + +Improvements: +- Apply SOLID principles +- Reduce cyclomatic complexity +- Improve type safety +- Enhance testability +- Optimize performance`, +} + +/** + * Build enhanced prompt with context. + */ +async function buildEnhancedPrompt(template, basePrompt, options = {}) { + const context = await getSmartContext(options) + + // Add project info + try { + const packageJsonPath = path.join(rootPath, 'package.json') + if (existsSync(packageJsonPath)) { + const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')) + context.projectName = packageJson.name + context.projectType = packageJson.type || 'commonjs' + context.testFramework = Object.keys( + packageJson.devDependencies || {}, + ).find(dep => ['vitest', 'jest', 'mocha'].includes(dep)) + } + } catch { + // Ignore if can't read package.json + } + + // Get template or use base prompt + let enhancedPrompt = basePrompt + if (PROMPT_TEMPLATES[template]) { + const templatePrompt = PROMPT_TEMPLATES[template](context) + enhancedPrompt = `${templatePrompt}\n\n${basePrompt}` + } + + // Add file context if priority files exist + if (context.priority?.length > 0) { + enhancedPrompt += `\n\nPRIORITY FILES TO FOCUS ON:\n${context.priority + .slice(0, 20) + .map((f, i) => `${i + 1}. ${f}`) + .join('\n')}` + } + + return enhancedPrompt +} + +/** + * Filter CI logs to extract only relevant failure information + * Removes runner setup noise and focuses on actual errors + */ +function filterCILogs(rawLogs) { + const lines = rawLogs.split('\n') + const relevantLines = [] + let inErrorSection = false + + for (const line of lines) { + // Skip runner metadata and setup + if ( + line.includes('Current runner version:') || + line.includes('Runner Image') || + line.includes('Operating System') || + line.includes('GITHUB_TOKEN') || + line.includes('Prepare workflow') || + line.includes('Prepare all required') || + line.includes('##[group]') || + line.includes('##[endgroup]') || + line.includes('Post job cleanup') || + line.includes('git config') || + line.includes('git submodule') || + line.includes('Cleaning up orphan') || + line.includes('secret source:') || + line.includes('[command]/usr/bin/git') + ) { + continue + } + + // Detect error sections + if ( + line.includes('##[error]') || + line.includes('Error:') || + line.includes('error TS') || + line.includes('FAIL') || + line.includes('✗') || + line.includes('❌') || + line.includes('failed') || + line.includes('ELIFECYCLE') + ) { + inErrorSection = true + relevantLines.push(line) + } else if (inErrorSection && line.trim() !== '') { + relevantLines.push(line) + // Keep context for 5 lines after error + if (relevantLines.length > 100) { + inErrorSection = false + } + } + } + + // If no errors found, return last 50 lines (might contain useful context) + if (relevantLines.length === 0) { + return lines.slice(-50).join('\n') + } + + return relevantLines.join('\n') +} + +/** + * Prepare Claude command arguments for Claude Code. + * Claude Code uses natural language prompts, not the same flags. + * We'll translate our flags into appropriate context. + */ +function prepareClaudeArgs(args = [], options = {}) { + const _opts = { __proto__: null, ...options } + const claudeArgs = [...args] + + // Smart model selection. + const task = _opts.prompt || _opts.command || 'general task' + const forceModel = _opts['the-brain'] + ? 'the-brain' + : _opts.pinky + ? 'pinky' + : null + + const mode = modelStrategy.selectMode(task, { + forceModel, + lastError: _opts.lastError, + }) + + const model = modelStrategy.selectModel(task, { + forceModel, + lastError: _opts.lastError, + }) + + // Track mode for caching and logging. + _opts._selectedMode = mode + _opts._selectedModel = model + + // Add --dangerously-skip-permissions unless --no-darkwing is specified + // "Let's get dangerous!" mode for automated CI fixes + if (!_opts['no-darkwing']) { + claudeArgs.push('--dangerously-skip-permissions') + } + + return claudeArgs +} + +/** + * Execute tasks in parallel with multiple workers. + * Default: 3 workers (balanced performance without overwhelming system) + */ +async function executeParallel(tasks, workers = 3) { + if (workers === 1 || tasks.length === 1) { + // Sequential execution + const results = [] + for (const task of tasks) { + results.push(await task()) + } + return results + } + + // Parallel execution with worker limit + log.substep(`🚀 Executing ${tasks.length} tasks with ${workers} workers`) + const results = [] + const executing = [] + + for (const task of tasks) { + const promise = task().then(result => { + executing.splice(executing.indexOf(promise), 1) + return result + }) + + results.push(promise) + executing.push(promise) + + if (executing.length >= workers) { + await Promise.race(executing) + } + } + + return Promise.all(results) +} + +/** + * Determine if parallel execution should be used. + */ +function shouldRunParallel(options = {}) { + const opts = { __proto__: null, ...options } + // Parallel is only used when: + // 1. --cross-repo is specified (multi-repo mode) + // 2. AND --seq is not specified + if (opts['cross-repo'] && !opts.seq) { + return true + } + return false +} + +/** + * Run tasks in parallel with progress tracking. + * NOTE: When running Claude agents in parallel, they must use stdio: 'pipe' to avoid + * conflicting interactive prompts. If agents need user interaction, they would queue + * and block each other. Use --seq flag for sequential execution with full interactivity. + */ +async function runParallel(tasks, description = 'tasks', taskNames = []) { + log.info(`Running ${tasks.length} ${description} in parallel...`) + + const startTime = Date.now() + let completed = 0 + + // Add progress tracking to each task + const trackedTasks = tasks.map((task, index) => { + const name = taskNames[index] || `Task ${index + 1}` + const taskStartTime = Date.now() + + return task.then( + result => { + completed++ + const elapsed = Math.round((Date.now() - taskStartTime) / 1000) + log.done( + `[${name}] Completed (${elapsed}s) - ${completed}/${tasks.length}`, + ) + return result + }, + error => { + completed++ + const elapsed = Math.round((Date.now() - taskStartTime) / 1000) + log.failed( + `[${name}] Failed (${elapsed}s) - ${completed}/${tasks.length}`, + ) + throw error + }, + ) + }) + + // Progress indicator + const progressInterval = setInterval(() => { + const elapsed = Math.round((Date.now() - startTime) / 1000) + const pending = tasks.length - completed + if (pending > 0) { + log.substep( + `Progress: ${completed}/${tasks.length} complete, ${pending} running (${elapsed}s elapsed)`, + ) + } + }, 15_000) + // Update every 15 seconds + + const results = await Promise.allSettled(trackedTasks) + clearInterval(progressInterval) + + const totalElapsed = Math.round((Date.now() - startTime) / 1000) + const succeeded = results.filter(r => r.status === 'fulfilled').length + const failed = results.filter(r => r.status === 'rejected').length + + if (failed > 0) { + log.warn( + `Completed in ${totalElapsed}s: ${succeeded} succeeded, ${failed} failed`, + ) + // Log errors with task names + results.forEach((result, index) => { + if (result.status === 'rejected') { + const name = taskNames[index] || `Task ${index + 1}` + log.error(`[${name}] failed: ${result.reason}`) + } + }) + } else { + log.success( + `All ${succeeded} ${description} completed successfully in ${totalElapsed}s`, + ) + } + + return results +} + +/** + * Ensure .claude directory is in .gitignore. + */ +async function ensureClaudeInGitignore() { + const gitignorePath = path.join(rootPath, '.gitignore') + + try { + // Check if .gitignore exists. + const gitignoreContent = await fs.readFile(gitignorePath, 'utf8') + const lines = gitignoreContent.split('\n') + + // Check if .claude is already ignored. + const hasClaudeEntry = lines.some(line => { + const trimmed = line.trim() + return ( + trimmed === '.claude' || + trimmed === '/.claude' || + trimmed === '.claude/' || + trimmed === '/.claude/' + ) + }) + + if (!hasClaudeEntry) { + // Add .claude to .gitignore. + log.warn('.claude directory not in .gitignore, adding it') + const updatedContent = `${gitignoreContent.trimEnd()}\n/.claude\n` + await fs.writeFile(gitignorePath, updatedContent) + log.done('Added /.claude to .gitignore') + } + } catch (e) { + if (e.code === 'ENOENT') { + // Create .gitignore with .claude entry. + log.warn('No .gitignore found, creating one') + await fs.writeFile(gitignorePath, '/.claude\n') + log.done('Created .gitignore with /.claude entry') + } else { + log.error(`Failed to check .gitignore: ${e.message}`) + } + } +} + +/** + * Find Socket projects in parent directory. + */ +async function findSocketProjects() { + const projects = [] + + for (const projectName of SOCKET_PROJECTS) { + const projectPath = path.join(parentPath, projectName) + const claudeMdPath = path.join(projectPath, 'CLAUDE.md') + + if (existsSync(projectPath) && existsSync(claudeMdPath)) { + projects.push({ + name: projectName, + path: projectPath, + claudeMdPath, + }) + } + } + + return projects +} + +/** + * Create a Claude prompt for syncing CLAUDE.md files. + */ +function createSyncPrompt(projectName, isRegistry = false) { + if (isRegistry) { + return `You are updating the CLAUDE.md file in the socket-registry project, which is the CANONICAL source for all cross-project Socket standards. + +Your task: +1. Review the current CLAUDE.md in socket-registry +2. Identify any sections that should be the authoritative source for ALL Socket projects +3. Ensure these sections are clearly marked as "SHARED STANDARDS" or similar +4. Keep the content well-organized and comprehensive + +The socket-registry/CLAUDE.md should contain: +- Cross-platform compatibility rules +- Node.js version requirements +- Safe file operations standards +- Git workflow standards +- Testing & coverage standards +- Package management standards +- Code style guidelines +- Error handling patterns +- Any other standards that apply to ALL Socket projects + +Output ONLY the updated CLAUDE.md content, nothing else.` + } + + return `You are updating the CLAUDE.md file in the ${projectName} project. + +The socket-registry/CLAUDE.md is the CANONICAL source for all cross-project standards. Your task: + +1. Read the canonical ../socket-registry/CLAUDE.md +2. Read the current CLAUDE.md in ${projectName} +3. Update ${projectName}/CLAUDE.md to: + - Reference the canonical socket-registry/CLAUDE.md for all shared standards + - Remove any redundant cross-project information that's already in socket-registry + - Keep ONLY project-specific guidelines and requirements + - Add a clear reference at the top pointing to socket-registry/CLAUDE.md as the canonical source + +The ${projectName}/CLAUDE.md should contain: +- A reference to socket-registry/CLAUDE.md as the canonical source +- Project-specific architecture notes +- Project-specific commands and workflows +- Project-specific dependencies or requirements +- Any unique patterns or rules for this project only + +Start the file with something like: +# CLAUDE.md + +**CANONICAL REFERENCE**: See ../socket-registry/CLAUDE.md for shared Socket standards. + +Then include only PROJECT-SPECIFIC content. + +Output ONLY the updated CLAUDE.md content, nothing else.` +} + +/** + * Update a project's CLAUDE.md using Claude. + */ +async function updateProjectClaudeMd(claudeCmd, project, options = {}) { + const _opts = { __proto__: null, ...options } + const { claudeMdPath, name } = project + const isRegistry = name === 'socket-registry' + + log.progress(`Updating ${name}/CLAUDE.md`) + + // Read current content. + const currentContent = await fs.readFile(claudeMdPath, 'utf8') + + // Read canonical content if not registry. + let canonicalContent = '' + if (!isRegistry) { + const canonicalPath = path.join(parentPath, 'socket-registry', 'CLAUDE.md') + if (existsSync(canonicalPath)) { + canonicalContent = await fs.readFile(canonicalPath, 'utf8') + } + } + + // Create the prompt. + const prompt = createSyncPrompt(name, isRegistry) + + // Build full context for Claude. + let fullPrompt = `${prompt}\n\n` + + if (!isRegistry && canonicalContent) { + fullPrompt += `===== CANONICAL socket-registry/CLAUDE.md ===== +${canonicalContent} + +` + } + + fullPrompt += `===== CURRENT ${name}/CLAUDE.md ===== +${currentContent} + +===== OUTPUT UPDATED ${name}/CLAUDE.md BELOW =====` + + // Call Claude to update the file. + const result = await runCommandWithOutput( + claudeCmd, + prepareClaudeArgs([], options), + { + input: fullPrompt, + stdio: ['pipe', 'pipe', 'pipe'], + }, + ) + + if (result.exitCode !== 0) { + log.failed(`Failed to update ${name}/CLAUDE.md`) + return false + } + + // Extract the updated content. + const updatedContent = result.stdout.trim() + + // Write the updated file. + await fs.writeFile(claudeMdPath, updatedContent) + log.done(`Updated ${name}/CLAUDE.md`) + + return true +} + +/** + * Commit changes in a project. + */ +async function commitChanges(project) { + const { name, path: projectPath } = project + + log.progress(`Committing changes in ${name}`) + + // Check if there are changes to commit. + const statusResult = await runCommandWithOutput( + 'git', + ['status', '--porcelain', 'CLAUDE.md'], + { + cwd: projectPath, + }, + ) + + if (!statusResult.stdout.trim()) { + log.done(`No changes in ${name}`) + return true + } + + // Stage the file. + await runCommand('git', ['add', 'CLAUDE.md'], { + cwd: projectPath, + stdio: 'pipe', + }) + + // Commit with appropriate message. + const message = + name === 'socket-registry' + ? 'Update CLAUDE.md as canonical source for cross-project standards' + : 'Sync CLAUDE.md with canonical socket-registry standards' + + const commitResult = await runCommandWithOutput( + 'git', + ['commit', '-m', message, '--no-verify'], + { + cwd: projectPath, + }, + ) + + if (commitResult.exitCode !== 0) { + log.failed(`Failed to commit in ${name}`) + return false + } + + log.done(`Committed changes in ${name}`) + return true +} + +/** + * Sync CLAUDE.md files across Socket projects. + */ +async function syncClaudeMd(claudeCmd, options = {}) { + const opts = { __proto__: null, ...options } + printHeader('CLAUDE.md Synchronization') + + // Find Socket projects. + log.progress('Finding Socket projects') + const projects = await findSocketProjects() + if (projects.length === 0) { + log.failed('No Socket projects found') + log.error('Expected projects in parent directory:') + SOCKET_PROJECTS.forEach(p => { + log.substep(path.join(parentPath, p)) + }) + return false + } + log.done(`Found ${projects.length} Socket projects`) + + // Process socket-registry first (it's the canonical source). + log.step('Updating canonical source') + const registryProject = projects.find(p => p.name === 'socket-registry') + if (registryProject) { + const success = await updateProjectClaudeMd( + claudeCmd, + registryProject, + options, + ) + if (!success && !opts['dry-run']) { + log.error('Failed to update canonical socket-registry/CLAUDE.md') + return false + } + } + + // Process other projects. + log.step('Updating project-specific files') + const otherProjects = projects.filter(p => p.name !== 'socket-registry') + + if (shouldRunParallel(opts) && otherProjects.length > 1) { + // Run in parallel + const tasks = otherProjects.map(project => + updateProjectClaudeMd(claudeCmd, project, options) + .then(success => ({ project: project.name, success })) + .catch(error => ({ project: project.name, success: false, error })), + ) + + const taskNames = projects.map(p => path.basename(p)) + const results = await runParallel(tasks, 'CLAUDE.md updates', taskNames) + + // Check for failures + results.forEach(result => { + if ( + result.status === 'fulfilled' && + !result.value.success && + !opts['dry-run'] + ) { + log.error(`Failed to update ${result.value.project}/CLAUDE.md`) + } + }) + } else { + // Run sequentially + for (const project of otherProjects) { + const success = await updateProjectClaudeMd(claudeCmd, project, options) + if (!success && !opts['dry-run']) { + log.error(`Failed to update ${project.name}/CLAUDE.md`) + // Continue with other projects. + } + } + } + + // Commit changes if not skipped. + if (!opts['skip-commit'] && !opts['dry-run']) { + log.step('Committing changes') + + if (shouldRunParallel(opts) && projects.length > 1) { + // Run commits in parallel + const tasks = projects.map(project => commitChanges(project)) + const taskNames = projects.map(p => path.basename(p)) + await runParallel(tasks, 'commits', taskNames) + } else { + // Run sequentially + for (const project of projects) { + await commitChanges(project) + } + } + } + + // Push if requested. + if (opts.push && !opts['dry-run']) { + log.step('Pushing changes') + + if (shouldRunParallel(opts) && projects.length > 1) { + // Run pushes in parallel + const tasks = projects.map(project => { + return runCommandWithOutput('git', ['push'], { + cwd: project.path, + }) + .then(pushResult => ({ + project: project.name, + success: pushResult.exitCode === 0, + })) + .catch(error => ({ + project: project.name, + success: false, + error, + })) + }) + + const results = await runParallel(tasks, 'pushes') + + // Report results + results.forEach(result => { + if (result.status === 'fulfilled' && result.value.success) { + log.done(`Pushed ${result.value.project}`) + } else { + log.failed(`Failed to push ${result.value.project}`) + } + }) + } else { + // Run sequentially + for (const project of projects) { + log.progress(`Pushing ${project.name}`) + const pushResult = await runCommandWithOutput('git', ['push'], { + cwd: project.path, + }) + + if (pushResult.exitCode === 0) { + log.done(`Pushed ${project.name}`) + } else { + log.failed(`Failed to push ${project.name}`) + } + } + } + } + + printFooter('CLAUDE.md synchronization complete!') + + if (!opts['skip-commit'] && !opts['dry-run']) { + log.info('\nNext steps:') + if (!opts.push) { + log.substep('Review changes with: git log --oneline -n 5') + log.substep('Push to remote with: git push (in each project)') + } else { + log.substep('Changes have been pushed to remote repositories') + } + } + + return true +} + +/** + * Scan a project for issues and generate a report. + */ +async function scanProjectForIssues(claudeCmd, project, options = {}) { + const _opts = { __proto__: null, ...options } + const { name, path: projectPath } = project + + log.progress(`Scanning ${name} for issues`) + + // Find source files to scan + const extensions = ['.js', '.mjs', '.ts', '.mts', '.jsx', '.tsx'] + const allFiles = [] + + async function findFiles(dir, depth = 0) { + // Limit depth to avoid excessive scanning + if (depth > 5) { + return + } + + try { + const entries = await fs.readdir(dir, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + + // Skip common directories to ignore. + if (entry.isDirectory()) { + if ( + [ + 'node_modules', + '.git', + 'dist', + 'build', + 'coverage', + '.cache', + ].includes(entry.name) + ) { + continue + } + await findFiles(fullPath, depth + 1) + } else if (entry.isFile()) { + const ext = path.extname(entry.name) + if (extensions.includes(ext)) { + allFiles.push(fullPath) + } + } + } + } catch (e) { + // Log permission errors but continue scanning. + if (e.code === 'EACCES' || e.code === 'EPERM') { + // Silently skip permission errors. + } else { + log.warn(`Error scanning ${dir}: ${e.message}`) + } + } + } + + await findFiles(projectPath) + + // Use smart context if available to prioritize files + let filesToScan = allFiles + if (_opts.smartContext !== false) { + const context = await getSmartContext({ + fileTypes: extensions, + maxFiles: 100, + }) + + if (context.priority.length > 0) { + // Prioritize recently changed files + const priorityFiles = context.priority + .map(f => path.join(projectPath, f)) + .filter(f => allFiles.includes(f)) + + // Add other files after priority ones + const otherFiles = allFiles.filter(f => !priorityFiles.includes(f)) + filesToScan = [...priorityFiles, ...otherFiles] + + log.substep(`Prioritizing ${priorityFiles.length} recently changed files`) + } + } + + // Limit total files to scan + const MAX_FILES = 500 + if (filesToScan.length > MAX_FILES) { + log.substep( + `Limiting scan to first ${MAX_FILES} files (${filesToScan.length} total found)`, + ) + filesToScan = filesToScan.slice(0, MAX_FILES) + } + + // Create enhanced scanning prompt with context + const basePrompt = `You are performing a security and quality audit on the ${name} project. + +Scan for the following issues: +1. **Logic bugs**: Incorrect conditions, off-by-one errors, wrong operators +2. **Race conditions**: Async/await issues, promise handling, concurrent access +3. **Cross-platform issues**: + - Hard-coded path separators (/ or \\) + - System-specific assumptions + - File path handling without path.join/path.resolve + - Platform-specific commands without checks +4. **File system failure cases**: + - Missing error handling for file operations + - No checks for file/directory existence + - Uncaught ENOENT, EACCES, EPERM errors +5. **Async failure cases**: + - Unhandled promise rejections + - Missing try/catch around async operations + - Fire-and-forget promises +6. **HTTP/API issues**: + - Missing timeout configurations + - No retry logic for transient failures + - Unhandled network errors +7. **Memory leaks**: + - Event listeners not cleaned up + - Large objects kept in closures + - Circular references +8. **Security issues**: + - Command injection vulnerabilities + - Path traversal vulnerabilities + - Unsafe use of eval or Function constructor + - Hardcoded secrets or credentials + +For each issue found, provide: +- File path and line number +- Issue type and severity (critical/high/medium/low) +- Description of the problem +- Suggested fix + +Format your response as a JSON array: +[ + { + "file": "path/to/file.js", + "line": 42, + "severity": "high", + "type": "race-condition", + "description": "Async operation without proper await", + "fix": "Add await before the async call" + } +] + +Files to scan: ${filesToScan.length} files in ${name} + +Provide ONLY the JSON array, nothing else.` + + // Use enhanced prompt for better context + const enhancedPrompt = await buildEnhancedPrompt('fix', basePrompt, { + maxFiles: 50, + smartContext: true, + }) + + // Call Claude to scan. + const result = await runCommandWithOutput( + claudeCmd, + prepareClaudeArgs([], options), + { + input: enhancedPrompt, + stdio: ['pipe', 'pipe', 'pipe'], + // 10MB buffer for large responses + maxBuffer: 1024 * 1024 * 10, + }, + ) + + if (result.exitCode !== 0) { + log.failed(`Failed to scan ${name}`) + return null + } + + log.done(`Scanned ${name}`) + + try { + return JSON.parse(result.stdout.trim()) + } catch { + log.warn(`Failed to parse scan results for ${name}`) + return null + } +} + +/** + * Autonomous fix session - auto-fixes high-confidence issues. + */ +async function autonomousFixSession( + claudeCmd, + scanResults, + projects, + options = {}, +) { + const opts = { __proto__: null, ...options } + printHeader('Auto-Fix Mode') + + // Group issues by severity. + const critical = [] + const high = [] + const medium = [] + const low = [] + + for (const project in scanResults) { + const issues = scanResults[project] || [] + for (const issue of issues) { + issue.project = project + switch (issue.severity) { + case 'critical': + critical.push(issue) + break + case 'high': + high.push(issue) + break + case 'medium': + medium.push(issue) + break + default: + low.push(issue) + } + } + } + + const totalIssues = critical.length + high.length + medium.length + low.length + + log.info('🎯 Auto-fix mode: Carefully fixing issues with double-checking') + console.log('\nIssues found:') + console.log(` ${colors.red(`Critical: ${critical.length}`)}`) + console.log(` ${colors.yellow(`High: ${high.length}`)}`) + console.log(` ${colors.cyan(`Medium: ${medium.length}`)}`) + console.log(` ${colors.gray(`Low: ${low.length}`)}`) + + if (totalIssues === 0) { + log.success('No issues found!') + return + } + + // Auto-fixable issue types (high confidence) + const autoFixableTypes = new Set([ + 'console-log', + 'missing-await', + 'unused-variable', + 'missing-semicolon', + 'wrong-import-path', + 'deprecated-api', + 'type-error', + 'lint-error', + ]) + + // Determine which issues to auto-fix + const toAutoFix = [...critical, ...high].filter(issue => { + // Auto-fix if type is in whitelist OR severity is critical + return issue.severity === 'critical' || autoFixableTypes.has(issue.type) + }) + + const toReview = [...critical, ...high, ...medium].filter(issue => { + return !toAutoFix.includes(issue) + }) + + log.step(`Auto-fixing ${toAutoFix.length} high-confidence issues`) + log.substep(`${toReview.length} issues will require manual review`) + + // Apply auto-fixes in parallel based on workers setting + const workers = Number.parseInt(opts.workers, 10) || 3 + if (toAutoFix.length > 0) { + const fixTasks = toAutoFix.map(issue => async () => { + const projectData = projects.find(p => p.name === issue.project) + if (!projectData) { + return false + } + + const fixPrompt = `Fix this issue automatically: +File: ${issue.file} +Line: ${issue.line} +Type: ${issue.type} +Severity: ${issue.severity} +Description: ${issue.description} +Suggested fix: ${issue.fix} + +Apply the fix and return ONLY the fixed code snippet.` + + const result = await runClaude(claudeCmd, fixPrompt, { + ...opts, + interactive: false, + cache: false, + }) + + if (result) { + log.done(`Fixed: ${issue.file}:${issue.line} - ${issue.type}`) + return true + } + return false + }) + + await executeParallel(fixTasks, workers) + } + + // Report issues that need review + if (toReview.length > 0) { + console.log(`\n${colors.yellow('Issues requiring manual review:')}`) + toReview.forEach((issue, i) => { + console.log( + `${i + 1}. [${issue.severity}] ${issue.file}:${issue.line} - ${issue.description}`, + ) + }) + console.log('\nRun with --prompt to fix these interactively') + } + + log.success('Autonomous fix session complete!') +} + +/** + * Interactive fix session with Claude. + */ +async function interactiveFixSession( + claudeCmd, + scanResults, + _projects, + options = {}, +) { + const _opts = { __proto__: null, ...options } + printHeader('Interactive Fix Session') + + // Group issues by severity. + const critical = [] + const high = [] + const medium = [] + const low = [] + + for (const project in scanResults) { + const issues = scanResults[project] || [] + for (const issue of issues) { + issue.project = project + switch (issue.severity) { + case 'critical': + critical.push(issue) + break + case 'high': + high.push(issue) + break + case 'medium': + medium.push(issue) + break + default: + low.push(issue) + } + } + } + + const totalIssues = critical.length + high.length + medium.length + low.length + + console.log('\nScan Results:') + console.log(` ${colors.red(`Critical: ${critical.length}`)}`) + console.log(` ${colors.yellow(`High: ${high.length}`)}`) + console.log(` ${colors.cyan(`Medium: ${medium.length}`)}`) + console.log(` ${colors.gray(`Low: ${low.length}`)}`) + console.log(` Total: ${totalIssues} issues found`) + + if (totalIssues === 0) { + log.success('No issues found!') + return + } + + // Start interactive session. + console.log( + `\n${colors.blue('Starting interactive fix session with Claude...')}`, + ) + console.log('Claude will help you fix these issues.') + console.log('Commands: fix , commit, push, exit\n') + + // Create a comprehensive prompt for Claude. + const sessionPrompt = `You are helping fix security and quality issues in Socket projects. + +Here are the issues found: + +CRITICAL ISSUES: +${critical.map((issue, i) => `${i + 1}. [${issue.project}] ${issue.file}:${issue.line} - ${issue.description}`).join('\n') || 'None'} + +HIGH SEVERITY: +${high.map((issue, i) => `${critical.length + i + 1}. [${issue.project}] ${issue.file}:${issue.line} - ${issue.description}`).join('\n') || 'None'} + +MEDIUM SEVERITY: +${medium.map((issue, i) => `${critical.length + high.length + i + 1}. [${issue.project}] ${issue.file}:${issue.line} - ${issue.description}`).join('\n') || 'None'} + +LOW SEVERITY: +${low.map((issue, i) => `${critical.length + high.length + medium.length + i + 1}. [${issue.project}] ${issue.file}:${issue.line} - ${issue.description}`).join('\n') || 'None'} + +You can: +1. Fix specific issues by number +2. Create commits (no AI attribution) +3. Push changes to remote +4. Provide guidance on fixing issues + +Start by recommending which issues to fix first.` + + // Launch Claude console in interactive mode. + await runCommand(claudeCmd, prepareClaudeArgs([], options), { + input: sessionPrompt, + stdio: 'inherit', + }) +} + +/** + * Run security and quality scan on Socket projects. + */ +async function runSecurityScan(claudeCmd, options = {}) { + const opts = { __proto__: null, ...options } + printHeader('Security & Quality Scanner') + + // Find projects to scan. + log.step('Finding projects to scan') + const projects = [] + + if (!opts['cross-repo']) { + // Default: Scan only current project. + const currentProjectName = path.basename(rootPath) + projects.push({ + name: currentProjectName, + path: rootPath, + }) + log.info('Scanning current project only') + } else { + // With --cross-repo: Scan all Socket projects. + for (const projectName of SOCKET_PROJECTS) { + const projectPath = path.join(parentPath, projectName) + if (existsSync(projectPath)) { + projects.push({ + name: projectName, + path: projectPath, + }) + } + } + } + + if (projects.length === 0) { + log.error('No projects found to scan') + return false + } + + log.success(`Found ${projects.length} project(s) to scan`) + + // Scan each project. + log.step('Scanning projects for issues') + const scanResults = {} + + if (shouldRunParallel(opts) && projects.length > 1) { + // Run scans in parallel + const tasks = projects.map(project => + scanProjectForIssues(claudeCmd, project, options) + .then(issues => ({ project: project.name, issues })) + .catch(error => ({ project: project.name, issues: null, error })), + ) + + const taskNames = projects.map(p => p.name) + const results = await runParallel(tasks, 'security scans', taskNames) + + // Collect results + results.forEach(result => { + if (result.status === 'fulfilled' && result.value.issues) { + scanResults[result.value.project] = result.value.issues + } + }) + } else { + // Run sequentially + for (const project of projects) { + const issues = await scanProjectForIssues(claudeCmd, project, options) + if (issues) { + scanResults[project.name] = issues + } + } + } + + // Generate report. + if (!opts['no-report']) { + log.step('Generating scan report') + // Ensure .claude is in .gitignore before writing scratch files. + await ensureClaudeInGitignore() + // Ensure .claude directory exists for scratch files. + await fs.mkdir(claudeDir, { recursive: true }) + const reportPath = path.join(claudeDir, 'security-scan-report.json') + await fs.writeFile(reportPath, JSON.stringify(scanResults, null, 2)) + log.done(`Report saved to: ${reportPath}`) + } + + // Start fix session based on mode. + if (opts.prompt) { + // Prompt mode - user approves each fix + await interactiveFixSession(claudeCmd, scanResults, projects, options) + } else { + // Default: Auto-fix mode with careful checking + await autonomousFixSession(claudeCmd, scanResults, projects, options) + } + + return true +} + +/** + * Run Claude-assisted commits across Socket projects. + * Default: operates on current project only. Use --cross-repo for all Socket projects. + * IMPORTANT: When running in parallel mode (--cross-repo), Claude agents run silently (stdio: 'pipe'). + * Interactive prompts would conflict if multiple agents needed user input simultaneously. + * Use --seq flag if you need interactive debugging across multiple repos. + */ +async function runClaudeCommit(claudeCmd, options = {}) { + const opts = { __proto__: null, ...options } + printHeader('Claude-Assisted Commit') + + // Find projects to commit in. + log.step('Finding projects to commit') + const projects = [] + + if (!opts['cross-repo']) { + // Default: Commit only in current project. + const currentProjectName = path.basename(rootPath) + projects.push({ + name: currentProjectName, + path: rootPath, + }) + log.info('Committing in current project only') + } else { + // With --cross-repo: Commit in all Socket projects with changes. + for (const projectName of SOCKET_PROJECTS) { + const projectPath = path.join(parentPath, projectName) + if (existsSync(projectPath)) { + // Check if project has changes. + const statusResult = await runCommandWithOutput( + 'git', + ['status', '--porcelain'], + { + cwd: projectPath, + }, + ) + + if (statusResult.stdout.trim()) { + projects.push({ + name: projectName, + path: projectPath, + changes: statusResult.stdout.trim(), + }) + } + } + } + } + + if (projects.length === 0) { + log.info('No projects with uncommitted changes found') + return true + } + + log.success(`Found ${projects.length} project(s) with changes`) + + // Process each project with changes. + if (shouldRunParallel(opts) && projects.length > 1) { + // Run commits in parallel + const tasks = projects.map(project => { + const commitTask = async () => { + log.step(`Processing ${project.name}`) + + // Show current changes. + if (project.changes) { + log.substep('Changes detected:') + const changeLines = project.changes.split('\n') + changeLines.slice(0, 10).forEach(line => { + log.substep(` ${line}`) + }) + if (changeLines.length > 10) { + log.substep(` ... and ${changeLines.length - 10} more`) + } + } + + // Build the commit prompt. + let prompt = `You are in the ${project.name} project directory at ${project.path}. + +Review the changes and create commits following these rules: +1. Commit changes +2. Create small, atomic commits +3. Follow claude.md rules for commit messages +4. NO AI attribution in commit messages +5. Use descriptive, concise commit messages` + + if (opts['no-verify']) { + prompt += ` +6. Use --no-verify flag when committing (git commit --no-verify)` + } + + prompt += ` + +Check the current git status, review changes, and commit them appropriately. +Remember: small commits, follow project standards, no AI attribution.` + + log.progress(`Committing changes in ${project.name}`) + + // Launch Claude console for this project. + const commitResult = await runCommandWithOutput( + claudeCmd, + prepareClaudeArgs([], options), + { + input: prompt, + cwd: project.path, + stdio: 'inherit', + }, + ) + + if (commitResult.exitCode === 0) { + log.done(`Committed changes in ${project.name}`) + return { project: project.name, success: true } + } + log.failed(`Failed to commit in ${project.name}`) + return { project: project.name, success: false } + } + + return commitTask() + }) + + await runParallel(tasks, 'commits') + } else { + // Run sequentially + for (const project of projects) { + log.step(`Processing ${project.name}`) + + // Show current changes. + if (project.changes) { + log.substep('Changes detected:') + const changeLines = project.changes.split('\n') + changeLines.slice(0, 10).forEach(line => { + log.substep(` ${line}`) + }) + if (changeLines.length > 10) { + log.substep(` ... and ${changeLines.length - 10} more`) + } + } + + // Build the commit prompt. + let prompt = `You are in the ${project.name} project directory at ${project.path}. + +Review the changes and create commits following these rules: +1. Commit changes +2. Create small, atomic commits +3. Follow claude.md rules for commit messages +4. NO AI attribution in commit messages +5. Use descriptive, concise commit messages` + + if (opts['no-verify']) { + prompt += ` +6. Use --no-verify flag when committing (git commit --no-verify)` + } + + prompt += ` + +Check the current git status, review changes, and commit them appropriately. +Remember: small commits, follow project standards, no AI attribution.` + + log.progress(`Committing changes in ${project.name}`) + + // Launch Claude console for this project. + const commitResult = await runCommandWithOutput( + claudeCmd, + prepareClaudeArgs([], options), + { + input: prompt, + cwd: project.path, + stdio: 'inherit', + }, + ) + + if (commitResult.exitCode === 0) { + log.done(`Committed changes in ${project.name}`) + } else { + log.failed(`Failed to commit in ${project.name}`) + } + } + } + + // Optionally push changes. + if (opts.push) { + log.step('Pushing changes to remote') + + if (shouldRunParallel(opts) && projects.length > 1) { + // Run pushes in parallel + const tasks = projects.map(project => { + return runCommandWithOutput('git', ['push'], { + cwd: project.path, + }) + .then(pushResult => ({ + project: project.name, + success: pushResult.exitCode === 0, + })) + .catch(error => ({ + project: project.name, + success: false, + error, + })) + }) + + const results = await runParallel(tasks, 'pushes') + + // Report results + results.forEach(result => { + if (result.status === 'fulfilled' && result.value.success) { + log.done(`Pushed ${result.value.project}`) + } else { + log.failed(`Failed to push ${result.value.project}`) + } + }) + } else { + // Run sequentially + for (const project of projects) { + log.progress(`Pushing ${project.name}`) + const pushResult = await runCommandWithOutput('git', ['push'], { + cwd: project.path, + }) + + if (pushResult.exitCode === 0) { + log.done(`Pushed ${project.name}`) + } else { + log.failed(`Failed to push ${project.name}`) + } + } + } + } + + printFooter('Claude-assisted commits complete!') + + if (!opts.push) { + log.info('\nNext steps:') + log.substep('Review commits with: git log --oneline -n 5') + log.substep('Push to remote with: git push (in each project)') + } + + return true +} + +/** + * Review code changes before committing. + */ +async function runCodeReview(claudeCmd, options = {}) { + const opts = { __proto__: null, ...options } + printHeader('Code Review') + + // Get git diff for staged changes. + const diffResult = await runCommandWithOutput('git', ['diff', '--cached']) + + if (!diffResult.stdout.trim()) { + log.info('No staged changes to review') + log.substep('Stage changes with: git add ') + return true + } + + const basePrompt = `Review the following staged changes: + +${diffResult.stdout} + +Provide specific feedback with file:line references. +Format your review as constructive feedback with severity levels (critical/high/medium/low). +Also check for CLAUDE.md compliance and cross-platform compatibility.` + + // Use enhanced prompt with context + const enhancedPrompt = await buildEnhancedPrompt('review', basePrompt, { + // Only staged changes + includeUncommitted: false, + commits: 10, + }) + + log.step('Starting code review with Claude') + await runClaude(claudeCmd, enhancedPrompt, opts) + + return true +} + +/** + * Analyze and manage dependencies. + */ +async function runDependencyAnalysis(claudeCmd, options = {}) { + const opts = { __proto__: null, ...options } + printHeader('Dependency Analysis') + + // Read package.json. + const packageJson = JSON.parse( + await fs.readFile(path.join(rootPath, 'package.json'), 'utf8'), + ) + + // Check for outdated packages. + log.progress('Checking for outdated packages') + const outdatedResult = await runCommandWithOutput('pnpm', [ + 'outdated', + '--json', + ]) + + let outdatedPackages = {} + try { + outdatedPackages = JSON.parse(outdatedResult.stdout || '{}') + } catch { + // Ignore parse errors. + } + log.done('Dependency check complete') + + const prompt = `Analyze the dependencies for ${packageJson.name}: + +Current dependencies: +${JSON.stringify(packageJson.dependencies || {}, null, 2)} + +Current devDependencies: +${JSON.stringify(packageJson.devDependencies || {}, null, 2)} + +Outdated packages: +${JSON.stringify(outdatedPackages, null, 2)} + +IMPORTANT Socket Requirements: +- All dependencies MUST be pinned to exact versions (no ^ or ~ prefixes) +- Use pnpm add --save-exact for all new dependencies +- GitHub CLI (gh) is required but installed separately (not via npm) + +Provide: +1. Version pinning issues (identify any deps with ^ or ~ prefixes) +2. Security vulnerability analysis +3. Unused dependency detection +4. Update recommendations with migration notes (using exact versions) +5. License compatibility check +6. Bundle size impact analysis +7. Alternative package suggestions + +Focus on actionable recommendations. Always recommend exact versions when suggesting updates.` + + await runClaude(claudeCmd, prompt, opts) + + return true +} + +/** + * Generate test cases for existing code. + */ +async function runTestGeneration(claudeCmd, options = {}) { + const opts = { __proto__: null, ...options } + printHeader('Test Generation') + + const { positionals = [] } = opts + const targetFile = positionals[0] + + if (!targetFile) { + log.error('Please specify a file to generate tests for') + log.substep('Usage: pnpm claude --test ') + return false + } + + const filePath = path.isAbsolute(targetFile) + ? targetFile + : path.join(rootPath, targetFile) + + if (!existsSync(filePath)) { + log.error(`File not found: ${targetFile}`) + return false + } + + const fileContent = await fs.readFile(filePath, 'utf8') + const fileName = path.basename(filePath) + + const prompt = `Generate comprehensive test cases for ${fileName}: + +${fileContent} + +Create unit tests that: +1. Cover all exported functions +2. Test edge cases and error conditions +3. Validate input/output contracts +4. Test async operations properly +5. Include proper setup/teardown +6. Use vitest testing framework +7. Follow Socket testing standards + +Output the complete test file content.` + + log.step(`Generating tests for ${fileName}`) + const result = await runCommandWithOutput( + claudeCmd, + prepareClaudeArgs([], opts), + { + input: prompt, + stdio: ['pipe', 'pipe', 'pipe'], + }, + ) + + if (result.exitCode === 0 && result.stdout) { + const testDir = path.join(rootPath, 'test') + if (!existsSync(testDir)) { + await fs.mkdir(testDir, { recursive: true }) + } + + const testFileName = fileName.replace(/\.(m?[jt]s)$/, '.test.$1') + const testFilePath = path.join(testDir, testFileName) + + await fs.writeFile(testFilePath, result.stdout.trim()) + log.success(`Test file created: ${testFilePath}`) + } + + return true +} + +/** + * Generate or update documentation. + */ +async function runDocumentation(claudeCmd, options = {}) { + const opts = { __proto__: null, ...options } + printHeader('Documentation Generation') + + const { positionals = [] } = opts + const targetPath = positionals[0] || rootPath + + const prompt = `Generate or update documentation for the project at ${targetPath}. + +Tasks: +1. Generate JSDoc comments for functions lacking documentation +2. Create/update API documentation +3. Improve README if needed +4. Document complex algorithms +5. Add usage examples +6. Document configuration options + +Follow Socket documentation standards. +Output the documentation updates or new content.` + + await runCommand(claudeCmd, [], { + input: prompt, + stdio: 'inherit', + cwd: targetPath, + }) + + return true +} + +/** + * Suggest code refactoring improvements. + */ +async function runRefactor(claudeCmd, options = {}) { + const opts = { __proto__: null, ...options } + printHeader('Code Refactoring Analysis') + + const { positionals = [] } = opts + const targetFile = positionals[0] + + if (!targetFile) { + log.error('Please specify a file to refactor') + log.substep('Usage: pnpm claude --refactor ') + return false + } + + const filePath = path.isAbsolute(targetFile) + ? targetFile + : path.join(rootPath, targetFile) + + if (!existsSync(filePath)) { + log.error(`File not found: ${targetFile}`) + return false + } + + const fileContent = await fs.readFile(filePath, 'utf8') + + const prompt = `Analyze and suggest refactoring for this code: + +${fileContent} + +Identify and fix: +1. Code smells (long functions, duplicate code, etc.) +2. Performance bottlenecks +3. Readability issues +4. Maintainability problems +5. Design pattern improvements +6. SOLID principle violations +7. Socket coding standards compliance + +Provide the refactored code with explanations.` + + await runClaude(claudeCmd, prompt, opts) + + return true +} + +/** + * Optimize code for performance. + */ +async function runOptimization(claudeCmd, options = {}) { + const opts = { __proto__: null, ...options } + printHeader('Performance Optimization') + + const { positionals = [] } = opts + const targetFile = positionals[0] + + if (!targetFile) { + log.error('Please specify a file to optimize') + log.substep('Usage: pnpm claude --optimize ') + return false + } + + const filePath = path.isAbsolute(targetFile) + ? targetFile + : path.join(rootPath, targetFile) + + if (!existsSync(filePath)) { + log.error(`File not found: ${targetFile}`) + return false + } + + const fileContent = await fs.readFile(filePath, 'utf8') + + const prompt = `Analyze and optimize this code for performance: + +${fileContent} + +Focus on: +1. Algorithm complexity improvements +2. Memory allocation reduction +3. Async operation optimization +4. Caching opportunities +5. Loop optimizations +6. Data structure improvements +7. V8 optimization tips +8. Bundle size reduction + +Provide optimized code with benchmarks/explanations.` + + await runClaude(claudeCmd, prompt, opts) + + return true +} + +/** + * Comprehensive security and quality audit. + */ +async function runAudit(claudeCmd, options = {}) { + const opts = { __proto__: null, ...options } + printHeader('Security & Quality Audit') + + log.step('Gathering project information') + + // Run various checks. + const [npmAudit, depCheck, licenseCheck] = await Promise.all([ + runCommandWithOutput('npm', ['audit', '--json']), + runCommandWithOutput('pnpm', ['licenses', 'list', '--json']), + fs.readFile(path.join(rootPath, 'package.json'), 'utf8'), + ]) + + const packageJson = JSON.parse(licenseCheck) + + const prompt = `Perform a comprehensive audit of the project: + +Package: ${packageJson.name}@${packageJson.version} + +NPM Audit Results: +${npmAudit.stdout} + +License Information: +${depCheck.stdout} + +Analyze: +1. Security vulnerabilities (with severity and fixes) +2. License compliance issues +3. Dependency risks +4. Code quality metrics +5. Best practice violations +6. Outdated dependencies with breaking changes +7. Supply chain risks + +Provide actionable recommendations with priorities.` + + await runClaude(claudeCmd, prompt, opts) + + return true +} + +/** + * Explain code or concepts. + */ +async function runExplain(claudeCmd, options = {}) { + const opts = { __proto__: null, ...options } + printHeader('Code Explanation') + + const { positionals = [] } = opts + const targetFile = positionals[0] + + if (!targetFile) { + log.error('Please specify a file or concept to explain') + log.substep('Usage: pnpm claude --explain ') + return false + } + + // Check if it's a file or a concept. + const filePath = path.isAbsolute(targetFile) + ? targetFile + : path.join(rootPath, targetFile) + + let prompt + if (existsSync(filePath)) { + const fileContent = await fs.readFile(filePath, 'utf8') + prompt = `Explain this code in detail: + +${fileContent} + +Provide: +1. Overall purpose and architecture +2. Function-by-function breakdown +3. Algorithm explanations +4. Data flow analysis +5. Dependencies and interactions +6. Performance characteristics +7. Potential improvements + +Make it educational and easy to understand.` + } else { + // Treat as a concept to explain. + prompt = `Explain the concept: ${targetFile} + +Provide: +1. Clear definition +2. How it works +3. Use cases +4. Best practices +5. Common pitfalls +6. Code examples +7. Related concepts + +Focus on practical understanding for developers.` + } + + await runClaude(claudeCmd, prompt, opts) + + return true +} + +/** + * Help with migrations. + */ +async function runMigration(claudeCmd, options = {}) { + const opts = { __proto__: null, ...options } + printHeader('Migration Assistant') + + const { positionals = [] } = opts + const migrationType = positionals[0] + + if (!migrationType) { + log.info('Available migration types:') + log.substep('node - Node.js version upgrade') + log.substep('deps - Dependency updates') + log.substep('esm - CommonJS to ESM') + log.substep('typescript - JavaScript to TypeScript') + log.substep('vitest - Jest/Mocha to Vitest') + return false + } + + const packageJson = JSON.parse( + await fs.readFile(path.join(rootPath, 'package.json'), 'utf8'), + ) + + const prompt = `Help migrate ${packageJson.name} for: ${migrationType} + +Current setup: +${JSON.stringify(packageJson, null, 2)} + +Provide: +1. Step-by-step migration guide +2. Breaking changes to address +3. Code modifications needed +4. Configuration updates +5. Testing strategy +6. Rollback plan +7. Common issues and solutions + +Be specific and actionable.` + + await runClaude(claudeCmd, prompt, opts) + + return true +} + +/** + * Clean up code by removing unused elements. + */ +async function runCleanup(claudeCmd, options = {}) { + const _opts = { __proto__: null, ...options } + printHeader('Code Cleanup') + + log.step('Analyzing codebase for cleanup opportunities') + + const prompt = `Analyze the project and identify cleanup opportunities: + +1. Unused imports and variables +2. Dead code paths +3. Commented-out code blocks +4. Duplicate code +5. Unused dependencies +6. Obsolete configuration +7. Empty files +8. Unreachable code + +For each item found: +- Specify file and line numbers +- Explain why it can be removed +- Note any potential risks + +Format as actionable tasks.` + + await runCommand(claudeCmd, [], { + input: prompt, + stdio: 'inherit', + cwd: rootPath, + }) + + return true +} + +/** + * Help with debugging issues. + */ +async function runDebug(claudeCmd, options = {}) { + const opts = { __proto__: null, ...options } + printHeader('Debugging Assistant') + + const { positionals = [] } = opts + const errorOrFile = positionals.join(' ') + + if (!errorOrFile) { + log.error('Please provide an error message or stack trace') + log.substep('Usage: pnpm claude --debug ""') + log.substep(' or: pnpm claude --debug ') + return false + } + + let debugContent = errorOrFile + + // Check if it's a file. + const possibleFile = path.isAbsolute(errorOrFile) + ? errorOrFile + : path.join(rootPath, errorOrFile) + if (existsSync(possibleFile)) { + debugContent = await fs.readFile(possibleFile, 'utf8') + } + + const prompt = `Help debug this issue: + +${debugContent} + +Provide: +1. Root cause analysis +2. Step-by-step debugging approach +3. Potential fixes with code +4. Prevention strategies +5. Related issues to check +6. Testing to verify the fix + +Be specific and actionable.` + + await runClaude(claudeCmd, prompt, opts) + + return true +} + +/** + * Generate a commit message using Claude non-interactively. + * @param {string} claudeCmd - Path to Claude CLI + * @param {string} cwd - Working directory + * @param {object} options - Options from parent command + * @returns {Promise} Generated commit message + */ +async function generateCommitMessage(claudeCmd, cwd, options = {}) { + const opts = { __proto__: null, ...options } + + // Get git diff of staged changes + const diffResult = await runCommandWithOutput('git', ['diff', '--cached'], { + cwd, + }) + + // Get git status + const statusResult = await runCommandWithOutput( + 'git', + ['status', '--short'], + { cwd }, + ) + + // Get recent commit messages for style consistency + const logResult = await runCommandWithOutput( + 'git', + ['log', '--oneline', '-n', '5'], + { cwd }, + ) + + const prompt = `Generate a concise commit message for these changes. + +Git status: +${statusResult.stdout || 'No status output'} + +Git diff (staged changes): +${diffResult.stdout || 'No diff output'} + +Recent commits (for style reference): +${logResult.stdout || 'No recent commits'} + +Requirements: +1. Write a clear, concise commit message (1-2 lines preferred) +2. Follow the style of recent commits +3. Focus on WHY the changes were made, not just WHAT changed +4. NO AI attribution (per CLAUDE.md rules) +5. NO emojis +6. Output ONLY the commit message text, nothing else + +Commit message:` + + // Run Claude non-interactively to generate commit message + const result = await new Promise((resolve, reject) => { + let stdout = '' + let stderr = '' + + const claudeProcess = spawn(claudeCmd, prepareClaudeArgs([], opts), { + cwd, + stdio: ['pipe', 'pipe', 'pipe'], + }) + + claudeProcess.stdout.on('data', data => { + stdout += data.toString() + }) + + claudeProcess.stderr.on('data', data => { + stderr += data.toString() + }) + + claudeProcess.on('close', code => { + if (code === 0) { + resolve(stdout.trim()) + } else { + reject( + new Error( + `Claude failed to generate commit message: ${stderr || 'Unknown error'}`, + ), + ) + } + }) + + claudeProcess.stdin.write(prompt) + claudeProcess.stdin.end() + }) + + // Extract just the commit message (Claude might add extra text) + // Look for the actual message after "Commit message:" or just use the whole output + const lines = result.split('\n').filter(line => line.trim()) + + // Return the first substantial line that looks like a commit message + for (const line of lines) { + const trimmed = line.trim() + // Skip common Claude preamble phrases + if ( + trimmed && + !trimmed.toLowerCase().startsWith('here') && + !trimmed.toLowerCase().startsWith('commit message:') && + !trimmed.startsWith('```') && + trimmed.length > 10 + ) { + return trimmed + } + } + + // Fallback to first non-empty line + return lines[0] || 'Fix local checks and update tests' +} + +/** + * Calculate adaptive poll delay based on CI state. + * Polls faster when jobs are running, slower when queued. + */ +function calculatePollDelay(status, attempt, hasActiveJobs = false) { + // If jobs are actively running, poll more frequently + if (hasActiveJobs || status === 'in_progress') { + // Start at 5s, gradually increase to 15s max + return Math.min(5000 + attempt * 2000, 15_000) + } + + // If queued or waiting, use longer intervals (30s) + if (status === 'queued' || status === 'waiting') { + return 30_000 + } + + // Default: moderate polling for unknown states (10s) + return 10_000 +} + +/** + * Priority levels for different CI job types. + * Higher priority jobs are fixed first since they often block other jobs. + */ +const JOB_PRIORITIES = { + build: 100, + compile: 100, + 'type check': 90, + typecheck: 90, + typescript: 90, + tsc: 90, + lint: 80, + eslint: 80, + prettier: 80, + 'unit test': 70, + test: 70, + jest: 70, + vitest: 70, + integration: 60, + e2e: 50, + coverage: 40, + report: 30, +} + +/** + * Get priority for a CI job based on its name. + * @param {string} jobName - The name of the CI job + * @returns {number} Priority level (higher = more important) + */ +function getJobPriority(jobName) { + const lowerName = jobName.toLowerCase() + + // Check for exact or partial matches + for (const [pattern, priority] of Object.entries(JOB_PRIORITIES)) { + if (lowerName.includes(pattern)) { + return priority + } + } + + // Default priority for unknown job types + return 50 +} + +/** + * Validate changes before pushing to catch common mistakes. + * @param {string} cwd - Working directory + * @returns {Promise<{valid: boolean, warnings: string[]}>} Validation result + */ +async function validateBeforePush(cwd) { + const warnings = [] + + // Check for common issues in staged changes + const diffResult = await runCommandWithOutput('git', ['diff', '--cached'], { + cwd, + }) + const diff = diffResult.stdout + + // Check 1: No console.log statements + if (diff.match(/^\+.*console\.log\(/m)) { + warnings.push( + `${colors.yellow('⚠')} Added console.log() statements detected`, + ) + } + + // Check 2: No .only in tests + if (diff.match(/^\+.*\.(only|skip)\(/m)) { + warnings.push(`${colors.yellow('⚠')} Test .only() or .skip() detected`) + } + + // Check 3: No debugger statements + if (diff.match(/^\+.*debugger[;\s]/m)) { + warnings.push(`${colors.yellow('⚠')} Debugger statement detected`) + } + + // Check 4: No TODO/FIXME without issue link + const todoMatches = diff.match(/^\+.*\/\/\s*(TODO|FIXME)(?!\s*\(#\d+\))/gim) + if (todoMatches && todoMatches.length > 0) { + warnings.push( + `${colors.yellow('⚠')} ${todoMatches.length} TODO/FIXME comment(s) without issue links`, + ) + } + + // Check 5: Package.json is valid JSON + if (diff.includes('package.json')) { + try { + const pkgPath = path.join(cwd, 'package.json') + const pkgContent = await fs.readFile(pkgPath, 'utf8') + JSON.parse(pkgContent) + } catch (e) { + warnings.push(`${colors.yellow('⚠')} Invalid package.json: ${e.message}`) + } + } + + return { valid: warnings.length === 0, warnings } +} + +/** + * Run all checks, push, and monitor CI until green. + * NOTE: This operates on the current repo by default. Use --cross-repo for all Socket projects. + * Multi-repo parallel execution would conflict with interactive prompts if fixes fail. + */ +async function runGreen(claudeCmd, options = {}) { + const opts = { __proto__: null, ...options } + const maxRetries = Number.parseInt(opts['max-retries'] || '3', 10) + const isDryRun = opts['dry-run'] + const MAX_AUTO_FIX_ATTEMPTS = Number.parseInt( + opts['max-auto-fixes'] || '10', + 10, + ) + const useNoVerify = opts['no-verify'] === true + + // Initialize storage and cleanup old data. + await initStorage() + await cleanupOldData() + + // Initialize trackers. + const costTracker = new CostTracker() + const progress = new ProgressTracker() + const snapshots = new SnapshotManager() + let fixCount = 0 + + printHeader('Green CI Pipeline') + + // Optional: Run pre-commit scan for proactive detection. + if (opts['pre-commit-scan']) { + log.step('Running proactive pre-commit scan') + const scanResult = await runPreCommitScan(claudeCmd) + + if (scanResult && !scanResult.safe) { + log.warn('Pre-commit scan detected potential issues:') + scanResult.issues.forEach(issue => { + const icon = + issue.severity === 'high' ? colors.red('✗') : colors.yellow('⚠') + log.substep( + `${icon} ${issue.type}: ${issue.description} ${colors.gray(`(${issue.confidence}% confidence)`)}`, + ) + }) + + // Ask if user wants to continue. + log.info('Continue anyway? (Ctrl+C to abort)') + await new Promise(resolve => setTimeout(resolve, 3000)) + } else if (scanResult?.safe) { + log.done('Pre-commit scan passed - no obvious issues detected') + } + } + + // Show initial progress. + progress.showProgress() + + // Track errors to avoid checking same error repeatedly + const seenErrors = new Set() + // Track CI errors by run ID + const ciErrorHistory = new Map() + + // Step 1: Run local checks + progress.startPhase('local-checks') + const repoName = path.basename(rootPath) + log.step(`Running local checks in ${colors.cyan(repoName)}`) + const localChecks = [ + { name: 'Install dependencies', cmd: 'pnpm', args: ['install'] }, + { name: 'Fix code style', cmd: 'pnpm', args: ['run', 'fix'] }, + { name: 'Run checks', cmd: 'pnpm', args: ['run', 'check'] }, + { name: 'Run coverage', cmd: 'pnpm', args: ['run', 'cover'] }, + { name: 'Run tests', cmd: 'pnpm', args: ['run', 'test', '--', '--update'] }, + ] + + let autoFixAttempts = 0 + let lastAnalysis = null + let lastErrorHash = null + + for (const check of localChecks) { + log.progress(`[${repoName}] ${check.name}`) + + if (isDryRun) { + log.done(`[DRY RUN] Would run: ${check.cmd} ${check.args.join(' ')}`) + continue + } + + // Add newline after progress indicator before command output + console.log('') + const result = await runCommandWithOutput(check.cmd, check.args, { + cwd: rootPath, + stdio: 'inherit', + }) + + if (result.exitCode !== 0) { + log.failed(`${check.name} failed`) + + // Track error to avoid repeated attempts on same error + const errorOutput = + result.stderr || result.stdout || 'No error output available' + const errorHash = hashError(errorOutput) + + if (seenErrors.has(errorHash)) { + log.error(`Detected same error again for "${check.name}"`) + log.substep('Skipping auto-fix to avoid infinite loop') + log.substep('Error appears unchanged from previous attempt') + return false + } + + seenErrors.add(errorHash) + autoFixAttempts++ + + // Analyze root cause before attempting fix. + const analysis = await analyzeRootCause(claudeCmd, errorOutput, { + checkName: check.name, + repoName, + attempts: autoFixAttempts, + }) + + // Save for history tracking. + lastAnalysis = analysis + lastErrorHash = errorHash + + // Display analysis to user. + if (analysis) { + displayAnalysis(analysis) + + // Warn if environmental issue. + if (analysis.isEnvironmental && analysis.confidence > 70) { + log.warn( + 'This looks like an environmental issue - fix may not help. Consider checking runner status.', + ) + } + } + + // Decide whether to auto-fix or go interactive + const isAutoMode = autoFixAttempts <= MAX_AUTO_FIX_ATTEMPTS + + if (isAutoMode) { + // Create snapshot before fix attempt for potential rollback. + await snapshots.createSnapshot(`before-fix-${autoFixAttempts}`) + log.substep(`Snapshot created: before-fix-${autoFixAttempts}`) + + // Attempt automatic fix + log.progress( + `[${repoName}] Auto-fix attempt ${autoFixAttempts}/${MAX_AUTO_FIX_ATTEMPTS}`, + ) + + // Build fix prompt with analysis if available. + const fixPrompt = `You are fixing a CI/build issue automatically. The command "${check.cmd} ${check.args.join(' ')}" failed in the ${path.basename(rootPath)} project. + +Error output: +${errorOutput} + +${ + analysis + ? ` +Root Cause Analysis: +- Problem: ${analysis.rootCause} +- Confidence: ${analysis.confidence}% +- Category: ${analysis.category} + +Recommended Fix Strategy: +${ + analysis.strategies[0] + ? `- ${analysis.strategies[0].name} (${analysis.strategies[0].probability}% success probability) + ${analysis.strategies[0].description} + Reasoning: ${analysis.strategies[0].reasoning}` + : 'No specific strategy recommended' +} +` + : '' +} + +Your task: +1. Analyze the error +2. Provide the exact fix needed +3. Use file edits, commands, or both to resolve the issue + +IMPORTANT: +- Be direct and specific - don't ask questions +- Provide complete solutions that will fix the error +- If the error is about missing dependencies, install pinned versions +- If it's a type error, fix the code +- If it's a lint error, fix the formatting +- If tests are failing, update snapshots or fix the test +- If a script is missing, check if there's a similar script name (e.g., 'cover' vs 'coverage') + +Fix this issue now by making the necessary changes.` + + // Run Claude non-interactively with timeout and progress + const startTime = Date.now() + // 2 minute timeout + const timeout = 120_000 + log.substep(`[${repoName}] Analyzing error...`) + + const claudeProcess = spawn(claudeCmd, prepareClaudeArgs([], opts), { + cwd: rootPath, + stdio: ['pipe', 'inherit', 'inherit'], + }) + + claudeProcess.stdin.write(fixPrompt) + claudeProcess.stdin.end() + + // Monitor progress with timeout + let isCleared = false + let progressInterval = null + const clearProgressInterval = () => { + if (!isCleared && progressInterval) { + clearInterval(progressInterval) + isCleared = true + } + } + + progressInterval = setInterval(() => { + const elapsed = Date.now() - startTime + if (elapsed > timeout) { + log.warn( + `[${repoName}] Claude fix timed out after ${Math.round(elapsed / 1000)}s`, + ) + clearProgressInterval() + claudeProcess.kill() + } else { + log.substep( + `[${repoName}] Claude working... (${Math.round(elapsed / 1000)}s)`, + ) + } + }, 10_000) + // Update every 10 seconds + + await new Promise(resolve => { + claudeProcess.on('close', () => { + clearProgressInterval() + const elapsed = Date.now() - startTime + log.done( + `[${repoName}] Claude fix completed in ${Math.round(elapsed / 1000)}s`, + ) + resolve() + }) + }) + + // Give file system a moment to sync + await new Promise(resolve => setTimeout(resolve, 1000)) + + // Retry the check + log.progress(`Retrying ${check.name}`) + const retryResult = await runCommandWithOutput(check.cmd, check.args, { + cwd: rootPath, + }) + + if (retryResult.exitCode !== 0) { + // Auto-fix didn't work - save failure to history. + if (lastAnalysis) { + await saveErrorHistory( + lastErrorHash, + 'failed', + lastAnalysis.strategies[0]?.name || 'auto-fix', + lastAnalysis.rootCause, + ) + } + + // Auto-fix didn't work + if (autoFixAttempts >= MAX_AUTO_FIX_ATTEMPTS) { + // Switch to interactive mode + log.warn(`Auto-fix failed after ${MAX_AUTO_FIX_ATTEMPTS} attempts`) + log.info('Switching to interactive mode for manual assistance') + + const interactivePrompt = `The command "${check.cmd} ${check.args.join(' ')}" is still failing after ${MAX_AUTO_FIX_ATTEMPTS} automatic fix attempts. + +Latest error output: +${retryResult.stderr || retryResult.stdout || 'No error output'} + +Previous automatic fixes were attempted but did not resolve the issue. This appears to be a more complex problem that requires interactive debugging. + +Please help me fix this issue. You can: +1. Analyze the error more carefully +2. Try different approaches +3. Ask me questions if needed +4. Suggest manual steps I should take + +Let's work through this together to get CI passing.` + + log.progress('Launching interactive Claude session') + await runCommand(claudeCmd, prepareClaudeArgs([], opts), { + input: interactivePrompt, + cwd: rootPath, + // Interactive mode + stdio: 'inherit', + }) + + // Try once more after interactive session + log.progress(`Final retry of ${check.name}`) + const finalResult = await runCommandWithOutput( + check.cmd, + check.args, + { + cwd: rootPath, + }, + ) + + if (finalResult.exitCode !== 0) { + log.error(`${check.name} still failing after manual intervention`) + log.substep( + 'Consider running the command manually to debug further', + ) + return false + } + } else { + log.warn(`Auto-fix attempt ${autoFixAttempts} failed, will retry`) + // Will try again on next iteration + continue + } + } + } else { + // Already exceeded auto attempts, go straight to interactive + log.warn('Maximum auto-fix attempts exceeded') + log.info('Please fix this issue interactively') + return false + } + } + + // Fix succeeded - save success to history. + if (lastAnalysis) { + await saveErrorHistory( + lastErrorHash, + 'success', + lastAnalysis.strategies[0]?.name || 'auto-fix', + lastAnalysis.rootCause, + ) + } + + log.done(`${check.name} passed`) + } + + // End local checks phase. + progress.endPhase() + progress.showProgress() + + // Step 2: Commit and push changes + progress.startPhase('commit-and-push') + log.step('Committing and pushing changes') + + // Check for uncommitted changes + const statusResult = await runCommandWithOutput( + 'git', + ['status', '--porcelain'], + { + cwd: rootPath, + }, + ) + + // Check if local branch is ahead of remote (unpushed commits) + const revListResult = await runCommandWithOutput( + 'git', + ['rev-list', '@{upstream}..HEAD', '--count'], + { + cwd: rootPath, + }, + ) + const unpushedCount = Number.parseInt(revListResult.stdout.trim() || '0', 10) + + if (statusResult.stdout.trim()) { + log.progress('Changes detected, committing') + + if (isDryRun) { + log.done('[DRY RUN] Would commit and push changes') + } else { + // Stage all changes + await runCommand('git', ['add', '.'], { cwd: rootPath }) + + // Generate commit message using Claude (non-interactive) + log.progress('Generating commit message with Claude') + const commitMessage = await generateCommitMessage( + claudeCmd, + rootPath, + opts, + ) + log.substep(`Commit message: ${commitMessage}`) + + const commitArgs = ['commit', '-m', commitMessage] + if (useNoVerify) { + commitArgs.push('--no-verify') + } + await runCommand('git', commitArgs, { + cwd: rootPath, + }) + fixCount++ + + // Validate before pushing + const validation = await validateBeforePush(rootPath) + if (!validation.valid) { + log.warn('Pre-push validation warnings:') + validation.warnings.forEach(warning => { + log.substep(warning) + }) + log.substep('Continuing with push (warnings are non-blocking)...') + } + + // Push + await runCommand('git', ['push'], { cwd: rootPath }) + log.done('Changes pushed to remote') + } + } else if (unpushedCount > 0) { + log.info( + `No uncommitted changes, but ${unpushedCount} unpushed commit(s) detected`, + ) + + if (isDryRun) { + log.done('[DRY RUN] Would push unpushed commits') + } else { + log.progress('Pushing unpushed commits') + + // Validate before pushing + const validation = await validateBeforePush(rootPath) + if (!validation.valid) { + log.warn('Pre-push validation warnings:') + validation.warnings.forEach(warning => { + log.substep(warning) + }) + log.substep('Continuing with push (warnings are non-blocking)...') + } + + // Push + await runCommand('git', ['push'], { cwd: rootPath }) + log.done('Unpushed commits pushed to remote') + } + } else { + log.info('No changes to commit and no unpushed commits') + } + + // End commit phase. + progress.endPhase() + progress.showProgress() + + // Step 3: Monitor CI workflow + progress.startPhase('ci-monitoring') + log.step('Monitoring CI workflow') + + if (isDryRun) { + log.done('[DRY RUN] Would monitor CI workflow') + printFooter('Green CI Pipeline (dry run) complete!') + return true + } + + // Check for GitHub CLI + const ghCheckCommand = WIN32 ? 'where' : 'which' + const ghCheck = await runCommandWithOutput(ghCheckCommand, ['gh']) + if (ghCheck.exitCode !== 0) { + log.error('GitHub CLI (gh) is required for CI monitoring') + console.log(`\n${colors.cyan('Installation Instructions:')}`) + console.log(` macOS: ${colors.green('brew install gh')}`) + console.log(` Ubuntu: ${colors.green('sudo apt install gh')}`) + console.log(` Fedora: ${colors.green('sudo dnf install gh')}`) + console.log(` Windows: ${colors.green('winget install --id GitHub.cli')}`) + console.log( + ` Other: ${colors.gray('https://github.com/cli/cli/blob/trunk/docs/install_linux.md')}`, + ) + console.log(`\n${colors.yellow('After installation:')}`) + console.log(` 1. Run: ${colors.green('gh auth login')}`) + console.log(' 2. Follow the prompts to authenticate') + console.log(` 3. Try again: ${colors.green('pnpm claude --green')}`) + return false + } + + // Ensure GitHub is authenticated (will handle login automatically) + const isGitHubAuthenticated = await ensureGitHubAuthenticated() + if (!isGitHubAuthenticated) { + log.error('Unable to authenticate with GitHub') + console.log( + colors.red('\nGitHub authentication is required for CI monitoring.'), + ) + console.log('Please ensure you can login to GitHub CLI and try again.') + return false + } + + // Get current commit SHA + const shaResult = await runCommandWithOutput('git', ['rev-parse', 'HEAD'], { + cwd: rootPath, + }) + let currentSha = shaResult.stdout.trim() + + // Get repo info + const remoteResult = await runCommandWithOutput( + 'git', + ['remote', 'get-url', 'origin'], + { + cwd: rootPath, + }, + ) + const remoteUrl = remoteResult.stdout.trim() + const repoMatch = remoteUrl.match(/github\.com[:/](.+?)\/(.+?)(\.git)?$/) + + if (!repoMatch) { + log.error('Could not determine GitHub repository from remote URL') + return false + } + + const [, owner, repoNameMatch] = repoMatch + const repo = repoNameMatch.replace('.git', '') + + // Check if commit is part of a PR + const prInfo = await checkIfCommitIsPartOfPR(currentSha, owner, repo) + if (prInfo.isPR) { + log.info( + `Commit is part of PR #${prInfo.prNumber}: ${colors.cyan(prInfo.prTitle)}`, + ) + log.substep(`PR state: ${prInfo.prState}`) + } else { + log.info('Commit is a direct push (not part of a PR)') + } + + // Monitor workflow with retries + let retryCount = 0 + let lastRunId = null + let pushTime = Date.now() + // Track which jobs we've already fixed (jobName -> true) + let fixedJobs = new Map() + // Track if we've made any commits during this workflow run + let hasPendingCommits = false + // Track polling attempts for adaptive delays + let pollAttempt = 0 + + while (retryCount < maxRetries) { + // Reset tracking for each new CI run + fixedJobs = new Map() + hasPendingCommits = false + pollAttempt = 0 + log.progress(`Checking CI status (attempt ${retryCount + 1}/${maxRetries})`) + + // Wait a bit for CI to start + if (retryCount === 0) { + log.substep('Waiting 10 seconds for CI to start...') + await new Promise(resolve => setTimeout(resolve, 10_000)) + } + + // Check workflow runs using gh CLI with better detection + const runsResult = await runCommandWithOutput( + 'gh', + [ + 'run', + 'list', + '--repo', + `${owner}/${repo}`, + '--limit', + '20', + '--json', + 'databaseId,status,conclusion,name,headSha,createdAt,headBranch', + ], + { + cwd: rootPath, + }, + ) + + if (runsResult.exitCode !== 0) { + log.failed('Failed to fetch workflow runs') + + // Provide debugging information + if (runsResult.stderr) { + console.log(colors.red('\nError details:')) + console.log(runsResult.stderr) + } + + // Common troubleshooting steps + console.log(colors.yellow('\nTroubleshooting:')) + console.log('1. Check GitHub CLI authentication:') + console.log(` ${colors.green('gh auth status')}`) + console.log('\n2. If not authenticated, login:') + console.log(` ${colors.green('gh auth login')}`) + console.log('\n3. Test repository access:') + console.log(` ${colors.green(`gh api repos/${owner}/${repo}`)}`) + console.log('\n4. Check if workflows exist:') + console.log( + ` ${colors.green(`gh workflow list --repo ${owner}/${repo}`)}`, + ) + console.log('\n5. View recent runs manually:') + console.log( + ` ${colors.green(`gh run list --repo ${owner}/${repo} --limit 5`)}`, + ) + + return false + } + + let runs + try { + runs = JSON.parse(runsResult.stdout || '[]') + } catch { + log.failed('Failed to parse workflow runs') + return false + } + + // Filter runs to find one matching our commit SHA or recent push + let matchingRun = null + + // Debug: log current SHA and available runs + if (pollAttempt === 0) { + log.substep( + `Looking for workflow runs for commit ${currentSha.substring(0, 7)}`, + ) + if (runs.length > 0) { + log.substep(`Found ${runs.length} recent runs, checking for matches...`) + } + } + + // First, try exact SHA match (both directions for robustness) + for (const run of runs) { + if ( + run.headSha === currentSha || + run.headSha?.startsWith(currentSha.substring(0, 7)) || + currentSha.startsWith(run.headSha?.substring(0, 7) || '') + ) { + matchingRun = run + log.substep(`Found SHA match for commit ${currentSha.substring(0, 7)}`) + break + } + } + + // If no exact match, look for runs created after our push + if (!matchingRun && runs.length > 0) { + for (const run of runs) { + if (run.createdAt) { + const runTime = new Date(run.createdAt).getTime() + // Check if run was created within 2 minutes BEFORE or after push + if (runTime >= pushTime - 120_000) { + matchingRun = run + log.substep(`Found workflow started around push time: ${run.name}`) + break + } + } + } + } + + // Last resort: if still no match on first attempt, monitor the newest run + if (!matchingRun && retryCount === 0 && runs.length > 0) { + const newestRun = runs[0] + if (newestRun.createdAt) { + const runTime = new Date(newestRun.createdAt).getTime() + // Only consider if created within last 10 minutes + if (Date.now() - runTime < 10 * 60 * 1000) { + matchingRun = newestRun + log.substep(`Monitoring recent workflow: ${newestRun.name}`) + } + } + } + + if (!matchingRun) { + // Use moderate delay when no run found yet (10s) + const delay = 10_000 + log.substep( + `No matching workflow runs found yet, waiting ${delay / 1000}s...`, + ) + await new Promise(resolve => setTimeout(resolve, delay)) + pollAttempt++ + continue + } + + const run = matchingRun + lastRunId = run.databaseId + + log.substep(`Workflow "${run.name}" status: ${run.status}`) + + // Show progress update every 5 polls. + if (pollAttempt % 5 === 0) { + progress.showProgress() + } + + // If workflow is queued, wait before checking again + if (run.status === 'queued' || run.status === 'waiting') { + const delay = calculatePollDelay(run.status, pollAttempt) + log.substep(`Waiting for workflow to start (${delay / 1000}s)...`) + await new Promise(resolve => setTimeout(resolve, delay)) + pollAttempt++ + continue + } + + if (run.status === 'completed') { + if (run.conclusion === 'success') { + // End CI monitoring phase. + progress.endPhase() + progress.complete() + + // Show final statistics. + await celebrateSuccess(costTracker, { + fixCount, + retries: pollAttempt, + }) + + // Show available snapshots for reference. + const snapshotList = snapshots.listSnapshots() + if (snapshotList.length > 0) { + console.log(colors.cyan('\n📸 Available Snapshots:')) + snapshotList.slice(0, 5).forEach(snap => { + console.log( + ` ${snap.label} ${colors.gray(`(${formatDuration(Date.now() - snap.timestamp)} ago)`)}`, + ) + }) + if (snapshotList.length > 5) { + console.log( + colors.gray(` ... and ${snapshotList.length - 5} more`), + ) + } + } + + printFooter('Green CI Pipeline complete!') + return true + } + log.failed(`CI workflow failed with conclusion: ${run.conclusion}`) + + // If we have pending commits from fixing jobs during execution, push them now + if (hasPendingCommits) { + log.progress('Pushing all fix commits') + await runCommand('git', ['push'], { cwd: rootPath }) + log.done(`Pushed ${fixedJobs.size} fix commit(s)`) + + // Update SHA and push time for next check + const newShaResult = await runCommandWithOutput( + 'git', + ['rev-parse', 'HEAD'], + { + cwd: rootPath, + }, + ) + currentSha = newShaResult.stdout.trim() + pushTime = Date.now() + + // Reset retry count for new commit - it deserves its own attempts + log.substep( + `New commit ${currentSha.substring(0, 7)}, resetting retry counter`, + ) + retryCount = 0 + + // Wait for new CI run to start + log.substep('Waiting 15 seconds for new CI run to start...') + await new Promise(resolve => setTimeout(resolve, 15_000)) + continue + } + + // No fixes were made during execution, handle as traditional completed workflow + if (retryCount < maxRetries - 1) { + // Fetch failure logs + log.progress('Fetching failure logs') + + const logsResult = await runCommandWithOutput( + 'gh', + [ + 'run', + 'view', + lastRunId.toString(), + '--repo', + `${owner}/${repo}`, + '--log-failed', + ], + { + cwd: rootPath, + }, + ) + // Add newline after progress indicator before next output + console.log('') + + // Filter and show summary of logs + const rawLogs = logsResult.stdout || 'No logs available' + const filteredLogs = filterCILogs(rawLogs) + + const logLines = filteredLogs.split('\n').slice(0, 10) + log.substep('Error summary:') + for (const line of logLines) { + if (line.trim()) { + log.substep(` ${line.trim().substring(0, 100)}`) + } + } + if (filteredLogs.split('\n').length > 10) { + log.substep( + ` ... (${filteredLogs.split('\n').length - 10} more lines)`, + ) + } + + // Check if we've seen this CI error before + const ciErrorHash = hashError(filteredLogs) + + if (ciErrorHistory.has(lastRunId)) { + log.error(`Already attempted fix for run ${lastRunId}`) + log.substep('Skipping to avoid repeated attempts on same CI run') + retryCount++ + continue + } + + if (seenErrors.has(ciErrorHash)) { + log.error('Detected same CI error pattern as previous attempt') + log.substep('Error appears unchanged after push') + log.substep( + `View run at: https://github.com/${owner}/${repo}/actions/runs/${lastRunId}`, + ) + return false + } + + ciErrorHistory.set(lastRunId, ciErrorHash) + seenErrors.add(ciErrorHash) + + // Analyze and fix with Claude + log.progress('Analyzing CI failure with Claude') + + // Keep logs under 2000 chars to avoid context issues + const truncatedLogs = + filteredLogs.length > 2000 + ? `${filteredLogs.substring(0, 2000)}\n... (truncated)` + : filteredLogs + + const fixPrompt = `Fix CI failures for commit ${currentSha.substring(0, 7)} in ${owner}/${repo}. + +Error logs: +${truncatedLogs} + +Fix all issues by making necessary file changes. Be direct, don't ask questions.` + + // Run Claude non-interactively to apply fixes + log.substep('Applying CI fixes...') + + // Track progress with timeout. + const fixStartTime = Date.now() + // 3 minutes timeout. + const fixTimeout = 180_000 + + // Create progress indicator + const progressInterval = setInterval(() => { + const elapsed = Date.now() - fixStartTime + if (elapsed > fixTimeout) { + log.warn('Claude fix timeout, proceeding...') + clearInterval(progressInterval) + } else { + log.progress( + `Claude analyzing and fixing... (${Math.round(elapsed / 1000)}s)`, + ) + } + // Update every 10 seconds. + }, 10_000) + + try { + // Write prompt to temp file + const tmpFile = path.join(rootPath, `.claude-fix-${Date.now()}.txt`) + await fs.writeFile(tmpFile, fixPrompt, 'utf8') + + const fixArgs = prepareClaudeArgs([], opts) + const claudeArgs = fixArgs.join(' ') + const claudeCommand = claudeArgs + ? `${claudeCmd} ${claudeArgs}` + : claudeCmd + + // Use script command to create pseudo-TTY for Ink compatibility + // Platform-specific script command syntax + let scriptCmd + if (WIN32) { + // Try winpty (comes with Git for Windows) + const winptyCheck = await runCommandWithOutput('where', ['winpty']) + if (winptyCheck.exitCode === 0) { + scriptCmd = `winpty ${claudeCommand} < "${tmpFile}"` + } else { + // No winpty, try direct (may fail with raw mode error) + scriptCmd = `${claudeCommand} < "${tmpFile}"` + } + } else { + // Unix/macOS: use script command with quoted command + scriptCmd = `script -q /dev/null sh -c '${claudeCommand} < "${tmpFile}"'` + } + + const exitCode = await new Promise((resolve, _reject) => { + const child = spawn(scriptCmd, [], { + stdio: 'inherit', + cwd: rootPath, + shell: true, + }) + + // Handle Ctrl+C gracefully + const sigintHandler = () => { + child.kill('SIGINT') + resolve(130) + } + process.on('SIGINT', sigintHandler) + + child.on('exit', code => { + process.off('SIGINT', sigintHandler) + resolve(code || 0) + }) + + child.on('error', () => { + process.off('SIGINT', sigintHandler) + resolve(1) + }) + }) + + // Clean up temp file + try { + await fs.unlink(tmpFile) + } catch {} + + if (exitCode !== 0) { + log.warn(`Claude fix exited with code ${exitCode}`) + } + } catch (error) { + log.warn(`Claude fix error: ${error.message}`) + } finally { + clearInterval(progressInterval) + log.done('Claude fix attempt completed') + } + + // Give Claude's changes a moment to complete + await new Promise(resolve => setTimeout(resolve, 3000)) + + // Run local checks again + log.progress('Running local checks after fixes') + // Add newline after progress indicator before command output + console.log('') + for (const check of localChecks) { + await runCommandWithOutput(check.cmd, check.args, { + cwd: rootPath, + stdio: 'inherit', + }) + } + + // Commit and push fixes + const fixStatusResult = await runCommandWithOutput( + 'git', + ['status', '--porcelain'], + { + cwd: rootPath, + }, + ) + + let pushedNewCommit = false + + if (fixStatusResult.stdout.trim()) { + log.progress('Committing CI fixes') + + // Show what files were changed + const changedFiles = fixStatusResult.stdout + .trim() + .split('\n') + .map(line => line.substring(3)) + .join(', ') + log.substep(`Changed files: ${changedFiles}`) + + // Stage all changes + await runCommand('git', ['add', '.'], { cwd: rootPath }) + + // Generate commit message using Claude (non-interactive) + log.progress('Generating CI fix commit message with Claude') + const commitMessage = await generateCommitMessage( + claudeCmd, + rootPath, + opts, + ) + log.substep(`Commit message: ${commitMessage}`) + + // Validate before committing + const validation = await validateBeforePush(rootPath) + if (!validation.valid) { + log.warn('Pre-commit validation warnings:') + validation.warnings.forEach(warning => { + log.substep(warning) + }) + } + + // Commit with generated message + const commitArgs = ['commit', '-m', commitMessage] + if (useNoVerify) { + commitArgs.push('--no-verify') + } + const commitResult = await runCommandWithOutput('git', commitArgs, { + cwd: rootPath, + }) + + if (commitResult.exitCode === 0) { + fixCount++ + // Push the commits + await runCommand('git', ['push'], { cwd: rootPath }) + log.done('Pushed fix commits') + + // Update SHA and push time for next check + const newShaResult = await runCommandWithOutput( + 'git', + ['rev-parse', 'HEAD'], + { + cwd: rootPath, + }, + ) + currentSha = newShaResult.stdout.trim() + pushTime = Date.now() + pushedNewCommit = true + + // Reset retry count for new commit - it deserves its own attempts + log.substep( + `New commit ${currentSha.substring(0, 7)}, resetting retry counter`, + ) + retryCount = 0 + + // Wait for new CI run to start + log.substep('Waiting 15 seconds for new CI run to start...') + await new Promise(resolve => setTimeout(resolve, 15_000)) + } else { + log.warn( + `Git commit failed: ${commitResult.stderr || commitResult.stdout}`, + ) + } + } + + // Only increment retry count if we didn't push a new commit + if (!pushedNewCommit) { + retryCount++ + } + } else { + log.error(`CI still failing after ${maxRetries} attempts`) + log.substep( + `View run at: https://github.com/${owner}/${repo}/actions/runs/${lastRunId}`, + ) + return false + } + } else { + // Workflow still running - check for failed jobs and fix them immediately + log.substep('Workflow still running, checking for failed jobs...') + + // Fetch jobs for this workflow run + const jobsResult = await runCommandWithOutput( + 'gh', + [ + 'run', + 'view', + lastRunId.toString(), + '--repo', + `${owner}/${repo}`, + '--json', + 'jobs', + ], + { + cwd: rootPath, + }, + ) + + if (jobsResult.exitCode === 0 && jobsResult.stdout) { + try { + const runData = JSON.parse(jobsResult.stdout) + const jobs = runData.jobs || [] + + // Check for any failed or cancelled jobs + const failedJobs = jobs.filter( + job => + job.conclusion === 'failure' || job.conclusion === 'cancelled', + ) + + // Find new failures we haven't fixed yet + const newFailures = failedJobs.filter(job => !fixedJobs.has(job.name)) + + if (newFailures.length > 0) { + log.failed(`Detected ${newFailures.length} new failed job(s)`) + + // Sort by priority - fix blocking issues first (build, typecheck, lint, tests) + // Higher priority first + const sortedFailures = newFailures.sort((a, b) => { + const priorityA = getJobPriority(a.name) + const priorityB = getJobPriority(b.name) + return priorityB - priorityA + }) + + if (sortedFailures.length > 1) { + log.substep('Processing in priority order (highest first):') + sortedFailures.forEach(job => { + const priority = getJobPriority(job.name) + log.substep(` [Priority ${priority}] ${job.name}`) + }) + } + + // Fix each failed job immediately + for (const job of sortedFailures) { + log.substep(`${colors.red('✗')} ${job.name}: ${job.conclusion}`) + + // Fetch logs for this specific failed job using job ID + log.progress(`Fetching logs for ${job.name}`) + const logsResult = await runCommandWithOutput( + 'gh', + [ + 'run', + 'view', + '--job', + job.databaseId.toString(), + '--repo', + `${owner}/${repo}`, + '--log', + ], + { + cwd: rootPath, + }, + ) + console.log('') + + // Filter logs to extract relevant errors + const rawLogs = logsResult.stdout || 'No logs available' + const filteredLogs = filterCILogs(rawLogs) + + // Show summary to user (not full logs) + const logLines = filteredLogs.split('\n').slice(0, 10) + log.substep('Error summary:') + for (const line of logLines) { + if (line.trim()) { + log.substep(` ${line.trim().substring(0, 100)}`) + } + } + if (filteredLogs.split('\n').length > 10) { + log.substep( + ` ... (${filteredLogs.split('\n').length - 10} more lines)`, + ) + } + + // Analyze and fix with Claude + log.progress(`Analyzing failure in ${job.name}`) + + // Keep logs under 2000 chars to avoid context issues + const truncatedLogs = + filteredLogs.length > 2000 + ? `${filteredLogs.substring(0, 2000)}\n... (truncated)` + : filteredLogs + + const fixPrompt = `Fix CI failure in "${job.name}" (run ${lastRunId}, commit ${currentSha.substring(0, 7)}). + +Status: ${job.conclusion} + +Error logs: +${truncatedLogs} + +Fix the issue by making necessary file changes. Be direct, don't ask questions.` + + // Run Claude non-interactively to apply fixes + log.substep(`Applying fix for ${job.name}...`) + + const fixStartTime = Date.now() + const fixTimeout = 180_000 + + const progressInterval = setInterval(() => { + const elapsed = Date.now() - fixStartTime + if (elapsed > fixTimeout) { + log.warn('Claude fix timeout, proceeding...') + clearInterval(progressInterval) + } else { + log.progress( + `Claude fixing ${job.name}... (${Math.round(elapsed / 1000)}s)`, + ) + } + }, 10_000) + + try { + // Write prompt to temp file + const tmpFile = path.join( + rootPath, + `.claude-fix-${Date.now()}.txt`, + ) + await fs.writeFile(tmpFile, fixPrompt, 'utf8') + + const fixArgs = prepareClaudeArgs([], opts) + const claudeArgs = fixArgs.join(' ') + const claudeCommand = claudeArgs + ? `${claudeCmd} ${claudeArgs}` + : claudeCmd + + // Debug: Show command being run + if (claudeArgs) { + log.substep(`Running: claude ${claudeArgs}`) + } + + // Use script command to create pseudo-TTY for Ink compatibility + // Platform-specific script command syntax + let scriptCmd + if (WIN32) { + // Try winpty (comes with Git for Windows) + const winptyCheck = await runCommandWithOutput('where', [ + 'winpty', + ]) + if (winptyCheck.exitCode === 0) { + scriptCmd = `winpty ${claudeCommand} < "${tmpFile}"` + } else { + // No winpty, try direct (may fail with raw mode error) + scriptCmd = `${claudeCommand} < "${tmpFile}"` + } + } else { + // Unix/macOS: use script command with quoted command + scriptCmd = `script -q /dev/null sh -c '${claudeCommand} < "${tmpFile}"'` + } + + const exitCode = await new Promise((resolve, _reject) => { + const child = spawn(scriptCmd, [], { + stdio: 'inherit', + cwd: rootPath, + shell: true, + }) + + // Handle Ctrl+C gracefully + const sigintHandler = () => { + child.kill('SIGINT') + resolve(130) + } + process.on('SIGINT', sigintHandler) + + child.on('exit', code => { + process.off('SIGINT', sigintHandler) + resolve(code || 0) + }) + + child.on('error', () => { + process.off('SIGINT', sigintHandler) + resolve(1) + }) + }) + + // Clean up temp file + try { + await fs.unlink(tmpFile) + } catch {} + + if (exitCode !== 0) { + log.warn(`Claude fix exited with code ${exitCode}`) + } + } catch (error) { + log.warn(`Claude fix error: ${error.message}`) + } finally { + clearInterval(progressInterval) + log.done(`Fix attempt for ${job.name} completed`) + } + + // Give Claude's changes a moment to complete + await new Promise(resolve => setTimeout(resolve, 2000)) + + // Run local checks + log.progress('Running local checks after fix') + console.log('') + for (const check of localChecks) { + await runCommandWithOutput(check.cmd, check.args, { + cwd: rootPath, + stdio: 'inherit', + }) + } + + // Check if there are changes to commit + const fixStatusResult = await runCommandWithOutput( + 'git', + ['status', '--porcelain'], + { + cwd: rootPath, + }, + ) + + if (fixStatusResult.stdout.trim()) { + log.progress(`Committing fix for ${job.name}`) + + const changedFiles = fixStatusResult.stdout + .trim() + .split('\n') + .map(line => line.substring(3)) + .join(', ') + log.substep(`Changed files: ${changedFiles}`) + + // Stage all changes + await runCommand('git', ['add', '.'], { cwd: rootPath }) + + // Generate commit message using Claude (non-interactive) + log.progress( + `Generating commit message for ${job.name} fix with Claude`, + ) + const commitMessage = await generateCommitMessage( + claudeCmd, + rootPath, + opts, + ) + log.substep(`Commit message: ${commitMessage}`) + + // Validate before committing + const validation = await validateBeforePush(rootPath) + if (!validation.valid) { + log.warn('Pre-commit validation warnings:') + validation.warnings.forEach(warning => { + log.substep(warning) + }) + } + + // Commit with generated message + const commitArgs = ['commit', '-m', commitMessage] + if (useNoVerify) { + commitArgs.push('--no-verify') + } + const commitResult = await runCommandWithOutput( + 'git', + commitArgs, + { + cwd: rootPath, + }, + ) + + if (commitResult.exitCode === 0) { + fixCount++ + log.done(`Committed fix for ${job.name}`) + hasPendingCommits = true + } else { + log.warn( + `Git commit failed: ${commitResult.stderr || commitResult.stdout}`, + ) + } + } else { + log.substep(`No changes to commit for ${job.name}`) + } + + // Mark this job as fixed + fixedJobs.set(job.name, true) + } + } + + // Show current status + if (fixedJobs.size > 0) { + log.substep( + `Fixed ${fixedJobs.size} job(s) so far (commits pending push)`, + ) + } + } catch (e) { + log.warn(`Failed to parse job data: ${e.message}`) + } + } + + // Wait and check again with adaptive polling + // Jobs are running, so poll more frequently + const delay = calculatePollDelay('in_progress', pollAttempt, true) + log.substep(`Checking again in ${delay / 1000}s...`) + await new Promise(resolve => setTimeout(resolve, delay)) + pollAttempt++ + } + } + + log.error(`Exceeded maximum retries (${maxRetries})`) + return false +} + +/** + * Continuous monitoring mode - watches for changes and auto-fixes issues. + */ +async function runWatchMode(claudeCmd, options = {}) { + const opts = { __proto__: null, ...options } + printHeader('Watch Mode - Continuous Monitoring') + + log.info('Starting continuous monitoring...') + log.substep('Press Ctrl+C to stop') + + const _watchPath = !opts['cross-repo'] ? rootPath : parentPath + const projects = !opts['cross-repo'] + ? [{ name: path.basename(rootPath), path: rootPath }] + : SOCKET_PROJECTS.map(name => ({ + name, + path: path.join(parentPath, name), + })).filter(p => existsSync(p.path)) + + log.substep(`Monitoring ${projects.length} project(s)`) + + // Track last scan time to avoid duplicate scans + const lastScanTime = new Map() + // 5 seconds between scans + const SCAN_COOLDOWN = 5000 + + // File watcher for each project + const watchers = [] + + for (const project of projects) { + log.substep(`Watching: ${project.name}`) + + const watcher = fs.watch( + project.path, + { recursive: true }, + async (_eventType, filename) => { + // Skip common ignore patterns + if ( + !filename || + filename.includes('node_modules') || + filename.includes('.git') || + filename.includes('dist') || + filename.includes('build') || + !filename.match(/\.(m?[jt]sx?)$/) + ) { + return + } + + const now = Date.now() + const lastScan = lastScanTime.get(project.name) || 0 + + // Cooldown to avoid rapid re-scans + if (now - lastScan < SCAN_COOLDOWN) { + return + } + + lastScanTime.set(project.name, now) + + log.progress(`Change detected in ${project.name}/${filename}`) + log.substep('Scanning for issues...') + + try { + // Run focused scan on changed file + const scanResults = await scanProjectForIssues(claudeCmd, project, { + ...opts, + focusFiles: [filename], + smartContext: true, + }) + + if (scanResults && Object.keys(scanResults).length > 0) { + log.substep('Issues detected, auto-fixing...') + + // Auto-fix in careful mode + await autonomousFixSession( + claudeCmd, + { [project.name]: scanResults }, + [project], + { + ...opts, + // Force auto-fix in watch mode + prompt: false, + }, + ) + } else { + log.done('No issues found') + } + } catch (error) { + log.failed(`Error scanning ${project.name}: ${error.message}`) + } + }, + ) + + watchers.push(watcher) + } + + // Periodic full scans (every 30 minutes) + const fullScanInterval = setInterval( + async () => { + log.step('Running periodic full scan') + + for (const project of projects) { + try { + const scanResults = await scanProjectForIssues( + claudeCmd, + project, + opts, + ) + + if (scanResults && Object.keys(scanResults).length > 0) { + await autonomousFixSession( + claudeCmd, + { [project.name]: scanResults }, + [project], + { + ...opts, + prompt: false, + }, + ) + } + } catch (error) { + log.failed(`Full scan error in ${project.name}: ${error.message}`) + } + } + // 30 minutes + }, + 30 * 60 * 1000, + ) + + // Handle graceful shutdown + process.on('SIGINT', () => { + console.log(`\n${colors.yellow('Stopping watch mode...')}`) + + // Clean up watchers + for (const watcher of watchers) { + watcher.close() + } + + // Clear interval + if (fullScanInterval) { + clearInterval(fullScanInterval) + } + + log.success('Watch mode stopped') + process.exitCode = 0 + + process.exit(0) + }) + + // Keep process alive + await new Promise(() => {}) +} + +/** + * Show available Claude operations. + */ +function showOperations() { + console.log('\nCore operations:') + console.log(' --commit Create commits with Claude assistance') + console.log( + ' --green Ensure all tests pass, push, monitor CI until green', + ) + console.log(' --push Create commits and push to remote') + console.log(' --sync Synchronize CLAUDE.md files across projects') + + console.log('\nCode quality:') + console.log(' --audit Security and quality audit') + console.log(' --clean Find unused code and imports') + console.log(' --fix Scan for bugs and security issues') + console.log(' --optimize Performance optimization analysis') + console.log(' --refactor Suggest code improvements') + console.log(' --review Review staged changes before committing') + + console.log('\nDevelopment:') + console.log(' --debug Help debug errors') + console.log(' --deps Analyze dependencies') + console.log(' --docs Generate documentation') + console.log(' --explain Explain code or concepts') + console.log(' --migrate Migration assistance') + console.log(' --test Generate test cases') + + console.log('\nUtility:') + console.log(' --help Show this help message') +} + +async function main() { + try { + // Parse arguments. + const { positionals, values } = parseArgs({ + options: { + // Core operations. + help: { + type: 'boolean', + default: false, + }, + sync: { + type: 'boolean', + default: false, + }, + commit: { + type: 'boolean', + default: false, + }, + push: { + type: 'boolean', + default: false, + }, + green: { + type: 'boolean', + default: false, + }, + // Code quality. + review: { + type: 'boolean', + default: false, + }, + fix: { + type: 'boolean', + default: false, + }, + refactor: { + type: 'boolean', + default: false, + }, + optimize: { + type: 'boolean', + default: false, + }, + clean: { + type: 'boolean', + default: false, + }, + audit: { + type: 'boolean', + default: false, + }, + // Development. + test: { + type: 'boolean', + default: false, + }, + docs: { + type: 'boolean', + default: false, + }, + explain: { + type: 'boolean', + default: false, + }, + debug: { + type: 'boolean', + default: false, + }, + deps: { + type: 'boolean', + default: false, + }, + migrate: { + type: 'boolean', + default: false, + }, + // Options. + 'no-verify': { + type: 'boolean', + default: false, + }, + 'dry-run': { + type: 'boolean', + default: false, + }, + 'skip-commit': { + type: 'boolean', + default: false, + }, + 'no-report': { + type: 'boolean', + default: false, + }, + 'no-interactive': { + type: 'boolean', + default: false, + }, + 'cross-repo': { + type: 'boolean', + default: false, + }, + 'no-darkwing': { + type: 'boolean', + default: false, + }, + seq: { + type: 'boolean', + default: false, + }, + 'max-retries': { + type: 'string', + default: '3', + }, + 'max-auto-fixes': { + type: 'string', + default: '10', + }, + pinky: { + type: 'boolean', + default: false, + }, + 'the-brain': { + type: 'boolean', + default: false, + }, + workers: { + type: 'string', + default: '3', + }, + watch: { + type: 'boolean', + default: false, + }, + prompt: { + type: 'boolean', + default: false, + }, + }, + allowPositionals: true, + strict: false, + }) + + // Check if any operation is specified. + const hasOperation = + values.sync || + values.fix || + values.commit || + values.push || + values.review || + values.refactor || + values.optimize || + values.clean || + values.audit || + values.test || + values.docs || + values.explain || + values.debug || + values.deps || + values.migrate || + values.green + + // Show help if requested or no operation specified. + if (values.help || !hasOperation) { + console.log('\nUsage: pnpm claude [operation] [options] [files...]') + console.log('\nClaude-powered utilities for Socket projects.') + showOperations() + console.log('\nOptions:') + console.log( + ' --cross-repo Operate on all Socket projects (default: current only)', + ) + console.log(' --dry-run Preview changes without writing files') + console.log( + ' --max-auto-fixes N Max auto-fix attempts (--green, default: 10)', + ) + console.log( + ' --max-retries N Max CI fix attempts (--green, default: 3)', + ) + console.log(' --no-darkwing Disable "Let\'s get dangerous!" mode') + console.log(' --no-report Skip generating scan report (--fix)') + console.log(' --no-verify Use --no-verify when committing') + console.log(' --pinky Use default model (Claude 3.5 Sonnet)') + console.log(' --prompt Prompt for approval before fixes (--fix)') + console.log(' --seq Run sequentially (default: parallel)') + console.log(" --skip-commit Update files but don't commit") + console.log( + ' --the-brain Use ultrathink mode - "Try to take over the world!"', + ) + console.log(' --watch Continuous monitoring mode') + console.log(' --workers N Number of parallel workers (default: 3)') + console.log('\nExamples:') + console.log( + ' pnpm claude --fix # Auto-fix issues (careful mode)', + ) + console.log( + ' pnpm claude --fix --prompt # Prompt for approval on each fix', + ) + console.log( + ' pnpm claude --fix --watch # Continuous monitoring & fixing', + ) + console.log(' pnpm claude --review # Review staged changes') + console.log(' pnpm claude --green # Ensure CI passes') + console.log( + ' pnpm claude --green --dry-run # Test green without real CI', + ) + console.log( + ' pnpm claude --fix --the-brain # Deep analysis with ultrathink mode', + ) + console.log(' pnpm claude --fix --workers 5 # Use 5 parallel workers') + console.log( + ' pnpm claude --test lib/utils.js # Generate tests for a file', + ) + console.log( + ' pnpm claude --refactor src/index.js # Suggest refactoring', + ) + console.log(' pnpm claude --push # Commit and push changes') + console.log(' pnpm claude --help # Show this help') + console.log('\nRequires:') + console.log(' - Claude Code CLI (claude) installed') + console.log(' - GitHub CLI (gh) for --green command') + process.exitCode = 0 + return + } + + // Check for Claude CLI. + log.step('Checking prerequisites') + log.progress('Checking for Claude Code CLI') + const claudeCmd = await checkClaude() + if (!claudeCmd) { + log.failed('Claude Code CLI not found') + log.error('Please install Claude Code to use these utilities') + console.log(`\n${colors.cyan('Installation Instructions:')}`) + console.log(' 1. Visit: https://docs.claude.com/en/docs/claude-code') + console.log(' 2. Or install via npm:') + console.log( + ` ${colors.green('npm install -g @anthropic/claude-desktop')}`, + ) + console.log(' 3. Or download directly:') + console.log(` macOS: ${colors.gray('brew install claude')}`) + console.log( + ` Linux: ${colors.gray('curl -fsSL https://docs.claude.com/install.sh | sh')}`, + ) + console.log( + ` Windows: ${colors.gray('Download from https://claude.ai/download')}`, + ) + console.log(`\n${colors.yellow('After installation:')}`) + console.log(` 1. Run: ${colors.green('claude')}`) + console.log(' 2. Sign in with your Anthropic account when prompted') + console.log(` 3. Try again: ${colors.green('pnpm claude --help')}`) + process.exitCode = 1 + return + } + + // Ensure Claude is authenticated + const isClaudeAuthenticated = await ensureClaudeAuthenticated(claudeCmd) + if (!isClaudeAuthenticated) { + log.error('Unable to authenticate with Claude Code') + console.log( + colors.red('\nAuthentication is required to use Claude utilities.'), + ) + console.log( + 'Please ensure Claude Code is properly authenticated and try again.', + ) + process.exitCode = 1 + return + } + + // Configure execution mode based on flags + const executionMode = { + workers: Number.parseInt(values.workers, 10) || 3, + watch: values.watch || false, + // Auto-fix by default unless --prompt + autoFix: !values.prompt, + model: values['the-brain'] + ? 'the-brain' + : values.pinky + ? 'pinky' + : 'auto', + } + + // Display execution mode + if (executionMode.workers > 1) { + log.substep(`🚀 Parallel mode: ${executionMode.workers} workers`) + } + if (executionMode.watch) { + log.substep('Watch mode: Continuous monitoring enabled') + } + if (!executionMode.autoFix) { + log.substep('Prompt mode: Fixes require approval') + } + + // Execute requested operation. + let success = true + const options = { ...values, positionals, executionMode } + + // Check if watch mode is enabled + if (executionMode.watch) { + // Start continuous monitoring + await runWatchMode(claudeCmd, options) + // Watch mode runs indefinitely + return + } + + // Core operations. + if (values.sync) { + success = await syncClaudeMd(claudeCmd, options) + } else if (values.commit) { + success = await runClaudeCommit(claudeCmd, options) + } else if (values.push) { + // --push combines commit and push. + success = await runClaudeCommit(claudeCmd, { ...options, push: true }) + } else if (values.green) { + success = await runGreen(claudeCmd, options) + } + // Code quality operations. + else if (values.review) { + success = await runCodeReview(claudeCmd, options) + } else if (values.fix) { + success = await runSecurityScan(claudeCmd, options) + } else if (values.refactor) { + success = await runRefactor(claudeCmd, options) + } else if (values.optimize) { + success = await runOptimization(claudeCmd, options) + } else if (values.clean) { + success = await runCleanup(claudeCmd, options) + } else if (values.audit) { + success = await runAudit(claudeCmd, options) + } + // Development operations. + else if (values.test) { + success = await runTestGeneration(claudeCmd, options) + } else if (values.docs) { + success = await runDocumentation(claudeCmd, options) + } else if (values.explain) { + success = await runExplain(claudeCmd, options) + } else if (values.debug) { + success = await runDebug(claudeCmd, options) + } else if (values.deps) { + success = await runDependencyAnalysis(claudeCmd, options) + } else if (values.migrate) { + success = await runMigration(claudeCmd, options) + } + + process.exitCode = success ? 0 : 1 + } catch (error) { + log.error(`Operation failed: ${error.message}`) + process.exitCode = 1 + } +} + +main().catch(console.error) diff --git a/packages/lib/scripts/clean.mjs b/packages/lib/scripts/clean.mjs new file mode 100644 index 000000000..a900c8709 --- /dev/null +++ b/packages/lib/scripts/clean.mjs @@ -0,0 +1,213 @@ +/** + * @fileoverview Unified clean runner with flag-based configuration. + * Removes build artifacts, caches, and other generated files. + */ + +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import { deleteAsync } from 'del' +import fastGlob from 'fast-glob' + +import { isQuiet } from '#socketsecurity/lib/argv/flags' +import { getDefaultLogger } from '#socketsecurity/lib/logger' +import { printHeader } from '#socketsecurity/lib/stdio/header' + +import { parseArgs } from './utils/parse-args.mjs' + +const logger = getDefaultLogger() + +const rootPath = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '..', +) + +/** + * Clean specific directories. + */ +async function cleanDirectories(tasks, options = {}) { + const { quiet = false } = options + + for (const task of tasks) { + const { name, pattern, patterns } = task + const patternsToDelete = patterns || [pattern] + + if (!quiet) { + logger.progress(`Cleaning ${name}`) + } + + try { + // Find all files/dirs matching the patterns + const files = await fastGlob(patternsToDelete, { + cwd: rootPath, + absolute: true, + dot: true, + onlyFiles: false, + markDirectories: true, + }) + + // Delete each file/directory + await deleteAsync(files) + + if (!quiet) { + if (files.length > 0) { + logger.done(`Cleaned ${name} (${files.length} items)`) + } else { + logger.done(`Cleaned ${name} (already clean)`) + } + } + } catch (error) { + if (!quiet) { + logger.error(`Failed to clean ${name}`) + console.error(error.message) + } + return 1 + } + } + + return 0 +} + +async function main() { + try { + // Parse arguments + const { values } = parseArgs({ + options: { + help: { + type: 'boolean', + default: false, + }, + all: { + type: 'boolean', + default: false, + }, + cache: { + type: 'boolean', + default: false, + }, + coverage: { + type: 'boolean', + default: false, + }, + dist: { + type: 'boolean', + default: false, + }, + types: { + type: 'boolean', + default: false, + }, + modules: { + type: 'boolean', + default: false, + }, + quiet: { + type: 'boolean', + default: false, + }, + silent: { + type: 'boolean', + default: false, + }, + }, + allowPositionals: false, + strict: false, + }) + + // Show help if requested + if (values.help) { + console.log('Clean Runner') + console.log('\nUsage: pnpm clean [options]') + console.log('\nOptions:') + console.log(' --help Show this help message') + console.log( + ' --all Clean everything (default if no flags)', + ) + console.log(' --cache Clean cache directories') + console.log(' --coverage Clean coverage reports') + console.log(' --dist Clean build output') + console.log(' --types Clean TypeScript declarations only') + console.log(' --modules Clean node_modules') + console.log(' --quiet, --silent Suppress progress messages') + console.log('\nExamples:') + console.log( + ' pnpm clean # Clean everything except node_modules', + ) + console.log(' pnpm clean --dist # Clean build output only') + console.log(' pnpm clean --cache --coverage # Clean cache and coverage') + console.log( + ' pnpm clean --all --modules # Clean everything including node_modules', + ) + process.exitCode = 0 + return + } + + const quiet = isQuiet(values) + + // Determine what to clean + const cleanAll = + values.all || + (!values.cache && + !values.coverage && + !values.dist && + !values.types && + !values.modules) + + const tasks = [] + + // Build task list + if (cleanAll || values.cache) { + tasks.push({ name: 'cache', pattern: '**/.cache' }) + } + + if (cleanAll || values.coverage) { + tasks.push({ name: 'coverage', pattern: 'coverage' }) + } + + if (cleanAll || values.dist) { + tasks.push({ + name: 'dist', + patterns: ['dist', '*.tsbuildinfo', '.tsbuildinfo'], + }) + } else if (values.types) { + tasks.push({ name: 'dist/types', patterns: ['dist/types'] }) + } + + if (values.modules) { + tasks.push({ name: 'node_modules', pattern: '**/node_modules' }) + } + + // Check if there's anything to clean + if (tasks.length === 0) { + if (!quiet) { + logger.info('Nothing to clean') + } + process.exitCode = 0 + return + } + + if (!quiet) { + printHeader('Clean Runner') + logger.step('Cleaning project directories') + } + + // Clean directories + const exitCode = await cleanDirectories(tasks, { quiet }) + + if (exitCode !== 0) { + if (!quiet) { + logger.error('Clean failed') + } + process.exitCode = exitCode + } else { + if (!quiet) { + logger.success('Clean completed successfully!') + } + } + } catch (error) { + logger.error(`Clean runner failed: ${error.message}`) + process.exitCode = 1 + } +} + +main().catch(console.error) diff --git a/packages/lib/scripts/cover.mjs b/packages/lib/scripts/cover.mjs new file mode 100644 index 000000000..7796e22cb --- /dev/null +++ b/packages/lib/scripts/cover.mjs @@ -0,0 +1,214 @@ +/** + * @fileoverview Coverage script that runs tests with coverage reporting. + * Masks test output and shows only the coverage summary. + */ + +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { parseArgs } from 'node:util' + +import { getDefaultLogger } from '#socketsecurity/lib/logger' +import { printHeader } from '#socketsecurity/lib/stdio/header' + +import { runCommandQuiet } from './utils/run-command.mjs' + +const logger = getDefaultLogger() + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const rootPath = path.join(__dirname, '..') + +// Parse custom flags +const { values } = parseArgs({ + options: { + 'code-only': { type: 'boolean', default: false }, + 'type-only': { type: 'boolean', default: false }, + summary: { type: 'boolean', default: false }, + }, + strict: false, +}) + +printHeader('Test Coverage') +console.log('') + +// Run vitest with coverage enabled, capturing output +// Filter out custom flags that vitest doesn't understand +const customFlags = ['--code-only', '--type-only', '--summary'] +const vitestArgs = [ + 'exec', + 'vitest', + 'run', + '--coverage', + ...process.argv.slice(2).filter(arg => !customFlags.includes(arg)), +] +const typeCoverageArgs = ['exec', 'type-coverage'] + +try { + let exitCode = 0 + let codeCoverageResult + let typeCoverageResult + + // Handle --type-only flag + if (values['type-only']) { + typeCoverageResult = await runCommandQuiet('pnpm', typeCoverageArgs, { + cwd: rootPath, + }) + exitCode = typeCoverageResult.exitCode + + // Display type coverage only + const typeCoverageOutput = ( + typeCoverageResult.stdout + typeCoverageResult.stderr + ).trim() + const typeCoverageMatch = typeCoverageOutput.match( + /\([\d\s/]+\)\s+([\d.]+)%/, + ) + + if (typeCoverageMatch) { + const typeCoveragePercent = Number.parseFloat(typeCoverageMatch[1]) + console.log() + console.log(' Coverage Summary') + console.log(' ───────────────────────────────') + console.log(` Type Coverage: ${typeCoveragePercent.toFixed(2)}%`) + console.log() + } + } + // Handle --code-only flag + else if (values['code-only']) { + codeCoverageResult = await runCommandQuiet('pnpm', vitestArgs, { + cwd: rootPath, + }) + exitCode = codeCoverageResult.exitCode + + // Process code coverage output only + const ansiRegex = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g') + const output = (codeCoverageResult.stdout + codeCoverageResult.stderr) + .replace(ansiRegex, '') + .replace(/(?:✧|︎|⚡)\s*/g, '') + .trim() + + // Extract and display test summary + const testSummaryMatch = output.match( + /Test Files\s+\d+[^\n]*\n[\s\S]*?Duration\s+[\d.]+m?s[^\n]*/, + ) + if (!values.summary && testSummaryMatch) { + console.log() + console.log(testSummaryMatch[0]) + console.log() + } + + // Extract and display coverage summary + const coverageHeaderMatch = output.match( + / % Coverage report from v8\n([-|]+)\n([^\n]+)\n\1/, + ) + const allFilesMatch = output.match(/All files\s+\|\s+([\d.]+)\s+\|[^\n]*/) + + if (coverageHeaderMatch && allFilesMatch) { + if (!values.summary) { + console.log(' % Coverage report from v8') + console.log(coverageHeaderMatch[1]) + console.log(coverageHeaderMatch[2]) + console.log(coverageHeaderMatch[1]) + console.log(allFilesMatch[0]) + console.log(coverageHeaderMatch[1]) + console.log() + } + + const codeCoveragePercent = Number.parseFloat(allFilesMatch[1]) + console.log(' Coverage Summary') + console.log(' ───────────────────────────────') + console.log(` Code Coverage: ${codeCoveragePercent.toFixed(2)}%`) + console.log() + } else if (exitCode !== 0) { + console.log('\n--- Output ---') + console.log(output) + } + } + // Default: run both code and type coverage + else { + codeCoverageResult = await runCommandQuiet('pnpm', vitestArgs, { + cwd: rootPath, + }) + exitCode = codeCoverageResult.exitCode + + // Run type coverage + typeCoverageResult = await runCommandQuiet('pnpm', typeCoverageArgs, { + cwd: rootPath, + }) + + // Combine and clean output + const ansiRegex = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g') + const output = (codeCoverageResult.stdout + codeCoverageResult.stderr) + .replace(ansiRegex, '') + .replace(/(?:✧|︎|⚡)\s*/g, '') + .trim() + + // Extract test summary + const testSummaryMatch = output.match( + /Test Files\s+\d+[^\n]*\n[\s\S]*?Duration\s+[\d.]+m?s[^\n]*/, + ) + + // Extract coverage summary + const coverageHeaderMatch = output.match( + / % Coverage report from v8\n([-|]+)\n([^\n]+)\n\1/, + ) + const allFilesMatch = output.match(/All files\s+\|\s+([\d.]+)\s+\|[^\n]*/) + + // Extract type coverage + const typeCoverageOutput = ( + typeCoverageResult.stdout + typeCoverageResult.stderr + ).trim() + const typeCoverageMatch = typeCoverageOutput.match( + /\([\d\s/]+\)\s+([\d.]+)%/, + ) + + // Display output + if (!values.summary && testSummaryMatch) { + console.log() + console.log(testSummaryMatch[0]) + console.log() + } + + if (coverageHeaderMatch && allFilesMatch) { + if (!values.summary) { + console.log(' % Coverage report from v8') + console.log(coverageHeaderMatch[1]) + console.log(coverageHeaderMatch[2]) + console.log(coverageHeaderMatch[1]) + console.log(allFilesMatch[0]) + console.log(coverageHeaderMatch[1]) + console.log() + } + + // Display cumulative summary + if (typeCoverageMatch) { + const codeCoveragePercent = Number.parseFloat(allFilesMatch[1]) + const typeCoveragePercent = Number.parseFloat(typeCoverageMatch[1]) + const cumulativePercent = ( + (codeCoveragePercent + typeCoveragePercent) / + 2 + ).toFixed(2) + + console.log(' Coverage Summary') + console.log(' ───────────────────────────────') + console.log(` Type Coverage: ${typeCoveragePercent.toFixed(2)}%`) + console.log(` Code Coverage: ${codeCoveragePercent.toFixed(2)}%`) + console.log(' ───────────────────────────────') + console.log(` Cumulative: ${cumulativePercent}%`) + console.log() + } + } else if (exitCode !== 0) { + console.log('\n--- Output ---') + console.log(output) + } + } + + if (exitCode === 0) { + logger.success('Coverage completed successfully') + } else { + logger.error('Coverage failed') + } + + process.exitCode = exitCode +} catch (error) { + logger.error(`Coverage script failed: ${error.message}`) + process.exitCode = 1 +} diff --git a/packages/lib/scripts/fix-build.mjs b/packages/lib/scripts/fix-build.mjs new file mode 100644 index 000000000..99e510be3 --- /dev/null +++ b/packages/lib/scripts/fix-build.mjs @@ -0,0 +1,62 @@ +/** + * @fileoverview Orchestrates all post-build fix scripts. + * Runs generate-package-exports and fix-external-imports in sequence. + */ + +import { isQuiet } from '#socketsecurity/lib/argv/flags' +import { getDefaultLogger } from '#socketsecurity/lib/logger' +import { printFooter, printHeader } from '#socketsecurity/lib/stdio/header' + +import { runSequence } from './utils/run-command.mjs' + +const logger = getDefaultLogger() + +async function main() { + const verbose = process.argv.includes('--verbose') + const quiet = isQuiet() + + if (!quiet) { + printHeader('Fixing Build Output') + } + + const fixArgs = [] + if (quiet) { + fixArgs.push('--quiet') + } + if (verbose) { + fixArgs.push('--verbose') + } + + const exitCode = await runSequence([ + { + args: ['scripts/generate-package-exports.mjs', ...fixArgs], + command: 'node', + }, + { + args: ['scripts/fix-path-aliases.mjs', ...fixArgs], + command: 'node', + }, + { + args: ['scripts/fix-external-imports.mjs', ...fixArgs], + command: 'node', + }, + { + args: ['scripts/fix-commonjs-exports.mjs', ...fixArgs], + command: 'node', + }, + ]) + + if (!quiet) { + printFooter() + } + + if (exitCode !== 0) { + logger.error('Build fixing failed') + process.exitCode = exitCode + } +} + +main().catch(error => { + logger.error(`Build fixing failed: ${error.message || error}`) + process.exitCode = 1 +}) diff --git a/packages/lib/scripts/fix-commonjs-exports.mjs b/packages/lib/scripts/fix-commonjs-exports.mjs new file mode 100644 index 000000000..931975769 --- /dev/null +++ b/packages/lib/scripts/fix-commonjs-exports.mjs @@ -0,0 +1,462 @@ +/** + * @fileoverview Fix CommonJS exports for Node.js ESM compatibility. + * Transforms esbuild's minified exports to clear module.exports = { ... } format. + */ + +import { parse } from '@babel/parser' +import { promises as fs } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import MagicString from 'magic-string' +import colors from 'yoctocolors-cjs' + +import { isQuiet } from '#socketsecurity/lib/argv/flags' +import { getDefaultLogger } from '#socketsecurity/lib/logger' + +const logger = getDefaultLogger() +const printCompletedHeader = title => console.log(colors.green(`✓ ${title}`)) + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const distDir = path.resolve(__dirname, '..', 'dist') + +/** + * Extract named exports from esbuild's minified pattern. + * Pattern: V(X,{exportName:()=>identifier,...});module.exports=z(X); + * + * @param {string} code - The file content + * @returns {{ exports: Array<{name: string, identifier: string}>, match: RegExpMatchArray } | null} + */ +function extractEsbuildExports(code) { + // First verify the code is valid JavaScript + try { + parse(code, { + sourceType: 'module', + plugins: [], + }) + } catch { + // If parsing fails, skip this file + return null + } + + // Pattern: V(X,{exportName:()=>identifier,...});module.exports=z(X); + // where V is the export setter, X is the exports object, z adds __esModule + const pattern = /V\((\w+),\{([^}]+)\}\);module\.exports=z\(\1\);/ + const match = code.match(pattern) + + if (!match) { + return null + } + + const exportsStr = match[2] + + // Parse the exports string: "exportName:()=>identifier,..." + const exports = [] + const exportRegex = /(\w+):\(\)=>(\w+)/g + let exportMatch + while ((exportMatch = exportRegex.exec(exportsStr)) !== null) { + exports.push({ name: exportMatch[1], identifier: exportMatch[2] }) + } + + if (exports.length === 0) { + return null + } + + return { exports, match } +} + +/** + * Process files in a directory and fix CommonJS exports. + * + * @param {string} dir - Directory to process + * @param {boolean} verbose - Show individual file fixes + * @returns {Promise} Number of files fixed + */ +async function processDirectory(dir, verbose = false) { + let fixedCount = 0 + + try { + const entries = await fs.readdir(dir, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + + if (entry.isDirectory()) { + fixedCount += await processDirectory(fullPath, verbose) + } else if (entry.isFile() && entry.name.endsWith('.js')) { + const content = await fs.readFile(fullPath, 'utf8') + const s = new MagicString(content) + let modified = false + + // Transform esbuild's minified export pattern to clear CommonJS exports + const exportsInfo = extractEsbuildExports(content) + if (exportsInfo) { + const { exports, match } = exportsInfo + + // Find the position of the match (this is early in the file, before definitions) + const matchStart = match.index + const matchEnd = matchStart + match[0].length + + // Remove the early export pattern + s.remove(matchStart, matchEnd) + + // Find the stub at the end: 0&&(module.exports={...}); + // This is where we should place the actual exports + const stubPattern = /0&&\(module\.exports=\{[^}]+\}\);/ + const stubMatch = content.match(stubPattern) + + // Build the clear CommonJS export + const exportLines = exports + .map(({ identifier, name }) => ` ${name}: ${identifier}`) + .join(',\n') + + const replacement = `module.exports = {\n${exportLines}\n};` + + if (stubMatch && stubMatch.index !== undefined) { + // Replace the stub with actual exports + s.overwrite( + stubMatch.index, + stubMatch.index + stubMatch[0].length, + replacement, + ) + } else { + // If no stub found, append at the end (before sourcemap comment) + const sourcemapComment = '//# sourceMappingURL=' + const sourcemapIndex = content.lastIndexOf(sourcemapComment) + if (sourcemapIndex !== -1) { + s.appendLeft(sourcemapIndex, `\n${replacement}\n`) + } else { + s.append(`\n${replacement}\n`) + } + } + + modified = true + } + + // Check if this is a single default export with __toCommonJS pattern + if ( + content.includes('module.exports = __toCommonJS(') && + content.includes('default: () => ') + ) { + // Parse AST to find the export pattern and value identifier + try { + const ast = parse(content, { + sourceType: 'module', + plugins: [], + }) + + let valueIdentifier = null + let exportCallStart = null + let exportCallEnd = null + let toCommonJSStart = null + let toCommonJSEnd = null + + // Find __export call with default export + const walk = node => { + if (!node || typeof node !== 'object') { + return + } + + // Look for: __export(name, { default: () => value_identifier }) + if ( + node.type === 'CallExpression' && + node.callee?.type === 'Identifier' && + node.callee.name === '__export' && + node.arguments?.length === 2 && + node.arguments[1].type === 'ObjectExpression' + ) { + const defaultProp = node.arguments[1].properties?.find( + p => + p.type === 'ObjectProperty' && + p.key?.name === 'default' && + p.value?.type === 'ArrowFunctionExpression', + ) + if (defaultProp?.value.body?.name) { + valueIdentifier = defaultProp.value.body.name + exportCallStart = node.start + exportCallEnd = node.end + } + } + + // Look for: module.exports = __toCommonJS(name) + if ( + node.type === 'AssignmentExpression' && + node.left?.type === 'MemberExpression' && + node.left.object?.name === 'module' && + node.left.property?.name === 'exports' && + node.right?.type === 'CallExpression' && + node.right.callee?.name === '__toCommonJS' + ) { + toCommonJSStart = node.start + toCommonJSEnd = node.end + } + + // Recursively walk + for (const key of Object.keys(node)) { + if (key === 'start' || key === 'end' || key === 'loc') { + continue + } + const value = node[key] + if (Array.isArray(value)) { + for (const item of value) { + walk(item) + } + } else { + walk(value) + } + } + } + + walk(ast.program) + + if ( + valueIdentifier && + exportCallStart !== null && + toCommonJSStart !== null + ) { + // Remove the __export call and surrounding statement + // Find the semicolon and newline after the call + let removeEnd = exportCallEnd + while ( + removeEnd < content.length && + (content[removeEnd] === ';' || content[removeEnd] === '\n') + ) { + removeEnd++ + } + s.remove(exportCallStart, removeEnd) + + // Replace the entire statement: module.exports = __toCommonJS(name); + // Find and include the semicolon + let statementEnd = toCommonJSEnd + while ( + statementEnd < content.length && + (content[statementEnd] === ';' || + content[statementEnd] === ' ' || + content[statementEnd] === '\n') + ) { + if (content[statementEnd] === ';') { + statementEnd++ + break + } + statementEnd++ + } + // Replace the entire statement with a comment + s.overwrite( + toCommonJSStart, + statementEnd, + '/* module.exports will be set at end of file */', + ) + + // Add module.exports at the end of the file + s.append(`\nmodule.exports = ${valueIdentifier};\n`) + + modified = true + } + } catch { + // If parsing fails, skip this optimization + } + } + + // Check if this is a single default export (legacy pattern) + // Only match 'exports.default =' that is NOT preceded by 'module.' or 'moduleN.' + if (content.includes('exports.default =')) { + // Transform exports.default = value to module.exports = value + let pos = 0 + while ((pos = content.indexOf('exports.default = ', pos)) !== -1) { + // Check if this is preceded by 'module.' or 'moduleN.' (from esbuild CommonJS wrapper) + const beforeModule = pos - 'module.'.length + const beforeModule2 = pos - 'module2.'.length + const isModule = + beforeModule >= 0 && + content.slice(beforeModule, pos) === 'module.' + const isModule2 = + beforeModule2 >= 0 && + content.slice(beforeModule2, pos) === 'module2.' + // Also check for generic moduleN. pattern + const beforeText = content.slice(Math.max(0, pos - 10), pos) + const hasModuleNPrefix = /module\d*\.$/.test(beforeText) + + if (isModule || isModule2 || hasModuleNPrefix) { + // Skip moduleN.exports.default (it's already from esbuild wrapper) + pos += 1 + continue + } + s.overwrite( + pos, + pos + 'exports.default = '.length, + 'module.exports = ', + ) + pos += 1 + modified = true + } + + // Remove the __esModule marker + const esModuleMarker = + 'Object.defineProperty(exports, "__esModule", { value: true });' + pos = content.indexOf(esModuleMarker) + if (pos !== -1) { + const endPos = pos + esModuleMarker.length + // Check if there's a newline after + if (content[endPos] === '\n') { + s.remove(pos, endPos + 1) + } else { + s.remove(pos, endPos) + } + modified = true + } + } + + // Fix require().default references for fixed modules using AST + try { + const ast = parse(content, { + sourceType: 'module', + plugins: [], + }) + + // Walk the AST to find MemberExpression nodes with .default + const walk = node => { + if (!node || typeof node !== 'object') { + return + } + + // Look for patterns like: require("./module").default + if ( + node.type === 'MemberExpression' && + node.property?.type === 'Identifier' && + node.property.name === 'default' && + node.object?.type === 'CallExpression' && + node.object.callee?.type === 'Identifier' && + node.object.callee.name === 'require' && + node.object.arguments?.length === 1 && + node.object.arguments[0].type === 'StringLiteral' + ) { + const modulePath = node.object.arguments[0].value + // Only fix relative imports (not external packages) + if (modulePath.startsWith('./') || modulePath.startsWith('../')) { + // Remove the .default property access + // Keep the require() call but remove .default + s.remove(node.object.end, node.end) + modified = true + } + } + + // Recursively walk all properties + for (const key of Object.keys(node)) { + if (key === 'start' || key === 'end' || key === 'loc') { + continue + } + const value = node[key] + if (Array.isArray(value)) { + for (const item of value) { + walk(item) + } + } else { + walk(value) + } + } + } + + walk(ast.program) + } catch { + // If parsing fails, skip AST-based fixes for this file + } + + // Fix module2.module.exports pattern (from external bundling) + // This is always incorrect and causes "Cannot set properties of undefined" + if (content.includes('module2.module.exports')) { + // Find and remove all occurrences of module2.module.exports lines + // Pattern matches: " module2.module.exports = value;\n" + const pattern = + /[ \t]*module2\.module\.exports\s*=\s*[^;]+;[ \t]*\n?/g + const matches = [...content.matchAll(pattern)] + + if (matches.length > 0) { + if (verbose) { + console.log( + ` Removing ${matches.length} module2.module.exports lines from ${path.basename(fullPath)}`, + ) + } + + // Process matches in reverse order to maintain correct indices + for (let i = matches.length - 1; i >= 0; i--) { + const match = matches[i] + const start = match.index + const end = start + match[0].length + s.remove(start, end) + modified = true + } + } + } + + // Fix relative paths ONLY for files in the root dist directory + const isRootFile = path.dirname(fullPath) === distDir + if ( + isRootFile && + (content.includes('require("../') || content.includes("require('../")) + ) { + let pos = 0 + while ((pos = content.indexOf('require("../', pos)) !== -1) { + s.overwrite( + pos + 'require("'.length, + pos + 'require("../'.length, + './', + ) + pos += 1 + modified = true + } + pos = 0 + while ((pos = content.indexOf("require('../", pos)) !== -1) { + s.overwrite( + pos + "require('".length, + pos + "require('../".length, + './', + ) + pos += 1 + modified = true + } + } + + if (modified) { + await fs.writeFile(fullPath, s.toString()) + if (verbose) { + const relativePath = path.relative(distDir, fullPath) + console.log(` Fixed ${relativePath}`) + } + fixedCount += 1 + } + } + } + } catch (error) { + // Skip directories that don't exist + if (error.code !== 'ENOENT') { + throw error + } + } + + return fixedCount +} + +async function fixConstantExports() { + const verbose = process.argv.includes('--verbose') + const quiet = isQuiet() + + try { + const fixedCount = await processDirectory(distDir, verbose) + + if (!quiet) { + const title = + fixedCount > 0 + ? `CommonJS Exports (${fixedCount} file${fixedCount === 1 ? '' : 's'})` + : 'CommonJS Exports (no changes)' + printCompletedHeader(title) + } + } catch (error) { + logger.error(`Failed to fix CommonJS exports: ${error.message}`) + process.exitCode = 1 + } +} + +fixConstantExports().catch(error => { + logger.error(`Build failed: ${error.message || error}`) + process.exitCode = 1 +}) diff --git a/packages/lib/scripts/fix-external-imports.mjs b/packages/lib/scripts/fix-external-imports.mjs new file mode 100644 index 000000000..f3c061c5c --- /dev/null +++ b/packages/lib/scripts/fix-external-imports.mjs @@ -0,0 +1,158 @@ +/** + * @fileoverview Fix external package imports to point to dist/external. + * Rewrites require('package') to require('./external/package') for bundled externals. + */ + +import { promises as fs } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import colors from 'yoctocolors-cjs' + +import { isQuiet } from '#socketsecurity/lib/argv/flags' +import { getDefaultLogger } from '#socketsecurity/lib/logger' + +import { externalPackages, scopedPackages } from './build-externals/config.mjs' + +const logger = getDefaultLogger() +const printCompletedHeader = title => console.log(colors.green(`✓ ${title}`)) + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const distDir = path.resolve(__dirname, '..', 'dist') +const distExternalDir = path.join(distDir, 'external') + +// Build list of all external packages to rewrite +const allExternalPackages = [ + ...externalPackages.map(p => p.name), + ...scopedPackages.flatMap(s => { + if (s.name) { + return [`${s.scope}/${s.name}`] + } + if (s.packages) { + return s.packages.map(name => `${s.scope}/${name}`) + } + return [] + }), +] + +/** + * Calculate the relative path from a file to the external directory. + * + * @param {string} filePath - The path to the file being processed + * @returns {string} The relative path prefix (e.g., './' or '../') + */ +function getExternalPathPrefix(filePath) { + const dir = path.dirname(filePath) + const relativePath = path.relative(dir, distExternalDir) + // Normalize to forward slashes and ensure it starts with ./ or ../ + const normalized = relativePath.replace(/\\/g, '/') + return normalized.startsWith('.') ? normalized : `./${normalized}` +} + +/** + * Rewrite external package imports in a file. + * + * @param {string} filePath - Path to the file to process + * @param {boolean} verbose - Show individual file fixes + * @returns {Promise} True if file was modified + */ +async function fixFileImports(filePath, verbose = false) { + let content = await fs.readFile(filePath, 'utf8') + let modified = false + + const externalPrefix = getExternalPathPrefix(filePath) + + for (const pkg of allExternalPackages) { + // Escape special regex characters in package name + const escapedPkg = pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + + // Match require('pkg') or require("pkg") + // Don't match if it's already pointing to ./external/ or ../external/ + const requirePattern = new RegExp( + `require\\((['"])(?!\\.\\.?\\/external\\/)${escapedPkg}\\1\\)`, + 'g', + ) + + if (requirePattern.test(content)) { + // Replace with require('./external/pkg') or require('../external/pkg') + const replacement = `require('${externalPrefix}/${pkg}')` + content = content.replace(requirePattern, replacement) + modified = true + } + } + + if (modified) { + await fs.writeFile(filePath, content) + if (verbose) { + const relativePath = path.relative(distDir, filePath) + console.log(` Fixed ${relativePath}`) + } + } + + return modified +} + +/** + * Process files in a directory and fix external imports. + * + * @param {string} dir - Directory to process + * @param {boolean} verbose - Show individual file fixes + * @returns {Promise} Number of files fixed + */ +async function processDirectory(dir, verbose = false) { + let fixedCount = 0 + + try { + const entries = await fs.readdir(dir, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + + // Skip the external directory itself + if (entry.isDirectory() && fullPath === distExternalDir) { + continue + } + + if (entry.isDirectory()) { + fixedCount += await processDirectory(fullPath, verbose) + } else if (entry.isFile() && entry.name.endsWith('.js')) { + const wasFixed = await fixFileImports(fullPath, verbose) + if (wasFixed) { + fixedCount += 1 + } + } + } + } catch (error) { + // Skip directories that don't exist + if (error.code !== 'ENOENT') { + throw error + } + } + + return fixedCount +} + +async function fixExternalImports() { + const verbose = process.argv.includes('--verbose') + const quiet = isQuiet() + + try { + const fixedCount = await processDirectory(distDir, verbose) + + if (!quiet) { + const title = + fixedCount > 0 + ? `External Imports (${fixedCount} file${fixedCount === 1 ? '' : 's'})` + : 'External Imports (no changes)' + printCompletedHeader(title) + } + } catch (error) { + logger.error(`Failed to fix external imports: ${error.message}`) + process.exitCode = 1 + } +} + +fixExternalImports().catch(error => { + logger.error(`Build failed: ${error.message || error}`) + process.exitCode = 1 +}) diff --git a/packages/lib/scripts/fix-path-aliases.mjs b/packages/lib/scripts/fix-path-aliases.mjs new file mode 100644 index 000000000..1ca75e8f0 --- /dev/null +++ b/packages/lib/scripts/fix-path-aliases.mjs @@ -0,0 +1,170 @@ +/** + * @fileoverview Fix internal path aliases (#lib/*, #constants/*, etc.) to relative paths. + * Rewrites require('#lib/foo') to require('../foo') based on file location. + */ + +import { promises as fs } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import colors from 'yoctocolors-cjs' + +import { isQuiet } from '#socketsecurity/lib/argv/flags' +import { getDefaultLogger } from '#socketsecurity/lib/logger' + +const logger = getDefaultLogger() +const printCompletedHeader = title => console.log(colors.green(`✓ ${title}`)) + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const distDir = path.resolve(__dirname, '..', 'dist') +const _srcDir = path.resolve(__dirname, '..', 'src') + +// Map of path aliases to their actual directories +const pathAliases = { + '#lib/': distDir, + '#constants/': path.join(distDir, 'constants'), + '#env/': path.join(distDir, 'env'), + '#packages/': path.join(distDir, 'packages'), + '#utils/': path.join(distDir, 'utils'), + '#types': path.join(distDir, 'types'), +} + +/** + * Calculate the relative path from a file to the target. + * + * @param {string} filePath - The path to the file being processed + * @param {string} targetPath - The path to the target file/directory + * @returns {string} The relative path (e.g., './foo' or '../bar') + */ +function getRelativePath(filePath, targetPath) { + const dir = path.dirname(filePath) + let relativePath = path.relative(dir, targetPath) + + // Normalize to forward slashes + relativePath = relativePath.replace(/\\/g, '/') + + // Ensure it starts with ./ or ../ + if (!relativePath.startsWith('.')) { + relativePath = `./${relativePath}` + } + + return relativePath +} + +/** + * Rewrite path alias imports in a file. + * + * @param {string} filePath - Path to the file to process + * @param {boolean} verbose - Show individual file fixes + * @returns {Promise} True if file was modified + */ +async function fixFileAliases(filePath, verbose = false) { + let content = await fs.readFile(filePath, 'utf8') + let modified = false + + for (const [alias, basePath] of Object.entries(pathAliases)) { + const isExact = !alias.endsWith('/') + + // Escape special regex characters + const escapedAlias = alias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + + // Match require('#lib/foo') or require("#lib/foo") or require('#types') + // Capture the quote style and the subpath + const requirePattern = new RegExp( + `require\\((['"])${escapedAlias}([^'"]*?)\\1\\)`, + 'g', + ) + + const matches = [...content.matchAll(requirePattern)] + + for (const match of matches) { + const [fullMatch, quote, subpath] = match + + // Calculate target path + const targetPath = isExact ? basePath : path.join(basePath, subpath || '') + + // Calculate relative path from this file + const relativePath = getRelativePath(filePath, targetPath) + + // Replace with require('./relative/path') + const replacement = `require(${quote}${relativePath}${quote})` + content = content.replace(fullMatch, replacement) + modified = true + } + } + + if (modified) { + await fs.writeFile(filePath, content) + if (verbose) { + const relativePath = path.relative(distDir, filePath) + console.log(` Fixed ${relativePath}`) + } + } + + return modified +} + +/** + * Process files in a directory and fix path aliases. + * + * @param {string} dir - Directory to process + * @param {boolean} verbose - Show individual file fixes + * @returns {Promise} Number of files fixed + */ +async function processDirectory(dir, verbose = false) { + let fixedCount = 0 + + try { + const entries = await fs.readdir(dir, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + + // Skip the external directory + if (entry.isDirectory() && entry.name === 'external') { + continue + } + + if (entry.isDirectory()) { + fixedCount += await processDirectory(fullPath, verbose) + } else if (entry.isFile() && entry.name.endsWith('.js')) { + const wasFixed = await fixFileAliases(fullPath, verbose) + if (wasFixed) { + fixedCount += 1 + } + } + } + } catch (error) { + // Skip directories that don't exist + if (error.code !== 'ENOENT') { + throw error + } + } + + return fixedCount +} + +async function fixPathAliases() { + const verbose = process.argv.includes('--verbose') + const quiet = isQuiet() + + try { + const fixedCount = await processDirectory(distDir, verbose) + + if (!quiet) { + const title = + fixedCount > 0 + ? `Path Aliases (${fixedCount} file${fixedCount === 1 ? '' : 's'})` + : 'Path Aliases (no changes)' + printCompletedHeader(title) + } + } catch (error) { + logger.error(`Failed to fix path aliases: ${error.message}`) + process.exitCode = 1 + } +} + +fixPathAliases().catch(error => { + logger.error(`Build failed: ${error.message || error}`) + process.exitCode = 1 +}) diff --git a/packages/lib/scripts/generate-package-exports.mjs b/packages/lib/scripts/generate-package-exports.mjs new file mode 100644 index 000000000..4b6950d09 --- /dev/null +++ b/packages/lib/scripts/generate-package-exports.mjs @@ -0,0 +1,240 @@ +/** @fileoverview Update registry package.json with exports, browser fields, and Node.js engine range. */ + +import { promises as fs } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import builtinNames from '@socketregistry/packageurl-js/data/npm/builtin-names.json' with { + type: 'json', +} +import fastGlob from 'fast-glob' + +import { toSortedObject } from '#socketsecurity/lib/objects' +import { readPackageJson } from '#socketsecurity/lib/packages' + +// Helper to write package.json with proper formatting +async function writePackageJson(filePath, data) { + const content = `${JSON.stringify(data, null, 2)}\n` + await fs.writeFile(filePath, content, 'utf8') +} + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +// Constants for socket-lib +const constants = { + EXT_DTS: '.d.ts', + EXT_JSON: '.json', + registryPkgPath: path.join(__dirname, '..'), + ignoreGlobs: [ + '**/node_modules/**', + '**/.git/**', + '**/dist/**', + '**/coverage/**', + '**/.cache/**', + '**/tmp/**', + '**/.DS_Store', + ], + PACKAGE_DEFAULT_NODE_RANGE: '>=22', +} + +const { EXT_DTS, EXT_JSON } = constants + +/** + * Generate exports and browser fields for registry package. + */ +async function main() { + const { registryPkgPath } = constants + const registryPkgJsonPath = path.join(registryPkgPath, 'package.json') + + const registryPkgJsonData = await readPackageJson(registryPkgJsonPath) + + // Create editable wrapper. + const registryEditablePkgJson = { + content: registryPkgJsonData, + save: async function () { + await writePackageJson(registryPkgJsonPath, this.content) + }, + update: function (updates) { + Object.assign(this.content, updates) + }, + } + + const registryPkgJson = registryEditablePkgJson.content + + const browser = { ...registryPkgJson.browser } + for (const builtinName of builtinNames) { + browser[builtinName] = false + } + + const registryPkgFiles = [ + ...(await fastGlob.glob(['**/*.{cjs,js,json,d.ts}'], { + cwd: registryPkgPath, + ignore: [ + '**/node_modules/**', + '**/.git/**', + '**/coverage/**', + '**/.cache/**', + '**/tmp/**', + '**/.DS_Store', + 'dist/external/**', + 'scripts/**', + 'src/**', + ], + gitignore: false, + })), + ] + + const isDebug = !!process.env.DEBUG + if (isDebug) { + console.log('Found', registryPkgFiles.length, 'files') + console.log('First 10:', registryPkgFiles.slice(0, 10)) + } + + const jsonExports = {} + const subpathExports = registryPkgFiles.reduce((o, p) => { + const ext = p.endsWith(EXT_DTS) ? EXT_DTS : path.extname(p) + // Strip 'dist/' prefix from export path but keep it in file path. + const exportPath = p.startsWith('dist/') ? p.slice(5) : p + const filePath = `./${p}` + + if (ext === EXT_JSON) { + jsonExports[`./${exportPath}`] = filePath + } else { + const extLessExportPath = `./${exportPath.slice(0, -ext.length)}` + const isDts = ext === EXT_DTS + if (o[extLessExportPath]) { + o[extLessExportPath][isDts ? 'types' : 'default'] = filePath + } else { + o[extLessExportPath] = { + // Order is significant. Default should be specified last. + types: isDts ? filePath : undefined, + default: isDts ? undefined : filePath, + } + } + const basename = path.basename(exportPath, ext) + if (basename === 'index') { + const dirname = path.dirname(exportPath) + const dirPath = dirname === '.' ? dirname : `./${dirname}` + if (o[dirPath]) { + o[dirPath][isDts ? 'types' : 'default'] = filePath + } else { + o[dirPath] = { + // Order is significant. Default should be specified last. + types: isDts ? filePath : undefined, + default: isDts ? undefined : filePath, + } + } + } + } + return o + }, {}) + + // Add kebab-case variants for all SCREAMING_SNAKE_CASE constant paths. + // Map both kebab-case and SCREAMING_SNAKE_CASE paths to the same files. + const aliasesToAdd = [] + for (const { 0: exportPath, 1: exportValue } of Object.entries( + subpathExports, + )) { + if ( + exportPath.startsWith('./lib/constants/') && + exportPath !== './lib/constants' + ) { + const pathAfterConstants = exportPath.slice('./lib/constants/'.length) + + // Check if this is a SCREAMING_SNAKE_CASE name. + if ( + pathAfterConstants.includes('_') && + /[A-Z]/.test(pathAfterConstants) + ) { + // Create kebab-case variant. + const kebabCasePath = `./lib/constants/${pathAfterConstants.toLowerCase().replace(/_/g, '-')}` + if (!subpathExports[kebabCasePath]) { + aliasesToAdd.push([kebabCasePath, exportValue]) + } + } + + // Check if this is a kebab-case name. + if ( + pathAfterConstants.includes('-') && + pathAfterConstants === pathAfterConstants.toLowerCase() + ) { + // Create SCREAMING_SNAKE_CASE variant. + const screamingSnakeCasePath = `./lib/constants/${pathAfterConstants.toUpperCase().replace(/-/g, '_')}` + if (!subpathExports[screamingSnakeCasePath]) { + aliasesToAdd.push([screamingSnakeCasePath, exportValue]) + } + } + } + } + + // Add all aliases after iteration completes. + for (const { 0: aliasPath, 1: aliasValue } of aliasesToAdd) { + subpathExports[aliasPath] = aliasValue + } + + // Create exports object with proper ordering: + // 1. Main exports (. and ./index) + // 2. SCREAMING_SNAKE_CASE constants + // 3. kebab-case constants + // 4. Non-constants lib exports + // 5. JSON files + const mainExports = {} + const jsonExports2 = {} + const libExports = {} + const screamingSnakeCaseExports = {} + const kebabCaseExports = {} + + for (const { 0: key, 1: value } of Object.entries({ + ...subpathExports, + ...jsonExports, + })) { + if (key === '.' || key === './index') { + mainExports[key] = value + } else if (key.endsWith('.json')) { + jsonExports2[key] = value + } else if (key.startsWith('./lib/constants/')) { + const pathAfterConstants = key.slice('./lib/constants/'.length) + // SCREAMING_SNAKE_CASE paths contain _ or start with uppercase + if ( + pathAfterConstants.includes('_') || + /^[A-Z]/.test(pathAfterConstants) + ) { + screamingSnakeCaseExports[key] = value + } else { + kebabCaseExports[key] = value + } + } else { + // Non-constants lib paths + libExports[key] = value + } + } + + // Ensure . comes before ./index + const sortedMainExports = {} + if (mainExports['.']) { + sortedMainExports['.'] = mainExports['.'] + } + if (mainExports['./index']) { + sortedMainExports['./index'] = mainExports['./index'] + } + + const exports = { + ...sortedMainExports, + ...toSortedObject(screamingSnakeCaseExports), + ...toSortedObject(kebabCaseExports), + ...toSortedObject(libExports), + ...toSortedObject(jsonExports2), + } + + registryEditablePkgJson.update({ + browser: toSortedObject(browser), + exports, + engines: { + ...registryEditablePkgJson.content.engines, + node: constants.PACKAGE_DEFAULT_NODE_RANGE, + }, + }) + await registryEditablePkgJson.save() +} + +main().catch(console.error) diff --git a/packages/lib/scripts/lint.mjs b/packages/lib/scripts/lint.mjs new file mode 100644 index 000000000..6881c3426 --- /dev/null +++ b/packages/lib/scripts/lint.mjs @@ -0,0 +1,493 @@ +/** + * @fileoverview Unified lint runner with flag-based configuration. + * Provides smart linting that can target affected files or lint everything. + */ + +import { existsSync, readFileSync } from 'node:fs' +import path from 'node:path' + +import { + getChangedFilesSync, + getStagedFilesSync, +} from '#socketsecurity/lib/git' + +import { isQuiet } from '#socketsecurity/lib/argv/flags' +import { getDefaultLogger } from '#socketsecurity/lib/logger' +import { printHeader } from '#socketsecurity/lib/stdio/header' + +import { parseArgs } from './utils/parse-args.mjs' +import { runCommandQuiet } from './utils/run-command.mjs' + +const logger = getDefaultLogger() + +// Files that trigger a full lint when changed +const CORE_FILES = new Set([ + 'src/constants.ts', + 'src/error.ts', + 'src/helpers.ts', + 'src/lang.ts', + 'src/objects.ts', + 'src/strings.ts', + 'src/validate.ts', + 'src/purl-type.ts', +]) + +// Config patterns that trigger a full lint +const CONFIG_PATTERNS = [ + '.config/**', + 'scripts/utils/**', + 'pnpm-lock.yaml', + 'tsconfig*.json', + 'eslint.config.*', +] + +/** + * Get Biome exclude patterns from biome.json. + */ +function getBiomeExcludePatterns() { + try { + const biomeConfigPath = path.join(process.cwd(), 'biome.json') + if (!existsSync(biomeConfigPath)) { + return [] + } + + const biomeConfig = JSON.parse(readFileSync(biomeConfigPath, 'utf8')) + const includes = biomeConfig['files']?.['includes'] ?? [] + + // Extract patterns that start with '!' (exclude patterns) + return ( + includes + .filter( + pattern => typeof pattern === 'string' && pattern.startsWith('!'), + ) + // Remove the '!' prefix + .map(pattern => pattern.slice(1)) + ) + } catch { + // If we can't read biome.json, return empty array + return [] + } +} + +/** + * Check if a file matches any of the exclude patterns. + */ +function isExcludedByBiome(file, excludePatterns) { + for (const pattern of excludePatterns) { + // Convert glob pattern to regex-like matching + // Support **/ for directory wildcards and * for filename wildcards + const regexPattern = pattern + // **/ matches any directory + .replace(/\*\*\//g, '.*') + // * matches any characters except / + .replace(/\*/g, '[^/]*') + // Escape dots + .replace(/\./g, '\\.') + + const regex = new RegExp(`^${regexPattern}$`) + if (regex.test(file)) { + return true + } + } + return false +} + +/** + * Check if we should run all linters based on changed files. + */ +function shouldRunAllLinters(changedFiles) { + for (const file of changedFiles) { + // Core library files + if (CORE_FILES.has(file)) { + return { runAll: true, reason: 'core files changed' } + } + + // Config or infrastructure files + for (const pattern of CONFIG_PATTERNS) { + if (file.includes(pattern.replace('**', ''))) { + return { runAll: true, reason: 'config files changed' } + } + } + } + + return { runAll: false } +} + +/** + * Filter files to only those that should be linted. + */ +function filterLintableFiles(files) { + const lintableExtensions = new Set([ + '.js', + '.mjs', + '.cjs', + '.ts', + '.cts', + '.mts', + '.json', + '.jsonc', + '.md', + '.yml', + '.yaml', + ]) + + const biomeExcludePatterns = getBiomeExcludePatterns() + + return files.filter(file => { + const ext = path.extname(file) + // Only lint files that have lintable extensions AND still exist. + if (!lintableExtensions.has(ext) || !existsSync(file)) { + return false + } + + // Filter out files excluded by biome.json + if (isExcludedByBiome(file, biomeExcludePatterns)) { + return false + } + + return true + }) +} + +/** + * Run linters on specific files. + */ +async function runLintOnFiles(files, options = {}) { + const { fix = false, quiet = false } = options + + if (!files.length) { + logger.substep('No files to lint') + return 0 + } + + if (!quiet) { + logger.progress(`Linting ${files.length} file(s)`) + } + + // Build the linter configurations. + const linters = [ + { + args: [ + 'exec', + 'biome', + 'check', + '--log-level=none', + ...(fix ? ['--write', '--unsafe'] : []), + ...files, + ], + name: 'biome', + enabled: true, + }, + { + args: [ + 'exec', + 'eslint', + '-c', + '.config/eslint.config.mjs', + '--report-unused-disable-directives', + ...(fix ? ['--fix'] : []), + ...files, + ], + name: 'eslint', + enabled: true, + }, + ] + + for (const { args, enabled } of linters) { + if (!enabled) { + continue + } + + const result = await runCommandQuiet('pnpm', args) + + if (result.exitCode !== 0) { + // Check if Biome simply had no files to process (not an error) + const isBiomeNoFilesError = result.stderr?.includes( + 'No files were processed in the specified paths', + ) + + if (isBiomeNoFilesError) { + // Biome had nothing to do - this is fine, continue to next linter + continue + } + + // When fixing, non-zero exit codes are normal if fixes were applied. + if (!fix || (result.stderr && result.stderr.trim().length > 0)) { + if (!quiet) { + logger.error('Linting failed') + } + if (result.stderr) { + console.error(result.stderr) + } + if (result.stdout && !fix) { + console.log(result.stdout) + } + return result.exitCode + } + } + } + + if (!quiet) { + logger.clearLine().done('Linting passed') + // Add newline after message + console.log('') + } + + return 0 +} + +/** + * Run linters on all files. + */ +async function runLintOnAll(options = {}) { + const { fix = false, quiet = false } = options + + if (!quiet) { + logger.progress('Linting all files') + } + + const linters = [ + { + args: [ + 'exec', + 'biome', + 'check', + ...(fix ? ['--write', '--unsafe'] : []), + '.', + ], + name: 'biome', + }, + { + args: [ + 'exec', + 'eslint', + '-c', + '.config/eslint.config.mjs', + '--report-unused-disable-directives', + ...(fix ? ['--fix'] : []), + '.', + ], + name: 'eslint', + }, + ] + + for (const { args } of linters) { + const result = await runCommandQuiet('pnpm', args) + + if (result.exitCode !== 0) { + // Check if Biome simply had no files to process (not an error) + const isBiomeNoFilesError = result.stderr?.includes( + 'No files were processed in the specified paths', + ) + + if (isBiomeNoFilesError) { + // Biome had nothing to do - this is fine, continue to next linter + continue + } + + // When fixing, non-zero exit codes are normal if fixes were applied. + if (!fix || (result.stderr && result.stderr.trim().length > 0)) { + if (!quiet) { + logger.error('Linting failed') + } + if (result.stderr) { + console.error(result.stderr) + } + if (result.stdout && !fix) { + console.log(result.stdout) + } + return result.exitCode + } + } + } + + if (!quiet) { + logger.clearLine().done('Linting passed') + // Add newline after message + console.log('') + } + + return 0 +} + +/** + * Get files to lint based on options. + */ +async function getFilesToLint(options) { + const { all, changed, staged } = options + + // If --all, return early + if (all) { + return { files: 'all', reason: 'all flag specified', mode: 'all' } + } + + // Get changed files + let changedFiles = [] + // Track what mode we're in + let mode = 'changed' + + if (staged) { + mode = 'staged' + changedFiles = getStagedFilesSync({ absolute: false }) + if (!changedFiles.length) { + return { files: null, reason: 'no staged files', mode } + } + } else if (changed) { + mode = 'changed' + changedFiles = getChangedFilesSync({ absolute: false }) + if (!changedFiles.length) { + return { files: null, reason: 'no changed files', mode } + } + } else { + // Default to changed files if no specific flag + mode = 'changed' + changedFiles = getChangedFilesSync({ absolute: false }) + if (!changedFiles.length) { + return { files: null, reason: 'no changed files', mode } + } + } + + // Check if we should run all based on changed files + const { reason, runAll } = shouldRunAllLinters(changedFiles) + if (runAll) { + return { files: 'all', reason, mode: 'all' } + } + + // Filter to lintable files + const lintableFiles = filterLintableFiles(changedFiles) + if (!lintableFiles.length) { + return { files: null, reason: 'no lintable files changed', mode } + } + + return { files: lintableFiles, reason: null, mode } +} + +async function main() { + try { + // Parse arguments + const { positionals, values } = parseArgs({ + options: { + help: { + type: 'boolean', + default: false, + }, + fix: { + type: 'boolean', + default: false, + }, + all: { + type: 'boolean', + default: false, + }, + changed: { + type: 'boolean', + default: false, + }, + staged: { + type: 'boolean', + default: false, + }, + quiet: { + type: 'boolean', + default: false, + }, + silent: { + type: 'boolean', + default: false, + }, + }, + allowPositionals: true, + strict: false, + }) + + // Show help if requested + if (values.help) { + console.log('Lint Runner') + console.log('\nUsage: pnpm lint [options] [files...]') + console.log('\nOptions:') + console.log(' --help Show this help message') + console.log(' --fix Automatically fix problems') + console.log(' --all Lint all files') + console.log(' --changed Lint changed files (default behavior)') + console.log(' --staged Lint staged files') + console.log(' --quiet, --silent Suppress progress messages') + console.log('\nExamples:') + console.log( + ' pnpm lint # Lint changed files (default)', + ) + console.log(' pnpm lint --fix # Fix issues in changed files') + console.log(' pnpm lint --all # Lint all files') + console.log(' pnpm lint --staged --fix # Fix issues in staged files') + console.log(' pnpm lint src/index.ts # Lint specific file(s)') + process.exitCode = 0 + return + } + + const quiet = isQuiet(values) + + if (!quiet) { + printHeader('Lint Runner') + console.log('') + } + + let exitCode = 0 + + // Handle positional arguments (specific files) + if (positionals.length > 0) { + const files = filterLintableFiles(positionals) + if (!quiet) { + logger.step('Linting specified files') + } + exitCode = await runLintOnFiles(files, { + fix: values.fix, + quiet, + }) + } else { + // Get files to lint based on flags + const { files, mode, reason } = await getFilesToLint(values) + + if (files === null) { + if (!quiet) { + logger.step('Skipping lint') + logger.substep(reason) + } + exitCode = 0 + } else if (files === 'all') { + if (!quiet) { + logger.step(`Linting all files (${reason})`) + } + exitCode = await runLintOnAll({ + fix: values.fix, + quiet, + }) + } else { + if (!quiet) { + const modeText = mode === 'staged' ? 'staged' : 'changed' + logger.step(`Linting ${modeText} files`) + } + exitCode = await runLintOnFiles(files, { + fix: values.fix, + quiet, + }) + } + } + + if (exitCode !== 0) { + if (!quiet) { + logger.error('') + console.log('Lint failed') + } + process.exitCode = exitCode + } else { + if (!quiet) { + console.log('') + logger.success('All lint checks passed!') + } + } + } catch (error) { + logger.error(`Lint runner failed: ${error.message}`) + process.exitCode = 1 + } +} + +main().catch(console.error) diff --git a/packages/lib/scripts/test.mjs b/packages/lib/scripts/test.mjs new file mode 100644 index 000000000..00ce4d121 --- /dev/null +++ b/packages/lib/scripts/test.mjs @@ -0,0 +1,507 @@ +/** + * @fileoverview Unified test runner that provides a smooth, single-script experience. + * Combines check, build, and test steps with clean, consistent output. + */ + +import { spawn } from 'node:child_process' +import { existsSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import { getDefaultLogger } from '#socketsecurity/lib/logger' +import { getDefaultSpinner } from '#socketsecurity/lib/spinner' +import { printHeader } from '#socketsecurity/lib/stdio/header' + +import { getLocalPackageAliases } from './utils/get-local-package-aliases.mjs' +import { getTestsToRun } from './utils/changed-test-mapper.mjs' +import { parseArgs } from './utils/parse-args.mjs' +import { onExit } from './utils/signal-exit.mjs' + +const logger = getDefaultLogger() +const spinner = getDefaultSpinner() + +const WIN32 = process.platform === 'win32' + +// Suppress non-fatal worker termination unhandled rejections +process.on('unhandledRejection', (reason, _promise) => { + const errorMessage = String(reason?.message || reason || '') + // Filter out known non-fatal worker termination errors + if ( + errorMessage.includes('Terminating worker thread') || + errorMessage.includes('ThreadTermination') + ) { + // Ignore these - they're cleanup messages from vitest worker threads + return + } + // Re-throw other unhandled rejections + throw reason +}) + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const rootPath = path.resolve(__dirname, '..') +const nodeModulesBinPath = path.join(rootPath, 'node_modules', '.bin') + +// Detect if external Socket packages are available for type checking +const localAliases = getLocalPackageAliases(rootPath) +const hasExternalPackages = Object.keys(localAliases).length > 0 +const tsconfigPath = hasExternalPackages + ? '.config/tsconfig.external-aliases.json' + : '.config/tsconfig.check.json' + +// Track running processes for cleanup +const runningProcesses = new Set() + +// Setup exit handler +const removeExitHandler = onExit((_code, signal) => { + // Stop spinner first + try { + spinner.stop() + } catch {} + + // Kill all running processes + for (const child of runningProcesses) { + try { + child.kill('SIGTERM') + } catch {} + } + + if (signal) { + console.log(`\nReceived ${signal}, cleaning up...`) + // Let onExit handle the exit with proper code + process.exitCode = 128 + (signal === 'SIGINT' ? 2 : 15) + } +}) + +async function runCommand(command, args = [], options = {}) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + stdio: 'inherit', + ...(process.platform === 'win32' && { shell: true }), + ...options, + }) + + runningProcesses.add(child) + + child.on('exit', code => { + runningProcesses.delete(child) + resolve(code || 0) + }) + + child.on('error', error => { + runningProcesses.delete(child) + reject(error) + }) + }) +} + +async function runCommandWithOutput(command, args = [], options = {}) { + return new Promise((resolve, reject) => { + let stdout = '' + let stderr = '' + + const child = spawn(command, args, { + ...(process.platform === 'win32' && { shell: true }), + ...options, + }) + + runningProcesses.add(child) + + if (child.stdout) { + child.stdout.on('data', data => { + stdout += data.toString() + }) + } + + if (child.stderr) { + child.stderr.on('data', data => { + stderr += data.toString() + }) + } + + child.on('exit', code => { + runningProcesses.delete(child) + resolve({ code: code || 0, stdout, stderr }) + }) + + child.on('error', error => { + runningProcesses.delete(child) + reject(error) + }) + }) +} + +async function runCheck() { + logger.step('Running checks') + + // Run fix (auto-format) quietly since it has its own output + spinner.start('Formatting code...') + let exitCode = await runCommand('pnpm', ['run', 'fix'], { + stdio: 'pipe', + }) + if (exitCode !== 0) { + spinner.stop() + logger.error('Formatting failed') + // Re-run with output to show errors + await runCommand('pnpm', ['run', 'fix']) + return exitCode + } + spinner.stop() + logger.success('Code formatted') + + // Run ESLint to check for remaining issues + spinner.start('Running ESLint...') + exitCode = await runCommand( + 'eslint', + [ + '--config', + '.config/eslint.config.mjs', + '--report-unused-disable-directives', + '.', + ], + { + stdio: 'pipe', + }, + ) + if (exitCode !== 0) { + spinner.stop() + logger.error('ESLint failed') + // Re-run with output to show errors + await runCommand('eslint', [ + '--config', + '.config/eslint.config.mjs', + '--report-unused-disable-directives', + '.', + ]) + return exitCode + } + spinner.stop() + logger.success('ESLint passed') + + // Run TypeScript check + spinner.start('Checking TypeScript...') + exitCode = await runCommand('tsgo', ['--noEmit', '-p', tsconfigPath], { + stdio: 'pipe', + }) + if (exitCode !== 0) { + spinner.stop() + logger.error('TypeScript check failed') + // Re-run with output to show errors + await runCommand('tsgo', ['--noEmit', '-p', tsconfigPath]) + return exitCode + } + spinner.stop() + logger.success('TypeScript check passed') + + return exitCode +} + +async function runBuild() { + const distIndexPath = path.join(rootPath, 'dist', 'index.js') + if (!existsSync(distIndexPath)) { + logger.step('Building project') + return runCommand('pnpm', ['run', 'build']) + } + return 0 +} + +async function runTests( + options, + positionals = [], + configPath = '.config/vitest.config.mts', +) { + const { all, coverage, force, staged, update } = options + const runAll = all || force + + // Get tests to run + const testInfo = getTestsToRun({ staged, all: runAll }) + const { mode, reason, tests: testsToRun } = testInfo + + // No tests needed + if (testsToRun === null) { + logger.substep('No relevant changes detected, skipping tests') + return 0 + } + + // Prepare vitest command + const vitestCmd = WIN32 ? 'vitest.cmd' : 'vitest' + const vitestPath = path.join(nodeModulesBinPath, vitestCmd) + + const vitestArgs = ['--config', configPath, 'run'] + + // Add coverage if requested + if (coverage) { + vitestArgs.push('--coverage') + } + + // Add update if requested + if (update) { + vitestArgs.push('--update') + } + + // Add test patterns if not running all + if (testsToRun === 'all') { + logger.step(`Running all tests (${reason})`) + } else { + const modeText = mode === 'staged' ? 'staged' : 'changed' + logger.step(`Running tests for ${modeText} files:`) + testsToRun.forEach(test => { + logger.substep(test) + }) + vitestArgs.push(...testsToRun) + } + + // Add any additional positional arguments + if (positionals.length > 0) { + vitestArgs.push(...positionals) + } + + const spawnOptions = { + cwd: rootPath, + env: { + ...process.env, + NODE_OPTIONS: + `${process.env.NODE_OPTIONS || ''} --max-old-space-size=${process.env.CI ? 8192 : 4096} --unhandled-rejections=warn`.trim(), + VITEST: '1', + }, + stdio: 'inherit', + } + + // Use interactive runner for interactive Ctrl+O experience when appropriate + if (process.stdout.isTTY) { + const { runTests } = await import('./utils/interactive-runner.mjs') + return runTests(vitestPath, vitestArgs, { + env: spawnOptions.env, + cwd: spawnOptions.cwd, + verbose: false, + }) + } + + // Fallback to execution with output capture to handle worker termination errors + const result = await runCommandWithOutput(vitestPath, vitestArgs, { + ...spawnOptions, + stdio: ['inherit', 'pipe', 'pipe'], + }) + + // Print output + if (result.stdout) { + process.stdout.write(result.stdout) + } + if (result.stderr) { + process.stderr.write(result.stderr) + } + + // Check if we have worker termination error but no test failures + const hasWorkerTerminationError = + (result.stdout + result.stderr).includes('Terminating worker thread') || + (result.stdout + result.stderr).includes('ThreadTermination') + + const output = result.stdout + result.stderr + const hasTestFailures = + output.includes('FAIL') || + (output.includes('Test Files') && output.match(/(\d+) failed/) !== null) || + (output.includes('Tests') && output.match(/Tests\s+\d+ failed/) !== null) + + // Override exit code if we only have worker termination errors + if (result.code !== 0 && hasWorkerTerminationError && !hasTestFailures) { + return 0 + } + + return result.code +} + +async function runIsolatedTests(options) { + const { coverage } = options + + logger.step('Running isolated tests') + + // Prepare vitest command + const vitestCmd = WIN32 ? 'vitest.cmd' : 'vitest' + const vitestPath = path.join(nodeModulesBinPath, vitestCmd) + + const vitestArgs = ['--config', '.config/vitest.config.isolated.mts', 'run'] + + // Add coverage if requested + if (coverage) { + vitestArgs.push('--coverage') + } + + const spawnOptions = { + cwd: rootPath, + env: { + ...process.env, + NODE_OPTIONS: + `${process.env.NODE_OPTIONS || ''} --max-old-space-size=${process.env.CI ? 8192 : 4096} --unhandled-rejections=warn`.trim(), + VITEST: '1', + }, + stdio: 'inherit', + } + + // Always use direct execution for isolated tests (simpler, more predictable) + const result = await runCommandWithOutput(vitestPath, vitestArgs, { + ...spawnOptions, + stdio: ['inherit', 'pipe', 'pipe'], + }) + + // Print output + if (result.stdout) { + process.stdout.write(result.stdout) + } + if (result.stderr) { + process.stderr.write(result.stderr) + } + + return result.code +} + +async function main() { + try { + // Parse arguments + const { positionals, values } = parseArgs({ + options: { + help: { + type: 'boolean', + default: false, + }, + fast: { + type: 'boolean', + default: false, + }, + quick: { + type: 'boolean', + default: false, + }, + 'skip-build': { + type: 'boolean', + default: false, + }, + staged: { + type: 'boolean', + default: false, + }, + all: { + type: 'boolean', + default: false, + }, + force: { + type: 'boolean', + default: false, + }, + cover: { + type: 'boolean', + default: false, + }, + coverage: { + type: 'boolean', + default: false, + }, + update: { + type: 'boolean', + default: false, + }, + }, + allowPositionals: true, + strict: false, + }) + + // Show help if requested + if (values.help) { + console.log('Test Runner') + console.log('\nUsage: pnpm test [options] [-- vitest-args...]') + console.log('\nOptions:') + console.log(' --help Show this help message') + console.log( + ' --fast, --quick Skip lint/type checks for faster execution', + ) + console.log(' --cover, --coverage Run tests with code coverage') + console.log(' --update Update test snapshots') + console.log(' --all, --force Run all tests regardless of changes') + console.log(' --staged Run tests affected by staged changes') + console.log(' --skip-build Skip the build step') + console.log('\nExamples:') + console.log( + ' pnpm test # Run checks, build, and tests for changed files', + ) + console.log(' pnpm test --all # Run all tests') + console.log( + ' pnpm test --fast # Skip checks for quick testing', + ) + console.log(' pnpm test --cover # Run with coverage report') + console.log(' pnpm test --fast --cover # Quick test with coverage') + console.log(' pnpm test --update # Update test snapshots') + console.log(' pnpm test -- --reporter=dot # Pass args to vitest') + process.exitCode = 0 + return + } + + printHeader('Test Runner') + + // Handle aliases + const skipChecks = values.fast || values.quick + const withCoverage = values.cover || values.coverage + + let exitCode = 0 + + // Run checks unless skipped + if (!skipChecks) { + exitCode = await runCheck() + if (exitCode !== 0) { + logger.error('Checks failed') + process.exitCode = exitCode + return + } + logger.success('All checks passed') + } + + // Run build unless skipped + if (!values['skip-build']) { + exitCode = await runBuild() + if (exitCode !== 0) { + logger.error('Build failed') + process.exitCode = exitCode + return + } + } + + // Run main tests + exitCode = await runTests( + { ...values, coverage: withCoverage }, + positionals, + ) + + if (exitCode !== 0) { + logger.error('Main tests failed') + process.exitCode = exitCode + return + } + + // Run isolated tests + exitCode = await runIsolatedTests({ coverage: withCoverage }) + + if (exitCode !== 0) { + logger.error('Isolated tests failed') + process.exitCode = exitCode + } else { + logger.success('All tests passed!') + } + } catch (error) { + // Ensure spinner is stopped + try { + spinner.stop() + } catch {} + logger.error(`Test runner failed: ${error.message}`) + process.exitCode = 1 + } finally { + // Ensure spinner is stopped + try { + spinner.stop() + } catch {} + removeExitHandler() + // Explicitly exit to prevent hanging + process.exit(process.exitCode || 0) + } +} + +main().catch(error => { + console.error(error) + process.exit(1) +}) diff --git a/packages/lib/scripts/update.mjs b/packages/lib/scripts/update.mjs new file mode 100644 index 000000000..6f303cda2 --- /dev/null +++ b/packages/lib/scripts/update.mjs @@ -0,0 +1,118 @@ +/** + * @fileoverview Monorepo-aware dependency update script - checks and updates dependencies. + * Uses taze to check for updates across all packages in the monorepo. + * + * Usage: + * node scripts/update.mjs [options] + * + * Options: + * --quiet Suppress progress output + * --verbose Show detailed output + * --apply Apply updates (default is check-only) + */ + +import { isQuiet, isVerbose } from '#socketsecurity/lib/argv/flags' +import { getDefaultLogger } from '#socketsecurity/lib/logger' +import { WIN32 } from '#socketsecurity/lib/constants/platform' +import { spawn } from '#socketsecurity/lib/spawn' + +async function main() { + const quiet = isQuiet() + const verbose = isVerbose() + const apply = process.argv.includes('--apply') + const logger = getDefaultLogger() + + try { + if (!quiet) { + logger.log('\n🔨 Monorepo Dependency Update\n') + } + + // Build taze command with appropriate flags for monorepo. + const tazeArgs = ['exec', 'taze', '-r'] + + if (apply) { + tazeArgs.push('-w') + if (!quiet) { + logger.progress('Updating dependencies across monorepo...') + } + } else { + if (!quiet) { + logger.progress('Checking for updates across monorepo...') + } + } + + // Run taze at root level (recursive flag will check all packages). + const result = await spawn('pnpm', tazeArgs, { + shell: WIN32, + stdio: quiet ? 'pipe' : 'inherit', + }) + + // Clear progress line. + if (!quiet) { + process.stdout.write('\r\x1b[K') + } + + // If applying updates, also update Socket packages. + if (apply && result.code === 0) { + if (!quiet) { + logger.progress('Updating Socket packages...') + } + + const socketResult = await spawn( + 'pnpm', + ['update', '@socketsecurity/*', '@socketregistry/*', '--latest', '-r'], + { + shell: WIN32, + stdio: quiet ? 'pipe' : 'inherit', + }, + ) + + // Clear progress line. + if (!quiet) { + process.stdout.write('\r\x1b[K') + } + + if (socketResult.code !== 0) { + if (!quiet) { + logger.fail('Failed to update Socket packages') + } + process.exitCode = 1 + return + } + } + + if (result.code !== 0) { + if (!quiet) { + if (apply) { + logger.fail('Failed to update dependencies') + } else { + logger.info('Updates available. Run with --apply to update') + } + } + process.exitCode = apply ? 1 : 0 + } else { + if (!quiet) { + if (apply) { + logger.success('Dependencies updated across all packages') + } else { + logger.success('All packages up to date') + } + logger.log('') + } + } + } catch (error) { + if (!quiet) { + logger.fail(`Update failed: ${error.message}`) + } + if (verbose) { + logger.error(error) + } + process.exitCode = 1 + } +} + +main().catch(e => { + const logger = getDefaultLogger() + logger.error(e) + process.exitCode = 1 +}) diff --git a/packages/lib/scripts/utils/alias-loader.mjs b/packages/lib/scripts/utils/alias-loader.mjs new file mode 100644 index 000000000..98f8622ec --- /dev/null +++ b/packages/lib/scripts/utils/alias-loader.mjs @@ -0,0 +1,54 @@ +/** + * @fileoverview Canonical Node.js ESM loader to alias local Socket packages. + * Used across all socket-* repositories for consistent local development. + * + * This file should be copied or imported from socket-registry to other repos. + * + * Usage: + * node --import=./scripts/register.mjs script.mjs + */ + +import { existsSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +import { getLocalPackageAliases } from './get-local-package-aliases.mjs' + +// Infer root directory from this loader's location. +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const rootPath = path.resolve(__dirname, '..', '..') + +// Get aliases from shared utility. +const aliases = getLocalPackageAliases(rootPath) + +export function resolve(specifier, context, nextResolve) { + // Check if specifier starts with an aliased package. + for (const [pkg, localPath] of Object.entries(aliases)) { + if (specifier === pkg || specifier.startsWith(`${pkg}/`)) { + // Replace package name with local path. + const subpath = specifier === pkg ? '' : specifier.slice(pkg.length) + + // Try multiple resolution strategies. + const candidates = [ + path.join(localPath, subpath), + path.join(localPath, `${subpath}.mjs`), + path.join(localPath, `${subpath}.js`), + path.join(localPath, 'dist', subpath), + path.join(localPath, 'dist', `${subpath}.mjs`), + path.join(localPath, 'dist', `${subpath}.js`), + ] + + for (const candidate of candidates) { + if (existsSync(candidate)) { + return nextResolve(pathToFileURL(candidate).href, context) + } + } + + // If nothing found, try the first candidate anyway. + return nextResolve(pathToFileURL(candidates[0]).href, context) + } + } + + // Pass through to default resolver. + return nextResolve(specifier, context) +} diff --git a/packages/lib/scripts/utils/changed-test-mapper.mjs b/packages/lib/scripts/utils/changed-test-mapper.mjs new file mode 100644 index 000000000..ba89f90fd --- /dev/null +++ b/packages/lib/scripts/utils/changed-test-mapper.mjs @@ -0,0 +1,176 @@ +/** + * @fileoverview Maps changed source files to test files for affected test running. + * Uses git utilities from socket-registry to detect changes. + */ + +import { existsSync } from 'node:fs' +import path from 'node:path' + +import { + getChangedFilesSync, + getStagedFilesSync, +} from '#socketsecurity/lib/git' +import { normalizePath } from '#socketsecurity/lib/path' + +const rootPath = path.resolve(process.cwd()) + +/** + * Core files that require running all tests when changed. + */ +const CORE_FILES = [ + 'src/helpers.ts', + 'src/strings.ts', + 'src/constants.ts', + 'src/lang.ts', + 'src/error.ts', + 'src/validate.ts', + 'src/normalize.ts', + 'src/encode.ts', + 'src/decode.ts', + 'src/objects.ts', +] + +/** + * Map source files to their corresponding test files. + * @param {string} filepath - Path to source file + * @returns {string[]} Array of test file paths + */ +function mapSourceToTests(filepath) { + const normalized = normalizePath(filepath) + + // Skip non-code files + const ext = path.extname(normalized) + const codeExtensions = ['.js', '.mjs', '.cjs', '.ts', '.cts', '.mts', '.json'] + if (!codeExtensions.includes(ext)) { + return [] + } + + // Core utilities affect all tests + if (CORE_FILES.some(f => normalized.includes(f))) { + return ['all'] + } + + // Map specific files to their test files + const basename = path.basename(normalized, path.extname(normalized)) + const testFile = `test/${basename}.test.ts` + + // Check if corresponding test exists + if (existsSync(path.join(rootPath, testFile))) { + return [testFile] + } + + // Special mappings + if (normalized.includes('src/package-url.ts')) { + return ['test/package-url.test.ts', 'test/integration.test.ts'] + } + if (normalized.includes('src/package-url-builder.ts')) { + return ['test/package-url-builder.test.ts', 'test/integration.test.ts'] + } + if (normalized.includes('src/url-converter.ts')) { + return ['test/url-converter.test.ts'] + } + if (normalized.includes('src/result.ts')) { + return ['test/result.test.ts'] + } + + // If no specific mapping, run all tests to be safe + return ['all'] +} + +/** + * Get affected test files to run based on changed files. + * @param {Object} options + * @param {boolean} options.staged - Use staged files instead of all changes + * @param {boolean} options.all - Run all tests + * @returns {{tests: string[] | 'all' | null, reason?: string, mode?: string}} Object with test patterns, reason, and mode + */ +export function getTestsToRun(options = {}) { + const { all = false, staged = false } = options + + // All mode runs all tests + if (all || process.env.FORCE_TEST === '1') { + return { tests: 'all', reason: 'explicit --all flag', mode: 'all' } + } + + // CI always runs all tests + if (process.env.CI === 'true') { + return { tests: 'all', reason: 'CI environment', mode: 'all' } + } + + // Get changed files + const changedFiles = staged ? getStagedFilesSync() : getChangedFilesSync() + const mode = staged ? 'staged' : 'changed' + + if (changedFiles.length === 0) { + // No changes, skip tests + return { tests: null, mode } + } + + const testFiles = new Set() + let runAllTests = false + let runAllReason = '' + + for (const file of changedFiles) { + const normalized = normalizePath(file) + + // Test files always run themselves + if (normalized.startsWith('test/') && normalized.includes('.test.')) { + // Skip deleted files. + if (existsSync(path.join(rootPath, file))) { + testFiles.add(file) + } + continue + } + + // Source files map to test files + if (normalized.startsWith('src/')) { + const tests = mapSourceToTests(normalized) + if (tests.includes('all')) { + runAllTests = true + runAllReason = 'core file changes' + break + } + for (const test of tests) { + // Skip deleted files. + if (existsSync(path.join(rootPath, test))) { + testFiles.add(test) + } + } + continue + } + + // Config changes run all tests + if (normalized.includes('vitest.config')) { + runAllTests = true + runAllReason = 'vitest config changed' + break + } + + if (normalized.includes('tsconfig')) { + runAllTests = true + runAllReason = 'TypeScript config changed' + break + } + + // Data changes run integration tests + if (normalized.startsWith('data/')) { + // Skip deleted files. + if (existsSync(path.join(rootPath, 'test/integration.test.ts'))) { + testFiles.add('test/integration.test.ts') + } + if (existsSync(path.join(rootPath, 'test/purl-types.test.ts'))) { + testFiles.add('test/purl-types.test.ts') + } + } + } + + if (runAllTests) { + return { tests: 'all', reason: runAllReason, mode: 'all' } + } + + if (testFiles.size === 0) { + return { tests: null, mode } + } + + return { tests: Array.from(testFiles), mode } +} diff --git a/packages/lib/scripts/utils/get-local-package-aliases.mjs b/packages/lib/scripts/utils/get-local-package-aliases.mjs new file mode 100644 index 000000000..580952560 --- /dev/null +++ b/packages/lib/scripts/utils/get-local-package-aliases.mjs @@ -0,0 +1,60 @@ +/** + * @fileoverview Canonical helper for resolving local Socket package aliases. + * Used across all socket-* repositories for consistent local development. + * + * This file should be copied or imported from socket-registry to other repos. + */ + +import { existsSync } from 'node:fs' +import path from 'node:path' + +/** + * Get aliases for local Socket packages when available. + * Falls back to published versions in CI or when packages don't exist. + * + * @param {string} [rootDir] - The root directory of the current project. Defaults to inferring from caller location. + * @returns {Record} Package aliases mapping (to dist folders for build tools). + */ +export function getLocalPackageAliases(rootDir) { + const aliases = {} + + // If no rootDir provided, try to infer from stack trace or use process.cwd(). + const baseDir = rootDir || process.cwd() + + // Check for ../socket-lib/dist for @socketsecurity/lib. + const libPath = path.join(baseDir, '..', 'socket-lib', 'dist') + if (existsSync(path.join(libPath, '../package.json'))) { + aliases['@socketsecurity/lib'] = libPath + } + + // Check for ../socket-packageurl-js/dist. + const packageurlPath = path.join( + baseDir, + '..', + 'socket-packageurl-js', + 'dist', + ) + if (existsSync(path.join(packageurlPath, '../package.json'))) { + aliases['@socketregistry/packageurl-js'] = packageurlPath + } + + // Check for ../socket-registry/registry/dist for @socketsecurity/registry. + const registryPath = path.join( + baseDir, + '..', + 'socket-registry', + 'registry', + 'dist', + ) + if (existsSync(path.join(registryPath, '../package.json'))) { + aliases['@socketsecurity/registry'] = registryPath + } + + // Check for ../socket-sdk-js/dist. + const sdkPath = path.join(baseDir, '..', 'socket-sdk-js', 'dist') + if (existsSync(path.join(sdkPath, '../package.json'))) { + aliases['@socketsecurity/sdk'] = sdkPath + } + + return aliases +} diff --git a/packages/lib/scripts/utils/interactive-runner.mjs b/packages/lib/scripts/utils/interactive-runner.mjs new file mode 100644 index 000000000..b1f539572 --- /dev/null +++ b/packages/lib/scripts/utils/interactive-runner.mjs @@ -0,0 +1,70 @@ +/** + * @fileoverview Interactive runner for commands with ctrl+o toggle. + * Standardized across all socket-* repositories. + */ + +import { runWithMask } from '#socketsecurity/lib/stdio/mask' + +/** + * Run a command with interactive output control. + * Standard experience across all socket-* repos. + * + * @param {string} command - Command to run + * @param {string[]} args - Command arguments + * @param {object} options - Options + * @param {string} options.message - Progress message + * @param {string} options.toggleText - Text after "ctrl+o" (default: "to see output") + * @param {boolean} options.showOnError - Show output on error (default: true) + * @param {boolean} options.verbose - Start in verbose mode (default: false) + * @returns {Promise} Exit code + */ +export async function runWithOutput(command, args = [], options = {}) { + const { + cwd = process.cwd(), + env = process.env, + message = 'Running', + toggleText = 'to see output', + verbose = false, + } = options + + return runWithMask(command, args, { + cwd, + env, + message, + showOutput: verbose, + toggleText, + }) +} + +/** + * Standard test runner with interactive output. + */ +export async function runTests(command, args, options = {}) { + return runWithOutput(command, args, { + message: 'Running tests', + toggleText: 'to see test output', + ...options, + }) +} + +/** + * Standard lint runner with interactive output. + */ +export async function runLint(command, args, options = {}) { + return runWithOutput(command, args, { + message: 'Running linter', + toggleText: 'to see lint results', + ...options, + }) +} + +/** + * Standard build runner with interactive output. + */ +export async function runBuild(command, args, options = {}) { + return runWithOutput(command, args, { + message: 'Building', + toggleText: 'to see build output', + ...options, + }) +} diff --git a/packages/lib/scripts/utils/parse-args.mjs b/packages/lib/scripts/utils/parse-args.mjs new file mode 100644 index 000000000..cb4084923 --- /dev/null +++ b/packages/lib/scripts/utils/parse-args.mjs @@ -0,0 +1,84 @@ +/** + * @fileoverview Simplified argument parsing for build scripts. + * Uses Node.js built-in util.parseArgs (available in Node 22+). + * + * This is intentionally separate from src/argv/parse.ts to avoid circular + * dependencies where build scripts depend on the built dist output. + */ + +import { parseArgs as nodeParseArgs } from 'node:util' + +/** + * Parse command-line arguments using Node.js built-in parseArgs. + * Simplified version for build scripts that don't need yargs-parser features. + * + * @param {object} config - Parse configuration + * @param {string[]} [config.args] - Arguments to parse (defaults to process.argv.slice(2)) + * @param {object} [config.options] - Options configuration + * @param {boolean} [config.strict] - Whether to throw on unknown options (default: false) + * @param {boolean} [config.allowPositionals] - Whether to allow positionals (default: true) + * @returns {{ values: object, positionals: string[] }} + */ +export function parseArgs(config = {}) { + const { + allowPositionals = true, + args = process.argv.slice(2), + options = {}, + strict = false, + } = config + + try { + const result = nodeParseArgs({ + args, + options, + strict, + allowPositionals, + }) + + return { + values: result.values, + positionals: result.positionals || [], + } + } catch (error) { + // If parsing fails in non-strict mode, return empty values + if (!strict) { + return { + values: {}, + positionals: args.filter(arg => !arg.startsWith('-')), + } + } + throw error + } +} + +/** + * Extract positional arguments from process.argv. + * + * @param {number} [startIndex=2] - Index to start from + * @returns {string[]} + */ +export function getPositionalArgs(startIndex = 2) { + const args = process.argv.slice(startIndex) + const positionals = [] + + for (const arg of args) { + // Stop at first flag + if (arg.startsWith('-')) { + break + } + positionals.push(arg) + } + + return positionals +} + +/** + * Check if a specific flag is present in argv. + * + * @param {string} flag - Flag name (without dashes) + * @param {string[]} [argv=process.argv] - Arguments array + * @returns {boolean} + */ +export function hasFlag(flag, argv = process.argv) { + return argv.includes(`--${flag}`) || argv.includes(`-${flag.charAt(0)}`) +} diff --git a/packages/lib/scripts/utils/run-command.mjs b/packages/lib/scripts/utils/run-command.mjs new file mode 100644 index 000000000..1a95592c5 --- /dev/null +++ b/packages/lib/scripts/utils/run-command.mjs @@ -0,0 +1,141 @@ +/** @fileoverview Utility for running shell commands with proper error handling. */ + +import { getDefaultLogger } from '#socketsecurity/lib/logger' +import { spawn, spawnSync } from '#socketsecurity/lib/spawn' + +const logger = getDefaultLogger() + +/** + * Run a command and return a promise that resolves with the exit code. + * @param {string} command - The command to run + * @param {string[]} args - Arguments to pass to the command + * @param {object} options - Spawn options + * @returns {Promise} Exit code + */ +export async function runCommand(command, args = [], options = {}) { + try { + const result = await spawn(command, args, { + stdio: 'inherit', + ...(process.platform === 'win32' && { shell: true }), + ...options, + }) + return result.code + } catch (error) { + // spawn() from @socketsecurity/lib throws on non-zero exit + // Return the exit code from the error + if (error && typeof error === 'object' && 'code' in error) { + return error.code + } + throw error + } +} + +/** + * Run a command synchronously. + * @param {string} command - The command to run + * @param {string[]} args - Arguments to pass to the command + * @param {object} options - Spawn options + * @returns {number} Exit code + */ +export function runCommandSync(command, args = [], options = {}) { + const result = spawnSync(command, args, { + stdio: 'inherit', + ...(process.platform === 'win32' && { shell: true }), + ...options, + }) + + return result.status || 0 +} + +/** + * Run a pnpm script. + * @param {string} scriptName - The pnpm script to run + * @param {string[]} extraArgs - Additional arguments + * @param {object} options - Spawn options + * @returns {Promise} Exit code + */ +export async function runPnpmScript(scriptName, extraArgs = [], options = {}) { + return runCommand('pnpm', ['run', scriptName, ...extraArgs], options) +} + +/** + * Run multiple commands in sequence, stopping on first failure. + * @param {Array<{command: string, args?: string[], options?: object}>} commands + * @returns {Promise} Exit code of first failing command, or 0 if all succeed + */ +export async function runSequence(commands) { + for (const { args = [], command, options = {} } of commands) { + const exitCode = await runCommand(command, args, options) + if (exitCode !== 0) { + return exitCode + } + } + return 0 +} + +/** + * Run multiple commands in parallel. + * @param {Array<{command: string, args?: string[], options?: object}>} commands + * @returns {Promise} Array of exit codes + */ +export async function runParallel(commands) { + const promises = commands.map(({ args = [], command, options = {} }) => + runCommand(command, args, options), + ) + const results = await Promise.allSettled(promises) + return results.map(r => (r.status === 'fulfilled' ? r.value : 1)) +} + +/** + * Run a command and suppress output. + * @param {string} command - The command to run + * @param {string[]} args - Arguments to pass to the command + * @param {object} options - Spawn options + * @returns {Promise<{exitCode: number, stdout: string, stderr: string}>} + */ +export async function runCommandQuiet(command, args = [], options = {}) { + try { + const result = await spawn(command, args, { + ...options, + ...(process.platform === 'win32' && { shell: true }), + stdio: 'pipe', + stdioString: true, + }) + + return { + exitCode: result.code, + stderr: result.stderr, + stdout: result.stdout, + } + } catch (error) { + // spawn() from @socketsecurity/lib throws on non-zero exit + // Return the exit code and output from the error + if ( + error && + typeof error === 'object' && + 'code' in error && + 'stdout' in error && + 'stderr' in error + ) { + return { + exitCode: error.code, + stderr: error.stderr, + stdout: error.stdout, + } + } + throw error + } +} + +/** + * Log and run a command. + * @param {string} description - Description of what the command does + * @param {string} command - The command to run + * @param {string[]} args - Arguments + * @param {object} options - Spawn options + * @returns {Promise} Exit code + */ +export async function logAndRun(description, command, args = [], options = {}) { + logger.log(description) + return runCommand(command, args, options) +} diff --git a/packages/lib/scripts/utils/signal-exit.mjs b/packages/lib/scripts/utils/signal-exit.mjs new file mode 100644 index 000000000..96447d160 --- /dev/null +++ b/packages/lib/scripts/utils/signal-exit.mjs @@ -0,0 +1,38 @@ +/** + * @fileoverview Simplified signal exit handler for build scripts. + * + * This is intentionally separate from src/lib/signal-exit.ts to avoid circular + * dependencies where build scripts depend on the built dist output. + */ + +/** + * Register a callback to run when process exits + * + * @param {(code: number, signal: string | null) => void} callback + * @returns {() => void} Cleanup function + */ +export function onExit(callback) { + const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'] + + const handler = signal => { + callback(process.exitCode || 0, signal) + } + + const exitHandler = () => { + callback(process.exitCode || 0, null) + } + + signals.forEach(signal => { + process.on(signal, handler) + }) + + process.on('exit', exitHandler) + + // Return cleanup function + return () => { + signals.forEach(signal => { + process.off(signal, handler) + }) + process.off('exit', exitHandler) + } +} diff --git a/packages/lib/scripts/validate-esbuild-minify.mjs b/packages/lib/scripts/validate-esbuild-minify.mjs new file mode 100644 index 000000000..d2b06c65a --- /dev/null +++ b/packages/lib/scripts/validate-esbuild-minify.mjs @@ -0,0 +1,86 @@ +/** + * @fileoverview Validates that esbuild configuration has minify: false. + * Minification breaks ESM/CJS interop and makes debugging harder. + */ + +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const rootPath = path.join(__dirname, '..') + +/** + * Validate esbuild configuration has minify: false. + */ +async function validateEsbuildMinify() { + const configPath = path.join(rootPath, '.config/esbuild.config.mjs') + + try { + // Dynamic import of the esbuild config + const config = await import(configPath) + + const violations = [] + + // Check buildConfig + if (config.buildConfig) { + if (config.buildConfig.minify !== false) { + violations.push({ + config: 'buildConfig', + value: config.buildConfig.minify, + message: 'buildConfig.minify must be false', + location: `${configPath}:242`, + }) + } + } + + // Check watchConfig + if (config.watchConfig) { + if (config.watchConfig.minify !== false) { + violations.push({ + config: 'watchConfig', + value: config.watchConfig.minify, + message: 'watchConfig.minify must be false', + location: `${configPath}:270`, + }) + } + } + + return violations + } catch (error) { + console.error(`Failed to load esbuild config: ${error.message}`) + process.exitCode = 1 + return [] + } +} + +async function main() { + const violations = await validateEsbuildMinify() + + if (violations.length === 0) { + console.log('✓ esbuild minify validation passed') + process.exitCode = 0 + return + } + + console.error('❌ esbuild minify validation failed\n') + + for (const violation of violations) { + console.error(` ${violation.message}`) + console.error(` Found: minify: ${violation.value}`) + console.error(' Expected: minify: false') + console.error(` Location: ${violation.location}`) + console.error('') + } + + console.error( + 'Minification breaks ESM/CJS interop and makes debugging harder.', + ) + console.error('') + + process.exitCode = 1 +} + +main().catch(error => { + console.error('Validation failed:', error) + process.exitCode = 1 +}) diff --git a/packages/lib/scripts/validate-file-count.mjs b/packages/lib/scripts/validate-file-count.mjs new file mode 100644 index 000000000..4d41d4c16 --- /dev/null +++ b/packages/lib/scripts/validate-file-count.mjs @@ -0,0 +1,113 @@ +/** + * @fileoverview Validates that commits don't contain too many files. + * + * Rules: + * - No single commit should contain 50+ files + * - Helps catch accidentally staging too many files or generated content + * - 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' + +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 + +/** + * Check if too many files are staged for commit. + */ +async function validateStagedFileCount() { + try { + // Check if we're in a git repository + const { stdout: gitRoot } = await execAsync( + 'git rev-parse --show-toplevel', + { + cwd: rootPath, + }, + ) + + // Not a git repository + if (!gitRoot.trim()) { + return null + } + + // Get list of staged files + const { stdout } = await execAsync('git diff --cached --name-only', { + cwd: rootPath, + }) + + const stagedFiles = stdout + .trim() + .split('\n') + .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 + } catch { + // Not a git repo or git not available + return null + } +} + +async function main() { + try { + const violation = await validateStagedFileCount() + + if (!violation) { + logger.success('Commit size is acceptable') + process.exitCode = 0 + return + } + + logger.error('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) + for (const file of filesToShow) { + logger.log(` ${file}`) + } + + if (violation.files.length > 20) { + logger.log(` ... and ${violation.files.length - 20} more files`) + } + + logger.log('') + logger.log( + 'Split into smaller commits, check for accidentally staged files, or exclude generated files.', + ) + logger.log('') + + process.exitCode = 1 + } catch (error) { + logger.error(`Validation failed: ${error.message}`) + process.exitCode = 1 + } +} + +main().catch(error => { + logger.error(`Validation failed: ${error}`) + process.exitCode = 1 +}) diff --git a/packages/lib/scripts/validate-file-size.mjs b/packages/lib/scripts/validate-file-size.mjs new file mode 100644 index 000000000..7aa1cbf59 --- /dev/null +++ b/packages/lib/scripts/validate-file-size.mjs @@ -0,0 +1,151 @@ +/** + * @fileoverview Validates that no individual files exceed size threshold. + * + * Rules: + * - No single file should exceed 2MB (2,097,152 bytes) + * - Helps prevent accidental commits of large binaries, data files, or artifacts + * - 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' + +const logger = getDefaultLogger() + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const rootPath = path.join(__dirname, '..') + +// Maximum file size: 2MB (2,097,152 bytes) +const MAX_FILE_SIZE = 2 * 1024 * 1024 + +// Directories to skip +const SKIP_DIRS = new Set([ + 'node_modules', + '.git', + 'dist', + 'build', + '.cache', + 'coverage', + '.next', + '.nuxt', + '.output', + '.turbo', + '.vercel', + '.vscode', + 'tmp', +]) + +/** + * 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 / k ** i).toFixed(2)} ${sizes[i]}` +} + +/** + * Recursively scan directory for files exceeding size limit. + */ +async function scanDirectory(dir, violations = []) { + try { + const entries = await fs.readdir(dir, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + + if (entry.isDirectory()) { + // Skip excluded directories and hidden directories (except .claude, .config, .github) + if ( + !SKIP_DIRS.has(entry.name) && + (!entry.name.startsWith('.') || + entry.name === '.claude' || + entry.name === '.config' || + entry.name === '.github') + ) { + await scanDirectory(fullPath, violations) + } + } else if (entry.isFile()) { + try { + const stats = await fs.stat(fullPath) + if (stats.size > MAX_FILE_SIZE) { + 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 + } + } + } + } catch { + // Skip directories we can't read + } + + return violations +} + +/** + * Validate file sizes in repository. + */ +async function validateFileSizes() { + const violations = await scanDirectory(rootPath) + + // Sort by size descending (largest first) + violations.sort((a, b) => b.size - a.size) + + return violations +} + +async function main() { + try { + const violations = await validateFileSizes() + + if (violations.length === 0) { + logger.success('All files are within size limits') + process.exitCode = 0 + return + } + + logger.error('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( + 'Reduce file sizes, move large files to external storage, or exclude from repository.', + ) + logger.log('') + + process.exitCode = 1 + } catch (error) { + logger.error(`Validation failed: ${error.message}`) + process.exitCode = 1 + } +} + +main().catch(error => { + logger.error(`Validation failed: ${error}`) + process.exitCode = 1 +}) diff --git a/packages/lib/scripts/validate-markdown-filenames.mjs b/packages/lib/scripts/validate-markdown-filenames.mjs new file mode 100644 index 000000000..2b808333c --- /dev/null +++ b/packages/lib/scripts/validate-markdown-filenames.mjs @@ -0,0 +1,304 @@ +/** + * @fileoverview Validates that markdown files follow naming conventions. + * + * Special files (allowed anywhere): + * - README.md, LICENSE + * + * Allowed SCREAMING_CASE (all caps) files (root, docs/, or .claude/ only): + * - AUTHORS.md, CHANGELOG.md, CITATION.md, CLAUDE.md + * - CODE_OF_CONDUCT.md, CONTRIBUTORS.md, CONTRIBUTING.md + * - COPYING, CREDITS.md, GOVERNANCE.md, MAINTAINERS.md + * - NOTICE.md, SECURITY.md, SUPPORT.md, TRADEMARK.md + * + * All other .md files must: + * - Be lowercase-with-hyphens + * - Be located within docs/ or .claude/ directories (any depth) + * - 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' + +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([ + 'AUTHORS', + 'CHANGELOG', + 'CITATION', + 'CLAUDE', + 'CODE_OF_CONDUCT', + 'CONTRIBUTORS', + 'CONTRIBUTING', + 'COPYING', + 'CREDITS', + 'GOVERNANCE', + 'LICENSE', + 'MAINTAINERS', + 'NOTICE', + 'README', + 'SECURITY', + 'SUPPORT', + 'TRADEMARK', +]) + +// Directories to skip +const SKIP_DIRS = new Set([ + 'node_modules', + '.git', + 'dist', + 'build', + '.cache', + 'coverage', + '.next', + '.nuxt', + '.output', +]) + +/** + * Check if a filename is in SCREAMING_CASE (all uppercase with optional underscores). + */ +function isScreamingCase(filename) { + // Remove extension for checking + const nameWithoutExt = filename.replace(/\.(md|MD)$/, '') + + // Check if it contains any lowercase letters + return /^[A-Z0-9_]+$/.test(nameWithoutExt) && /[A-Z]/.test(nameWithoutExt) +} + +/** + * Check if a filename is lowercase-with-hyphens. + */ +function isLowercaseHyphenated(filename) { + // Remove extension for checking + const nameWithoutExt = filename.replace(/\.md$/, '') + + // Must be lowercase letters, numbers, and hyphens only + return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(nameWithoutExt) +} + +/** + * Recursively find all markdown files. + */ +async function findMarkdownFiles(dir, files = []) { + try { + const entries = await fs.readdir(dir, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + + if (entry.isDirectory()) { + if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) { + await findMarkdownFiles(fullPath, files) + } + } else if (entry.isFile()) { + // Check for .md files or LICENSE (no extension) + if (entry.name.endsWith('.md') || entry.name === 'LICENSE') { + files.push(fullPath) + } + } + } + } catch { + // Skip directories we can't read + } + + return files +} + +/** + * Check if file is in an allowed location for SCREAMING_CASE files. + * SCREAMING_CASE files can only be at: root, docs/, or .claude/ (top level only). + */ +function isInAllowedLocationForScreamingCase(filePath) { + const relativePath = path.relative(rootPath, filePath) + const dir = path.dirname(relativePath) + + // Allow at root level + if (dir === '.') { + return true + } + + // Allow in docs/ folder (but not subdirectories) + if (dir === 'docs') { + return true + } + + // Allow in .claude/ folder (but not subdirectories) + if (dir === '.claude') { + return true + } + + return false +} + +/** + * Check if file is in an allowed location for regular markdown files. + * Regular .md files must be within docs/ or .claude/ directories. + */ +function isInAllowedLocationForRegularMd(filePath) { + const relativePath = path.relative(rootPath, filePath) + const dir = path.dirname(relativePath) + + // Must be within docs/ (any depth) + if (dir === 'docs' || dir.startsWith('docs/')) { + return true + } + + // Must be within .claude/ (any depth) + if (dir === '.claude' || dir.startsWith('.claude/')) { + return true + } + + return false +} + +/** + * Validate a markdown filename. + */ +function validateFilename(filePath) { + const filename = path.basename(filePath) + const nameWithoutExt = filename.replace(/\.(md|MD)$/, '') + const relativePath = path.relative(rootPath, filePath) + + // README.md and LICENSE are special - allowed anywhere + // Valid - allowed in any location + if (nameWithoutExt === 'README' || nameWithoutExt === 'LICENSE') { + return null + } + + // Check if it's an allowed SCREAMING_CASE file + if (ALLOWED_SCREAMING_CASE.has(nameWithoutExt)) { + // Must be in an allowed location (root, docs/, or .claude/) + if (!isInAllowedLocationForScreamingCase(filePath)) { + return { + file: relativePath, + filename, + issue: 'SCREAMING_CASE files only allowed at root, docs/, or .claude/', + suggestion: `Move to root, docs/, or .claude/, or rename to ${filename.toLowerCase().replace(/_/g, '-')}`, + } + } + // Valid + return null + } + + // Check if it's in SCREAMING_CASE but not allowed + if (isScreamingCase(filename)) { + return { + file: relativePath, + filename, + issue: 'SCREAMING_CASE not allowed', + suggestion: filename.toLowerCase().replace(/_/g, '-'), + } + } + + // Check if it has .MD extension (should be .md) + if (filename.endsWith('.MD')) { + return { + file: path.relative(rootPath, filePath), + filename, + issue: 'Extension should be lowercase .md', + suggestion: filename.replace(/\.MD$/, '.md'), + } + } + + // Check if it's properly lowercase-hyphenated + if (!isLowercaseHyphenated(filename)) { + // Try to suggest a corrected version + const nameOnly = filename.replace(/\.md$/, '') + const suggested = nameOnly + .toLowerCase() + .replace(/[_\s]+/g, '-') + .replace(/[^a-z0-9-]/g, '') + + return { + file: relativePath, + filename, + issue: 'Must be lowercase-with-hyphens', + suggestion: `${suggested}.md`, + } + } + + // Regular markdown files must be in docs/ or .claude/ + if (!isInAllowedLocationForRegularMd(filePath)) { + return { + file: relativePath, + filename, + issue: 'Markdown files must be in docs/ or .claude/ directories', + suggestion: `Move to docs/${filename} or .claude/${filename}`, + } + } + + // Valid + return null +} + +/** + * Validate all markdown filenames. + */ +async function validateMarkdownFilenames() { + const files = await findMarkdownFiles(rootPath) + const violations = [] + + for (const file of files) { + const violation = validateFilename(file) + if (violation) { + violations.push(violation) + } + } + + return violations +} + +async function main() { + try { + const violations = await validateMarkdownFilenames() + + if (violations.length === 0) { + logger.success('All markdown filenames follow conventions') + process.exitCode = 0 + return + } + + logger.error('Markdown filename violations found') + logger.log('') + logger.log('Special files (allowed anywhere):') + logger.log(' README.md, LICENSE') + logger.log('') + logger.log('Allowed SCREAMING_CASE files (root, docs/, or .claude/ only):') + logger.log(' AUTHORS.md, CHANGELOG.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('Rename files to follow conventions.') + logger.log('') + + process.exitCode = 1 + } catch (error) { + logger.error(`Validation failed: ${error.message}`) + process.exitCode = 1 + } +} + +main().catch(error => { + logger.error(`Validation failed: ${error}`) + process.exitCode = 1 +}) diff --git a/packages/lib/scripts/validate-no-cdn-refs.mjs b/packages/lib/scripts/validate-no-cdn-refs.mjs new file mode 100644 index 000000000..86a320edc --- /dev/null +++ b/packages/lib/scripts/validate-no-cdn-refs.mjs @@ -0,0 +1,213 @@ +/** + * @fileoverview Validates that there are no CDN references in the codebase. + * + * This is a preventative check to ensure no hardcoded CDN URLs are introduced. + * The project deliberately avoids CDN dependencies for security and reliability. + * + * Blocked CDN domains: + * - unpkg.com + * - cdn.jsdelivr.net + * - esm.sh + * - cdn.skypack.dev + * - ga.jspm.io + */ + +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() + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const rootPath = path.join(__dirname, '..') + +// CDN domains to block +const CDN_PATTERNS = [ + /unpkg\.com/i, + /cdn\.jsdelivr\.net/i, + /esm\.sh/i, + /cdn\.skypack\.dev/i, + /ga\.jspm\.io/i, +] + +// Directories to skip +const SKIP_DIRS = new Set([ + 'node_modules', + '.git', + 'dist', + 'build', + '.cache', + 'coverage', + '.next', + '.nuxt', + '.output', + '.turbo', + '.type-coverage', + '.yarn', +]) + +// File extensions to check +const TEXT_EXTENSIONS = new Set([ + '.js', + '.mjs', + '.cjs', + '.ts', + '.mts', + '.cts', + '.jsx', + '.tsx', + '.json', + '.md', + '.html', + '.htm', + '.css', + '.yml', + '.yaml', + '.xml', + '.svg', + '.txt', + '.sh', + '.bash', +]) + +/** + * Check if file should be scanned. + */ +function shouldScanFile(filename) { + const ext = path.extname(filename).toLowerCase() + return TEXT_EXTENSIONS.has(ext) +} + +/** + * Recursively find all text files to scan. + */ +async function findTextFiles(dir, files = []) { + try { + const entries = await fs.readdir(dir, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + + if (entry.isDirectory()) { + // Skip certain directories and hidden directories (except .github) + if ( + !SKIP_DIRS.has(entry.name) && + (!entry.name.startsWith('.') || entry.name === '.github') + ) { + await findTextFiles(fullPath, files) + } + } else if (entry.isFile() && shouldScanFile(entry.name)) { + files.push(fullPath) + } + } + } catch { + // Skip directories we can't read + } + + return files +} + +/** + * Check file contents for CDN references. + */ +async function checkFileForCdnRefs(filePath) { + // Skip this validator script itself (it mentions CDN domains by necessity) + if (filePath.endsWith('validate-no-cdn-refs.mjs')) { + return [] + } + + try { + const content = await fs.readFile(filePath, 'utf8') + const lines = content.split('\n') + const violations = [] + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + const lineNumber = i + 1 + + for (const pattern of CDN_PATTERNS) { + if (pattern.test(line)) { + const match = line.match(pattern) + violations.push({ + file: path.relative(rootPath, filePath), + line: lineNumber, + content: line.trim(), + cdnDomain: match[0], + }) + } + } + } + + return violations + } catch (error) { + // Skip files we can't read (likely binary despite extension) + if (error.code === 'EISDIR' || error.message.includes('ENOENT')) { + return [] + } + // For other errors, try to continue + return [] + } +} + +/** + * Validate all files for CDN references. + */ +async function validateNoCdnRefs() { + const files = await findTextFiles(rootPath) + const allViolations = [] + + for (const file of files) { + const violations = await checkFileForCdnRefs(file) + allViolations.push(...violations) + } + + return allViolations +} + +async function main() { + try { + const violations = await validateNoCdnRefs() + + if (violations.length === 0) { + logger.success('No CDN references found') + process.exitCode = 0 + return + } + + logger.fail(`Found ${violations.length} CDN reference(s)`) + logger.log('') + logger.log('CDN URLs are not allowed in this codebase for security and') + logger.log('reliability reasons. Please use npm packages instead.') + logger.log('') + logger.log('Blocked CDN domains:') + logger.log(' - unpkg.com') + logger.log(' - cdn.jsdelivr.net') + logger.log(' - esm.sh') + logger.log(' - cdn.skypack.dev') + logger.log(' - ga.jspm.io') + logger.log('') + logger.log('Violations:') + logger.log('') + + for (const violation of violations) { + logger.log(` ${violation.file}:${violation.line}`) + logger.log(` Domain: ${violation.cdnDomain}`) + logger.log(` Content: ${violation.content}`) + logger.log('') + } + + logger.log('Remove CDN references and use npm dependencies instead.') + logger.log('') + + process.exitCode = 1 + } catch (error) { + logger.fail(`Validation failed: ${error.message}`) + process.exitCode = 1 + } +} + +main().catch(error => { + logger.fail(`Unexpected error: ${error.message}`) + process.exitCode = 1 +}) diff --git a/packages/lib/scripts/validate-no-extraneous-dependencies.mjs b/packages/lib/scripts/validate-no-extraneous-dependencies.mjs new file mode 100644 index 000000000..44196cf69 --- /dev/null +++ b/packages/lib/scripts/validate-no-extraneous-dependencies.mjs @@ -0,0 +1,325 @@ +/** + * @fileoverview Validates that all require() calls in dist/ resolve to valid dependencies or files. + * + * Uses @babel/parser to accurately detect require() specifiers and validates: + * - Bare specifiers (package names) must be Node.js built-ins or in dependencies/peerDependencies + * - Relative specifiers (./file or ../file) must point to existing files + * + * Rules: + * - External packages (require() calls in dist/) must be in dependencies or peerDependencies + * - Bundled packages should NOT appear as require() calls (code is bundled/inlined) + * - devDependencies should NOT be required from dist/ (not installed by consumers) + * - Relative imports must resolve to existing files in dist/ + * + * This ensures consumers can run the published package. + */ + +import { existsSync, promises as fs } from 'node:fs' +import { builtinModules } from 'node:module' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import { parse } from '@babel/parser' +import traverseModule from '@babel/traverse' +import * as t from '@babel/types' + +const traverse = traverseModule.default + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const rootPath = path.join(__dirname, '..') + +// Node.js builtins to recognize (including node: prefix variants) +const BUILTIN_MODULES = new Set([ + ...builtinModules, + ...builtinModules.map(m => `node:${m}`), +]) + +/** + * Parse JavaScript code into AST + */ +function parseCode(code, filePath) { + try { + return parse(code, { + allowImportExportEverywhere: true, + allowReturnOutsideFunction: true, + sourceType: 'unambiguous', + }) + } catch (error) { + throw new Error(`Failed to parse ${filePath}: ${error.message}`) + } +} + +/** + * Extract all require() specifiers from a file using Babel AST + */ +async function extractRequireSpecifiers(filePath) { + const content = await fs.readFile(filePath, 'utf8') + const ast = parseCode(content, filePath) + const specifiers = [] + + traverse(ast, { + CallExpression(astPath) { + const { node } = astPath + + // Check if this is a require() call + if ( + t.isIdentifier(node.callee, { name: 'require' }) && + node.arguments.length > 0 && + t.isStringLiteral(node.arguments[0]) + ) { + const specifier = node.arguments[0].value + const { column, line } = node.loc.start + specifiers.push({ + specifier, + line, + column, + }) + } + }, + }) + + return specifiers +} + +/** + * Check if a specifier is a bare specifier (package name, not relative path) + */ +function isBareSpecifier(specifier) { + return !specifier.startsWith('.') && !specifier.startsWith('/') +} + +/** + * Get package name from a bare specifier (strip subpaths) + */ +function getPackageName(specifier) { + // Scoped package: @scope/package or @scope/package/subpath + if (specifier.startsWith('@')) { + const parts = specifier.split('/') + if (parts.length >= 2) { + return `${parts[0]}/${parts[1]}` + } + return specifier + } + + // Regular package: package or package/subpath + const parts = specifier.split('/') + return parts[0] +} + +/** + * Check if a relative require path resolves to an existing file + */ +function checkFileExists(specifier, fromFile) { + const fromDir = path.dirname(fromFile) + const extensions = ['', '.js', '.mjs', '.cjs', '.json', '.node'] + + // Try with different extensions + for (const ext of extensions) { + const fullPath = path.resolve(fromDir, specifier + ext) + if (existsSync(fullPath)) { + return { exists: true, resolvedPath: fullPath } + } + } + + // Try as directory with index file + const dirPath = path.resolve(fromDir, specifier) + for (const indexFile of [ + 'index.js', + 'index.mjs', + 'index.cjs', + 'index.json', + ]) { + const indexPath = path.join(dirPath, indexFile) + if (existsSync(indexPath)) { + return { exists: true, resolvedPath: indexPath } + } + } + + return { exists: false, resolvedPath: null } +} + +/** + * Find all JavaScript files in dist directory recursively + */ +async function findDistFiles(distPath) { + const files = [] + + try { + const entries = await fs.readdir(distPath, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(distPath, entry.name) + + if (entry.isDirectory()) { + // Check ALL directories including dist/external/ + files.push(...(await findDistFiles(fullPath))) + } else if ( + entry.name.endsWith('.js') || + entry.name.endsWith('.mjs') || + entry.name.endsWith('.cjs') + ) { + files.push(fullPath) + } + } + } catch { + // Directory doesn't exist or can't be read + return [] + } + + return files +} + +/** + * Read and parse package.json + */ +async function readPackageJson() { + const packageJsonPath = path.join(rootPath, 'package.json') + const content = await fs.readFile(packageJsonPath, 'utf8') + return JSON.parse(content) +} + +/** + * Validate require() calls in dist/ files + */ +async function validateNoExtraneousDependencies() { + const pkg = await readPackageJson() + + const dependencies = new Set(Object.keys(pkg.dependencies || {})) + const devDependencies = new Set(Object.keys(pkg.devDependencies || {})) + const peerDependencies = new Set(Object.keys(pkg.peerDependencies || {})) + + // Find all JS files in dist/ + const distPath = path.join(rootPath, 'dist') + const allFiles = await findDistFiles(distPath) + + if (allFiles.length === 0) { + console.log('ℹ No dist files found - run build first') + return { errors: [] } + } + + const errors = [] + + for (const file of allFiles) { + try { + const specifiers = await extractRequireSpecifiers(file) + const relativePath = path.relative(rootPath, file) + + for (const { column, line, specifier } of specifiers) { + // Skip subpath imports (# prefixed imports) + if (specifier.startsWith('#')) { + continue + } + + // Skip internal src/external/ wrapper paths (used by socket-lib pattern) + if (specifier.includes('/external/')) { + continue + } + + if (isBareSpecifier(specifier)) { + // Check if it's a Node.js built-in + const packageName = getPackageName(specifier) + + if ( + specifier.startsWith('node:') || + BUILTIN_MODULES.has(specifier) || + BUILTIN_MODULES.has(packageName) + ) { + // Built-in module, all good + continue + } + + // Check if package is in dependencies or peerDependencies + // NOTE: devDependencies are NOT acceptable in dist/ - they don't get installed by consumers + if ( + !dependencies.has(packageName) && + !peerDependencies.has(packageName) + ) { + const inDevDeps = devDependencies.has(packageName) + errors.push({ + file: relativePath, + line, + column, + specifier, + packageName, + type: 'missing-dependency', + message: inDevDeps + ? `Package "${packageName}" is in devDependencies but required in dist/ (should be in dependencies or bundled)` + : `Package "${packageName}" is not declared in dependencies or peerDependencies`, + }) + } + } else { + // Relative or absolute path - check if file exists + const { exists } = checkFileExists(specifier, file) + + if (!exists) { + errors.push({ + file: relativePath, + line, + column, + specifier, + type: 'missing-file', + message: `File "${specifier}" does not exist`, + }) + } + } + } + } catch (error) { + errors.push({ + file: path.relative(rootPath, file), + type: 'parse-error', + message: error.message, + }) + } + } + + return { errors } +} + +async function main() { + try { + const { errors } = await validateNoExtraneousDependencies() + + if (errors.length === 0) { + console.log('✓ No extraneous dependencies found') + process.exitCode = 0 + return + } + + console.error('✗ Found extraneous or missing dependencies:\n') + + for (const error of errors) { + if (error.type === 'missing-dependency') { + console.error( + ` ${error.file}:${error.line}:${error.column} - ${error.message}`, + ) + console.error(` require('${error.specifier}')`) + if ( + error.message.includes('is in devDependencies but required in dist/') + ) { + console.error( + ` Fix: Move "${error.packageName}" to dependencies OR bundle it (add to esbuild external exclusion)\n`, + ) + } else { + console.error( + ` Fix: Add "${error.packageName}" to dependencies or peerDependencies\n`, + ) + } + } else if (error.type === 'missing-file') { + console.error( + ` ${error.file}:${error.line}:${error.column} - ${error.message}`, + ) + console.error(` require('${error.specifier}')`) + console.error(' Fix: Create the missing file or fix the path\n') + } else if (error.type === 'parse-error') { + console.error(` ${error.file} - ${error.message}\n`) + } + } + + process.exitCode = 1 + } catch (error) { + console.error('Validation failed:', error.message) + process.exitCode = 1 + } +} + +main() diff --git a/packages/lib/scripts/validate-no-link-deps.mjs b/packages/lib/scripts/validate-no-link-deps.mjs new file mode 100755 index 000000000..9b883cbf3 --- /dev/null +++ b/packages/lib/scripts/validate-no-link-deps.mjs @@ -0,0 +1,151 @@ +/** + * @fileoverview Validates that no package.json files contain link: dependencies. + * Link dependencies are prohibited - use workspace: or catalog: instead. + */ + +import { promises as fs } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const rootPath = path.join(__dirname, '..') + +/** + * Find all package.json files in the repository. + */ +async function findPackageJsonFiles(dir) { + const files = [] + const entries = await fs.readdir(dir, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + + // Skip node_modules, .git, and build directories. + if ( + entry.name === 'node_modules' || + entry.name === '.git' || + entry.name === 'build' || + entry.name === 'dist' + ) { + continue + } + + if (entry.isDirectory()) { + files.push(...(await findPackageJsonFiles(fullPath))) + } else if (entry.name === 'package.json') { + files.push(fullPath) + } + } + + return files +} + +/** + * Check if a package.json contains link: dependencies. + */ +async function checkPackageJson(filePath) { + const content = await fs.readFile(filePath, 'utf8') + const pkg = JSON.parse(content) + + const violations = [] + + // Check dependencies. + if (pkg.dependencies) { + for (const [name, version] of Object.entries(pkg.dependencies)) { + if (typeof version === 'string' && version.startsWith('link:')) { + violations.push({ + file: filePath, + field: 'dependencies', + package: name, + value: version, + }) + } + } + } + + // Check devDependencies. + if (pkg.devDependencies) { + for (const [name, version] of Object.entries(pkg.devDependencies)) { + if (typeof version === 'string' && version.startsWith('link:')) { + violations.push({ + file: filePath, + field: 'devDependencies', + package: name, + value: version, + }) + } + } + } + + // Check peerDependencies. + if (pkg.peerDependencies) { + for (const [name, version] of Object.entries(pkg.peerDependencies)) { + if (typeof version === 'string' && version.startsWith('link:')) { + violations.push({ + file: filePath, + field: 'peerDependencies', + package: name, + value: version, + }) + } + } + } + + // Check optionalDependencies. + if (pkg.optionalDependencies) { + for (const [name, version] of Object.entries(pkg.optionalDependencies)) { + if (typeof version === 'string' && version.startsWith('link:')) { + violations.push({ + file: filePath, + field: 'optionalDependencies', + package: name, + value: version, + }) + } + } + } + + return violations +} + +async function main() { + const packageJsonFiles = await findPackageJsonFiles(rootPath) + const allViolations = [] + + for (const file of packageJsonFiles) { + const violations = await checkPackageJson(file) + allViolations.push(...violations) + } + + if (allViolations.length > 0) { + console.error('❌ Found link: dependencies (prohibited)') + console.error('') + console.error( + 'Use workspace: protocol for monorepo packages or catalog: for centralized versions.', + ) + console.error('') + + for (const violation of allViolations) { + const relativePath = path.relative(rootPath, violation.file) + console.error(` ${relativePath}`) + console.error( + ` ${violation.field}.${violation.package}: "${violation.value}"`, + ) + } + + console.error('') + console.error('Replace link: with:') + console.error(' - workspace: for monorepo packages') + console.error(' - catalog: for centralized version management') + console.error('') + + process.exitCode = 1 + } else { + console.log('✓ No link: dependencies found') + } +} + +main().catch(error => { + console.error('Validation failed:', error) + process.exitCode = 1 +}) diff --git a/packages/lib/src/abort.ts b/packages/lib/src/abort.ts new file mode 100644 index 000000000..63e7ad192 --- /dev/null +++ b/packages/lib/src/abort.ts @@ -0,0 +1,50 @@ +/** + * @fileoverview Abort signal utilities. + */ + +/** + * Create a composite AbortSignal from multiple signals. + */ +export function createCompositeAbortSignal( + ...signals: Array +): AbortSignal { + const validSignals = signals.filter(s => s != null) as AbortSignal[] + + if (validSignals.length === 0) { + return new AbortController().signal + } + + if (validSignals.length === 1) { + return validSignals[0]! + } + + const controller = new AbortController() + + for (const signal of validSignals) { + if (signal.aborted) { + controller.abort() + return controller.signal + } + signal.addEventListener('abort', () => controller.abort(), { once: true }) + } + + return controller.signal +} + +/** + * Create an AbortSignal that triggers after a timeout. + */ +export function createTimeoutSignal(ms: number): AbortSignal { + if (typeof ms !== 'number' || Number.isNaN(ms)) { + throw new TypeError('timeout must be a number') + } + if (!Number.isFinite(ms)) { + throw new TypeError('timeout must be a finite number') + } + if (ms <= 0) { + throw new TypeError('timeout must be a positive number') + } + const controller = new AbortController() + setTimeout(() => controller.abort(), ms) + return controller.signal +} diff --git a/packages/lib/src/agent.ts b/packages/lib/src/agent.ts new file mode 100644 index 000000000..b82919cea --- /dev/null +++ b/packages/lib/src/agent.ts @@ -0,0 +1,428 @@ +/** + * @fileoverview Package manager agent for executing npm, pnpm, and yarn commands. + * Provides cross-platform utilities with optimized flags and security defaults. + * + * SECURITY: Array-Based Arguments Prevent Command Injection + * + * All functions in this module (execNpm, execPnpm, execYarn) use array-based + * arguments when calling spawn(). This is the PRIMARY DEFENSE against command + * injection attacks. + * + * When arguments are passed as an array: + * spawn(cmd, ['install', packageName, '--flag'], options) + * + * Node.js handles escaping automatically. Each argument is passed directly to + * the OS without shell interpretation. Shell metacharacters like ; | & $ ( ) + * are treated as LITERAL STRINGS, not as commands. + * + * Example: If packageName = "lodash; rm -rf /", the package manager will try to + * install a package literally named "lodash; rm -rf /" (which doesn't exist), + * rather than executing the malicious command. + * + * This approach is secure even when shell: true is used on Windows for .cmd + * file resolution, because Node.js properly escapes each array element. + */ + +import { getCI } from '#env/ci' + +import { WIN32 } from '#constants/platform' +import { execBin } from './bin' +import { isDebug } from './debug' +import { findUpSync } from './fs' +import { getOwn } from './objects' +import type { SpawnOptions } from './spawn' +import { spawn } from './spawn' + +// Note: npm flag checking is done with regex patterns in the is*Flag functions below. + +const pnpmIgnoreScriptsFlags = new Set([ + '--ignore-scripts', + '--no-ignore-scripts', +]) + +const pnpmFrozenLockfileFlags = new Set([ + '--frozen-lockfile', + '--no-frozen-lockfile', +]) + +const pnpmInstallCommands = new Set(['install', 'i']) + +// Commands that support --ignore-scripts flag in pnpm: +// Installation-related: install, add, update, remove, link, unlink, import, rebuild. +const pnpmInstallLikeCommands = new Set([ + 'install', + 'i', + 'add', + 'update', + 'up', + 'remove', + 'rm', + 'link', + 'ln', + 'unlink', + 'import', + 'rebuild', + 'rb', +]) + +// Commands that support --ignore-scripts flag in yarn: +// Similar to npm/pnpm: installation-related commands. +const yarnInstallLikeCommands = new Set([ + 'install', + 'add', + 'upgrade', + 'remove', + 'link', + 'unlink', + 'import', +]) + +/** + * Execute npm commands with optimized flags and settings. + * + * SECURITY: Uses array-based arguments to prevent command injection. All elements + * in the args array are properly escaped by Node.js when passed to spawn(). + */ +/*@__NO_SIDE_EFFECTS__*/ +export function execNpm(args: string[], options?: SpawnOptions | undefined) { + const useDebug = isDebug() + const terminatorPos = args.indexOf('--') + const npmArgs = ( + terminatorPos === -1 ? args : args.slice(0, terminatorPos) + ).filter( + (a: string) => + !isNpmAuditFlag(a) && !isNpmFundFlag(a) && !isNpmProgressFlag(a), + ) + const otherArgs = terminatorPos === -1 ? [] : args.slice(terminatorPos) + const logLevelArgs = + // The default value of loglevel is "notice". We default to "warn" which is + // one level quieter. + useDebug || npmArgs.some(isNpmLoglevelFlag) ? [] : ['--loglevel', 'warn'] + // SECURITY: Array-based arguments prevent command injection. Each element is + // passed directly to the OS without shell interpretation. + // + // NOTE: We don't apply hardening flags to npm because: + // 1. npm is a trusted system tool installed with Node.js + // 2. npm requires full system access (filesystem, network, child processes) + // 3. Hardening flags would prevent npm from functioning (even with --allow-* grants) + // 4. The permission model is intended for untrusted user code, not package managers + // + // We also use the npm binary wrapper instead of calling cli.js directly because + // cli.js exports a function that needs to be invoked with process as an argument. + const npmBin = /*@__PURE__*/ require('#constants/agents').NPM_BIN_PATH + return spawn( + npmBin, + [ + // Even though '--loglevel=error' is passed npm will still run through + // code paths for 'audit' and 'fund' unless '--no-audit' and '--no-fund' + // flags are passed. + '--no-audit', + '--no-fund', + // Add `--no-progress` and `--silent` flags to fix input being swallowed + // by the spinner when running the command with recent versions of npm. + '--no-progress', + // Add '--loglevel=error' if a loglevel flag is not provided and the + // SOCKET_DEBUG environment variable is not truthy. + ...logLevelArgs, + ...npmArgs, + ...otherArgs, + ], + { + __proto__: null, + // On Windows, npm is a .cmd file that requires shell to execute. + shell: WIN32, + ...options, + } as SpawnOptions, + ) +} + +export interface PnpmOptions extends SpawnOptions { + allowLockfileUpdate?: boolean +} + +/** + * Execute pnpm commands with optimized flags and settings. + * + * SECURITY: Uses array-based arguments to prevent command injection. All elements + * in the args array are properly escaped by Node.js when passed to execBin(). + */ +/*@__NO_SIDE_EFFECTS__*/ + +export function execPnpm(args: string[], options?: PnpmOptions | undefined) { + const { allowLockfileUpdate, ...extBinOpts } = { + __proto__: null, + ...options, + } as PnpmOptions + const useDebug = isDebug() + const terminatorPos = args.indexOf('--') + const pnpmArgs = ( + terminatorPos === -1 ? args : args.slice(0, terminatorPos) + ).filter((a: string) => !isNpmProgressFlag(a)) + const otherArgs = terminatorPos === -1 ? [] : args.slice(terminatorPos) + + const firstArg = pnpmArgs[0] + const supportsIgnoreScripts = firstArg + ? pnpmInstallLikeCommands.has(firstArg) + : false + + // pnpm uses --loglevel for all commands. + const logLevelArgs = + useDebug || pnpmArgs.some(isPnpmLoglevelFlag) ? [] : ['--loglevel', 'warn'] + + // Only add --ignore-scripts for commands that support it. + const hasIgnoreScriptsFlag = pnpmArgs.some(isPnpmIgnoreScriptsFlag) + const ignoreScriptsArgs = + !supportsIgnoreScripts || hasIgnoreScriptsFlag ? [] : ['--ignore-scripts'] + + // In CI environments, pnpm uses --frozen-lockfile by default which prevents lockfile updates. + // For commands that need to update the lockfile (like install with new packages/overrides), + // we need to explicitly add --no-frozen-lockfile in CI mode if not already present. + const frozenLockfileArgs = [] + if ( + getCI() && + allowLockfileUpdate && + firstArg && + isPnpmInstallCommand(firstArg) && + !pnpmArgs.some(isPnpmFrozenLockfileFlag) + ) { + frozenLockfileArgs.push('--no-frozen-lockfile') + } + + // Note: pnpm doesn't have a --no-progress flag. It uses --reporter instead. + // We removed --no-progress as it causes "Unknown option" errors with pnpm. + + // SECURITY: Array-based arguments prevent command injection. Each element is + // passed directly to the OS without shell interpretation. + return execBin( + 'pnpm', + [ + // Add '--loglevel=warn' if a loglevel flag is not provided and debug is off. + ...logLevelArgs, + // Add '--ignore-scripts' by default for security (only for installation commands). + ...ignoreScriptsArgs, + // Add '--no-frozen-lockfile' in CI when lockfile updates are needed. + ...frozenLockfileArgs, + ...pnpmArgs, + ...otherArgs, + ], + extBinOpts, + ) +} + +/** + * Execute yarn commands with optimized flags and settings. + * + * SECURITY: Uses array-based arguments to prevent command injection. All elements + * in the args array are properly escaped by Node.js when passed to execBin(). + */ +/*@__NO_SIDE_EFFECTS__*/ +export function execYarn( + args: string[], + options?: import('./spawn').SpawnOptions, +) { + const useDebug = isDebug() + const terminatorPos = args.indexOf('--') + const yarnArgs = ( + terminatorPos === -1 ? args : args.slice(0, terminatorPos) + ).filter((a: string) => !isNpmProgressFlag(a)) + const otherArgs = terminatorPos === -1 ? [] : args.slice(terminatorPos) + + const firstArg = yarnArgs[0] + const supportsIgnoreScripts = firstArg + ? yarnInstallLikeCommands.has(firstArg) + : false + + // Yarn uses --silent flag for quieter output. + const logLevelArgs = + useDebug || yarnArgs.some(isNpmLoglevelFlag) ? [] : ['--silent'] + + // Only add --ignore-scripts for commands that support it. + const hasIgnoreScriptsFlag = yarnArgs.some(isPnpmIgnoreScriptsFlag) + const ignoreScriptsArgs = + !supportsIgnoreScripts || hasIgnoreScriptsFlag ? [] : ['--ignore-scripts'] + + // SECURITY: Array-based arguments prevent command injection. Each element is + // passed directly to the OS without shell interpretation. + return execBin( + 'yarn', + [ + // Add '--silent' if a loglevel flag is not provided and debug is off. + ...logLevelArgs, + // Add '--ignore-scripts' by default for security (only for installation commands). + ...ignoreScriptsArgs, + ...yarnArgs, + ...otherArgs, + ], + { + __proto__: null, + ...options, + } as SpawnOptions, + ) +} + +/** + * Check if a command argument is an npm audit flag. + */ +/*@__NO_SIDE_EFFECTS__*/ +export function isNpmAuditFlag(cmdArg: string): boolean { + return /^--(no-)?audit(=.*)?$/.test(cmdArg) +} + +/** + * Check if a command argument is an npm fund flag. + */ +/*@__NO_SIDE_EFFECTS__*/ +export function isNpmFundFlag(cmdArg: string): boolean { + return /^--(no-)?fund(=.*)?$/.test(cmdArg) +} + +/** + * Check if a command argument is an npm loglevel flag. + */ +/*@__NO_SIDE_EFFECTS__*/ +export function isNpmLoglevelFlag(cmdArg: string): boolean { + // https://docs.npmjs.com/cli/v11/using-npm/logging#setting-log-levels + if (/^--loglevel(=.*)?$/.test(cmdArg)) { + return true + } + // Check for long form flags + if (/^--(silent|verbose|info|warn|error|quiet)$/.test(cmdArg)) { + return true + } + // Check for shorthand flags + return /^-(s|q|d|dd|ddd|v)$/.test(cmdArg) +} + +/** + * Check if a command argument is an npm node-options flag. + */ +/*@__NO_SIDE_EFFECTS__*/ +export function isNpmNodeOptionsFlag(cmdArg: string): boolean { + // https://docs.npmjs.com/cli/v9/using-npm/config#node-options + return /^--node-options(=.*)?$/.test(cmdArg) +} + +/** + * Check if a command argument is an npm progress flag. + */ +/*@__NO_SIDE_EFFECTS__*/ +export function isNpmProgressFlag(cmdArg: string): boolean { + return /^--(no-)?progress(=.*)?$/.test(cmdArg) +} + +/** + * Check if a command argument is a pnpm ignore-scripts flag. + */ +/*@__NO_SIDE_EFFECTS__*/ +export function isPnpmIgnoreScriptsFlag(cmdArg: string): boolean { + return pnpmIgnoreScriptsFlags.has(cmdArg) +} + +/** + * Check if a command argument is a pnpm frozen-lockfile flag. + */ +/*@__NO_SIDE_EFFECTS__*/ +export function isPnpmFrozenLockfileFlag(cmdArg: string): boolean { + return pnpmFrozenLockfileFlags.has(cmdArg) +} + +/** + * Check if a command argument is a pnpm install command. + */ +/*@__NO_SIDE_EFFECTS__*/ +export function isPnpmInstallCommand(cmdArg: string): boolean { + return pnpmInstallCommands.has(cmdArg) +} + +/** + * Alias for isNpmLoglevelFlag for pnpm usage. + */ +export const isPnpmLoglevelFlag = isNpmLoglevelFlag + +/** + * Execute a package.json script using the appropriate package manager. + * Automatically detects pnpm, yarn, or npm based on lockfiles. + */ +export interface ExecScriptOptions extends SpawnOptions { + prepost?: boolean | undefined +} + +/*@__NO_SIDE_EFFECTS__*/ +export function execScript( + scriptName: string, + args?: string[] | readonly string[] | ExecScriptOptions | undefined, + options?: ExecScriptOptions | undefined, +) { + // Handle overloaded signatures: execScript(name, options) or execScript(name, args, options). + let resolvedOptions: ExecScriptOptions | undefined + let resolvedArgs: string[] + if (!Array.isArray(args) && args !== null && typeof args === 'object') { + resolvedOptions = args as ExecScriptOptions + resolvedArgs = [] + } else { + resolvedOptions = options + resolvedArgs = (args || []) as string[] + } + const { prepost, ...spawnOptions } = { + __proto__: null, + ...resolvedOptions, + } as ExecScriptOptions + + // If shell: true is passed, run the command directly as a shell command. + if (spawnOptions.shell === true) { + return spawn(scriptName, resolvedArgs, spawnOptions) + } + + const useNodeRun = + !prepost && /*@__PURE__*/ require('#constants/node').supportsNodeRun() + + // Detect package manager based on lockfile by traversing up from current directory. + const cwd = + (getOwn(spawnOptions, 'cwd') as string | undefined) ?? process.cwd() + + // Check for pnpm-lock.yaml. + const pnpmLockPath = findUpSync( + /*@__INLINE__*/ require('#constants/agents').PNPM_LOCK_YAML, + { cwd }, + ) as string | undefined + if (pnpmLockPath) { + return execPnpm(['run', scriptName, ...resolvedArgs], spawnOptions) + } + + // Check for package-lock.json. + // When in an npm workspace, use npm run to ensure workspace binaries are available. + const packageLockPath = findUpSync( + /*@__INLINE__*/ require('#constants/agents').PACKAGE_LOCK_JSON, + { cwd }, + ) as string | undefined + if (packageLockPath) { + return execNpm(['run', scriptName, ...resolvedArgs], spawnOptions) + } + + // Check for yarn.lock. + const yarnLockPath = findUpSync( + /*@__INLINE__*/ require('#constants/agents').YARN_LOCK, + { cwd }, + ) as string | undefined + if (yarnLockPath) { + return execYarn(['run', scriptName, ...resolvedArgs], spawnOptions) + } + + return spawn( + /*@__PURE__*/ require('#constants/node').getExecPath(), + [ + .../*@__PURE__*/ require('#constants/node').getNodeNoWarningsFlags(), + ...(useNodeRun + ? ['--run'] + : [ + /*@__PURE__*/ require('#constants/agents').NPM_REAL_EXEC_PATH, + 'run', + ]), + scriptName, + ...resolvedArgs, + ], + { + ...spawnOptions, + }, + ) +} diff --git a/packages/lib/src/ansi.ts b/packages/lib/src/ansi.ts new file mode 100644 index 000000000..7e57ab575 --- /dev/null +++ b/packages/lib/src/ansi.ts @@ -0,0 +1,47 @@ +/** + * @fileoverview ANSI escape code utilities. + * Provides constants and helpers for terminal formatting. + */ + +// ANSI escape codes - commonly used sequences. +export const ANSI_RESET = '\x1b[0m' +export const ANSI_BOLD = '\x1b[1m' +export const ANSI_DIM = '\x1b[2m' +export const ANSI_ITALIC = '\x1b[3m' +export const ANSI_UNDERLINE = '\x1b[4m' +export const ANSI_STRIKETHROUGH = '\x1b[9m' + +// ANSI escape code regex to strip colors/formatting. +// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequences use control characters. +const ANSI_REGEX = /\x1b\[[0-9;]*m/g + +/** + * Create a regular expression for matching ANSI escape codes. + * + * Inlined ansi-regex: + * https://socket.dev/npm/package/ansi-regexp/overview/6.2.2 + * MIT License + * Copyright (c) Sindre Sorhus (https://sindresorhus.com) + */ +/*@__NO_SIDE_EFFECTS__*/ +export function ansiRegex(options?: { onlyFirst?: boolean }): RegExp { + const { onlyFirst } = options ?? {} + // Valid string terminator sequences are BEL, ESC\, and 0x9c. + const ST = '(?:\\u0007\\u001B\\u005C|\\u009C)' + // OSC sequences only: ESC ] ... ST (non-greedy until the first ST). + const osc = `(?:\\u001B\\][\\s\\S]*?${ST})` + // CSI and related: ESC/C1, optional intermediates, optional params (supports ; and :) then final byte. + const csi = + '[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]' + const pattern = `${osc}|${csi}` + return new RegExp(pattern, onlyFirst ? undefined : 'g') +} + +/** + * Strip ANSI escape codes from text. + * Uses the inlined ansi-regex for matching. + */ +/*@__NO_SIDE_EFFECTS__*/ +export function stripAnsi(text: string): string { + return text.replace(ANSI_REGEX, '') +} diff --git a/packages/lib/src/argv/flags.ts b/packages/lib/src/argv/flags.ts new file mode 100644 index 000000000..bd5e2276f --- /dev/null +++ b/packages/lib/src/argv/flags.ts @@ -0,0 +1,342 @@ +/** + * Common flag utilities for Socket CLI applications. + * Provides consistent flag checking across all Socket projects. + */ + +/** + * Flag values object from parsed arguments. + */ +export interface FlagValues { + [key: string]: unknown + quiet?: boolean + silent?: boolean + verbose?: boolean + help?: boolean + all?: boolean + fix?: boolean + force?: boolean + 'dry-run'?: boolean + json?: boolean + debug?: boolean + watch?: boolean + coverage?: boolean + cover?: boolean + update?: boolean + staged?: boolean + changed?: boolean +} + +const processArg = [...process.argv] + +/** + * Accepted input types for flag checking functions. + * Can be parsed flag values, process.argv array, or undefined (uses process.argv). + */ +export type FlagInput = FlagValues | string[] | readonly string[] | undefined + +/** + * Get the appropriate log level based on flags. + * Returns 'silent', 'error', 'warn', 'info', 'verbose', or 'debug'. + * Accepts FlagValues object, process.argv array, or undefined (uses process.argv). + */ +export function getLogLevel(input?: FlagInput): string { + if (isQuiet(input)) { + return 'silent' + } + if (isDebug(input)) { + return 'debug' + } + if (isVerbose(input)) { + return 'verbose' + } + return 'info' +} + +/** + * Check if all flag is set. + * Accepts FlagValues object, process.argv array, or undefined (uses process.argv). + */ +export function isAll(input?: FlagInput): boolean { + if (!input) { + return processArg.includes('--all') + } + if (Array.isArray(input)) { + return input.includes('--all') + } + return !!(input as FlagValues).all +} + +/** + * Check if changed files mode is enabled. + * Accepts FlagValues object, process.argv array, or undefined (uses process.argv). + */ +export function isChanged(input?: FlagInput): boolean { + if (!input) { + return processArg.includes('--changed') + } + if (Array.isArray(input)) { + return input.includes('--changed') + } + return !!(input as FlagValues).changed +} + +/** + * Check if coverage mode is enabled. + * Checks both 'coverage' and 'cover' flags. + * Accepts FlagValues object, process.argv array, or undefined (uses process.argv). + */ +export function isCoverage(input?: FlagInput): boolean { + if (!input) { + return processArg.includes('--coverage') || processArg.includes('--cover') + } + if (Array.isArray(input)) { + return input.includes('--coverage') || input.includes('--cover') + } + return !!((input as FlagValues).coverage || (input as FlagValues).cover) +} + +/** + * Check if debug mode is enabled. + * Accepts FlagValues object, process.argv array, or undefined (uses process.argv). + */ +export function isDebug(input?: FlagInput): boolean { + if (!input) { + return processArg.includes('--debug') + } + if (Array.isArray(input)) { + return input.includes('--debug') + } + return !!(input as FlagValues).debug +} + +/** + * Check if dry-run mode is enabled. + * Accepts FlagValues object, process.argv array, or undefined (uses process.argv). + */ +export function isDryRun(input?: FlagInput): boolean { + if (!input) { + return processArg.includes('--dry-run') + } + if (Array.isArray(input)) { + return input.includes('--dry-run') + } + return !!(input as FlagValues)['dry-run'] +} + +/** + * Check if fix/autofix mode is enabled. + * Accepts FlagValues object, process.argv array, or undefined (uses process.argv). + */ +export function isFix(input?: FlagInput): boolean { + if (!input) { + return processArg.includes('--fix') + } + if (Array.isArray(input)) { + return input.includes('--fix') + } + return !!(input as FlagValues).fix +} + +/** + * Check if force mode is enabled. + * Accepts FlagValues object, process.argv array, or undefined (uses process.argv). + */ +export function isForce(input?: FlagInput): boolean { + if (!input) { + return processArg.includes('--force') + } + if (Array.isArray(input)) { + return input.includes('--force') + } + return !!(input as FlagValues).force +} + +/** + * Check if help flag is set. + * Accepts FlagValues object, process.argv array, or undefined (uses process.argv). + */ +export function isHelp(input?: FlagInput): boolean { + if (!input) { + return processArg.includes('--help') || processArg.includes('-h') + } + if (Array.isArray(input)) { + return input.includes('--help') || input.includes('-h') + } + return !!(input as FlagValues).help +} + +/** + * Check if JSON output is requested. + * Accepts FlagValues object, process.argv array, or undefined (uses process.argv). + */ +export function isJson(input?: FlagInput): boolean { + if (!input) { + return processArg.includes('--json') + } + if (Array.isArray(input)) { + return input.includes('--json') + } + return !!(input as FlagValues).json +} + +/** + * Check if quiet/silent mode is enabled. + * Accepts FlagValues object, process.argv array, or undefined (uses process.argv). + */ +export function isQuiet(input?: FlagInput): boolean { + if (!input) { + return processArg.includes('--quiet') || processArg.includes('--silent') + } + if (Array.isArray(input)) { + return input.includes('--quiet') || input.includes('--silent') + } + return !!((input as FlagValues).quiet || (input as FlagValues).silent) +} + +/** + * Check if staged files mode is enabled. + * Accepts FlagValues object, process.argv array, or undefined (uses process.argv). + */ +export function isStaged(input?: FlagInput): boolean { + if (!input) { + return processArg.includes('--staged') + } + if (Array.isArray(input)) { + return input.includes('--staged') + } + return !!(input as FlagValues).staged +} + +/** + * Check if update mode is enabled (for snapshots, dependencies, etc). + * Accepts FlagValues object, process.argv array, or undefined (uses process.argv). + */ +export function isUpdate(input?: FlagInput): boolean { + if (!input) { + return processArg.includes('--update') || processArg.includes('-u') + } + if (Array.isArray(input)) { + return input.includes('--update') || input.includes('-u') + } + return !!(input as FlagValues).update +} + +/** + * Check if verbose mode is enabled. + * Accepts FlagValues object, process.argv array, or undefined (uses process.argv). + */ +export function isVerbose(input?: FlagInput): boolean { + if (!input) { + return processArg.includes('--verbose') + } + if (Array.isArray(input)) { + return input.includes('--verbose') + } + return !!(input as FlagValues).verbose +} + +/** + * Check if watch mode is enabled. + * Accepts FlagValues object, process.argv array, or undefined (uses process.argv). + */ +export function isWatch(input?: FlagInput): boolean { + if (!input) { + return processArg.includes('--watch') || processArg.includes('-w') + } + if (Array.isArray(input)) { + return input.includes('--watch') || input.includes('-w') + } + return !!(input as FlagValues).watch +} + +/** + * Common flag definitions for parseArgs configuration. + * Can be spread into parseArgs options for consistency. + */ +export const COMMON_FLAGS = { + all: { + type: 'boolean' as const, + default: false, + description: 'Target all files', + }, + changed: { + type: 'boolean' as const, + default: false, + description: 'Target changed files', + }, + coverage: { + type: 'boolean' as const, + default: false, + description: 'Run with coverage', + }, + cover: { + type: 'boolean' as const, + default: false, + description: 'Run with coverage (alias)', + }, + debug: { + type: 'boolean' as const, + default: false, + description: 'Enable debug output', + }, + 'dry-run': { + type: 'boolean' as const, + default: false, + description: 'Perform a dry run', + }, + fix: { + type: 'boolean' as const, + default: false, + description: 'Automatically fix issues', + }, + force: { + type: 'boolean' as const, + default: false, + description: 'Force the operation', + }, + help: { + type: 'boolean' as const, + default: false, + short: 'h', + description: 'Show help', + }, + json: { + type: 'boolean' as const, + default: false, + description: 'Output as JSON', + }, + quiet: { + type: 'boolean' as const, + default: false, + short: 'q', + description: 'Suppress output', + }, + silent: { + type: 'boolean' as const, + default: false, + description: 'Suppress all output', + }, + staged: { + type: 'boolean' as const, + default: false, + description: 'Target staged files', + }, + update: { + type: 'boolean' as const, + default: false, + short: 'u', + description: 'Update snapshots/deps', + }, + verbose: { + type: 'boolean' as const, + default: false, + short: 'v', + description: 'Verbose output', + }, + watch: { + type: 'boolean' as const, + default: false, + short: 'w', + description: 'Watch mode', + }, +} diff --git a/packages/lib/src/argv/parse.ts b/packages/lib/src/argv/parse.ts new file mode 100644 index 000000000..c731b2a39 --- /dev/null +++ b/packages/lib/src/argv/parse.ts @@ -0,0 +1,270 @@ +/** + * Argument parsing utilities for CLI applications. + * Uses yargs-parser internally for robust argument parsing with Node.js parseArgs-compatible API. + */ + +import yargsParser from 'yargs-parser' + +/** + * Yargs parser options interface. + */ +interface YargsOptions { + // Array of option names that should be treated as booleans. + boolean?: string[] | undefined + // Array of option names that should be treated as strings. + string?: string[] | undefined + // Array of option names that should accept multiple values. + array?: string[] | undefined + // Map of short aliases to full option names. + alias?: Record | undefined + // Default values for options. + default?: Record | undefined + // Transform functions to coerce parsed values. + coerce?: Record unknown> | undefined + // Whether to treat unknown options as positional arguments. + 'unknown-options-as-args'?: boolean | undefined + // Whether to parse numeric strings as numbers. + 'parse-numbers'?: boolean | undefined + // Whether to parse positional arguments as numbers. + 'parse-positional-numbers'?: boolean | undefined + // Whether to support --no-