feat: implement RFC npm#897 — npm approve-scripts review-report mode#1
feat: implement RFC npm#897 — npm approve-scripts review-report mode#1Copilot wants to merge 186 commits into
Conversation
|
@copilot parseCommandFile is silently incomplete for shell pipelines. "node install.js && node patch.js" only finds install.js. Since lifecycle scripts often chain commands, this could silently miss referenced files. Consider adding a signals: ['multi-command-script'] hint so reviewers know the scan is partial. If multiple commands then check each command and build distinct list from all commands the file references and such. SIGNAL_PATTERNS for uses-child-process only detects require('child_process') — not ESM import { exec } from 'child_process'. The import regex (LOCAL_IMPORT_FROM_RE) only handles relative paths, so bare import { exec } from 'child_process' goes undetected. This is a gap given the feature's security focus. dep-path-walker.js accesses arborist internals directly (node.edgesIn, node.package, node.isProjectRoot, etc.). These aren't part of a stable public API. Not a blocker, try to reimplement with known public apis but if you can't then add a comment saying so. resolveLocalRef doesn't read package.json#main — e.g., require('./lib') where lib/package.json has "main": "build/index.js" won't be followed. Fix this. |
… indicators
Adds seven new indicator registry entries covering the full spectrum of
JavaScript/Ruby task runners and build tools that appear in npm lifecycle
scripts alongside Grunt/Make/Just:
Task runners (emit make-build signal):
gulpfile.js — Gulp v3/v4 (streaming task runner; extracts task names,
external tool calls, child process spawns)
Jakefile — Jake (JS Make alternative; task/file/directory declarations)
Cakefile — Cake CoffeeScript task runner (requires subcommand to
avoid English word 'cake')
Rakefile — Ruby Make (for native C/Ruby extensions; task :name syntax)
Taskfile.yml — go-task YAML runner (requires subcommand to avoid generic
'task' word in prose / Windows CLI tools)
brunch-config.js — Brunch build assembler (plugin chain, external tools)
Package manager (emits runtime-installer signal):
bower.json — Bower front-end package manager; bower install in postinstall
installs more packages outside npm's lockfile — supply-chain
risk similar to runtime npm/yarn install.
Also:
- Add gulp and brunch to NATIVE_BUILD_COMMAND_PATTERN (consistent with grunt)
- Update make-build SIGNAL_DESCRIPTION to enumerate all covered task runners
- Update runtime-installer description to mention bower
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
✅ PASS — approve-scripts smoke reportAll packages with lifecycle scripts are correctly identified as pending approval.
|
✅ SUCCESS — indicator-suggestions deep scanCommand: Full scan output (stdout + stderr)Generated indicator-suggestions.json{
"$ai": {
"role": "You are an expert in the npm ecosystem, native Node.js addon build tooling, supply-chain security, and the structure of indicator-definitions.js in the npm/cli repository.",
"thisFileIs": "A dataset generated by scripts/build-indicator-suggestions.js. It scans popular npm packages for lifecycle scripts and compares them against the INDICATOR_REGISTRY in lib/utils/indicator-definitions.js. Your job is to review the data below and propose concrete improvements to that file.",
"howToReadThisFile": "Every top-level section (meta, coverage, existingDefinitionCoverage, uncategorizedPackages, commandPatternGaps) has a \"description\" field that explains what the section contains and how to interpret it, and a \"data\" field with the actual content. Read the description first, then inspect data.",
"sourceFile": "lib/utils/indicator-definitions.js",
"tasks": [
{
"priority": 1,
"task": "Review commandPatternGaps.data where frequency >= 3 OR weeklyDownloadTotal >= 50000.",
"action": "For each: decide whether the token warrants a NEW indicator entry (new build tool not yet covered) or just an additional commandPattern on an EXISTING entry. Ignore tokens that are generic JS keywords (const, require, stdio, inherit) or non-build tools (tsc, oclif, lint, rimraf)."
},
{
"priority": 2,
"task": "Review uncategorizedPackages.data sorted by weeklyDownloads descending.",
"action": "For each: examine lifecycleScripts, commandTokens, detectedSignals, and inferredIndicatorFiles. Propose a new INDICATOR_REGISTRY entry OR explain why it should not be added. Focus on packages with weeklyDownloads > 10000."
},
{
"priority": 3,
"task": "Review existingDefinitionCoverage.data entries with low matchedCount.",
"action": "Propose additional commandPatterns that would match real packages listed in the uncategorized or gap sections."
}
],
"outputFormat": {
"forNewIndicatorEntry": {
"file": "<the filename used as the registry key, e.g. \"Gruntfile.js\">",
"label": "<short human-readable label>",
"commandPatterns": [
"<regex source strings — will be compiled with new RegExp(...)>"
],
"signals": {
"onFound": [
"<signal name>"
],
"onWarning": []
},
"scannerSteps": [
"{ type: \"regex\", pattern: \"...\", group: 1, label: \"...\" }",
"{ type: \"regex-all\", pattern: \"...\", group: 1, label: \"...\" }",
"{ type: \"glob\", pattern: \"**/*.ext\", label: \"...\", maxDisplay: 20 }",
"{ type: \"signal\", pattern: \"...\", signal: \"<signal-name>\" }"
],
"addToNativeBuildCommandPattern": "<true if this tool compiles native code>",
"rationale": "<why this indicator is needed and what real packages triggered it>"
},
"forExistingEntryUpdate": {
"existingKey": "<key in INDICATOR_REGISTRY to update>",
"addCommandPatterns": [
"<new regex source strings>"
],
"rationale": "<which packages would now be matched>"
}
},
"availableSignals": [
"native-build — Compiles a native binary (.node addon, .so, .dylib) via node-gyp, CMake, Autoconf, or similar",
"rust-native — Compiles a Rust-backed native addon (Cargo.toml / neon)",
"wasm-build — Compiles a WebAssembly (.wasm) module",
"android-native — Android JNI/NDK native module",
"gyp-conditions — binding.gyp has platform/arch conditions (may behave differently per OS)",
"make-build — Makefile or task-runner driven build (Grunt/Gulp/Jake/Cake/Rake/Brunch/Taskfile) that can execute arbitrary shell commands",
"binary-download — Downloads a pre-built binary at install time (node-pre-gyp, prebuild-install, etc.)",
"activates-bundled-binary — Makes a file bundled inside the npm tarball executable (chmod +x / fs.chmod with execute bit)",
"runtime-installer — Installs additional packages at runtime via npm/pnpm/yarn/bower install as a child process",
"external-url — Fetches a URL (curl/wget/node https) at install time",
"source-downloader — Fetches source code (curl/wget/git clone) at install time",
"obfuscation-pattern — Uses Base64/hex decoding or eval/Function() to hide executed code"
]
},
"meta": {
"description": "Run metadata: when generated, registry size limit (topN), whether cross-package import following was enabled (deepScan), and which indicator filenames are currently registered.",
"data": {
"generatedAt": "2026-06-22T04:02:03.729Z",
"topN": 2,
"deepScan": true,
"registryDefinitions": [
"binding.gyp",
"Cargo.toml",
"build.rs",
"CMakeLists.txt",
"CMakePresets.json",
"configure.ac",
"android/build.gradle",
"binary-downloader",
"bundled-binary-installer",
"runtime-installer",
"source-downloader",
"Makefile",
"bower.json",
"brunch-config.js",
"Gruntfile.js",
"gulpfile.js",
"Jakefile",
"Cakefile",
"Rakefile",
"Taskfile.yml",
"Justfile"
]
}
},
"coverage": {
"description": "Aggregate counts for this run. totalScanned = registry pages fetched. uniqueNamesConsidered = distinct package names seen. withLifecycleScripts = had install/postinstall/etc. matchedByExistingDefinitions = covered by at least one indicator. uncategorizedBuildPackages = build signal present but no indicator matched. lifecycleOnlyNoBuildHint = lifecycle scripts with no build tool detected.",
"data": {
"totalScanned": 500,
"uniqueNamesConsidered": 522,
"withLifecycleScripts": 82,
"matchedByExistingDefinitions": 18,
"uncategorizedBuildPackages": 4,
"lifecycleOnlyNoBuildHint": 60
}
},
"existingDefinitionCoverage": {
"description": "Per-indicator match counts against real packages. Each entry in data is keyed by indicator filename (e.g. \"binding.gyp\") with matchedCount = how many scanned packages matched that indicator's commandPatterns, and packages = the matched names. Low matchedCount relative to expected prevalence suggests commandPatterns need broadening.",
"data": {
"source-downloader": {
"matchedCount": 5,
"packages": [
"@infisical/cli",
"gws-with-audit-log",
"netlify",
"netlify-cli",
"wasm-pack"
]
},
"runtime-installer": {
"matchedCount": 4,
"packages": [
"9router",
"hardhat-deploy",
"netlify",
"netlify-cli"
]
},
"bundled-binary-installer": {
"matchedCount": 3,
"packages": [
"9router",
"agent-browser",
"cline"
]
},
"Cargo.toml": {
"matchedCount": 2,
"packages": [
"agent-browser",
"bun-pty"
]
},
"binding.gyp": {
"matchedCount": 2,
"packages": [
"eas-cli",
"ovsx"
]
},
"binary-downloader": {
"matchedCount": 1,
"packages": [
"cloudflared"
]
},
"gulpfile.js": {
"matchedCount": 1,
"packages": [
"axios"
]
}
}
},
"uncategorizedPackages": {
"description": "Packages that have a build signal (native compile, binary download, runtime-installer, etc.) but no existing indicator definition matched them. Each item has: name, version, weeklyDownloads, lifecycleScripts, buildDependencies, commandTokens, inferredIndicatorFiles, detectedSignals, suggestedSignal. Sorted by weeklyDownloads descending — highest-value gaps first.",
"data": [
{
"name": "@netlify/zip-it-and-ship-it",
"version": "15.0.1",
"weeklyDownloads": 531758,
"lifecycleScripts": {
"prepack": "npm run build"
},
"buildDependencies": [],
"commandTokens": [],
"inferredIndicatorFiles": [],
"suggestedSignal": null
},
{
"name": "hugo-extended",
"version": "0.163.3",
"weeklyDownloads": 240864,
"lifecycleScripts": {
"postinstall": "node postinstall.js"
},
"buildDependencies": [],
"commandTokens": [],
"inferredIndicatorFiles": [],
"detectedSignals": [
"reads-process-env",
"requires-local-file"
],
"suggestedSignal": "reads-process-env",
"scannedFiles": [
"postinstall.js"
]
},
{
"name": "@mintlify/cli",
"version": "4.0.1235",
"weeklyDownloads": 193556,
"lifecycleScripts": {
"prepare": "npm run build"
},
"buildDependencies": [],
"commandTokens": [],
"inferredIndicatorFiles": [
"binding.gyp"
],
"suggestedSignal": "native-build"
},
{
"name": "@claude-flow/cli",
"version": "3.12.4",
"weeklyDownloads": 46522,
"lifecycleScripts": {
"postinstall": "node ./scripts/postinstall.cjs"
},
"buildDependencies": [],
"commandTokens": [],
"inferredIndicatorFiles": [],
"detectedSignals": [
"writes-file"
],
"suggestedSignal": "writes-file",
"scannedFiles": [
"scripts/postinstall.cjs"
]
}
]
},
"commandPatternGaps": {
"description": "Command tokens that appear in multiple uncategorized build packages but are not covered by any existing commandPatterns entry. Each item has: token, frequency (package count), weeklyDownloadTotal, packages, suggestedCommandPattern. Sorted by frequency × downloads — entries with frequency >= 3 or weeklyDownloadTotal >= 50000 are highest priority.",
"data": []
}
}
|
script-risk-scanner.js: - references-credential-env-var: add bracket notation (process.env['KEY']) and expand to CI secrets (NODE_AUTH_TOKEN, ACTIONS_RUNTIME_TOKEN, RUNNER_TOKEN, GOOGLE_APPLICATION_CREDENTIALS, AZURE_CLIENT_SECRET) - network-access: add undici, ky, needle, phin, cross-fetch; add ESM import-from form so ESM-only HTTP packages are caught; global fetch() already covered by existing pattern - writes-outside-package: remove process.cwd() heuristic — fired on any path construction including read-only ones (too many false positives); keep only the '../' string-literal heuristic - dynamic-require: new signal — require() with a non-literal argument (variable/expression); loaded module cannot be statically determined - wasm-load: new signal — WebAssembly.instantiate/compile/streaming; pre-compiled WASM payloads bypass all JS-level static analysis - binary-download: extend XXXX_BINARY_PATH to also match XXXX_BINARY (covers packages that set a binary name var, not a full path) - runtime-installer: add bun (bun add/install) and deno (deno install) alongside existing npm/yarn/pnpm coverage indicator-definitions.js: - Fix floating Makefile comment (was between android/build.gradle and binary-downloader; now correctly placed above the Makefile entry) - Cargo.toml: add 'cargo install' commandPattern - New: meson.build indicator (native-build signal; targets, deps, programs) - New: build.gradle root-level entry (android-native; covers React Native packages that place Gradle at the package root, not android/) - New variant aliases: gulpfile.ts, gulpfile.mjs, Gruntfile.coffee, Jakefile.js, Taskfile.yaml — registry key IS the on-disk filename so each variant needs its own entry - NATIVE_BUILD_COMMAND_PATTERN: add meson setup/compile/build and ninja - SIGNAL_DESCRIPTIONS: add wasm-load and dynamic-require; update native-build to mention Meson; update runtime-installer to mention bun/deno Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
✅ PASS — approve-scripts smoke reportAll packages with lifecycle scripts are correctly identified as pending approval.
|
✅ SUCCESS — indicator-suggestions deep scanCommand: Full scan output (stdout + stderr)Generated indicator-suggestions.json{
"$ai": {
"role": "You are an expert in the npm ecosystem, native Node.js addon build tooling, supply-chain security, and the structure of indicator-definitions.js in the npm/cli repository.",
"thisFileIs": "A dataset generated by scripts/build-indicator-suggestions.js. It scans popular npm packages for lifecycle scripts and compares them against the INDICATOR_REGISTRY in lib/utils/indicator-definitions.js. Your job is to review the data below and propose concrete improvements to that file.",
"howToReadThisFile": "Every top-level section (meta, coverage, existingDefinitionCoverage, uncategorizedPackages, commandPatternGaps) has a \"description\" field that explains what the section contains and how to interpret it, and a \"data\" field with the actual content. Read the description first, then inspect data.",
"sourceFile": "lib/utils/indicator-definitions.js",
"tasks": [
{
"priority": 1,
"task": "Review commandPatternGaps.data where frequency >= 3 OR weeklyDownloadTotal >= 50000.",
"action": "For each: decide whether the token warrants a NEW indicator entry (new build tool not yet covered) or just an additional commandPattern on an EXISTING entry. Ignore tokens that are generic JS keywords (const, require, stdio, inherit) or non-build tools (tsc, oclif, lint, rimraf)."
},
{
"priority": 2,
"task": "Review uncategorizedPackages.data sorted by weeklyDownloads descending.",
"action": "For each: examine lifecycleScripts, commandTokens, detectedSignals, and inferredIndicatorFiles. Propose a new INDICATOR_REGISTRY entry OR explain why it should not be added. Focus on packages with weeklyDownloads > 10000."
},
{
"priority": 3,
"task": "Review existingDefinitionCoverage.data entries with low matchedCount.",
"action": "Propose additional commandPatterns that would match real packages listed in the uncategorized or gap sections."
}
],
"outputFormat": {
"forNewIndicatorEntry": {
"file": "<the filename used as the registry key, e.g. \"Gruntfile.js\">",
"label": "<short human-readable label>",
"commandPatterns": [
"<regex source strings — will be compiled with new RegExp(...)>"
],
"signals": {
"onFound": [
"<signal name>"
],
"onWarning": []
},
"scannerSteps": [
"{ type: \"regex\", pattern: \"...\", group: 1, label: \"...\" }",
"{ type: \"regex-all\", pattern: \"...\", group: 1, label: \"...\" }",
"{ type: \"glob\", pattern: \"**/*.ext\", label: \"...\", maxDisplay: 20 }",
"{ type: \"signal\", pattern: \"...\", signal: \"<signal-name>\" }"
],
"addToNativeBuildCommandPattern": "<true if this tool compiles native code>",
"rationale": "<why this indicator is needed and what real packages triggered it>"
},
"forExistingEntryUpdate": {
"existingKey": "<key in INDICATOR_REGISTRY to update>",
"addCommandPatterns": [
"<new regex source strings>"
],
"rationale": "<which packages would now be matched>"
}
},
"availableSignals": [
"native-build — Compiles a native binary (.node addon, .so, .dylib) via node-gyp, CMake, Meson, Autoconf, or similar",
"rust-native — Compiles a Rust-backed native addon (Cargo.toml / neon)",
"wasm-build — Compiles a WebAssembly (.wasm) module from Rust source (wasm-bindgen)",
"wasm-load — Loads a pre-compiled WebAssembly binary at runtime via WebAssembly.instantiate/compile — content is not inspectable by static analysis",
"android-native — Android JNI/NDK native module",
"gyp-conditions — binding.gyp has platform/arch conditions (may behave differently per OS)",
"make-build — Makefile or task-runner driven build (Grunt/Gulp/Jake/Cake/Rake/Brunch/Taskfile) that can execute arbitrary shell commands",
"binary-download — Downloads a pre-built binary at install time (node-pre-gyp, prebuild-install, etc.)",
"activates-bundled-binary — Makes a file bundled inside the npm tarball executable (chmod +x / fs.chmod with execute bit)",
"runtime-installer — Installs additional packages at runtime via npm/pnpm/yarn/bun/deno/bower install as a child process",
"external-url — Fetches a URL (curl/wget/node https) at install time",
"source-downloader — Fetches source code (curl/wget/git clone) at install time",
"dynamic-require — Calls require() with a non-literal argument — the loaded module cannot be statically determined",
"obfuscation-pattern — Uses Base64/hex decoding or eval/Function() to hide executed code"
]
},
"meta": {
"description": "Run metadata: when generated, registry size limit (topN), whether cross-package import following was enabled (deepScan), and which indicator filenames are currently registered.",
"data": {
"generatedAt": "2026-06-22T04:29:57.138Z",
"topN": 2,
"deepScan": true,
"registryDefinitions": [
"binding.gyp",
"Cargo.toml",
"build.rs",
"CMakeLists.txt",
"CMakePresets.json",
"configure.ac",
"meson.build",
"android/build.gradle",
"binary-downloader",
"bundled-binary-installer",
"runtime-installer",
"source-downloader",
"build.gradle",
"Makefile",
"bower.json",
"brunch-config.js",
"Gruntfile.js",
"gulpfile.js",
"Jakefile",
"Cakefile",
"Rakefile",
"Taskfile.yml",
"gulpfile.ts",
"gulpfile.mjs",
"Gruntfile.coffee",
"Jakefile.js",
"Taskfile.yaml",
"Justfile"
]
}
},
"coverage": {
"description": "Aggregate counts for this run. totalScanned = registry pages fetched. uniqueNamesConsidered = distinct package names seen. withLifecycleScripts = had install/postinstall/etc. matchedByExistingDefinitions = covered by at least one indicator. uncategorizedBuildPackages = build signal present but no indicator matched. lifecycleOnlyNoBuildHint = lifecycle scripts with no build tool detected.",
"data": {
"totalScanned": 500,
"uniqueNamesConsidered": 551,
"withLifecycleScripts": 95,
"matchedByExistingDefinitions": 18,
"uncategorizedBuildPackages": 2,
"lifecycleOnlyNoBuildHint": 75
}
},
"existingDefinitionCoverage": {
"description": "Per-indicator match counts against real packages. Each entry in data is keyed by indicator filename (e.g. \"binding.gyp\") with matchedCount = how many scanned packages matched that indicator's commandPatterns, and packages = the matched names. Low matchedCount relative to expected prevalence suggests commandPatterns need broadening.",
"data": {
"binding.gyp": {
"matchedCount": 6,
"packages": [
"@azure/msal-node-extensions",
"@vscode/spdlog",
"ibm_db",
"node-liblzma",
"spdlog",
"v8-profiler-next"
]
},
"runtime-installer": {
"matchedCount": 3,
"packages": [
"9router",
"bun",
"v8-profiler-next"
]
},
"source-downloader": {
"matchedCount": 3,
"packages": [
"@pact-foundation/pact-node",
"bun",
"ibm_db"
]
},
"bundled-binary-installer": {
"matchedCount": 1,
"packages": [
"9router"
]
},
"gulpfile.js": {
"matchedCount": 1,
"packages": [
"axios"
]
},
"gulpfile.ts": {
"matchedCount": 1,
"packages": [
"axios"
]
},
"gulpfile.mjs": {
"matchedCount": 1,
"packages": [
"axios"
]
},
"android/build.gradle": {
"matchedCount": 1,
"packages": [
"@op-engineering/op-sqlite"
]
},
"Makefile": {
"matchedCount": 1,
"packages": [
"zlib"
]
}
}
},
"uncategorizedPackages": {
"description": "Packages that have a build signal (native compile, binary download, runtime-installer, etc.) but no existing indicator definition matched them. Each item has: name, version, weeklyDownloads, lifecycleScripts, buildDependencies, commandTokens, inferredIndicatorFiles, detectedSignals, suggestedSignal. Sorted by weeklyDownloads descending — highest-value gaps first.",
"data": [
{
"name": "whatsapp-web.js",
"version": "1.34.7",
"weeklyDownloads": 95137,
"lifecycleScripts": {
"prepare": "is-ci || husky"
},
"buildDependencies": [],
"commandTokens": [
"is-ci"
],
"inferredIndicatorFiles": [],
"suggestedSignal": null
},
{
"name": "pact-node",
"version": "1.3.8",
"weeklyDownloads": 0,
"lifecycleScripts": {
"postinstall": "node ./scripts/check-dependencies.js"
},
"buildDependencies": [],
"commandTokens": [],
"inferredIndicatorFiles": [],
"suggestedSignal": null,
"scannedFiles": [
"scripts/check-dependencies.js"
]
}
]
},
"commandPatternGaps": {
"description": "Command tokens that appear in multiple uncategorized build packages but are not covered by any existing commandPatterns entry. Each item has: token, frequency (package count), weeklyDownloadTotal, packages, suggestedCommandPattern. Sorted by frequency × downloads — entries with frequency >= 3 or weeklyDownloadTotal >= 50000 are highest priority.",
"data": []
}
}
|
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
✅ PASS — approve-scripts smoke reportAll packages with lifecycle scripts are correctly identified as pending approval.
|
✅ SUCCESS — indicator-suggestions deep scanCommand: Full scan output (stdout + stderr)Generated indicator-suggestions.json{
"$ai": {
"role": "You are an expert in the npm ecosystem, native Node.js addon build tooling, supply-chain security, and the structure of indicator-definitions.js in the npm/cli repository.",
"thisFileIs": "A dataset generated by scripts/build-indicator-suggestions.js. It scans popular npm packages for lifecycle scripts and compares them against the INDICATOR_REGISTRY in lib/utils/indicator-definitions.js. Your job is to review the data below and propose concrete improvements to that file.",
"howToReadThisFile": "Every top-level section (meta, coverage, existingDefinitionCoverage, uncategorizedPackages, commandPatternGaps) has a \"description\" field that explains what the section contains and how to interpret it, and a \"data\" field with the actual content. Read the description first, then inspect data.",
"sourceFile": "lib/utils/indicator-definitions.js",
"tasks": [
{
"priority": 1,
"task": "Review commandPatternGaps.data where frequency >= 3 OR weeklyDownloadTotal >= 50000.",
"action": "For each: decide whether the token warrants a NEW indicator entry (new build tool not yet covered) or just an additional commandPattern on an EXISTING entry. Ignore tokens that are generic JS keywords (const, require, stdio, inherit) or non-build tools (tsc, oclif, lint, rimraf)."
},
{
"priority": 2,
"task": "Review uncategorizedPackages.data sorted by weeklyDownloads descending.",
"action": "For each: examine lifecycleScripts, commandTokens, detectedSignals, and inferredIndicatorFiles. Propose a new INDICATOR_REGISTRY entry OR explain why it should not be added. Focus on packages with weeklyDownloads > 10000."
},
{
"priority": 3,
"task": "Review existingDefinitionCoverage.data entries with low matchedCount.",
"action": "Propose additional commandPatterns that would match real packages listed in the uncategorized or gap sections."
}
],
"outputFormat": {
"forNewIndicatorEntry": {
"file": "<the filename used as the registry key, e.g. \"Gruntfile.js\">",
"label": "<short human-readable label>",
"commandPatterns": [
"<regex source strings — will be compiled with new RegExp(...)>"
],
"signals": {
"onFound": [
"<signal name>"
],
"onWarning": []
},
"scannerSteps": [
"{ type: \"regex\", pattern: \"...\", group: 1, label: \"...\" }",
"{ type: \"regex-all\", pattern: \"...\", group: 1, label: \"...\" }",
"{ type: \"glob\", pattern: \"**/*.ext\", label: \"...\", maxDisplay: 20 }",
"{ type: \"signal\", pattern: \"...\", signal: \"<signal-name>\" }"
],
"addToNativeBuildCommandPattern": "<true if this tool compiles native code>",
"rationale": "<why this indicator is needed and what real packages triggered it>"
},
"forExistingEntryUpdate": {
"existingKey": "<key in INDICATOR_REGISTRY to update>",
"addCommandPatterns": [
"<new regex source strings>"
],
"rationale": "<which packages would now be matched>"
}
},
"availableSignals": [
"native-build — Compiles a native binary (.node addon, .so, .dylib) via node-gyp, CMake, Meson, Autoconf, or similar",
"rust-native — Compiles a Rust-backed native addon (Cargo.toml / neon)",
"wasm-build — Compiles a WebAssembly (.wasm) module from Rust source (wasm-bindgen)",
"wasm-load — Loads a pre-compiled WebAssembly binary at runtime via WebAssembly.instantiate/compile — content is not inspectable by static analysis",
"android-native — Android JNI/NDK native module",
"gyp-conditions — binding.gyp has platform/arch conditions (may behave differently per OS)",
"make-build — Makefile or task-runner driven build (Grunt/Gulp/Jake/Cake/Rake/Brunch/Taskfile) that can execute arbitrary shell commands",
"binary-download — Downloads a pre-built binary at install time (node-pre-gyp, prebuild-install, etc.)",
"activates-bundled-binary — Makes a file bundled inside the npm tarball executable (chmod +x / fs.chmod with execute bit)",
"runtime-installer — Installs additional packages at runtime via npm/pnpm/yarn/bun/deno/bower install as a child process",
"external-url — Fetches a URL (curl/wget/node https) at install time",
"source-downloader — Fetches source code (curl/wget/git clone) at install time",
"dynamic-require — Calls require() with a non-literal argument — the loaded module cannot be statically determined",
"obfuscation-pattern — Uses Base64/hex decoding or eval/Function() to hide executed code"
]
},
"meta": {
"description": "Run metadata: when generated, registry size limit (topN), whether cross-package import following was enabled (deepScan), and which indicator filenames are currently registered.",
"data": {
"generatedAt": "2026-06-22T04:39:10.090Z",
"topN": 100,
"deepScan": true,
"registryDefinitions": [
"binding.gyp",
"Cargo.toml",
"build.rs",
"CMakeLists.txt",
"CMakePresets.json",
"configure.ac",
"meson.build",
"android/build.gradle",
"binary-downloader",
"bundled-binary-installer",
"runtime-installer",
"source-downloader",
"build.gradle",
"Makefile",
"bower.json",
"brunch-config.js",
"Gruntfile.js",
"gulpfile.js",
"Jakefile",
"Cakefile",
"Rakefile",
"Taskfile.yml",
"gulpfile.ts",
"gulpfile.mjs",
"Gruntfile.coffee",
"Jakefile.js",
"Taskfile.yaml",
"Justfile"
]
}
},
"coverage": {
"description": "Aggregate counts for this run. totalScanned = registry pages fetched. uniqueNamesConsidered = distinct package names seen. withLifecycleScripts = had install/postinstall/etc. matchedByExistingDefinitions = covered by at least one indicator. uncategorizedBuildPackages = build signal present but no indicator matched. lifecycleOnlyNoBuildHint = lifecycle scripts with no build tool detected.",
"data": {
"totalScanned": 1000,
"uniqueNamesConsidered": 1054,
"withLifecycleScripts": 193,
"matchedByExistingDefinitions": 55,
"uncategorizedBuildPackages": 11,
"lifecycleOnlyNoBuildHint": 127
}
},
"existingDefinitionCoverage": {
"description": "Per-indicator match counts against real packages. Each entry in data is keyed by indicator filename (e.g. \"binding.gyp\") with matchedCount = how many scanned packages matched that indicator's commandPatterns, and packages = the matched names. Low matchedCount relative to expected prevalence suggests commandPatterns need broadening.",
"data": {
"bundled-binary-installer": {
"matchedCount": 16,
"packages": [
"9router",
"@aashari/mcp-server-atlassian-jira",
"@googleworkspace/cli",
"@jheavenknows/bluerouter",
"@kilocode/cli",
"@kitelev/exocortex-cli",
"@mimo-ai/cli",
"@xdevplatform/xurl",
"agent-browser",
"clang-format-node",
"cline",
"codexui-android",
"dingtalk-workspace-cli",
"opensrc",
"paqad-ai",
"wonjang-agent"
]
},
"runtime-installer": {
"matchedCount": 12,
"packages": [
"9router",
"@bike4mind/cli",
"@iola_adm/iola-cli",
"@jheavenknows/bluerouter",
"@kilocode/cli",
"@qoder-ai/qodercli",
"agent-afk",
"codeam-cli",
"hardhat-deploy",
"netlify",
"netlify-cli",
"omnius"
]
},
"source-downloader": {
"matchedCount": 9,
"packages": [
"@googleworkspace/cli",
"@infisical/cli",
"@iola_adm/iola-cli",
"@marp-team/marp-cli",
"gws-with-audit-log",
"instar",
"netlify",
"netlify-cli",
"wasm-pack"
]
},
"Cargo.toml": {
"matchedCount": 4,
"packages": [
"agent-browser",
"bun-pty",
"oh-my-codex",
"opensrc"
]
},
"binding.gyp": {
"matchedCount": 4,
"packages": [
"@bike4mind/cli",
"better-sqlite3",
"eas-cli",
"ovsx"
]
},
"gulpfile.js": {
"matchedCount": 2,
"packages": [
"axios",
"npkill"
]
},
"gulpfile.ts": {
"matchedCount": 2,
"packages": [
"axios",
"npkill"
]
},
"gulpfile.mjs": {
"matchedCount": 2,
"packages": [
"axios",
"npkill"
]
},
"binary-downloader": {
"matchedCount": 1,
"packages": [
"cloudflared"
]
},
"android/build.gradle": {
"matchedCount": 1,
"packages": [
"skykoi"
]
},
"build.gradle": {
"matchedCount": 1,
"packages": [
"skykoi"
]
},
"Makefile": {
"matchedCount": 1,
"packages": [
"zlib"
]
}
}
},
"uncategorizedPackages": {
"description": "Packages that have a build signal (native compile, binary download, runtime-installer, etc.) but no existing indicator definition matched them. Each item has: name, version, weeklyDownloads, lifecycleScripts, buildDependencies, commandTokens, inferredIndicatorFiles, detectedSignals, suggestedSignal. Sorted by weeklyDownloads descending — highest-value gaps first.",
"data": [
{
"name": "@netlify/zip-it-and-ship-it",
"version": "15.0.1",
"weeklyDownloads": 531758,
"lifecycleScripts": {
"prepack": "npm run build"
},
"buildDependencies": [],
"commandTokens": [],
"inferredIndicatorFiles": [],
"suggestedSignal": null
},
{
"name": "hugo-extended",
"version": "0.163.3",
"weeklyDownloads": 240864,
"lifecycleScripts": {
"postinstall": "node postinstall.js"
},
"buildDependencies": [],
"commandTokens": [],
"inferredIndicatorFiles": [],
"detectedSignals": [
"reads-process-env",
"requires-local-file"
],
"suggestedSignal": "reads-process-env",
"scannedFiles": [
"postinstall.js"
]
},
{
"name": "@mintlify/cli",
"version": "4.0.1235",
"weeklyDownloads": 193556,
"lifecycleScripts": {
"prepare": "npm run build"
},
"buildDependencies": [],
"commandTokens": [],
"inferredIndicatorFiles": [
"binding.gyp"
],
"suggestedSignal": "native-build"
},
{
"name": "@claude-flow/cli",
"version": "3.12.4",
"weeklyDownloads": 46522,
"lifecycleScripts": {
"postinstall": "node ./scripts/postinstall.cjs"
},
"buildDependencies": [],
"commandTokens": [],
"inferredIndicatorFiles": [],
"detectedSignals": [
"writes-file"
],
"suggestedSignal": "writes-file",
"scannedFiles": [
"scripts/postinstall.cjs"
]
},
{
"name": "@salesforce/cli-plugins-testkit",
"version": "5.3.62",
"weeklyDownloads": 38653,
"lifecycleScripts": {
"prepare": "sf-install",
"prepack": "sf-prepack"
},
"buildDependencies": [],
"commandTokens": [
"sf-install",
"sf-prepack"
],
"inferredIndicatorFiles": [],
"suggestedSignal": null
},
{
"name": "@zendesk/zcli-apps",
"version": "1.1.0",
"weeklyDownloads": 12515,
"lifecycleScripts": {
"prepack": "tsc && ../../scripts/prepack.sh"
},
"buildDependencies": [],
"commandTokens": [
"tsc"
],
"inferredIndicatorFiles": [],
"suggestedSignal": null
},
{
"name": "adhdev",
"version": "0.9.81",
"weeklyDownloads": 10868,
"lifecycleScripts": {
"preinstall": "node -e \"const major=Number.parseInt(process.versions.node.split('.')[0],10); if (process.platform === 'win32' && major >= 24) { console.error('\\n✗ ADHDev does not currently support Node.js 24+ on Windows.\\n Install Node.js 22.x on Windows, then retry.\\n'); process.exit(1); }\""
},
"buildDependencies": [],
"commandTokens": [
"const",
"major",
"number.parseint",
"process.versions.node.split",
"process.platform",
"win32",
"console.error",
"adhdev",
"does",
"not",
"currently",
"support",
"windows",
"retry"
],
"inferredIndicatorFiles": [],
"suggestedSignal": null
},
{
"name": "firebase-tools-with-isolate",
"version": "15.22.0",
"weeklyDownloads": 10385,
"lifecycleScripts": {
"prepare": "npm run clean && npm run build:publish"
},
"buildDependencies": [],
"commandTokens": [
"publish"
],
"inferredIndicatorFiles": [],
"suggestedSignal": null
},
{
"name": "@mablhq/mabl-cli",
"version": "2.115.13",
"weeklyDownloads": 9286,
"lifecycleScripts": {
"postinstall": "node ./util/postInstallMessage.js"
},
"buildDependencies": [],
"commandTokens": [
"util"
],
"inferredIndicatorFiles": [],
"detectedSignals": [
"uses-child-process",
"reads-process-env",
"requires-local-file",
"writes-file",
"file-unreadable"
],
"suggestedSignal": "uses-child-process",
"scannedFiles": [
"util/postInstallMessage.js",
"→ child_process",
"→ fs",
"→ path",
"→ chalk",
"util/agentInstallRegistry.js",
"→ os"
]
},
{
"name": "@gitlawb/openclaude",
"version": "0.19.0",
"weeklyDownloads": 7660,
"lifecycleScripts": {
"prepack": "npm run build"
},
"buildDependencies": [],
"commandTokens": [],
"inferredIndicatorFiles": [],
"suggestedSignal": null
},
{
"name": "@zendesk/zcli-co
…(truncated — 16078 chars total, see artifact for full file) |
…ge.json)
resolveRelPosix returned null for not-yet-fetched files that already carry
a file extension (e.g. .json, .ts, .yaml), causing the caller's else branch
to append '.js' and try to fetch 'package.json.js' -- which always 404s.
Two-part fix:
- resolveRelPosix: after the extension-probe loop, if absPath already has a
non-empty extension and is within pkgCacheDir, return the bare relPosix
immediately so the caller can fetch it rather than falling through to null
- fetchWithRefs else-branch: check path.extname(rel) and use the path as-is
when it already has an extension; only append .js for bare specifiers
This means require('../package.json') (and similar .ts/.yaml/.json refs)
will now be correctly fetched and defanged in the deep scan cache.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
With 5 concurrent workers, completion lines appear after the work
finishes -- a hung package shows nothing. Adding a 'fetching pkg@ver...'
and 'scanning pkg@ver...' line before each call means the last
unmatched line identifies the problematic package.
Output pattern in DeepScan:
scanning opencv-build@0.1.9...
opencv-build@0.1.9 -- 1 indicator(s)
scanning magic-comments-loader@3.0.0...
magic-comments-loader@3.0.0
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add UNQUOTED_URL_RE to findExternalUrls so shell scripts with unquoted URLs (e.g. curl https://...) emit the external-url signal - Add external-url detection to detectSignals (bare URL check) - Add maxDepth option to scanPackageScripts so callers can override the recursion cap; update test to pass maxDepth:3 instead of relying on the global MAX_DEPTH constant - Add maxFiles option to scanPackageScripts + scanFile to cap BFS at MAX_FILES_DEEP_SCAN=100 per package in deep analysis - Add maybeSignalFilesLimitReached helper that appends 'files-limit-reached' signal when the cap is hit - In build-indicator-suggestions: add NODE_BUILTIN_MODULES set and isValidNpmPackageName validator; skip both in fetchBarePackage and in deepAnalyzePackage's fetchedPkgs loop to prevent scanning Node built-ins and invalid names like 'LLM_TENSOR_NAMES' (false positives from JS comments matching BARE_IMPORT_FROM_RE) - Skip self-referencing packages in deepAnalyzePackage loop to avoid double-scanning the same 258-file bundle (node-llama-cpp hang fix) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lock kill Deep cache directories now use name@version (e.g. node-llama-cpp@3.18.1/) instead of name-only dirs. Benefits: - Different versions of the same package get isolated cache entries - Cache validity check no longer needs to compare version field in meta (the dir name already encodes it); hashDirTree still guards against content changes - same package@v1 and package@v2 can coexist as separate manifests fetchBarePackage stores versioned entries in fetchedPkgs (name@version strings) so deepAnalyzePackage can locate the correct versioned dir via parseDeepPkgEntry(). Old-format bare-name entries are silently skipped. Allow same package different versions in manifests/candidates: - inStore now keys on name@version not just name - --add flag supports name@version syntax (e.g. --add react@17,react@18) and resolves them via the registry at the specified version tag - getPackageManifest() parses optional @Version suffix from the input name Lock file zombie-kill: when a stale heartbeat lock belongs to a PID that is still alive (hung process), send SIGTERM + SIGKILL instead of silently overwriting the lock — prevents two concurrent instances racing over the same cache files. Also adds 256 KB file-size guard for fetchedPkgs bare-dep scans to skip compiler bundles like typescript.js (9 MB) that would block the event loop. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…s for packages.json Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ointing guard, always-resolve bareFollows - Remove if (!fromCache) guard on follow-resolution: cached packages must still have their bareFollows resolved so discovered deps are not lost when a run is interrupted after file-fetch but before the checkpoint saved them. - Add esolvedFollows field to deep-scan meta (.meta.json): after resolving a package's bare follows via unpkg, the resulting name@version list is written back to meta. Subsequent cache-hits use this list directly, skipping the HTTP round-trips entirely. - Add isCheckpointing flag in DrainMode.DeepFetch: prevents two workers from racing to write the checkpoint files concurrently. If a save is already in flight the second call is a no-op; the in-flight save will capture any state added before its snapshot was taken (savePackageCache builds its snapshot synchronously before any await). - Final checkpoint() call after workers complete is unconditional (guard is always false at that point) so the last batch of candidates is always saved. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Packages discovered via deep-scan require() follows are added to candidates
as 'name@version' strings — they never appear in npm search result pages so
searchDownloads.get() always returns undefined, leaving weeklyDownloads: 0.
When weekly is not in the search-result cache, fetch it from
api.npmjs.org/downloads/point/last-week/{name} inline during the Candidates
drain. Failure is non-critical (weeklyDownloads stays 0) so deep-scan
packages still appear in the output even if the downloads API is down.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
✅ PASS — approve-scripts smoke reportAll packages with lifecycle scripts are correctly identified as pending approval.
|
✅ SUCCESS — indicator-suggestions deep scanCommand: Full scan output (stdout + stderr)Generated indicator-suggestions.json{
"$ai": {
"role": "You are an expert in the npm ecosystem, native Node.js addon build tooling, supply-chain security, and the structure of indicator-definitions.js in the npm/cli repository.",
"thisFileIs": "A dataset generated by scripts/build-indicator-suggestions.js. It scans popular npm packages for lifecycle scripts and compares them against the INDICATOR_REGISTRY in lib/utils/indicator-definitions.js. Your job is to review the data below and propose concrete improvements to that file.",
"howToReadThisFile": "Every top-level section (meta, coverage, existingDefinitionCoverage, uncategorizedPackages, commandPatternGaps) has a \"description\" field that explains what the section contains and how to interpret it, and a \"data\" field with the actual content. Read the description first, then inspect data.",
"sourceFile": "lib/utils/indicator-definitions.js",
"tasks": [
{
"priority": 1,
"task": "Review commandPatternGaps.data where frequency >= 3 OR weeklyDownloadTotal >= 50000.",
"action": "For each: decide whether the token warrants a NEW indicator entry (new build tool not yet covered) or just an additional commandPattern on an EXISTING entry. Ignore tokens that are generic JS keywords (const, require, stdio, inherit) or non-build tools (tsc, oclif, lint, rimraf)."
},
{
"priority": 2,
"task": "Review uncategorizedPackages.data sorted by weeklyDownloads descending.",
"action": "For each: examine lifecycleScripts, commandTokens, detectedSignals, and inferredIndicatorFiles. Propose a new INDICATOR_REGISTRY entry OR explain why it should not be added. Focus on packages with weeklyDownloads > 10000."
},
{
"priority": 3,
"task": "Review existingDefinitionCoverage.data entries with low matchedCount.",
"action": "Propose additional commandPatterns that would match real packages listed in the uncategorized or gap sections."
}
],
"outputFormat": {
"forNewIndicatorEntry": {
"file": "<the filename used as the registry key, e.g. \"Gruntfile.js\">",
"label": "<short human-readable label>",
"commandPatterns": [
"<regex source strings — will be compiled with new RegExp(...)>"
],
"signals": {
"onFound": [
"<signal name>"
],
"onWarning": []
},
"scannerSteps": [
"{ type: \"regex\", pattern: \"...\", group: 1, label: \"...\" }",
"{ type: \"regex-all\", pattern: \"...\", group: 1, label: \"...\" }",
"{ type: \"glob\", pattern: \"**/*.ext\", label: \"...\", maxDisplay: 20 }",
"{ type: \"signal\", pattern: \"...\", signal: \"<signal-name>\" }"
],
"addToNativeBuildCommandPattern": "<true if this tool compiles native code>",
"rationale": "<why this indicator is needed and what real packages triggered it>"
},
"forExistingEntryUpdate": {
"existingKey": "<key in INDICATOR_REGISTRY to update>",
"addCommandPatterns": [
"<new regex source strings>"
],
"rationale": "<which packages would now be matched>"
}
},
"availableSignals": [
"native-build — Compiles a native binary (.node addon, .so, .dylib) via node-gyp, CMake, Meson, Autoconf, or similar",
"rust-native — Compiles a Rust-backed native addon (Cargo.toml / neon)",
"wasm-build — Compiles a WebAssembly (.wasm) module from Rust source (wasm-bindgen)",
"wasm-load — Loads a pre-compiled WebAssembly binary at runtime via WebAssembly.instantiate/compile — content is not inspectable by static analysis",
"android-native — Android JNI/NDK native module",
"gyp-conditions — binding.gyp has platform/arch conditions (may behave differently per OS)",
"make-build — Makefile or task-runner driven build (Grunt/Gulp/Jake/Cake/Rake/Brunch/Taskfile) that can execute arbitrary shell commands",
"binary-download — Downloads a pre-built binary at install time (node-pre-gyp, prebuild-install, etc.)",
"activates-bundled-binary — Makes a file bundled inside the npm tarball executable (chmod +x / fs.chmod with execute bit)",
"runtime-installer — Installs additional packages at runtime via npm/pnpm/yarn/bun/deno/bower install as a child process",
"external-url — Fetches a URL (curl/wget/node https) at install time",
"source-downloader — Fetches source code (curl/wget/git clone) at install time",
"dynamic-require — Calls require() with a non-literal argument — the loaded module cannot be statically determined",
"obfuscation-pattern — Uses Base64/hex decoding or eval/Function() to hide executed code"
]
},
"meta": {
"description": "Run metadata: when generated, registry size limit (topN), whether cross-package import following was enabled (deepScan), and which indicator filenames are currently registered.",
"data": {
"generatedAt": "2026-06-22T11:56:33.302Z",
"topN": 100,
"deepScan": true,
"registryDefinitions": [
"binding.gyp",
"Cargo.toml",
"build.rs",
"CMakeLists.txt",
"CMakePresets.json",
"configure.ac",
"meson.build",
"android/build.gradle",
"binary-downloader",
"bundled-binary-installer",
"runtime-installer",
"source-downloader",
"build.gradle",
"Makefile",
"bower.json",
"brunch-config.js",
"Gruntfile.js",
"gulpfile.js",
"Jakefile",
"Cakefile",
"Rakefile",
"Taskfile.yml",
"gulpfile.ts",
"gulpfile.mjs",
"Gruntfile.coffee",
"Jakefile.js",
"Taskfile.yaml",
"Justfile"
]
}
},
"coverage": {
"description": "Aggregate counts for this run. totalScanned = registry pages fetched. uniqueNamesConsidered = distinct package names seen. withLifecycleScripts = had install/postinstall/etc. matchedByExistingDefinitions = covered by at least one indicator. uncategorizedBuildPackages = build signal present but no indicator matched. lifecycleOnlyNoBuildHint = lifecycle scripts with no build tool detected.",
"data": {
"totalScanned": 500,
"uniqueNamesConsidered": 564,
"withLifecycleScripts": 126,
"matchedByExistingDefinitions": 91,
"uncategorizedBuildPackages": 0,
"lifecycleOnlyNoBuildHint": 35
}
},
"existingDefinitionCoverage": {
"description": "Per-indicator match counts against real packages. Each entry in data is keyed by indicator filename (e.g. \"binding.gyp\") with matchedCount = how many scanned packages matched that indicator's commandPatterns, and packages = the matched names. Low matchedCount relative to expected prevalence suggests commandPatterns need broadening.",
"data": {
"android/build.gradle": {
"matchedCount": 33,
"packages": [
"@callstack/react-native-brownfield",
"@capacitor-community/admob",
"@capacitor-community/camera-preview",
"@capacitor-community/http",
"@capacitor/background-runner",
"@capacitor/inappbrowser",
"@codetrix-studio/capacitor-google-auth",
"@gluestack-style/animation-resolver",
"@gluestack-style/legend-motion-animation-driver",
"@legendapp/motion",
"@newrelic/newrelic-capacitor-plugin",
"@react-navigation/native-stack",
"@revenuecat/purchases-capacitor",
"@revenuecat/purchases-capacitor-ui",
"@sentry/capacitor",
"capacitor-branch-deep-links",
"expo-native-emojis-popup",
"expo-native-sheet-emojis",
"llama-cpp-capacitor",
"react-native-circular-progress-indicator",
"react-native-fast-crypto",
"react-native-haptic-feedback",
"react-native-image-picker",
"react-native-markdown-renderer",
"react-native-navigation-mode",
"react-native-network-logger",
"react-native-pdf-jsi",
"react-native-rate-app",
"react-native-reanimated",
"react-native-restart-newarch",
"react-native-scanbot-sdk",
"react-native-screenguard",
"react-native-worklets"
]
},
"binding.gyp": {
"matchedCount": 26,
"packages": [
"@discordjs/opus",
"@figma/nodegit",
"@jasonscheirer/native-progress-bar",
"@samyok/annoy",
"@sentry-internal/node-native-stacktrace",
"@sentry/node-native-stacktrace",
"annoy",
"cf-prefs",
"lru-native2",
"native-hdr-histogram",
"native-machine-id",
"native-progress-bar",
"native-reg",
"node-liblzma",
"node-mac-permissions",
"node-window-manager",
"nodegit",
"raknet-native",
"ref",
"ref-napi",
"rsa-keygen",
"segfault-handler",
"socketcan",
"unsafe-pointer",
"zmq",
"zstd-napi"
]
},
"build.gradle": {
"matchedCount": 12,
"packages": [
"@capacitor-community/admob",
"@capacitor-community/camera-preview",
"@capacitor/background-runner",
"@capacitor/inappbrowser",
"@newrelic/newrelic-capacitor-plugin",
"@revenuecat/purchases-capacitor",
"@revenuecat/purchases-capacitor-ui",
"@sentry/capacitor",
"llama-cpp-capacitor",
"react-native-pdf-jsi",
"react-native-reanimated",
"react-native-worklets"
]
},
"CMakeLists.txt": {
"matchedCount": 5,
"packages": [
"llama-cpp-capacitor",
"raknet-native",
"react-native-reanimated",
"react-native-worklets",
"zeromq"
]
},
"CMakePresets.json": {
"matchedCount": 5,
"packages": [
"llama-cpp-capacitor",
"raknet-native",
"react-native-reanimated",
"react-native-worklets",
"zeromq"
]
},
"runtime-installer": {
"matchedCount": 3,
"packages": [
"9router",
"@sentry/capacitor",
"raknet-native"
]
},
"Makefile": {
"matchedCount": 3,
"packages": [
"@d-fischer/cross-fetch",
"annoy",
"zmq"
]
},
"Cargo.toml": {
"matchedCount": 2,
"packages": [
"@kreuzberg/node",
"bun-pty"
]
},
"bundled-binary-installer": {
"matchedCount": 1,
"packages": [
"9router"
]
},
"source-downloader": {
"matchedCount": 1,
"packages": [
"raknet-native"
]
}
}
},
"uncategorizedPackages": {
"description": "Packages that have a build signal (native compile, binary download, runtime-installer, etc.) but no existing indicator definition matched them. Each item has: name, version, weeklyDownloads, lifecycleScripts, buildDependencies, commandTokens, inferredIndicatorFiles, detectedSignals, suggestedSignal. Sorted by weeklyDownloads descending — highest-value gaps first.",
"data": []
},
"commandPatternGaps": {
"description": "Command tokens that appear in multiple uncategorized build packages but are not covered by any existing commandPatterns entry. Each item has: token, frequency (package count), weeklyDownloadTotal, packages, suggestedCommandPattern. Sorted by frequency × downloads — entries with frequency >= 3 or weeklyDownloadTotal >= 50000 are highest priority.",
"data": []
}
}
|
savePackageCache now sorts the packages array by name then version before writing so file diffs are stable across runs (insertions no longer cause unrelated line churn). indicator-suggestions-demo.yml: - Uploads indicator-suggestions.packages.json as an artifact alongside indicator-suggestions.json and scan-output.txt. - Adds a collapsible packages.json section (first 10k chars) to the PR comment body so reviewers can inspect the manifest store inline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
✅ PASS — approve-scripts smoke reportAll packages with lifecycle scripts are correctly identified as pending approval.
|
✅ SUCCESS — indicator-suggestions deep scanCommand: Full scan output (stdout + stderr)Generated indicator-suggestions.json{
"$ai": {
"role": "You are an expert in the npm ecosystem, native Node.js addon build tooling, supply-chain security, and the structure of indicator-definitions.js in the npm/cli repository.",
"thisFileIs": "A dataset generated by scripts/build-indicator-suggestions.js. It scans popular npm packages for lifecycle scripts and compares them against the INDICATOR_REGISTRY in lib/utils/indicator-definitions.js. Your job is to review the data below and propose concrete improvements to that file.",
"howToReadThisFile": "Every top-level section (meta, coverage, existingDefinitionCoverage, uncategorizedPackages, commandPatternGaps) has a \"description\" field that explains what the section contains and how to interpret it, and a \"data\" field with the actual content. Read the description first, then inspect data.",
"sourceFile": "lib/utils/indicator-definitions.js",
"tasks": [
{
"priority": 1,
"task": "Review commandPatternGaps.data where frequency >= 3 OR weeklyDownloadTotal >= 50000.",
"action": "For each: decide whether the token warrants a NEW indicator entry (new build tool not yet covered) or just an additional commandPattern on an EXISTING entry. Ignore tokens that are generic JS keywords (const, require, stdio, inherit) or non-build tools (tsc, oclif, lint, rimraf)."
},
{
"priority": 2,
"task": "Review uncategorizedPackages.data sorted by weeklyDownloads descending.",
"action": "For each: examine lifecycleScripts, commandTokens, detectedSignals, and inferredIndicatorFiles. Propose a new INDICATOR_REGISTRY entry OR explain why it should not be added. Focus on packages with weeklyDownloads > 10000."
},
{
"priority": 3,
"task": "Review existingDefinitionCoverage.data entries with low matchedCount.",
"action": "Propose additional commandPatterns that would match real packages listed in the uncategorized or gap sections."
}
],
"outputFormat": {
"forNewIndicatorEntry": {
"file": "<the filename used as the registry key, e.g. \"Gruntfile.js\">",
"label": "<short human-readable label>",
"commandPatterns": [
"<regex source strings — will be compiled with new RegExp(...)>"
],
"signals": {
"onFound": [
"<signal name>"
],
"onWarning": []
},
"scannerSteps": [
"{ type: \"regex\", pattern: \"...\", group: 1, label: \"...\" }",
"{ type: \"regex-all\", pattern: \"...\", group: 1, label: \"...\" }",
"{ type: \"glob\", pattern: \"**/*.ext\", label: \"...\", maxDisplay: 20 }",
"{ type: \"signal\", pattern: \"...\", signal: \"<signal-name>\" }"
],
"addToNativeBuildCommandPattern": "<true if this tool compiles native code>",
"rationale": "<why this indicator is needed and what real packages triggered it>"
},
"forExistingEntryUpdate": {
"existingKey": "<key in INDICATOR_REGISTRY to update>",
"addCommandPatterns": [
"<new regex source strings>"
],
"rationale": "<which packages would now be matched>"
}
},
"availableSignals": [
"native-build — Compiles a native binary (.node addon, .so, .dylib) via node-gyp, CMake, Meson, Autoconf, or similar",
"rust-native — Compiles a Rust-backed native addon (Cargo.toml / neon)",
"wasm-build — Compiles a WebAssembly (.wasm) module from Rust source (wasm-bindgen)",
"wasm-load — Loads a pre-compiled WebAssembly binary at runtime via WebAssembly.instantiate/compile — content is not inspectable by static analysis",
"android-native — Android JNI/NDK native module",
"gyp-conditions — binding.gyp has platform/arch conditions (may behave differently per OS)",
"make-build — Makefile or task-runner driven build (Grunt/Gulp/Jake/Cake/Rake/Brunch/Taskfile) that can execute arbitrary shell commands",
"binary-download — Downloads a pre-built binary at install time (node-pre-gyp, prebuild-install, etc.)",
"activates-bundled-binary — Makes a file bundled inside the npm tarball executable (chmod +x / fs.chmod with execute bit)",
"runtime-installer — Installs additional packages at runtime via npm/pnpm/yarn/bun/deno/bower install as a child process",
"external-url — Fetches a URL (curl/wget/node https) at install time",
"source-downloader — Fetches source code (curl/wget/git clone) at install time",
"dynamic-require — Calls require() with a non-literal argument — the loaded module cannot be statically determined",
"obfuscation-pattern — Uses Base64/hex decoding or eval/Function() to hide executed code"
]
},
"meta": {
"description": "Run metadata: when generated, registry size limit (topN), whether cross-package import following was enabled (deepScan), and which indicator filenames are currently registered.",
"data": {
"generatedAt": "2026-06-22T12:06:12.539Z",
"topN": 100,
"deepScan": true,
"registryDefinitions": [
"binding.gyp",
"Cargo.toml",
"build.rs",
"CMakeLists.txt",
"CMakePresets.json",
"configure.ac",
"meson.build",
"android/build.gradle",
"binary-downloader",
"bundled-binary-installer",
"runtime-installer",
"source-downloader",
"build.gradle",
"Makefile",
"bower.json",
"brunch-config.js",
"Gruntfile.js",
"gulpfile.js",
"Jakefile",
"Cakefile",
"Rakefile",
"Taskfile.yml",
"gulpfile.ts",
"gulpfile.mjs",
"Gruntfile.coffee",
"Jakefile.js",
"Taskfile.yaml",
"Justfile"
]
}
},
"coverage": {
"description": "Aggregate counts for this run. totalScanned = registry pages fetched. uniqueNamesConsidered = distinct package names seen. withLifecycleScripts = had install/postinstall/etc. matchedByExistingDefinitions = covered by at least one indicator. uncategorizedBuildPackages = build signal present but no indicator matched. lifecycleOnlyNoBuildHint = lifecycle scripts with no build tool detected.",
"data": {
"totalScanned": 1000,
"uniqueNamesConsidered": 1030,
"withLifecycleScripts": 157,
"matchedByExistingDefinitions": 27,
"uncategorizedBuildPackages": 1,
"lifecycleOnlyNoBuildHint": 129
}
},
"existingDefinitionCoverage": {
"description": "Per-indicator match counts against real packages. Each entry in data is keyed by indicator filename (e.g. \"binding.gyp\") with matchedCount = how many scanned packages matched that indicator's commandPatterns, and packages = the matched names. Low matchedCount relative to expected prevalence suggests commandPatterns need broadening.",
"data": {
"gulpfile.js": {
"matchedCount": 4,
"packages": [
"axios",
"easymde",
"microsoft-cognitiveservices-speech-sdk",
"stackdriver-errors-js"
]
},
"gulpfile.ts": {
"matchedCount": 4,
"packages": [
"axios",
"easymde",
"microsoft-cognitiveservices-speech-sdk",
"stackdriver-errors-js"
]
},
"gulpfile.mjs": {
"matchedCount": 4,
"packages": [
"axios",
"easymde",
"microsoft-cognitiveservices-speech-sdk",
"stackdriver-errors-js"
]
},
"binding.gyp": {
"matchedCount": 3,
"packages": [
"c15t",
"tree-sitter-javascript",
"weak-napi"
]
},
"android/build.gradle": {
"matchedCount": 3,
"packages": [
"@bugsnag/react-native",
"@gluestack-style/react",
"@likashefqet/react-native-image-zoom"
]
},
"runtime-installer": {
"matchedCount": 2,
"packages": [
"9router",
"appium"
]
},
"source-downloader": {
"matchedCount": 2,
"packages": [
"@tsparticles/engine",
"appium"
]
},
"bundled-binary-installer": {
"matchedCount": 1,
"packages": [
"9router"
]
},
"Makefile": {
"matchedCount": 1,
"packages": [
"dependency-cruiser"
]
},
"binary-downloader": {
"matchedCount": 1,
"packages": [
"appium"
]
},
"Gruntfile.js": {
"matchedCount": 1,
"packages": [
"@joint/core"
]
},
"Gruntfile.coffee": {
"matchedCount": 1,
"packages": [
"@joint/core"
]
}
}
},
"uncategorizedPackages": {
"description": "Packages that have a build signal (native compile, binary download, runtime-installer, etc.) but no existing indicator definition matched them. Each item has: name, version, weeklyDownloads, lifecycleScripts, buildDependencies, commandTokens, inferredIndicatorFiles, detectedSignals, suggestedSignal. Sorted by weeklyDownloads descending — highest-value gaps first.",
"data": [
{
"name": "@azure/static-web-apps-cli",
"version": "2.0.9",
"weeklyDownloads": 102539,
"lifecycleScripts": {
"prepare": "npm run build"
},
"buildDependencies": [],
"commandTokens": [],
"inferredIndicatorFiles": [],
"suggestedSignal": null,
"scannedFiles": [
"package.json"
]
}
]
},
"commandPatternGaps": {
"description": "Command tokens that appear in multiple uncategorized build packages but are not covered by any existing commandPatterns entry. Each item has: token, frequency (package count), weeklyDownloadTotal, packages, suggestedCommandPattern. Sorted by frequency × downloads — entries with frequency >= 3 or weeklyDownloadTotal >= 50000 are highest priority.",
"data": []
}
}
Generated indicator-suggestions.packages.json (manifest store){
"generatedAt": "2026-06-22T12:06:12.517Z",
"count": 157,
"discoveryState": {
"keywordCursors": {
"keywords:javascript": {
"from": 1000,
"startedAt": "2026-06-22T12:04:07.826Z",
"scannedAt": "2026-06-22T12:05:05.347Z"
}
}
},
"seenOnlyNames": [
"tslib",
"typescript",
"@babel/parser",
"jsesc",
"espree",
"@typescript-eslint/typescript-estree",
"@typescript-eslint/parser",
"esquery",
"@eslint/js",
"prelude-ls",
"eslint",
"fdir",
"diff",
"human-signals",
"esprima",
"typescript-eslint",
"serialize-javascript",
"@inquirer/type",
"uglify-js",
"inquirer",
"@inquirer/core",
"regexpu-core",
"@inquirer/figures",
"@webassemblyjs/wasm-parser",
"@webassemblyjs/wast-printer",
"regenerate",
"@webassemblyjs/ast",
"@inquirer/confirm",
"embla-carousel",
"embla-carousel-reactive-utils",
"embla-carousel-react",
"@xmldom/xmldom",
"@inquirer/external-editor",
"reflect-metadata",
"ast-types-flow",
"@inquirer/ansi",
"@inquirer/prompts",
"enquirer",
"@inquirer/password",
"environment",
"@inquirer/checkbox",
"@inquirer/select",
"@inquirer/rawlist",
"cluster-key-slot",
"@inquirer/number",
"@inquirer/input",
"@azure/abort-controller",
"@inquirer/expand",
"@supabase/storage-js",
"has-values",
"@inquirer/search",
"@inquirer/editor",
"is-reference",
"@supabase/supabase-js",
"@supabase/realtime-js",
"jquery",
"redis-parser",
"is-what",
"@jsonjoy.com/codegen",
"oxc-parser",
"@formatjs/ecma402-abstract",
"redis-errors",
"@speed-highlight/core",
"@azure/logger",
"@oxc-parser/binding-linux-x64-gnu",
"ext",
"@azure/core-lro",
"canvg",
"three",
"openid-client",
"jpeg-js",
"oauth4webapi",
"oxlint",
"remark-mdx",
"knip",
"micromark-extension-mdx-expression",
"micromark-extension-mdx-jsx",
"@pmmmwh/react-refresh-webpack-plugin",
"pdf-lib",
"@typescript/native-preview",
"memoizerific",
"strip-comments",
"css-has-pseudo",
"@oxc-parser/binding-linux-x64-musl",
"postcss-focus-visible",
"eslint-config-airbnb-base",
"hast-util-to-estree",
"gray-matter",
"tsyringe",
"@azure/storage-blob",
"oxfmt",
"css-blank-pseudo",
"goober",
"@juggle/resize-observer",
"@oxlint/binding-linux-x64-gnu",
"@typescript/native-preview-linux-x64",
"fuzzysort",
"array-slice",
"map-or-similar",
"cardinal",
"@oxfmt/binding-linux-x64-gnu",
"recma-build-jsx",
"recma-parse",
"recma-stringify",
"javascript-natural-sort",
"babylon",
"objectorarray",
"rehype-recma",
"recma-jsx",
"object.defaults",
"oxc-transform",
"@swc/cli",
"is-expression",
"js-string-escape",
"bmp-js",
"resedit",
"@webassemblyjs/wast-parser",
"karma",
"html-minifier",
"editions",
"pe-library",
"acorn-node",
"js2xmlparser",
"@hey-api/openapi-ts",
"gifwrap",
"eslint-config-airbnb",
"mark.js",
"@oxc-transform/binding-linux-x64-gnu",
"detect-gpu",
"js-library-detector",
"jsdoc",
"reserved-identifiers",
"@appium/support",
"@appium/schema",
"@appium/types",
"blueimp-md5",
"@oxlint/binding-linux-x64-musl",
"material-colors",
"has-symbol-support-x",
"cloudevents",
"inversify",
"has-to-string-tag-x",
"@hey-api/codegen-core",
"embla-carousel-autoplay",
"to-valid-identifier",
"pdfmake",
"fastestsmallesttextencoderdecoder",
"browserify",
"@promptbook/utils",
"eol",
"@oxfmt/binding-linux-x64-musl",
"tsparticles",
"dash-ast",
"bmp-ts",
"xmldom",
"@oxc-parser/binding-linux-arm64-gnu",
"@appium/tsconfig",
"@tsparticles/confetti",
"redux-saga",
"notistack",
"@tsparticles/vue3",
"@tsparticles/angular",
"@appium/logger",
"@tsparticles/particles",
"@tsparticles/fireworks",
"flatpickr",
"tesseract.js-core",
"@tsparticles/basic",
"@tsparticles/vue2",
"@tsparticles/slim",
"cropperjs",
"@oxc-transform/binding-linux-x64-musl",
"inquirer-autocomplete-prompt",
"@tsparticles/svelte",
"meriyah",
"coffeescript",
"imask",
"get-assigned-identifiers",
"@microsoft/dynamicproto-js",
"oxc-minify",
"@lottiefiles/dotlottie-web",
"array-initial",
"es5-shim",
"enzyme",
"merge-anything",
"js-file-download",
"@redux-saga/core",
"reserved",
"array-last",
"jks-js",
"@tsparticles/all",
"@lingui/message-utils",
"@inversifyjs/core",
"undeclared-identifiers",
"@appium/base-driver",
"@oxc-minify/binding-linux-x64-gnu",
"html2pdf.js",
"@oxc-parser/binding-linux-arm64-musl",
"@lottiefiles/dotlottie-react",
"gradle-to-js",
"@vimeo/player",
"@bugsnag/js",
"@oxc-parser/binding-darwin-arm64",
"@inversifyjs/common",
"weakmap-polyfill",
"@nevware21/ts-utils",
"@assistant-ui/react",
"@inversifyjs/reflect-metadata-utils",
"@stitches/core",
"tocbot",
"@stitches/react",
"tinymce",
"@oxc-parser/binding-darwin-x64",
"@appium/base-plugin",
"@appium/docutils",
"@probe.gl/stats",
"@typescript/native-preview-linux-arm64",
"@math.gl/core",
"@gsap/react",
"@nevware21/ts-async",
"js-binary-schema-parser",
"@math.gl/web-mercator",
"e2b",
"gifuct-js",
"heimdalljs",
"broccoli-funnel",
"@math.gl/types",
"array.prototype.toreversed",
"@probe.gl/log",
"intl-tel-input",
"@probe.gl/env",
"@oxc-parser/binding-win32-x64-msvc",
"is-string-blank",
"@oxlint/binding-linux-arm64-gnu",
"@oxc-minify/binding-linux-x64-musl",
"@mikro-orm/core",
"@azure/monitor-opentelemetry-exporter",
"@tracetail/js",
"heimdalljs-logger",
"v8n",
"react-phone-input-2",
"@oxc-parser/binding-linux-arm-gnueabihf",
"bwip-js",
"serialize-to-js",
"@oxc-parser/binding-linux-riscv64-gnu",
"embla-carousel-fade",
"bitcoinjs-lib",
"@oxc-parser/binding-wasm32-wasi",
"@oxc-parser/binding-android-arm64",
"@appium/strongbox",
"@oxc-parser/binding-win32-arm64-msvc",
"lokijs",
"@math.gl/polygon",
"@oxc-parser/binding-freebsd-x64",
"@oxc-parser/binding-win32-ia32-msvc",
"@oxc-parser/binding-linux-s390x-gnu",
"@oxc-parser/binding-linux-arm-musleabihf",
"@oxlint/binding-darwin-arm64",
"@azure/monitor-opentelemetry",
"@oxc-parser/binding-linux-riscv64-musl",
"@oxc-parser/binding-openharmony-arm64",
"@math.gl/sun",
"@oxc-parser/binding-android-arm-eabi",
"@mikro-orm/postgresql",
"mikro-orm",
"@oxfmt/binding-linux-arm64-gnu",
"broccoli-babel-transpiler",
"@typescript/native-preview-darwin-arm64",
"babel-extract-comments",
"@pact-foundation/pact",
"@oxc-parser/binding-linux-ppc64-gnu",
"@tsparticles/ribbons",
"wicked-good-xpath",
"is-identifier",
"identifier-regex",
"react-select-event",
"pdf2json",
"flowbite",
"@inversifyjs/prototype-utils",
"blueimp-canvas-to-blob",
"react-hot-loader",
"@mikro-orm/knex",
"rollbar",
"@oxlint/binding-win32-x64-msvc",
"embla-carousel-auto-scroll",
"jira.js",
"@azure/storage-queue",
"embla-carousel-vue",
"@oxlint/binding-linux-arm64-musl",
"@inversifyjs/container",
"@thednp/shorty",
"@typescript/native-preview-darwin-x64",
"apollo-client",
"@oxfmt/binding-darwin-arm64",
"postcss-functions",
"@splitsoftware/splitio-commons",
"@typescript/native-preview-win32-x64",
"@oxlint/binding-darwin-x64",
"@deepgram/captions",
"@mikro-orm/migrations",
"guess-json-indent",
"payload",
"optional-js",
"@math.gl/culling",
"string-byte-slice",
"string-byte-length",
"@inversifyjs/plugin",
"@oxc-transform/binding-linux-arm64-gnu",
"embla-carousel-auto-height",
"@mikro-orm/cli",
"embla-carousel-class-names",
"@splitsoftware/splitio",
"truncate-json",
"karma-sourcemap-loader",
"inquirer-autocomplete-standalone",
"@math.gl/geospatial",
"@copilotkit/shared",
"@typescript/native-preview-win32-arm64",
"@oxc-transform/binding-darwin-arm64",
"fontless",
"@netlify/config",
"get-object",
"flow-bin",
"js-image-generator",
"@oxfmt/binding-win32-x64-msvc",
"ts-key-enum",
"@tsparticles/updater-opacity",
"@oxlint/binding-win32-arm64-msvc",
"blueimp-load-image",
"scope-analyzer",
"sherif",
"@tsparticles/shape-circle",
"@tsparticles/plugin-hex-color",
"@tinyhttp/type-is",
"froala-editor",
"flowbite-datepicker",
"@phun-ky/typeof",
"@maskito/core",
"broccoli-caching-writer",
"@tsparticles/shape-emoji",
"react-svg",
"sa-sdk-javascript",
"@oxc-transform/binding-win32-x64-msvc",
"@tsparticles/updater-size",
"fallow",
"@tsparticles/updater-out-modes",
"matter-js",
"@bufbuild/cel",
"@oxc-transform/binding-linux-arm64-musl",
"sherif-linux-x64",
"@tsparticles/updater-life",
"@tsparticles/updater-rotate",
"@blocknote/core",
"@tsparticles/shape-polygon",
"@oxfmt/binding-darwin-x64",
"@tsparticles/shape-star",
"@tsparticles/shape-image",
"react-native-size-matters",
"@oxfmt/binding-linux-arm64-musl",
"cleave.js",
"@amcharts/amcharts5",
"@tanem/svg-injector",
"buble",
"@tsparticles/plugin-hsl-color",
"@tsparticles/plugin-rgb-color",
"gulp-babel",
"subtag",
"@copilotkit/runtime-client-gql",
"@tsparticles/shape-square",
"@bufbuild/cel-spec",
"deslop-js",
"@blocknote/react",
"wgsl_reflect",
"xmldom-qsa",
"medium-zoom",
"markerjs2",
"@oxf
…(truncated — 301139 chars total, see artifact for full file) |
Remove inline per-package download fetch from the Candidates worker. Packages not found in searchDownloads are now pushed to manifests with state:'lifecycle' and weeklyDownloads:0 immediately; a dedicated Downloads drain resolves their counts in one pass at the end. DrainMode.Downloads: - Filters manifests where state === 'lifecycle' (no search-result count) - Non-scoped packages batched up to 128 per request using the npm bulk downloads API (comma-separated names in the URL path) - Scoped packages fetched individually (bulk endpoint doesn't support them) - Both paths use fetchJson — redirect-following and 429 back-off included - Single-name vs multi-name response shape handled (flat vs nested object) - Saves checkpoint after resolution drain(DrainMode.Downloads) is called once after all search/deep BFS loops complete and before the final savePackageCache, keeping the hot candidate- drain loop free of extra HTTP round-trips. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Names with 2+ dots in the package portion look like domain names (registry.npmjs.org, cdn.example.com) not npm packages. Legitimate packages with dots (core.js, socket.io, highlight.js) have at most one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Stale meta.json files written before the hostname-rejection fix could still contain invalid entries (e.g. registry.npmjs.org@1.0.1) in resolvedFollows. The fast-path now parses name@version, checks isValidNpmPackageName(name), and skips NODE_BUILTIN_MODULES — same gates as the slow-path. Also add [scan] progress lines before scanPackageScripts and scanBuildIndicatorsForPackage in deepAnalyzePackage so a stalled package is immediately visible in stderr instead of silently blocking a worker. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Header now says 'checking N packages against M indicators' (M = 28) - Per-package pre-line changed from 'scanning' to 'checking' - deepAnalyzePackage logs 'checking pkg@v against N indicators' before the indicator scan so a stall is immediately visible - Result line always shows indicator count: '— N indicator(s)' even when N=0 (previously blank), [cached] appended when served from cache Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
After the summary line, print a sub-line per matched indicator:
blake3-neon@0.2.0 — 2 indicator(s)
✓ binding.gyp — GYP build descriptor [native-build]
✓ Cargo.toml — Rust build descriptor [native-build]
Packages with 0 indicators get no sub-lines. Signals are shown in
brackets. Works for both fresh scans and cache hits.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Export detectClues and investigate from indicator-scanner.js so deepAnalyzePackage can run them one-at-a-time with a log line before each step: [deepScan] pkg: scanning JS files... [deepScan] pkg: JS scan done (N refs) — detecting clues... [deepScan] pkg: checking against 28 indicators... [deepScan] pkg: N clue(s) found — investigating... [deepScan] pkg: investigating binding.gyp... [deepScan] pkg: binding.gyp done [deepScan] pkg: investigating Cargo.toml... ... If the process stalls, the last printed line identifies exactly which indicator file caused the hang. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
re.exec() in a while loop requires the g (global) flag to advance lastIndex between iterations. Without it, lastIndex stays 0 and the loop spins forever on the first match — causing the DeepScan hang on bootstrap-treeview (bower.json) and any brunch-config.js package. Fix in two places: 1. genericScanner: always forces g flag when constructing the regex for the exec loop, so a missing g in a pattern definition cannot hang again. 2. indicator-definitions.js: add missing g flag to the two offending regex-all patterns (bower.json 'dependencies' and brunch-config.js 'plugins'), making the definitions self-consistent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
✅ PASS — approve-scripts smoke reportAll packages with lifecycle scripts are correctly identified as pending approval.
|

feat: implement RFC npm#897 — npm approve-scripts review-report mode
Summary
Implements RFC #897: adds a first-class review-report mode to
npm approve-scripts --allow-scripts-pendingthat turns the existing pending-script listing into a structured, auditable report suitable for human review or AI-assisted analysis.Configuration & Arguments
--allow-scripts-report-formatControls the output format when
--allow-scripts-pendingis used.markdown(default)jsonnullUsage:
Notes:
--allow-scripts-report-formatthrows a usage error if specified without--allow-scripts-pending..npmrcis silently ignored when--allow-scripts-pendingis absent, so it does not interfere with normalapprove/denyflows.--jsontakes precedence over--allow-scripts-report-formatunless--allow-scripts-report-format=nullis explicitly passed.The problem RFC npm#897 solves
npm approve-scripts --allow-scripts-pendingalready identifies which packages need approval and shows their lifecycle script commands, but it leaves developers to manually trace what those commands actually do. In practice this causes approval fatigue and reflexive approvals — especially for transitive dependencies whose names are unfamiliar. The RFC calls for a report mode that surfaces the concrete execution path: which files the scripts load, what risk signals appear in those files, how the package entered the dependency graph, and whether its scripts have changed since the last approved version. The goal is to turn approval from "do I trust this package name?" into "do I trust this specific code path?" and to produce auditable evidence that can be committed to a repository.New review-report mode
Running
npm approve-scripts --allow-scripts-pendingnow automatically generates a Markdown review report (the existing--jsonflag selects JSON output instead). The new--allow-scripts-report-format=nullflag opt-out falls back to the legacy compact text listing. The--allow-scripts-report-formatconfig option is gated behind--allow-scripts-pendingand throws a usage error if specified without it.For each pending package the report includes:
directortransitive, and all graph paths through which it was introduced (up to 8 paths), built by walking arborist'sedgesIngraph (dep-path-walker.js).script-change-classifier.js).script-risk-scanner.js): the scanner parses each lifecycle command to find directly-referenced JS/shell files, then followsrequire()/importreferences up to 3 levels deep, reads each file in 64 KB chunks (with overlap to catch boundary-straddling patterns), hashes each file with SHA-256, and emits signals for patterns such asuses-child-process,network-access,references-credential-env-var,writes-outside-package,base64-decode-exec,obfuscation-pattern,jsfuck-obfuscation, and more. Files over 50 MB are partially scanned and flagged. The scanner never executes code and makes no network requests.node-gyporbinding.gyp, the scanner parses the binding.gyp file and extracts target names, source files, libraries, include directories, and whether conditional logic is present (gyp-scanner.js).review-report-formatter.jsrenders the collected data as a focused Markdown document with a risk summary section (highlightingHIGH_RISK_SIGNALSsuch as eval, VM, credential env vars, and obfuscation) and a "suggested focus areas" callout, or as a structured JSON object for programmatic consumption.A standalone
scripts/generate-allow-scripts-report.jsscript is also added to allow report generation outside the CLI (e.g. from CI scripts).GitHub Actions demo workflow
.github/workflows/allow-scripts-demo.ymlis added to exercise the new report mode on every PR that touches the relevant source files. It runsnpm ci --ignore-scripts, generates both Markdown and JSON reports, validates the JSON output (asserting all listed packages havependingstatus), and posts a formatted summary comment to the PR with the full report embedded in collapsible sections.Bug fix:
bundleDependenciesevasion incollectUnreviewedScriptsworkspaces/arborist/lib/unreviewed-scripts.jspreviously skipped any node wherenode.inBundlewas true.inBundleis set for any bundled dependency — including packages listed in the root project's ownbundleDependencies. A root-projectbundleDependenciesentry is still fetched from the registry and installed normally; its lifecycle scripts will run. The guard is changed tonode.inDepBundle, which is only true when the bundler is a non-root package (i.e. the dep is physically pre-built inside a third-party tarball). This closes the gap where a root-level bundled dep could silently bypass the unreviewed-scripts check and the approval workflow.Smoke test and environment fixes
approve-scripts-reportsmoke-test fixture adds"bundleDependencies": ["canvas"]to exercise thebundleDependenciesevasion fix end-to-end.smoke-tests/test/fixtures/setup.jsnow forwardsNODE_EXTRA_CA_CERTSinto spawned npm child processes so that smoke tests work correctly in environments with a custom CA certificate (e.g. enterprise CI).Indicator detection improvements
Driven by analysis of a
indicator-suggestions.jsondeep scan across 539 npm packages:bundled-binary-installerindicator — packages like@icp-sdk/ic-wasmthat pre-bundle a platform binary inside the npm tarball andchmod +xit during postinstall are now classified under a dedicatedbundled-binary-installerindicator (emitting theactivates-bundled-binarysignal), distinct frombinary-downloaderwhich is reserved for packages that fetch a binary from the network. Both indicators may fire together when a package both downloads and makes a binary executable. Themakes-executablesignal is the discriminator: a telemetry POST or JSON config download never needs to flip the execute bit.binary-downloaderis now strictly network-only — themakes-executabletrigger has been removed frombinary-downloader. It fires solely on command patterns (install-binary,download-binary, etc.) or thebinary-downloadcontent signal (network fetch patterns detected in the install script).source-downloaderlabel corrected — renamed from"External source or binary downloader"to"Lifecycle script URL fetch"with an explicit low-confidence caveat. URL presence in lifecycle code does not by itself confirm a binary or source download; it may be a telemetry endpoint, CDN for config, or docs reference.hasBuildHintupdated — the zero-I/O hint check returnstruewhenmakes-executableis among the signals on an already-scanned file, ensuring the full indicator scan is invoked for these packages.Files changed
lib/utils/script-risk-scanner.js(new) — static file walker and signal detector for lifecycle script fileslib/utils/review-report-formatter.js(new) — Markdown and JSON report rendererlib/utils/dep-path-walker.js(new) — arborist graph walker that computes all dependency paths from root to a nodelib/utils/gyp-scanner.js(new) — binding.gyp parser extracting native build targets and sourceslib/utils/script-change-classifier.js(new) — classifies whether a package's scripts changed relative to a previously approved versionlib/utils/indicator-definitions.js— newbundled-binary-installerindicator withactivates-bundled-binarysignal;binary-downloaderrestricted to network-fetch detection only;source-downloaderlabel correctedlib/utils/indicator-scanner.js—detectCluesandhasBuildHintupdated to handlemakes-executabletrigger signalscripts/generate-allow-scripts-report.js(new) — standalone report generator scriptlib/utils/allow-scripts-cmd.js— wires the new utilities into theapprove-scriptscommand; adds--allow-scripts-report-formatparam; addsrunReviewReport()workspaces/arborist/lib/unreviewed-scripts.js— fixinBundle→inDepBundleto close bundleDependencies evasion.github/workflows/allow-scripts-demo.yml(new) — CI demo workflow that generates and posts a report on PRssmoke-tests/— new fixture, smoke test, and CA cert forwarding fixtest/— unit tests for all five new utility modules, new indicator trigger tests, and updates to existing command testsdocs/— updated command docs and new RFC implementation plan documentworkspaces/config/lib/definitions/definitions.js— newallow-scripts-report-formatconfig definition