bigpowers 2.93.0 → 2.93.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gemini/extensions/bigpowers/gemini-extension.json +1 -1
- package/.pi/package.json +1 -1
- package/CHANGELOG.md +14 -0
- package/README.md +2 -0
- package/bin/bigpowers.js +9 -6
- package/bin/setup.js +11 -5
- package/package.json +3 -2
- package/scripts/lib/__pycache__/atomic_write.cpython-310.pyc +0 -0
- package/scripts/lib/__pycache__/link_utils.cpython-310.pyc +0 -0
- package/scripts/lib/__pycache__/srp_engine_links.cpython-310.pyc +0 -0
- package/scripts/lib/__pycache__/srp_engine_okf.cpython-310.pyc +0 -0
- package/scripts/lib/cli-version.js +29 -0
- package/scripts/lib/golden-suite-gates.sh +7 -0
- package/scripts/test-bigpowers-cli-version.sh +104 -0
- package/scripts/test-prod-dependency-audit.sh +95 -0
package/.pi/package.json
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [2.93.2](https://github.com/danielvm-git/bigpowers/compare/v2.93.1...v2.93.2) (2026-11-03)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **deps:** clear the production dependency advisories ([#200](https://github.com/danielvm-git/bigpowers/issues/200)) ([1ff6dea](https://github.com/danielvm-git/bigpowers/commit/1ff6dea73d7fdc06ebb3172839758f7a1be995c4))
|
|
7
|
+
|
|
8
|
+
## [2.93.1](https://github.com/danielvm-git/bigpowers/compare/v2.93.0...v2.93.1) (2026-11-03)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **cli:** report the installed version after a self-update ([#198](https://github.com/danielvm-git/bigpowers/issues/198)) ([4a2d8d0](https://github.com/danielvm-git/bigpowers/commit/4a2d8d0131d593c83c86151f6c716d2bfae96498))
|
|
14
|
+
|
|
1
15
|
# [2.93.0](https://github.com/danielvm-git/bigpowers/compare/v2.92.0...v2.93.0) (2026-11-03)
|
|
2
16
|
|
|
3
17
|
|
package/README.md
CHANGED
|
@@ -67,6 +67,8 @@ Use **arrow keys** to navigate, **space** to select, and **Enter** to confirm.
|
|
|
67
67
|
|
|
68
68
|

|
|
69
69
|
|
|
70
|
+
*Screenshot from an earlier release; the current installer may differ.*
|
|
71
|
+
|
|
70
72
|
### From source (contributors)
|
|
71
73
|
|
|
72
74
|
```bash
|
package/bin/bigpowers.js
CHANGED
|
@@ -10,7 +10,9 @@ const path = require('path');
|
|
|
10
10
|
|
|
11
11
|
const ROOT = path.dirname(path.dirname(__filename));
|
|
12
12
|
const SKILLS_MARKER = path.join(os.homedir(), '.claude', 'skills', 'survey-context');
|
|
13
|
-
|
|
13
|
+
// Version strings come from disk, not from a require cache captured at startup:
|
|
14
|
+
// `update` reinstalls this package mid-process (BUG-2026-11-03-cli-update-stale-version).
|
|
15
|
+
const { readPackageVersion } = require(path.join(ROOT, 'scripts', 'lib', 'cli-version'));
|
|
14
16
|
|
|
15
17
|
function isInstalled() {
|
|
16
18
|
try {
|
|
@@ -95,10 +97,11 @@ if (cmd === 'status') {
|
|
|
95
97
|
const count = fs.readdirSync(path.join(os.homedir(), '.claude', 'skills'))
|
|
96
98
|
.filter(d => fs.lstatSync(path.join(os.homedir(), '.claude', 'skills', d)).isSymbolicLink())
|
|
97
99
|
.length;
|
|
98
|
-
|
|
100
|
+
const version = readPackageVersion(ROOT);
|
|
101
|
+
console.log(`bigpowers v${version} — ${count} skills installed`);
|
|
99
102
|
const latest = latestPublishedVersion();
|
|
100
|
-
if (latest && latest !==
|
|
101
|
-
console.log(`\n⬆️ A newer release is available: v${latest} (you have v${
|
|
103
|
+
if (latest && latest !== version) {
|
|
104
|
+
console.log(`\n⬆️ A newer release is available: v${latest} (you have v${version}).`);
|
|
102
105
|
console.log(' Upgrade: npm install -g bigpowers@latest && bigpowers update');
|
|
103
106
|
}
|
|
104
107
|
} else {
|
|
@@ -123,10 +126,10 @@ Commands:
|
|
|
123
126
|
|
|
124
127
|
// Default: check state and guide
|
|
125
128
|
if (isInstalled()) {
|
|
126
|
-
console.log(`bigpowers v${
|
|
129
|
+
console.log(`bigpowers v${readPackageVersion(ROOT)} — already installed.`);
|
|
127
130
|
console.log('Commands: bigpowers {setup|update|status|help}');
|
|
128
131
|
} else {
|
|
129
|
-
console.log(`🚀 bigpowers v${
|
|
132
|
+
console.log(`🚀 bigpowers v${readPackageVersion(ROOT)} — skills not yet installed.`);
|
|
130
133
|
console.log(' Run: bigpowers setup');
|
|
131
134
|
process.exit(1);
|
|
132
135
|
}
|
package/bin/setup.js
CHANGED
|
@@ -16,9 +16,9 @@ const { execSync, spawn } = require('child_process');
|
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const path = require('path');
|
|
18
18
|
const { installGlobal, installLocal, uninstallTool, detectExistingInstall } = require('../scripts/lib/install-helpers');
|
|
19
|
+
const { readPackageVersion } = require('../scripts/lib/cli-version');
|
|
19
20
|
|
|
20
21
|
const ROOT = path.resolve(path.dirname(__filename), '..');
|
|
21
|
-
const pkg = require(path.join(ROOT, 'package.json'));
|
|
22
22
|
|
|
23
23
|
// ── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
24
24
|
const c = {
|
|
@@ -30,7 +30,11 @@ const c = {
|
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
// ── ASCII banner ─────────────────────────────────────────────────────────────
|
|
33
|
-
|
|
33
|
+
// The version is a parameter, read fresh from disk at render time: `update`
|
|
34
|
+
// reinstalls this package mid-process, so a value captured at startup is the one
|
|
35
|
+
// that was just replaced (BUG-2026-11-03-cli-update-stale-version).
|
|
36
|
+
function banner(version) {
|
|
37
|
+
return `
|
|
34
38
|
${c.cyan('╔════════════════════════════════════════════════════════════════════════════╗')}
|
|
35
39
|
${c.cyan('║')} ${c.cyan('║')}
|
|
36
40
|
${c.cyan('║')} ${c.bold(c.cyan('██████╗ ██╗ ██████╗ ██████╗ ██████╗ █ █ ███████╗██████╗ ███████╗'))} ${c.cyan('║')}
|
|
@@ -41,9 +45,10 @@ ${c.cyan('║')} ${c.bold(c.cyan('██████╔╝██║╚██
|
|
|
41
45
|
${c.cyan('║')} ${c.bold(c.cyan('╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝'))} ${c.cyan('║')}
|
|
42
46
|
${c.cyan('║')} ${c.cyan('║')}
|
|
43
47
|
${c.cyan('║')} ${c.dim('Spec-Driven & Test-First Skills for Solo Developers')} ${c.cyan('║')}
|
|
44
|
-
${c.cyan('║')} ${c.dim(`v${
|
|
48
|
+
${c.cyan('║')} ${c.dim(`v${version}`)} ${c.cyan('║')}
|
|
45
49
|
${c.cyan('╚════════════════════════════════════════════════════════════════════════════╝')}
|
|
46
50
|
`;
|
|
51
|
+
}
|
|
47
52
|
|
|
48
53
|
// ── Tool definitions ─────────────────────────────────────────────────────────
|
|
49
54
|
const TOOLS = [
|
|
@@ -164,8 +169,9 @@ async function handleUninstall() {
|
|
|
164
169
|
async function main() {
|
|
165
170
|
const { intro, outro, select, multiselect, spinner, confirm, isCancel } = await clack();
|
|
166
171
|
|
|
167
|
-
|
|
168
|
-
|
|
172
|
+
const version = readPackageVersion(ROOT); // after a self-update this is the NEW version
|
|
173
|
+
console.log(banner(version));
|
|
174
|
+
intro(c.bold(`bigpowers v${version}`));
|
|
169
175
|
|
|
170
176
|
// ── Step 0: Detect existing install ────────────────────────────────────
|
|
171
177
|
const hasExisting = detectExistingInstall();
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bigpowers",
|
|
3
|
-
"version": "2.93.
|
|
3
|
+
"version": "2.93.2",
|
|
4
4
|
"description": "73 agent skills synthesizing 17 years of software engineering discipline into a prescriptive methodology for solo developers",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"setup": "node bin/setup.js",
|
|
8
8
|
"compliance": "bash scripts/audit-compliance.sh specs/verifications/features && bash scripts/validate-doctrine.sh && python3 scripts/check-skill-links.py",
|
|
9
|
+
"audit:prod": "npm audit --omit=dev",
|
|
9
10
|
"doctrine": "bash scripts/validate-doctrine.sh",
|
|
10
11
|
"sync": "bash scripts/sync-skills.sh",
|
|
11
12
|
"validate-specs": "bash scripts/validate-specs-yaml.sh",
|
|
@@ -65,7 +66,7 @@
|
|
|
65
66
|
"remark-frontmatter": "^5.0.0",
|
|
66
67
|
"remark-parse": "^11.0.0",
|
|
67
68
|
"unified": "^11.0.5",
|
|
68
|
-
"zod": "^
|
|
69
|
+
"zod": "^4.6.7"
|
|
69
70
|
},
|
|
70
71
|
"devDependencies": {
|
|
71
72
|
"@ctxo/lang-typescript": "^0.11.0",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// story: BUG-2026-11-03-cli-update-stale-version
|
|
2
|
+
// cli-version.js — read the installed version from disk, never from a require cache.
|
|
3
|
+
//
|
|
4
|
+
// `bigpowers update` runs `npm install -g bigpowers@latest` inside a *running*
|
|
5
|
+
// process. bin/bigpowers.js has already require()d package.json by then, so when
|
|
6
|
+
// bin/setup.js (required after the reinstall) reads the same resolved path, Node
|
|
7
|
+
// hands back the cached object: the banner printed v2.88.5 while v2.92.0 was what
|
|
8
|
+
// had just been installed, and `bigpowers install` — a fresh process — correctly
|
|
9
|
+
// reported 2.92.0.
|
|
10
|
+
//
|
|
11
|
+
// readFileSync always sees the new file, so every user-facing version string goes
|
|
12
|
+
// through here.
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
/** Version of the package rooted at `root`, read fresh from disk. */
|
|
20
|
+
function readPackageVersion(root) {
|
|
21
|
+
try {
|
|
22
|
+
const text = fs.readFileSync(path.join(root, 'package.json'), 'utf8');
|
|
23
|
+
return JSON.parse(text).version || 'unknown';
|
|
24
|
+
} catch {
|
|
25
|
+
return 'unknown';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { readPackageVersion };
|
|
@@ -53,6 +53,13 @@ GOLDEN_GATES=(
|
|
|
53
53
|
"target-contracts:bash scripts/test-target-contracts.sh:false"
|
|
54
54
|
# Registry-driven replacements for the 25 hand-written per-target scripts.
|
|
55
55
|
"install-hub:bash scripts/test-install-hub.sh:false"
|
|
56
|
+
# Whether the package *builds* was gated; whether its tree is *safe* was not.
|
|
57
|
+
# `npm audit --omit=dev` was 10 advisories (1 critical) with nothing watching.
|
|
58
|
+
"prod-dependency-audit:bash scripts/test-prod-dependency-audit.sh:false"
|
|
59
|
+
# `bigpowers update` runs `npm install -g @latest` inside a live process; a
|
|
60
|
+
# version captured by require() at startup then reports the release that was
|
|
61
|
+
# just replaced (BUG-2026-11-03-cli-update-stale-version).
|
|
62
|
+
"bigpowers-cli-version:bash scripts/test-bigpowers-cli-version.sh:false"
|
|
56
63
|
"adapter-render:bash scripts/test-adapter-render.sh:false"
|
|
57
64
|
"adapters:bash scripts/test-adapters.sh:false"
|
|
58
65
|
# Previously orphaned: present in scripts/ but reachable from no gate.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: BUG-2026-11-03-cli-update-stale-version
|
|
3
|
+
# test-bigpowers-cli-version.sh — `update` must report the version it just installed.
|
|
4
|
+
#
|
|
5
|
+
# Reproduces the reported symptom without touching npm or the real HOME:
|
|
6
|
+
# bigpowers update → banner v2.88.5 (wrong: a require() cache captured at startup,
|
|
7
|
+
# before `npm install -g bigpowers@latest`)
|
|
8
|
+
# bigpowers install → banner v2.92.0 (right: a fresh process reads package.json)
|
|
9
|
+
#
|
|
10
|
+
# Three assertions:
|
|
11
|
+
# 1. readPackageVersion() sees the on-disk version even after package.json was
|
|
12
|
+
# already require()d and then rewritten (the exact mechanism).
|
|
13
|
+
# 2. the installer's banner renders that fresh value — end to end, from a copy
|
|
14
|
+
# of bin/ whose version changes between process start and banner render.
|
|
15
|
+
# 3. neither CLI entry point still interpolates a cached `pkg.version` into a
|
|
16
|
+
# user-facing line (static guard: the cache is invisible at runtime until
|
|
17
|
+
# the day a self-update happens).
|
|
18
|
+
#
|
|
19
|
+
# Usage: bash scripts/test-bigpowers-cli-version.sh
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib/skill-common.sh"
|
|
22
|
+
resolve_repo_root
|
|
23
|
+
cd "$REPO_ROOT"
|
|
24
|
+
|
|
25
|
+
source scripts/lib/test-assertions.sh
|
|
26
|
+
TA_PASS=0
|
|
27
|
+
TA_FAIL=0
|
|
28
|
+
TA_TMPDIR="$(mktemp -d)"
|
|
29
|
+
trap ta_cleanup EXIT
|
|
30
|
+
|
|
31
|
+
echo "=== test-bigpowers-cli-version.sh ==="
|
|
32
|
+
|
|
33
|
+
# ── 1. readPackageVersion ignores the require cache ─────────────────────────
|
|
34
|
+
mkdir -p "$TA_TMPDIR/pkgver"
|
|
35
|
+
if [ ! -f scripts/lib/cli-version.js ]; then
|
|
36
|
+
ta_fail "(1) scripts/lib/cli-version.js is missing — nothing reads the version from disk"
|
|
37
|
+
CACHE_OUT=""
|
|
38
|
+
else
|
|
39
|
+
CACHE_OUT="$(node -e '
|
|
40
|
+
const fs = require("fs"), path = require("path");
|
|
41
|
+
const dir = process.argv[1];
|
|
42
|
+
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ version: "2.88.5" }));
|
|
43
|
+
const cached = require(path.join(dir, "package.json")); // what bin/bigpowers.js does at startup
|
|
44
|
+
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ version: "2.92.0" })); // npm install -g @latest
|
|
45
|
+
const { readPackageVersion } = require(process.argv[2]);
|
|
46
|
+
process.stdout.write(`${cached.version}|${readPackageVersion(dir)}`);
|
|
47
|
+
' "$TA_TMPDIR/pkgver" "$REPO_ROOT/scripts/lib/cli-version.js")"
|
|
48
|
+
fi
|
|
49
|
+
if [ "$CACHE_OUT" = "2.88.5|2.92.0" ]; then
|
|
50
|
+
ta_pass "(1) readPackageVersion sees the on-disk version after package.json is rewritten"
|
|
51
|
+
else
|
|
52
|
+
ta_fail "(1) version read followed the require cache: got '$CACHE_OUT' (want '2.88.5|2.92.0')"
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# ── 2. the banner an `update` run prints carries the new version ────────────
|
|
56
|
+
# Drive the exact sequence bin/bigpowers.js uses: read package.json at startup,
|
|
57
|
+
# let `npm install -g bigpowers@latest` replace it, then require bin/setup.js —
|
|
58
|
+
# which renders the banner. stdin at EOF makes setup draw the banner and exit, so
|
|
59
|
+
# nothing is installed and no prompt is answered.
|
|
60
|
+
FAKE_PKG="$TA_TMPDIR/fakepkg"
|
|
61
|
+
mkdir -p "$FAKE_PKG"
|
|
62
|
+
cp -R "$REPO_ROOT/bin" "$FAKE_PKG/bin"
|
|
63
|
+
cp -R "$REPO_ROOT/scripts" "$FAKE_PKG/scripts"
|
|
64
|
+
ln -sfn "$REPO_ROOT/node_modules" "$FAKE_PKG/node_modules"
|
|
65
|
+
printf '{"name":"bigpowers-fake","version":"2.88.5"}\n' > "$FAKE_PKG/package.json"
|
|
66
|
+
cat > "$FAKE_PKG/driver.js" <<'DRIVER'
|
|
67
|
+
// Mimics `bigpowers update`: snapshot at startup, reinstall over it, then render.
|
|
68
|
+
const { execFileSync } = require('child_process');
|
|
69
|
+
const fs = require('fs');
|
|
70
|
+
const path = require('path');
|
|
71
|
+
|
|
72
|
+
const root = __dirname;
|
|
73
|
+
const pkg = require(path.join(root, 'package.json')); // 2.88.5 — the version the process booted with
|
|
74
|
+
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'bigpowers-fake', version: '2.92.0' }));
|
|
75
|
+
process.stdout.write(`startup=${pkg.version}\n`);
|
|
76
|
+
require(path.join(root, 'bin', 'setup.js')); // prints the banner
|
|
77
|
+
DRIVER
|
|
78
|
+
BANNER_OUT="$(
|
|
79
|
+
cd "$FAKE_PKG"
|
|
80
|
+
HOME="$TA_TMPDIR/home" perl -e 'alarm 25; exec @ARGV' node driver.js < /dev/null 2>&1 || true
|
|
81
|
+
)"
|
|
82
|
+
if printf '%s' "$BANNER_OUT" | grep -q "startup=2.88.5" && printf '%s' "$BANNER_OUT" | grep -q "v2.92.0"; then
|
|
83
|
+
ta_pass "(2) the banner after a self-update reports the installed version (2.92.0), not the startup one (2.88.5)"
|
|
84
|
+
elif printf '%s' "$BANNER_OUT" | grep -q "v2.88.5"; then
|
|
85
|
+
ta_fail "(2) the banner reported the startup version (v2.88.5) after 2.92.0 was installed"
|
|
86
|
+
else
|
|
87
|
+
ta_fail "(2) no banner version found in the update flow"
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# ── 3. no user-facing line interpolates the startup snapshot ────────────────
|
|
91
|
+
# `pkg.version` is the value captured when the process started. Anywhere it
|
|
92
|
+
# reaches the user after a self-update, it is the version that was replaced.
|
|
93
|
+
STALE=0
|
|
94
|
+
for f in bin/setup.js bin/bigpowers.js; do
|
|
95
|
+
grep -nE 'pkg\.version' "$f" | grep -vE '^\s*[0-9]+:\s*//' >/dev/null 2>&1 && STALE=1
|
|
96
|
+
done
|
|
97
|
+
if [ "$STALE" -eq 0 ]; then
|
|
98
|
+
ta_pass "(3) neither bin/setup.js nor bin/bigpowers.js prints a cached pkg.version"
|
|
99
|
+
else
|
|
100
|
+
ta_fail "(3) a CLI entry point still interpolates pkg.version (stale after self-update)"
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
echo "$TA_PASS passed, $TA_FAIL failed"
|
|
104
|
+
[[ "$TA_FAIL" -eq 0 ]]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: BUG-2026-11-03-prod-dependency-advisories
|
|
3
|
+
# test-prod-dependency-audit.sh — the shipped dependency graph must stay advisory-free.
|
|
4
|
+
#
|
|
5
|
+
# `npm audit --omit=dev` covers what users actually install. It was 10 advisories
|
|
6
|
+
# (1 critical) on 2.93.1 with nothing watching it: the only dependency gates were
|
|
7
|
+
# about whether the package *builds*, not whether its tree is safe.
|
|
8
|
+
#
|
|
9
|
+
# Two failure modes, both loud:
|
|
10
|
+
# 1. any advisory in the production tree → FAIL, listing package + severity
|
|
11
|
+
# 2. the audit could not run (no registry) → FAIL, naming the escape hatch
|
|
12
|
+
#
|
|
13
|
+
# Nothing here passes silently. To skip it deliberately (offline work), the golden
|
|
14
|
+
# runner's own mechanism is the documented path:
|
|
15
|
+
# GOLDEN_SKIP_GATES=prod-dependency-audit bash scripts/run-verification-gates.sh
|
|
16
|
+
# ...which records the gate as SKIPPED in the report rather than green.
|
|
17
|
+
#
|
|
18
|
+
# Dev-tree advisories are out of scope: they are concentrated in the npm bundle
|
|
19
|
+
# inside @semantic-release/npm and need major release-tool upgrades
|
|
20
|
+
# (BUG-2026-11-03-dev-dependency-advisories).
|
|
21
|
+
#
|
|
22
|
+
# Usage: bash scripts/test-prod-dependency-audit.sh
|
|
23
|
+
set -euo pipefail
|
|
24
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib/skill-common.sh"
|
|
25
|
+
resolve_repo_root
|
|
26
|
+
cd "$REPO_ROOT"
|
|
27
|
+
|
|
28
|
+
source scripts/lib/test-assertions.sh
|
|
29
|
+
TA_PASS=0
|
|
30
|
+
TA_FAIL=0
|
|
31
|
+
TA_TMPDIR="$(mktemp -d)"
|
|
32
|
+
trap ta_cleanup EXIT
|
|
33
|
+
|
|
34
|
+
echo "=== test-prod-dependency-audit.sh ==="
|
|
35
|
+
|
|
36
|
+
if ! command -v npm >/dev/null 2>&1; then
|
|
37
|
+
ta_fail "npm is unavailable — cannot audit the production dependency graph"
|
|
38
|
+
echo "$TA_PASS passed, $TA_FAIL failed"
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
AUDIT_JSON="$TA_TMPDIR/audit-prod.json"
|
|
43
|
+
# npm audit exits 1 when it finds advisories — capture, do not abort.
|
|
44
|
+
npm audit --omit=dev --json >"$AUDIT_JSON" 2>"$TA_TMPDIR/audit-prod.err" || true
|
|
45
|
+
|
|
46
|
+
REPORT="$(
|
|
47
|
+
python3 - "$AUDIT_JSON" <<'PY'
|
|
48
|
+
import json, sys
|
|
49
|
+
|
|
50
|
+
path = sys.argv[1]
|
|
51
|
+
try:
|
|
52
|
+
data = json.load(open(path, encoding="utf-8"))
|
|
53
|
+
except Exception:
|
|
54
|
+
print("UNPARSEABLE")
|
|
55
|
+
sys.exit(0)
|
|
56
|
+
|
|
57
|
+
meta = data.get("metadata") or {}
|
|
58
|
+
counts = meta.get("vulnerabilities")
|
|
59
|
+
if counts is None:
|
|
60
|
+
print("UNPARSEABLE")
|
|
61
|
+
sys.exit(0)
|
|
62
|
+
|
|
63
|
+
total = counts.get("total", 0)
|
|
64
|
+
rows = []
|
|
65
|
+
for name, info in sorted((data.get("vulnerabilities") or {}).items()):
|
|
66
|
+
severity = info.get("severity", "?")
|
|
67
|
+
via = info.get("via") or []
|
|
68
|
+
title = ""
|
|
69
|
+
for entry in via:
|
|
70
|
+
if isinstance(entry, dict) and entry.get("title"):
|
|
71
|
+
title = entry["title"]
|
|
72
|
+
break
|
|
73
|
+
rows.append(f"{severity:<9} {name} {title[:70]}")
|
|
74
|
+
print(f"TOTAL={total}")
|
|
75
|
+
for row in rows:
|
|
76
|
+
print(f" {row}")
|
|
77
|
+
PY
|
|
78
|
+
)"
|
|
79
|
+
|
|
80
|
+
if [[ "$REPORT" == UNPARSEABLE* ]]; then
|
|
81
|
+
ta_fail "npm audit produced no parseable report — the registry was probably unreachable:"
|
|
82
|
+
sed -n '1,5p' "$TA_TMPDIR/audit-prod.err" | sed 's/^/ /' >&2
|
|
83
|
+
ta_fail "run with network, or skip deliberately: GOLDEN_SKIP_GATES=prod-dependency-audit"
|
|
84
|
+
else
|
|
85
|
+
TOTAL="$(grep -oE 'TOTAL=[0-9]+' <<< "$REPORT" | cut -d= -f2)"
|
|
86
|
+
if [[ "${TOTAL:-0}" -eq 0 ]]; then
|
|
87
|
+
ta_pass "no advisories in the production dependency tree (npm audit --omit=dev)"
|
|
88
|
+
else
|
|
89
|
+
ta_fail "$TOTAL advisor$([[ "$TOTAL" -eq 1 ]] && echo 'y' || echo 'ies') in the production dependency tree:"
|
|
90
|
+
sed -n '2,$p' <<< "$REPORT" | sed 's/^/ /' >&2
|
|
91
|
+
fi
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
echo "$TA_PASS passed, $TA_FAIL failed"
|
|
95
|
+
[[ "$TA_FAIL" -eq 0 ]]
|