@jenga-ai/agent 1.2.4 → 1.3.0
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/README.md +1 -0
- package/agents/developer.md +18 -0
- package/agents/scrum-master.md +1 -0
- package/agents/tester.md +18 -0
- package/hooks/on_session_end.sh +27 -0
- package/package.json +18 -17
- package/skills/commit/SKILL.md +11 -1
- package/skills/dev-done/SKILL.md +46 -0
- package/skills/dev-done/scripts/classify-commit-outcome.sh +114 -0
- package/skills/init/SKILL.md +7 -6
- package/skills/init/assets/scope-thresholds_template.json +7 -0
- package/skills/init/scripts/init.sh +6 -0
- package/skills/publish/SKILL.md +66 -0
- package/skills/publish/adapters/npm-ci.md +34 -0
- package/skills/publish/adapters/npm.md +18 -0
- package/skills/publish/assets/ci-contract.md +27 -0
- package/skills/publish/assets/publish.example.json +27 -0
- package/skills/publish/schemas/publish.schema.json +20 -0
- package/skills/publish/scripts/npm_ci_pipeline.sh +29 -0
- package/skills/publish/scripts/npm_stage_inspect.sh +829 -0
- package/skills/publish/scripts/npm_stage_pipeline.sh +427 -0
- package/skills/publish/scripts/publish_common.sh +16 -0
- package/skills/publish/scripts/show_history.sh +12 -5
- package/skills/publish/scripts/validate_npm_stage_env.sh +184 -0
- package/skills/publish/scripts/write_ledger_entry.sh +92 -2
- package/skills/reconcile/SKILL.md +121 -11
- package/skills/reconcile/assets/report_format.md +17 -0
- package/skills/reconcile/scripts/resolve-reconcile-scope.sh +489 -0
- package/skills/uncharted/SKILL.md +200 -21
- package/skills/uncharted/scripts/directory-triage.sh +342 -0
- package/skills/uncharted/scripts/elicitation-state.sh +457 -0
- package/templates/SCRUM_BOARD_SCHEMA.md +57 -0
- package/templates/agent-context.md.tpl +23 -11
- package/templates/copilot-instructions.md.tpl +21 -11
- package/mcp/router/README.md +0 -19
- package/mcp/router/embedder.js +0 -23
- package/mcp/router/index.js +0 -204
- package/mcp/router/matcher.js +0 -87
- package/mcp/router/package-lock.json +0 -1048
- package/mcp/router/package.json +0 -11
- package/mcp/router/skill-index.js +0 -104
- package/skills/route/SKILL.md +0 -180
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# npm_stage_inspect.sh — `/publish stage`: list/view/download a staged npm
|
|
3
|
+
# release, run the pre-approval smoke test, and approve or reject it.
|
|
4
|
+
#
|
|
5
|
+
# This script is the story's core value: staging without testing just
|
|
6
|
+
# relocates the risk. `test` installs the exact staged tarball into an
|
|
7
|
+
# isolated scratch directory and smoke-tests it; `approve` refuses to run
|
|
8
|
+
# unless a passing test is on record for that exact stage id (or the
|
|
9
|
+
# operator explicitly overrides with a reason).
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# npm_stage_inspect.sh <sub> [args] [--config <path>] [--dry-run] [--json]
|
|
13
|
+
#
|
|
14
|
+
# Sub-commands:
|
|
15
|
+
# list [<package-spec>] wraps `npm stage list`
|
|
16
|
+
# view <stage-id> wraps `npm stage view`
|
|
17
|
+
# download <stage-id> [--out <dir>] wraps `npm stage download`
|
|
18
|
+
# test <stage-id> [--keep] pre-approval smoke test (see below)
|
|
19
|
+
# approve <stage-id> [--otp <otp>] [--force <reason>]
|
|
20
|
+
# wraps `npm stage approve`, gated
|
|
21
|
+
# by a test interlock
|
|
22
|
+
# reject <stage-id> wraps `npm stage reject` — the
|
|
23
|
+
# discard path for a staged
|
|
24
|
+
# version. npm's own docs page
|
|
25
|
+
# (https://docs.npmjs.com/staged-publishing)
|
|
26
|
+
# omits this command entirely;
|
|
27
|
+
# it is documented here so it is
|
|
28
|
+
# discoverable.
|
|
29
|
+
#
|
|
30
|
+
# `test` — the pre-approval smoke test:
|
|
31
|
+
# 1. Download the staged tarball via `npm stage download <stage-id>`.
|
|
32
|
+
# 2. Create a scratch directory via `mktemp -d`, always OUTSIDE this
|
|
33
|
+
# repository's working tree (asserted explicitly, in addition to
|
|
34
|
+
# `mktemp` already guaranteeing it in any normal environment) — the
|
|
35
|
+
# repo's own node_modules is never touched.
|
|
36
|
+
# 3. `npm init -y`, then `npm install <path-to-tarball>` in the scratch
|
|
37
|
+
# dir — installing the tarball by path, never by registry spec, so the
|
|
38
|
+
# artifact under test is exactly the staged bytes.
|
|
39
|
+
# 4. Run `npm.stage.smoke_cmd` from the matching target's config when set,
|
|
40
|
+
# else the documented default (see `_default_smoke_check` below), which
|
|
41
|
+
# verifies what `scripts/postinstall.js` actually does today: mirrors
|
|
42
|
+
# `skills/` and `agents/` into BOTH `.claude/` and `.agents/` under the
|
|
43
|
+
# consumer root, writes `.jenga-version`, and bootstraps
|
|
44
|
+
# `.github/copilot-instructions.md`. It deliberately does NOT check for
|
|
45
|
+
# `hooks/`, `scripts/`, or `templates/` landing in the project root —
|
|
46
|
+
# commit 619198b already narrowed postinstall's copy set to `skills/`
|
|
47
|
+
# and `agents/` only; those three dirs stay inside
|
|
48
|
+
# `node_modules/@jenga-ai/agent/` and are sourced from there at
|
|
49
|
+
# runtime. See project/documentation/plans/E22_S09_T03-plan.md for the
|
|
50
|
+
# full write-up of this correction relative to an earlier, stale
|
|
51
|
+
# description of postinstall's behavior.
|
|
52
|
+
# 5. Clean up the scratch directory on both success and failure, unless
|
|
53
|
+
# `--keep` was passed.
|
|
54
|
+
# 6. Write a `stage_tested` ledger entry (stage id + pass/fail `--result`).
|
|
55
|
+
#
|
|
56
|
+
# Exit codes:
|
|
57
|
+
# 0 success (or --dry-run/--help completed cleanly)
|
|
58
|
+
# 3 the underlying `npm stage <sub>` command failed, the tarball install
|
|
59
|
+
# failed, or the smoke test failed
|
|
60
|
+
# 4 usage error, unknown sub-command, or the `approve` test interlock
|
|
61
|
+
# refused (no passing test on record and no `--force <reason>`)
|
|
62
|
+
#
|
|
63
|
+
# Security note: `approve --otp` uses the exact argv-prescan-before-parsing
|
|
64
|
+
# guard documented in npm_stage_pipeline.sh (E22_S09_T02) — command tracing
|
|
65
|
+
# is suspended before OTP is ever assigned to a variable, for the rest of the
|
|
66
|
+
# run, if argv contains --otp and tracing was already active. Captured
|
|
67
|
+
# output is additionally scrubbed of the literal OTP value before it is ever
|
|
68
|
+
# printed. The OTP is never written to the ledger, never printed, and never
|
|
69
|
+
# appears in --json output.
|
|
70
|
+
|
|
71
|
+
set -euo pipefail
|
|
72
|
+
|
|
73
|
+
EXIT_OK=0
|
|
74
|
+
EXIT_OP_FAILED=3
|
|
75
|
+
EXIT_USAGE=4
|
|
76
|
+
|
|
77
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
78
|
+
|
|
79
|
+
if REPO_ROOT="$(git -C "${SCRIPT_DIR}" rev-parse --show-toplevel 2>/dev/null)"; then
|
|
80
|
+
:
|
|
81
|
+
else
|
|
82
|
+
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# shellcheck source=publish_common.sh
|
|
86
|
+
source "${SCRIPT_DIR}/publish_common.sh"
|
|
87
|
+
|
|
88
|
+
WRITE_LEDGER_SCRIPT="${SCRIPT_DIR}/write_ledger_entry.sh"
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# OTP tracing guard — MUST run at the top level, before `main "$@"` is ever
|
|
92
|
+
# called, and before any argv containing --otp is passed to a function.
|
|
93
|
+
#
|
|
94
|
+
# This script dispatches through main() -> cmd_approve() rather than being
|
|
95
|
+
# one flat top-level body (unlike npm_stage_pipeline.sh, E22_S09_T02, whose
|
|
96
|
+
# guard lives at its own top level with nothing to dispatch through). That
|
|
97
|
+
# dispatch is exactly why the guard cannot simply live inside cmd_approve as
|
|
98
|
+
# a narrower, subcommand-scoped check: when this script is invoked as
|
|
99
|
+
# `bash -x npm_stage_inspect.sh approve <id> --otp <value>`, bash's xtrace
|
|
100
|
+
# prints the *expanded arguments of each function-call site* —
|
|
101
|
+
# `+ main approve <id> ... --otp <value>` and, inside main's body,
|
|
102
|
+
# `+ cmd_approve <id> ... --otp <value>` — the moment each call is made,
|
|
103
|
+
# before that function's own body (and any guard living inside it) ever
|
|
104
|
+
# executes. Verified empirically: an OTP guard placed inside cmd_approve
|
|
105
|
+
# left both of those call-site trace lines carrying the literal OTP in
|
|
106
|
+
# `bash -x` output, even though the guard correctly redacted everything
|
|
107
|
+
# printed by the function bodies themselves. Suspending tracing here, before
|
|
108
|
+
# `main "$@"` is invoked at all, prevents both call-site trace lines from
|
|
109
|
+
# ever being emitted in the first place.
|
|
110
|
+
#
|
|
111
|
+
# A narrower window — e.g. suspending tracing only around the `npm stage
|
|
112
|
+
# approve` call itself — is NOT sufficient for the same reason documented in
|
|
113
|
+
# npm_stage_pipeline.sh: xtrace prints the *expanded* value of a variable
|
|
114
|
+
# for every subsequent line that references it. The only reliable guard is
|
|
115
|
+
# to suspend tracing before OTP is ever assigned to a variable (or passed
|
|
116
|
+
# into a traced function call) and never re-enable it for the rest of the
|
|
117
|
+
# run.
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
_OTP_PRESENT_IN_ARGV=0
|
|
120
|
+
for _otp_scan_arg in "$@"; do
|
|
121
|
+
case "${_otp_scan_arg}" in
|
|
122
|
+
--otp|--otp=*)
|
|
123
|
+
_OTP_PRESENT_IN_ARGV=1
|
|
124
|
+
;;
|
|
125
|
+
esac
|
|
126
|
+
done
|
|
127
|
+
unset _otp_scan_arg
|
|
128
|
+
if (( _OTP_PRESENT_IN_ARGV )); then
|
|
129
|
+
case "$-" in
|
|
130
|
+
*x*) set +x ;;
|
|
131
|
+
esac
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
usage() {
|
|
135
|
+
cat <<'USAGE'
|
|
136
|
+
Usage: npm_stage_inspect.sh <sub> [args] [--config <path>] [--dry-run] [--json]
|
|
137
|
+
|
|
138
|
+
Sub-commands:
|
|
139
|
+
list [<package-spec>] List staged versions (readable
|
|
140
|
+
table by default; raw JSON under
|
|
141
|
+
--json)
|
|
142
|
+
view <stage-id> Show a staged version's details
|
|
143
|
+
download <stage-id> [--out <dir>] Download the staged tarball to
|
|
144
|
+
<dir> (default: a fresh scratch
|
|
145
|
+
directory, reported on success)
|
|
146
|
+
test <stage-id> [--keep] Install the staged tarball into
|
|
147
|
+
an isolated scratch directory
|
|
148
|
+
outside the repo and smoke-test
|
|
149
|
+
it; writes a stage_tested ledger
|
|
150
|
+
entry. --keep retains the scratch
|
|
151
|
+
dir for debugging.
|
|
152
|
+
approve <stage-id> [--otp <otp>] [--force <reason>]
|
|
153
|
+
Approve a staged version for
|
|
154
|
+
release. Refuses without a
|
|
155
|
+
passing `test` on record for this
|
|
156
|
+
exact stage id, unless --force
|
|
157
|
+
<reason> is given. --otp is
|
|
158
|
+
passed through when supplied;
|
|
159
|
+
npm prompts interactively
|
|
160
|
+
otherwise.
|
|
161
|
+
reject <stage-id> Discard a staged version — the
|
|
162
|
+
discard path omitted from npm's
|
|
163
|
+
own staged-publishing docs page.
|
|
164
|
+
|
|
165
|
+
Global flags (accepted by every sub-command):
|
|
166
|
+
--config <path> Path to publish.json (default: <repo-root>/publish.json,
|
|
167
|
+
then project/configs/publish.json)
|
|
168
|
+
--dry-run Print the resolved npm command(s)/steps instead of
|
|
169
|
+
running them; makes no registry-mutating call
|
|
170
|
+
--json Emit a machine-readable JSON summary instead of a
|
|
171
|
+
human-readable one (list/view pass through npm's own
|
|
172
|
+
--json output verbatim)
|
|
173
|
+
USAGE
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fail_usage() {
|
|
177
|
+
usage >&2
|
|
178
|
+
printf 'npm stage inspect error: %s\n' "$1" >&2
|
|
179
|
+
exit "${EXIT_USAGE}"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
log_info() { printf '→ %s\n' "$1"; }
|
|
183
|
+
log_warn() { printf '⚠ %s\n' "$1" >&2; }
|
|
184
|
+
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
# Config resolution — mirrors validate_npm_stage_env.sh's search order.
|
|
187
|
+
# Returns non-zero (empty stdout) rather than failing the script outright:
|
|
188
|
+
# list/view/download/reject can all operate without a config at all, and
|
|
189
|
+
# test/approve fall back to documented defaults when unresolved.
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
resolve_config_path() {
|
|
192
|
+
local explicit="$1"
|
|
193
|
+
if [[ -n "${explicit}" ]]; then
|
|
194
|
+
publish_normalize_path "${explicit}"
|
|
195
|
+
return 0
|
|
196
|
+
fi
|
|
197
|
+
if [[ -f "${REPO_ROOT}/publish.json" ]]; then
|
|
198
|
+
printf '%s\n' "${REPO_ROOT}/publish.json"
|
|
199
|
+
return 0
|
|
200
|
+
fi
|
|
201
|
+
if [[ -f "${PUBLISH_DEFAULT_CONFIG_FILE}" ]]; then
|
|
202
|
+
printf '%s\n' "${PUBLISH_DEFAULT_CONFIG_FILE}"
|
|
203
|
+
return 0
|
|
204
|
+
fi
|
|
205
|
+
return 1
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
# Stage metadata helpers — `npm stage view --json` output shape is an
|
|
210
|
+
# unverifiable-in-this-environment npm CLI surface (npm >= 11.15.0 is very
|
|
211
|
+
# new; no real binary or network access to npm's docs here), same caveat
|
|
212
|
+
# already documented for npm_stage_pipeline.sh's stage-id capture. Parsed
|
|
213
|
+
# defensively with fallback key names; a failed/empty view is tolerated
|
|
214
|
+
# everywhere it's used — callers fall back to documented defaults.
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
_stage_view_json() {
|
|
217
|
+
npm stage view "$1" --json 2>/dev/null || true
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
_stage_view_field() {
|
|
221
|
+
local view_json="$1"
|
|
222
|
+
shift
|
|
223
|
+
[[ -n "${view_json}" ]] || { printf ''; return; }
|
|
224
|
+
printf '%s' "${view_json}" | jq -e . >/dev/null 2>&1 || { printf ''; return; }
|
|
225
|
+
printf '%s' "${view_json}" | jq -r "$1" 2>/dev/null || true
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
_stage_package_name() {
|
|
229
|
+
_stage_view_field "$1" '.name // .package // .packageName // empty'
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
_stage_package_version() {
|
|
233
|
+
_stage_view_field "$1" '.version // empty'
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# resolve_stage_target <package-name> <config-path>
|
|
237
|
+
# Prints four tab-separated fields: target_name target_type smoke_cmd
|
|
238
|
+
# require_test_before_approve. Falls back to "unknown"/"npm"/""/"true" when
|
|
239
|
+
# no config, no matching target, or no npm.stage block is found — test and
|
|
240
|
+
# approve must still work against an unconfigured target using documented
|
|
241
|
+
# defaults.
|
|
242
|
+
resolve_stage_target() {
|
|
243
|
+
local package_name="$1" config_path="$2"
|
|
244
|
+
local target_name="unknown" target_type="npm" smoke_cmd="" require_test="true"
|
|
245
|
+
|
|
246
|
+
if [[ -n "${config_path}" && -f "${config_path}" ]] && command -v jq >/dev/null 2>&1; then
|
|
247
|
+
local target_json=""
|
|
248
|
+
if [[ -n "${package_name}" ]]; then
|
|
249
|
+
target_json="$(jq -c --arg pkg "${package_name}" \
|
|
250
|
+
'.targets[]? | select((.type == "npm" or .type == "npm-ci") and (.npm.package_name // "") == $pkg)' \
|
|
251
|
+
"${config_path}" 2>/dev/null | head -n 1)"
|
|
252
|
+
fi
|
|
253
|
+
if [[ -z "${target_json}" ]]; then
|
|
254
|
+
# No package-name match (or view failed) — fall back to the first
|
|
255
|
+
# configured npm/npm-ci target that has a stage block, so the common
|
|
256
|
+
# single-target setup still resolves.
|
|
257
|
+
target_json="$(jq -c '.targets[]? | select((.type == "npm" or .type == "npm-ci") and (.npm.stage // null) != null)' \
|
|
258
|
+
"${config_path}" 2>/dev/null | head -n 1)"
|
|
259
|
+
fi
|
|
260
|
+
if [[ -z "${target_json}" ]]; then
|
|
261
|
+
target_json="$(jq -c '.targets[]? | select(.type == "npm" or .type == "npm-ci")' \
|
|
262
|
+
"${config_path}" 2>/dev/null | head -n 1)"
|
|
263
|
+
fi
|
|
264
|
+
if [[ -n "${target_json}" ]]; then
|
|
265
|
+
target_name="$(printf '%s' "${target_json}" | jq -r '.name // "unknown"')"
|
|
266
|
+
target_type="$(printf '%s' "${target_json}" | jq -r '.type // "npm"')"
|
|
267
|
+
smoke_cmd="$(printf '%s' "${target_json}" | jq -r '.npm.stage.smoke_cmd // empty')"
|
|
268
|
+
require_test="$(printf '%s' "${target_json}" | jq -r '(.npm.stage.require_test_before_approve // true)')"
|
|
269
|
+
fi
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
# A plain tab is unsuitable as the field delimiter here: bash's `read`
|
|
273
|
+
# treats tab as "IFS whitespace" regardless of what IFS is set to, which
|
|
274
|
+
# collapses consecutive delimiters and silently drops empty fields (e.g.
|
|
275
|
+
# an unset smoke_cmd) rather than preserving them as empty. The ASCII
|
|
276
|
+
# Unit Separator (0x1F) is not treated specially and is never expected to
|
|
277
|
+
# appear in any of these values.
|
|
278
|
+
printf '%s\x1f%s\x1f%s\x1f%s\n' "${target_name}" "${target_type}" "${smoke_cmd}" "${require_test}"
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# ---------------------------------------------------------------------------
|
|
282
|
+
# Tarball download — shared by `download` and `test`. Since the exact output
|
|
283
|
+
# shape/naming convention of `npm stage download` is unverifiable in this
|
|
284
|
+
# environment, this snapshots the destination directory's file listing
|
|
285
|
+
# before and after running the command (with cwd set to the destination, so
|
|
286
|
+
# wherever the tarball lands, it lands inside it) and diffs to find the new
|
|
287
|
+
# file — robust regardless of naming convention.
|
|
288
|
+
# ---------------------------------------------------------------------------
|
|
289
|
+
_download_stage_tarball() {
|
|
290
|
+
local stage_id="$1" dest_dir="$2"
|
|
291
|
+
mkdir -p "${dest_dir}"
|
|
292
|
+
dest_dir="$(cd "${dest_dir}" && pwd)"
|
|
293
|
+
|
|
294
|
+
local before after new_file=""
|
|
295
|
+
before="$(find "${dest_dir}" -maxdepth 1 -type f 2>/dev/null | sort)"
|
|
296
|
+
|
|
297
|
+
local output status=0
|
|
298
|
+
output="$(cd "${dest_dir}" && npm stage download "${stage_id}" 2>&1)" || status=$?
|
|
299
|
+
if [[ ${status} -ne 0 ]]; then
|
|
300
|
+
printf '%s\n' "${output}" >&2
|
|
301
|
+
printf 'npm stage inspect: npm stage download failed (exit %s).\n' "${status}" >&2
|
|
302
|
+
return "${EXIT_OP_FAILED}"
|
|
303
|
+
fi
|
|
304
|
+
|
|
305
|
+
after="$(find "${dest_dir}" -maxdepth 1 -type f 2>/dev/null | sort)"
|
|
306
|
+
new_file="$(comm -13 <(printf '%s\n' "${before}") <(printf '%s\n' "${after}") | head -n 1)"
|
|
307
|
+
|
|
308
|
+
if [[ -z "${new_file}" ]]; then
|
|
309
|
+
new_file="$(printf '%s\n' "${output}" | grep -Eo '[^[:space:]"'"'"']+\.tgz' | head -n 1)"
|
|
310
|
+
if [[ -n "${new_file}" && "${new_file}" != /* ]]; then
|
|
311
|
+
new_file="${dest_dir}/${new_file}"
|
|
312
|
+
fi
|
|
313
|
+
fi
|
|
314
|
+
|
|
315
|
+
if [[ -z "${new_file}" || ! -f "${new_file}" ]]; then
|
|
316
|
+
printf 'npm stage inspect: could not locate the downloaded tarball in %s after "npm stage download %s". npm output:\n%s\n' \
|
|
317
|
+
"${dest_dir}" "${stage_id}" "${output}" >&2
|
|
318
|
+
return "${EXIT_OP_FAILED}"
|
|
319
|
+
fi
|
|
320
|
+
|
|
321
|
+
printf '%s\n' "${new_file}"
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
# _installed_package_info <scratch-dir>
|
|
325
|
+
# Prints "<name>\x1f<version>" for the single dependency npm installed from
|
|
326
|
+
# the tarball into a fresh `npm init -y` scratch project — the ground-truth
|
|
327
|
+
# name/version actually installed (read from the scratch project's own
|
|
328
|
+
# package.json "dependencies" key, then the installed package's own
|
|
329
|
+
# package.json), independent of whatever `npm stage view` reports. Uses the
|
|
330
|
+
# ASCII Unit Separator (0x1F) as the field delimiter, not a tab — see the
|
|
331
|
+
# comment in resolve_stage_target for why a tab silently drops empty fields
|
|
332
|
+
# under bash's `read`.
|
|
333
|
+
_installed_package_info() {
|
|
334
|
+
local scratch_dir="$1"
|
|
335
|
+
local dep_name
|
|
336
|
+
dep_name="$(jq -r '.dependencies // {} | keys[0] // empty' "${scratch_dir}/package.json" 2>/dev/null || true)"
|
|
337
|
+
if [[ -z "${dep_name}" ]]; then
|
|
338
|
+
printf '\x1f\n'
|
|
339
|
+
return
|
|
340
|
+
fi
|
|
341
|
+
local pkg_json="${scratch_dir}/node_modules/${dep_name}/package.json"
|
|
342
|
+
if [[ ! -f "${pkg_json}" ]]; then
|
|
343
|
+
printf '%s\x1f\n' "${dep_name}"
|
|
344
|
+
return
|
|
345
|
+
fi
|
|
346
|
+
local version
|
|
347
|
+
version="$(jq -r '.version // empty' "${pkg_json}" 2>/dev/null || true)"
|
|
348
|
+
printf '%s\x1f%s\n' "${dep_name}" "${version}"
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
# _default_smoke_check <scratch-dir>
|
|
352
|
+
# See the header comment for why this checks .claude/.agents mirroring
|
|
353
|
+
# rather than a project-root copy of hooks/scripts/templates.
|
|
354
|
+
_default_smoke_check() {
|
|
355
|
+
local scratch_dir="$1"
|
|
356
|
+
local -a missing=()
|
|
357
|
+
local dir
|
|
358
|
+
|
|
359
|
+
for dir in .claude/skills .claude/agents .agents/skills .agents/agents; do
|
|
360
|
+
if [[ ! -d "${scratch_dir}/${dir}" ]] || [[ -z "$(ls -A "${scratch_dir}/${dir}" 2>/dev/null)" ]]; then
|
|
361
|
+
missing+=("${dir}/ (missing or empty)")
|
|
362
|
+
fi
|
|
363
|
+
done
|
|
364
|
+
|
|
365
|
+
[[ -f "${scratch_dir}/.jenga-version" ]] || missing+=(".jenga-version (not written)")
|
|
366
|
+
[[ -f "${scratch_dir}/.github/copilot-instructions.md" ]] || missing+=(".github/copilot-instructions.md (not bootstrapped)")
|
|
367
|
+
|
|
368
|
+
if (( ${#missing[@]} > 0 )); then
|
|
369
|
+
printf 'default smoke check FAILED — postinstall did not produce the expected layout under %s:\n' "${scratch_dir}"
|
|
370
|
+
printf ' - %s\n' "${missing[@]}"
|
|
371
|
+
return 1
|
|
372
|
+
fi
|
|
373
|
+
|
|
374
|
+
printf 'default smoke check passed — postinstall populated .claude/{skills,agents}, .agents/{skills,agents}, .jenga-version, and .github/copilot-instructions.md under %s\n' "${scratch_dir}"
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
# _cleanup_scratch_dir <scratch-dir> <keep-flag>
|
|
378
|
+
# Invoked from an EXIT trap registered by cmd_test, with both arguments
|
|
379
|
+
# baked into the trap command string at registration time (see the comment
|
|
380
|
+
# at the trap registration site for why — dynamic-scope lookup of the
|
|
381
|
+
# calling function's `local`s does not survive `exit` unwinding the call
|
|
382
|
+
# stack before traps run).
|
|
383
|
+
_cleanup_scratch_dir() {
|
|
384
|
+
local dir="$1" keep_flag="$2"
|
|
385
|
+
if [[ "${keep_flag}" == "1" ]]; then
|
|
386
|
+
log_warn "--keep set: leaving scratch dir at ${dir}"
|
|
387
|
+
return
|
|
388
|
+
fi
|
|
389
|
+
rm -rf "${dir}"
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
_write_stage_tested_ledger() {
|
|
393
|
+
local stage_id="$1" target_name="$2" target_type="$3" config_path="$4" result="$5" version="$6"
|
|
394
|
+
local -a cmd=(bash "${WRITE_LEDGER_SCRIPT}" "${target_name}" "${target_type}" stage_tested "" --stage-id "${stage_id}" --result "${result}")
|
|
395
|
+
[[ -n "${config_path}" ]] && cmd+=(--config "${config_path}")
|
|
396
|
+
[[ -n "${version}" ]] && cmd+=(--version "${version}")
|
|
397
|
+
"${cmd[@]}" || log_warn "failed to write 'stage_tested' ledger entry for stage ${stage_id} (result: ${result})"
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
# ---------------------------------------------------------------------------
|
|
401
|
+
# list [<package-spec>] [--config <path>] [--dry-run] [--json]
|
|
402
|
+
# ---------------------------------------------------------------------------
|
|
403
|
+
cmd_list() {
|
|
404
|
+
local config_arg="" dry_run=0 json_out=0
|
|
405
|
+
local -a positional=()
|
|
406
|
+
while [[ $# -gt 0 ]]; do
|
|
407
|
+
case "$1" in
|
|
408
|
+
--config) [[ $# -ge 2 ]] || fail_usage "--config requires a value"; config_arg="$2"; shift 2 ;;
|
|
409
|
+
--dry-run) dry_run=1; shift ;;
|
|
410
|
+
--json) json_out=1; shift ;;
|
|
411
|
+
-h|--help) usage; exit "${EXIT_OK}" ;;
|
|
412
|
+
-*) fail_usage "list: unknown flag: $1" ;;
|
|
413
|
+
*) positional+=("$1"); shift ;;
|
|
414
|
+
esac
|
|
415
|
+
done
|
|
416
|
+
[[ ${#positional[@]} -le 1 ]] || fail_usage "list: too many arguments"
|
|
417
|
+
local package_spec="${positional[0]:-}"
|
|
418
|
+
# config_arg is accepted for interface consistency across sub-commands;
|
|
419
|
+
# `list` itself needs no config (npm stage list takes no target/config).
|
|
420
|
+
: "${config_arg}"
|
|
421
|
+
|
|
422
|
+
local -a cmd=(npm stage list)
|
|
423
|
+
[[ -n "${package_spec}" ]] && cmd+=("${package_spec}")
|
|
424
|
+
cmd+=(--json)
|
|
425
|
+
|
|
426
|
+
if (( dry_run )); then
|
|
427
|
+
log_info "[dry-run] resolved command: $(printf '%q ' "${cmd[@]}")"
|
|
428
|
+
exit "${EXIT_OK}"
|
|
429
|
+
fi
|
|
430
|
+
|
|
431
|
+
local output status=0
|
|
432
|
+
output="$("${cmd[@]}" 2>&1)" || status=$?
|
|
433
|
+
if [[ ${status} -ne 0 ]]; then
|
|
434
|
+
printf '%s\n' "${output}" >&2
|
|
435
|
+
printf 'npm stage inspect: npm stage list failed (exit %s).\n' "${status}" >&2
|
|
436
|
+
exit "${EXIT_OP_FAILED}"
|
|
437
|
+
fi
|
|
438
|
+
|
|
439
|
+
if (( json_out )); then
|
|
440
|
+
printf '%s\n' "${output}"
|
|
441
|
+
exit "${EXIT_OK}"
|
|
442
|
+
fi
|
|
443
|
+
|
|
444
|
+
if ! printf '%s' "${output}" | jq -e . >/dev/null 2>&1; then
|
|
445
|
+
printf '%s\n' "${output}"
|
|
446
|
+
exit "${EXIT_OK}"
|
|
447
|
+
fi
|
|
448
|
+
|
|
449
|
+
printf '%-24s %-30s %-10s %-10s %s\n' 'STAGE ID' 'PACKAGE' 'VERSION' 'DIST-TAG' 'STAGED AT'
|
|
450
|
+
printf '%-24s %-30s %-10s %-10s %s\n' '--------' '-------' '-------' '--------' '---------'
|
|
451
|
+
printf '%s' "${output}" | jq -r '
|
|
452
|
+
( if (type == "array") then . else (.stages? // .items? // []) end ) as $entries
|
|
453
|
+
| $entries[]?
|
|
454
|
+
| [ (.id // .stageId // .stage_id // "-"),
|
|
455
|
+
(.name // .package // .packageName // "-"),
|
|
456
|
+
(.version // "-"),
|
|
457
|
+
(.tag // .distTag // .dist_tag // "-"),
|
|
458
|
+
(.stagedAt // .staged_at // .created // .createdAt // "-") ]
|
|
459
|
+
| @tsv
|
|
460
|
+
' | while IFS=$'\t' read -r sid pkg ver tag staged; do
|
|
461
|
+
printf '%-24s %-30s %-10s %-10s %s\n' "${sid}" "${pkg}" "${ver}" "${tag}" "${staged}"
|
|
462
|
+
done
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
# ---------------------------------------------------------------------------
|
|
466
|
+
# view <stage-id> [--config <path>] [--dry-run] [--json]
|
|
467
|
+
# ---------------------------------------------------------------------------
|
|
468
|
+
cmd_view() {
|
|
469
|
+
local config_arg="" dry_run=0 json_out=0
|
|
470
|
+
local -a positional=()
|
|
471
|
+
while [[ $# -gt 0 ]]; do
|
|
472
|
+
case "$1" in
|
|
473
|
+
--config) [[ $# -ge 2 ]] || fail_usage "--config requires a value"; config_arg="$2"; shift 2 ;;
|
|
474
|
+
--dry-run) dry_run=1; shift ;;
|
|
475
|
+
--json) json_out=1; shift ;;
|
|
476
|
+
-h|--help) usage; exit "${EXIT_OK}" ;;
|
|
477
|
+
-*) fail_usage "view: unknown flag: $1" ;;
|
|
478
|
+
*) positional+=("$1"); shift ;;
|
|
479
|
+
esac
|
|
480
|
+
done
|
|
481
|
+
[[ ${#positional[@]} -ge 1 ]] || fail_usage "view: <stage-id> is required"
|
|
482
|
+
[[ ${#positional[@]} -le 1 ]] || fail_usage "view: too many arguments"
|
|
483
|
+
local stage_id="${positional[0]}"
|
|
484
|
+
: "${config_arg}"
|
|
485
|
+
|
|
486
|
+
local -a cmd=(npm stage view "${stage_id}")
|
|
487
|
+
(( json_out )) && cmd+=(--json)
|
|
488
|
+
|
|
489
|
+
if (( dry_run )); then
|
|
490
|
+
log_info "[dry-run] resolved command: $(printf '%q ' "${cmd[@]}")"
|
|
491
|
+
exit "${EXIT_OK}"
|
|
492
|
+
fi
|
|
493
|
+
|
|
494
|
+
local output status=0
|
|
495
|
+
output="$("${cmd[@]}" 2>&1)" || status=$?
|
|
496
|
+
printf '%s\n' "${output}"
|
|
497
|
+
if [[ ${status} -ne 0 ]]; then
|
|
498
|
+
printf 'npm stage inspect: npm stage view failed (exit %s).\n' "${status}" >&2
|
|
499
|
+
exit "${EXIT_OP_FAILED}"
|
|
500
|
+
fi
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
# ---------------------------------------------------------------------------
|
|
504
|
+
# download <stage-id> [--out <dir>] [--config <path>] [--dry-run] [--json]
|
|
505
|
+
# ---------------------------------------------------------------------------
|
|
506
|
+
cmd_download() {
|
|
507
|
+
local config_arg="" dry_run=0 json_out=0 out_dir=""
|
|
508
|
+
local -a positional=()
|
|
509
|
+
while [[ $# -gt 0 ]]; do
|
|
510
|
+
case "$1" in
|
|
511
|
+
--config) [[ $# -ge 2 ]] || fail_usage "--config requires a value"; config_arg="$2"; shift 2 ;;
|
|
512
|
+
--dry-run) dry_run=1; shift ;;
|
|
513
|
+
--json) json_out=1; shift ;;
|
|
514
|
+
--out) [[ $# -ge 2 ]] || fail_usage "--out requires a value"; out_dir="$2"; shift 2 ;;
|
|
515
|
+
-h|--help) usage; exit "${EXIT_OK}" ;;
|
|
516
|
+
-*) fail_usage "download: unknown flag: $1" ;;
|
|
517
|
+
*) positional+=("$1"); shift ;;
|
|
518
|
+
esac
|
|
519
|
+
done
|
|
520
|
+
[[ ${#positional[@]} -ge 1 ]] || fail_usage "download: <stage-id> is required"
|
|
521
|
+
[[ ${#positional[@]} -le 1 ]] || fail_usage "download: too many arguments"
|
|
522
|
+
local stage_id="${positional[0]}"
|
|
523
|
+
: "${config_arg}"
|
|
524
|
+
|
|
525
|
+
if (( dry_run )); then
|
|
526
|
+
log_info "[dry-run] resolved command: npm stage download $(printf '%q' "${stage_id}") (cwd: ${out_dir:-<a fresh mktemp -d>})"
|
|
527
|
+
exit "${EXIT_OK}"
|
|
528
|
+
fi
|
|
529
|
+
|
|
530
|
+
if [[ -z "${out_dir}" ]]; then
|
|
531
|
+
out_dir="$(mktemp -d "${TMPDIR:-/tmp}/npm-stage-download.XXXXXX")"
|
|
532
|
+
fi
|
|
533
|
+
|
|
534
|
+
local tarball_path
|
|
535
|
+
tarball_path="$(_download_stage_tarball "${stage_id}" "${out_dir}")" || exit "${EXIT_OP_FAILED}"
|
|
536
|
+
|
|
537
|
+
if (( json_out )); then
|
|
538
|
+
jq -cn --arg stage_id "${stage_id}" --arg path "${tarball_path}" '{stage_id: $stage_id, tarball_path: $path}'
|
|
539
|
+
else
|
|
540
|
+
printf 'Downloaded stage %s to %s\n' "${stage_id}" "${tarball_path}"
|
|
541
|
+
fi
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
# ---------------------------------------------------------------------------
|
|
545
|
+
# test <stage-id> [--keep] [--config <path>] [--dry-run] [--json]
|
|
546
|
+
# ---------------------------------------------------------------------------
|
|
547
|
+
cmd_test() {
|
|
548
|
+
local config_arg="" dry_run=0 json_out=0 keep=0
|
|
549
|
+
local -a positional=()
|
|
550
|
+
while [[ $# -gt 0 ]]; do
|
|
551
|
+
case "$1" in
|
|
552
|
+
--config) [[ $# -ge 2 ]] || fail_usage "--config requires a value"; config_arg="$2"; shift 2 ;;
|
|
553
|
+
--dry-run) dry_run=1; shift ;;
|
|
554
|
+
--json) json_out=1; shift ;;
|
|
555
|
+
--keep) keep=1; shift ;;
|
|
556
|
+
-h|--help) usage; exit "${EXIT_OK}" ;;
|
|
557
|
+
-*) fail_usage "test: unknown flag: $1" ;;
|
|
558
|
+
*) positional+=("$1"); shift ;;
|
|
559
|
+
esac
|
|
560
|
+
done
|
|
561
|
+
[[ ${#positional[@]} -ge 1 ]] || fail_usage "test: <stage-id> is required"
|
|
562
|
+
[[ ${#positional[@]} -le 1 ]] || fail_usage "test: too many arguments"
|
|
563
|
+
local stage_id="${positional[0]}"
|
|
564
|
+
|
|
565
|
+
local config_path=""
|
|
566
|
+
config_path="$(resolve_config_path "${config_arg}" || true)"
|
|
567
|
+
|
|
568
|
+
local view_json package_name_hint
|
|
569
|
+
view_json="$(_stage_view_json "${stage_id}")"
|
|
570
|
+
package_name_hint="$(_stage_package_name "${view_json}")"
|
|
571
|
+
|
|
572
|
+
local target_name target_type smoke_cmd require_test
|
|
573
|
+
IFS=$'\x1f' read -r target_name target_type smoke_cmd require_test < <(resolve_stage_target "${package_name_hint}" "${config_path}")
|
|
574
|
+
|
|
575
|
+
if (( dry_run )); then
|
|
576
|
+
log_info "[dry-run] would: download stage ${stage_id}; npm init -y && npm install <tarball> in a mktemp -d scratch dir outside ${REPO_ROOT}; then run: ${smoke_cmd:-<documented default smoke check>}"
|
|
577
|
+
exit "${EXIT_OK}"
|
|
578
|
+
fi
|
|
579
|
+
|
|
580
|
+
local scratch_dir
|
|
581
|
+
scratch_dir="$(mktemp -d "${TMPDIR:-/tmp}/npm-stage-test.XXXXXX")"
|
|
582
|
+
|
|
583
|
+
# Defense in depth: mktemp already guarantees a location outside the repo
|
|
584
|
+
# working tree in any normal environment; assert it explicitly so a
|
|
585
|
+
# misconfigured TMPDIR can never silently install into (or overwrite) the
|
|
586
|
+
# repo's own node_modules.
|
|
587
|
+
local scratch_real repo_real
|
|
588
|
+
scratch_real="$(cd "${scratch_dir}" && pwd -P)"
|
|
589
|
+
repo_real="$(cd "${REPO_ROOT}" && pwd -P)"
|
|
590
|
+
case "${scratch_real}" in
|
|
591
|
+
"${repo_real}"|"${repo_real}"/*)
|
|
592
|
+
printf 'npm stage inspect: refusing to run — scratch dir %s resolved inside the repository (%s). Check TMPDIR.\n' "${scratch_real}" "${repo_real}" >&2
|
|
593
|
+
rm -rf "${scratch_dir}"
|
|
594
|
+
exit "${EXIT_OP_FAILED}"
|
|
595
|
+
;;
|
|
596
|
+
esac
|
|
597
|
+
|
|
598
|
+
# Register cleanup via a top-level function invoked with explicit
|
|
599
|
+
# arguments baked into the trap command string at registration time —
|
|
600
|
+
# NOT a closure referencing cmd_test's `local $scratch_dir`/`$keep`
|
|
601
|
+
# directly. `exit` unwinds the function call stack (popping `local`
|
|
602
|
+
# bindings) before running EXIT traps, so a trap body that reads those
|
|
603
|
+
# names by dynamic scope sees them as unset once triggered; passing the
|
|
604
|
+
# values as positional arguments sidesteps that entirely.
|
|
605
|
+
# shellcheck disable=SC2064 # intentional: expand now, not at signal time
|
|
606
|
+
trap "_cleanup_scratch_dir $(printf '%q' "${scratch_dir}") $(printf '%q' "${keep}")" EXIT
|
|
607
|
+
|
|
608
|
+
log_info "[test] downloading stage ${stage_id}..."
|
|
609
|
+
local tarball_path
|
|
610
|
+
tarball_path="$(_download_stage_tarball "${stage_id}" "${scratch_dir}")" || exit "${EXIT_OP_FAILED}"
|
|
611
|
+
|
|
612
|
+
log_info "[test] installing tarball into isolated scratch project at ${scratch_dir}..."
|
|
613
|
+
local install_output install_status=0
|
|
614
|
+
install_output="$(cd "${scratch_dir}" && npm init -y >/dev/null 2>&1 && npm install "${tarball_path}" 2>&1)" || install_status=$?
|
|
615
|
+
if [[ ${install_status} -ne 0 ]]; then
|
|
616
|
+
printf '%s\n' "${install_output}" >&2
|
|
617
|
+
printf 'npm stage inspect: tarball install failed (exit %s).\n' "${install_status}" >&2
|
|
618
|
+
_write_stage_tested_ledger "${stage_id}" "${target_name}" "${target_type}" "${config_path}" fail ""
|
|
619
|
+
exit "${EXIT_OP_FAILED}"
|
|
620
|
+
fi
|
|
621
|
+
|
|
622
|
+
local installed_name installed_version
|
|
623
|
+
IFS=$'\x1f' read -r installed_name installed_version < <(_installed_package_info "${scratch_dir}")
|
|
624
|
+
[[ -n "${installed_version}" ]] || installed_version="$(_stage_package_version "${view_json}")"
|
|
625
|
+
|
|
626
|
+
local smoke_status=0 smoke_output=""
|
|
627
|
+
if [[ -n "${smoke_cmd}" ]]; then
|
|
628
|
+
log_info "[test] running configured npm.stage.smoke_cmd: ${smoke_cmd}"
|
|
629
|
+
smoke_output="$(cd "${scratch_dir}" && bash -c "${smoke_cmd}" 2>&1)" || smoke_status=$?
|
|
630
|
+
else
|
|
631
|
+
log_info "[test] running default smoke check (postinstall file-copy verification)..."
|
|
632
|
+
smoke_output="$(_default_smoke_check "${scratch_dir}" 2>&1)" || smoke_status=$?
|
|
633
|
+
fi
|
|
634
|
+
|
|
635
|
+
if [[ ${smoke_status} -ne 0 ]]; then
|
|
636
|
+
printf '%s\n' "${smoke_output}" >&2
|
|
637
|
+
printf 'npm stage inspect: smoke test FAILED for stage %s (package %s).\n' "${stage_id}" "${installed_name:-unknown}" >&2
|
|
638
|
+
_write_stage_tested_ledger "${stage_id}" "${target_name}" "${target_type}" "${config_path}" fail "${installed_version}"
|
|
639
|
+
exit "${EXIT_OP_FAILED}"
|
|
640
|
+
fi
|
|
641
|
+
|
|
642
|
+
printf '%s\n' "${smoke_output}"
|
|
643
|
+
log_info "[test] smoke test PASSED for stage ${stage_id} (package ${installed_name:-unknown})."
|
|
644
|
+
_write_stage_tested_ledger "${stage_id}" "${target_name}" "${target_type}" "${config_path}" pass "${installed_version}"
|
|
645
|
+
|
|
646
|
+
if (( json_out )); then
|
|
647
|
+
jq -cn --arg stage_id "${stage_id}" --arg pkg "${installed_name:-}" --arg version "${installed_version:-}" \
|
|
648
|
+
'{stage_id: $stage_id, package: $pkg, version: $version, result: "pass"}'
|
|
649
|
+
fi
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
# ---------------------------------------------------------------------------
|
|
653
|
+
# approve <stage-id> [--otp <otp>] [--force <reason>] [--config <path>] [--dry-run] [--json]
|
|
654
|
+
# ---------------------------------------------------------------------------
|
|
655
|
+
cmd_approve() {
|
|
656
|
+
# The OTP tracing guard runs once, at the top level of this script, before
|
|
657
|
+
# `main "$@"` is ever called — see the comment there for why a guard
|
|
658
|
+
# placed only here (inside cmd_approve) is provably insufficient: bash's
|
|
659
|
+
# xtrace prints the expanded arguments of the `main "$@"` and
|
|
660
|
+
# `cmd_approve "$@"` call sites themselves, before this function's body
|
|
661
|
+
# ever runs.
|
|
662
|
+
|
|
663
|
+
local config_arg="" dry_run=0 json_out=0 otp="" force_reason="" force_given=0
|
|
664
|
+
local -a positional=()
|
|
665
|
+
while [[ $# -gt 0 ]]; do
|
|
666
|
+
case "$1" in
|
|
667
|
+
--config) [[ $# -ge 2 ]] || fail_usage "--config requires a value"; config_arg="$2"; shift 2 ;;
|
|
668
|
+
--dry-run) dry_run=1; shift ;;
|
|
669
|
+
--json) json_out=1; shift ;;
|
|
670
|
+
--otp) [[ $# -ge 2 ]] || fail_usage "--otp requires a value"; otp="$2"; shift 2 ;;
|
|
671
|
+
--otp=*) otp="${1#*=}"; shift ;;
|
|
672
|
+
--force) [[ $# -ge 2 ]] || fail_usage "--force requires a <reason>"; force_given=1; force_reason="$2"; shift 2 ;;
|
|
673
|
+
-h|--help) usage; exit "${EXIT_OK}" ;;
|
|
674
|
+
-*) fail_usage "approve: unknown flag: $1" ;;
|
|
675
|
+
*) positional+=("$1"); shift ;;
|
|
676
|
+
esac
|
|
677
|
+
done
|
|
678
|
+
[[ ${#positional[@]} -ge 1 ]] || fail_usage "approve: <stage-id> is required"
|
|
679
|
+
[[ ${#positional[@]} -le 1 ]] || fail_usage "approve: too many arguments"
|
|
680
|
+
local stage_id="${positional[0]}"
|
|
681
|
+
|
|
682
|
+
if (( force_given )); then
|
|
683
|
+
[[ -n "${force_reason}" ]] || fail_usage "approve: --force requires a non-empty <reason>"
|
|
684
|
+
fi
|
|
685
|
+
|
|
686
|
+
local config_path=""
|
|
687
|
+
config_path="$(resolve_config_path "${config_arg}" || true)"
|
|
688
|
+
|
|
689
|
+
local view_json package_name package_version
|
|
690
|
+
view_json="$(_stage_view_json "${stage_id}")"
|
|
691
|
+
package_name="$(_stage_package_name "${view_json}")"
|
|
692
|
+
package_version="$(_stage_package_version "${view_json}")"
|
|
693
|
+
|
|
694
|
+
local target_name target_type smoke_cmd require_test
|
|
695
|
+
IFS=$'\x1f' read -r target_name target_type smoke_cmd require_test < <(resolve_stage_target "${package_name}" "${config_path}")
|
|
696
|
+
: "${smoke_cmd}"
|
|
697
|
+
|
|
698
|
+
if [[ "${require_test}" == "true" ]] && (( ! force_given )); then
|
|
699
|
+
local history_file
|
|
700
|
+
history_file="$(publish_resolve_history_file "${config_path}")"
|
|
701
|
+
if ! publish_history_has_passing_stage_test "${history_file}" "${stage_id}"; then
|
|
702
|
+
printf 'npm stage inspect: approve refused — stage %s has no passing test on record in %s.\n' "${stage_id}" "${history_file}" >&2
|
|
703
|
+
printf 'Fix: run "npm_stage_inspect.sh test %s" first, or pass --force <reason> to override.\n' "${stage_id}" >&2
|
|
704
|
+
exit "${EXIT_USAGE}"
|
|
705
|
+
fi
|
|
706
|
+
fi
|
|
707
|
+
|
|
708
|
+
local -a exec_cmd=(npm stage approve "${stage_id}")
|
|
709
|
+
local -a display_cmd=("${exec_cmd[@]}")
|
|
710
|
+
if [[ -n "${otp}" ]]; then
|
|
711
|
+
display_cmd+=(--otp '********')
|
|
712
|
+
# Tracing was already suspended for the rest of this run (if it was
|
|
713
|
+
# active) by the guard above, the moment argv was found to contain
|
|
714
|
+
# --otp — before OTP was ever assigned to a variable. Nothing further
|
|
715
|
+
# to do here.
|
|
716
|
+
exec_cmd+=(--otp "${otp}")
|
|
717
|
+
fi
|
|
718
|
+
|
|
719
|
+
if (( dry_run )); then
|
|
720
|
+
log_info "[dry-run] resolved command: $(printf '%q ' "${display_cmd[@]}")"
|
|
721
|
+
exit "${EXIT_OK}"
|
|
722
|
+
fi
|
|
723
|
+
|
|
724
|
+
log_info "[approve] resolved command: $(printf '%q ' "${display_cmd[@]}")"
|
|
725
|
+
|
|
726
|
+
local output status=0
|
|
727
|
+
output="$("${exec_cmd[@]}" 2>&1)" || status=$?
|
|
728
|
+
|
|
729
|
+
# Defensive redaction: strip the literal OTP from captured output before
|
|
730
|
+
# it is ever printed, in case npm echoed the argv back in an error
|
|
731
|
+
# message.
|
|
732
|
+
if [[ -n "${otp}" ]]; then
|
|
733
|
+
output="${output//"${otp}"/********}"
|
|
734
|
+
fi
|
|
735
|
+
printf '%s\n' "${output}"
|
|
736
|
+
|
|
737
|
+
if [[ ${status} -ne 0 ]]; then
|
|
738
|
+
printf 'npm stage inspect: npm stage approve failed (exit %s).\n' "${status}" >&2
|
|
739
|
+
exit "${EXIT_OP_FAILED}"
|
|
740
|
+
fi
|
|
741
|
+
|
|
742
|
+
local -a ledger_cmd=(bash "${WRITE_LEDGER_SCRIPT}" "${target_name}" "${target_type}" approved "" --stage-id "${stage_id}")
|
|
743
|
+
[[ -n "${config_path}" ]] && ledger_cmd+=(--config "${config_path}")
|
|
744
|
+
[[ -n "${package_version}" ]] && ledger_cmd+=(--version "${package_version}")
|
|
745
|
+
(( force_given )) && ledger_cmd+=(--reason "${force_reason}")
|
|
746
|
+
"${ledger_cmd[@]}" || log_warn "failed to write 'approved' ledger entry for stage ${stage_id}"
|
|
747
|
+
|
|
748
|
+
if (( json_out )); then
|
|
749
|
+
jq -cn --arg stage_id "${stage_id}" --arg pkg "${package_name:-}" '{stage_id: $stage_id, package: $pkg, state: "approved"}'
|
|
750
|
+
fi
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
# ---------------------------------------------------------------------------
|
|
754
|
+
# reject <stage-id> [--config <path>] [--dry-run] [--json]
|
|
755
|
+
# ---------------------------------------------------------------------------
|
|
756
|
+
cmd_reject() {
|
|
757
|
+
local config_arg="" dry_run=0 json_out=0
|
|
758
|
+
local -a positional=()
|
|
759
|
+
while [[ $# -gt 0 ]]; do
|
|
760
|
+
case "$1" in
|
|
761
|
+
--config) [[ $# -ge 2 ]] || fail_usage "--config requires a value"; config_arg="$2"; shift 2 ;;
|
|
762
|
+
--dry-run) dry_run=1; shift ;;
|
|
763
|
+
--json) json_out=1; shift ;;
|
|
764
|
+
-h|--help) usage; exit "${EXIT_OK}" ;;
|
|
765
|
+
-*) fail_usage "reject: unknown flag: $1" ;;
|
|
766
|
+
*) positional+=("$1"); shift ;;
|
|
767
|
+
esac
|
|
768
|
+
done
|
|
769
|
+
[[ ${#positional[@]} -ge 1 ]] || fail_usage "reject: <stage-id> is required"
|
|
770
|
+
[[ ${#positional[@]} -le 1 ]] || fail_usage "reject: too many arguments"
|
|
771
|
+
local stage_id="${positional[0]}"
|
|
772
|
+
|
|
773
|
+
local config_path=""
|
|
774
|
+
config_path="$(resolve_config_path "${config_arg}" || true)"
|
|
775
|
+
|
|
776
|
+
local view_json package_name package_version
|
|
777
|
+
view_json="$(_stage_view_json "${stage_id}")"
|
|
778
|
+
package_name="$(_stage_package_name "${view_json}")"
|
|
779
|
+
package_version="$(_stage_package_version "${view_json}")"
|
|
780
|
+
|
|
781
|
+
local target_name target_type smoke_cmd require_test
|
|
782
|
+
IFS=$'\x1f' read -r target_name target_type smoke_cmd require_test < <(resolve_stage_target "${package_name}" "${config_path}")
|
|
783
|
+
: "${smoke_cmd}" "${require_test}"
|
|
784
|
+
|
|
785
|
+
local -a cmd=(npm stage reject "${stage_id}")
|
|
786
|
+
|
|
787
|
+
if (( dry_run )); then
|
|
788
|
+
log_info "[dry-run] resolved command: $(printf '%q ' "${cmd[@]}")"
|
|
789
|
+
exit "${EXIT_OK}"
|
|
790
|
+
fi
|
|
791
|
+
|
|
792
|
+
local output status=0
|
|
793
|
+
output="$("${cmd[@]}" 2>&1)" || status=$?
|
|
794
|
+
printf '%s\n' "${output}"
|
|
795
|
+
if [[ ${status} -ne 0 ]]; then
|
|
796
|
+
printf 'npm stage inspect: npm stage reject failed (exit %s).\n' "${status}" >&2
|
|
797
|
+
exit "${EXIT_OP_FAILED}"
|
|
798
|
+
fi
|
|
799
|
+
|
|
800
|
+
local -a ledger_cmd=(bash "${WRITE_LEDGER_SCRIPT}" "${target_name}" "${target_type}" rejected "" --stage-id "${stage_id}")
|
|
801
|
+
[[ -n "${config_path}" ]] && ledger_cmd+=(--config "${config_path}")
|
|
802
|
+
[[ -n "${package_version}" ]] && ledger_cmd+=(--version "${package_version}")
|
|
803
|
+
"${ledger_cmd[@]}" || log_warn "failed to write 'rejected' ledger entry for stage ${stage_id}"
|
|
804
|
+
|
|
805
|
+
if (( json_out )); then
|
|
806
|
+
jq -cn --arg stage_id "${stage_id}" --arg pkg "${package_name:-}" '{stage_id: $stage_id, package: $pkg, state: "rejected"}'
|
|
807
|
+
fi
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
# ---------------------------------------------------------------------------
|
|
811
|
+
# Dispatch
|
|
812
|
+
# ---------------------------------------------------------------------------
|
|
813
|
+
main() {
|
|
814
|
+
[[ $# -ge 1 ]] || fail_usage "a sub-command is required"
|
|
815
|
+
local sub="$1"
|
|
816
|
+
shift
|
|
817
|
+
case "${sub}" in
|
|
818
|
+
list) cmd_list "$@" ;;
|
|
819
|
+
view) cmd_view "$@" ;;
|
|
820
|
+
download) cmd_download "$@" ;;
|
|
821
|
+
test) cmd_test "$@" ;;
|
|
822
|
+
approve) cmd_approve "$@" ;;
|
|
823
|
+
reject) cmd_reject "$@" ;;
|
|
824
|
+
-h|--help) usage; exit "${EXIT_OK}" ;;
|
|
825
|
+
*) fail_usage "unknown sub-command: '${sub}'" ;;
|
|
826
|
+
esac
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
main "$@"
|