@jenga-ai/agent 3.0.0 → 3.1.1

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.
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/build-pages-site.sh
3
+ #
4
+ # Deterministically (re)generates the wiki-derived pages of the GitHub Pages
5
+ # documentation site (docs/*.md) from the existing wiki mirror under
6
+ # project/.wiki/. This is the sync mechanism behind E41_S13_T01's
7
+ # source-of-truth decision ("restructure from the wiki, keep both in sync"):
8
+ # project/.wiki/ stays the canonical, doc-sync-maintained content; this
9
+ # script is the deterministic transform from that content into the
10
+ # multi-page, navigable Pages site.
11
+ #
12
+ # WHEN TO RE-RUN: after `j.doc-sync` (or any manual edit) updates
13
+ # project/.wiki/documentation.md, project/.wiki/intro-guide.md, or
14
+ # project/.wiki/concepts/*.md, re-run this script to refresh the Pages site
15
+ # so it doesn't silently drift from the wiki the way README.md and the wiki
16
+ # itself have drifted from each other before (see skills/doc-sync/SKILL.md
17
+ # for the doc-sync side of this convention).
18
+ #
19
+ # WHAT THIS SCRIPT DOES NOT TOUCH:
20
+ # - docs/_config.yml, docs/index.md — hand-authored site structure/config,
21
+ # not derived from wiki content. Edit these directly if the page set or
22
+ # nav changes.
23
+ # - docs/README.md — maintainer-facing decision record, not a rendered
24
+ # Jekyll page and not wiki-derived content.
25
+ #
26
+ # WHAT THIS SCRIPT REGENERATES (always overwritten, never hand-edit these):
27
+ # - docs/getting-started.md <- project/.wiki/intro-guide.md
28
+ # - docs/concepts.md <- project/.wiki/concepts/*.md (concatenated)
29
+ # - docs/skills.md <- project/.wiki/documentation.md "## Skills"
30
+ # - docs/agents.md <- project/.wiki/documentation.md "## Agents"
31
+ # - docs/hooks.md <- project/.wiki/documentation.md "## Hooks"
32
+ # - docs/mcp-tools.md <- project/.wiki/documentation.md "## MCP Tools"
33
+ # - docs/reference.md <- project/.wiki/documentation.md
34
+ # "## Directory Structure" +
35
+ # "## Agent Communication Contract"
36
+ # (plus a short hand-authored index blurb
37
+ # linking to skills/agents/hooks/mcp-tools)
38
+ #
39
+ # KNOWN LIMITATION: this is a structural transform, not a fact-checker. It
40
+ # ports project/.wiki/documentation.md's content as-is, including whatever
41
+ # is currently stale in that file (e.g. it documents ~30 skills under the
42
+ # old bare `/<name>` invocation form and does not yet list every skill in
43
+ # skills/, including the `j-<name>` twins — a pre-existing wiki staleness
44
+ # gap, not something introduced by this script). Fixing the wiki's own
45
+ # staleness is doc-sync's job, not this script's — this script only keeps
46
+ # the Pages site faithful to whatever the wiki currently says.
47
+ #
48
+ # HEADING-STABILITY ASSUMPTION: section extraction below is keyed to exact
49
+ # top-level ("## ") heading text in documentation.md (Agents, Skills, MCP
50
+ # Tools, Hooks, Directory Structure, Agent Communication Contract). If any
51
+ # of those headings are renamed, update the `case` statement in
52
+ # extract_sections() to match.
53
+ #
54
+ # Usage: scripts/build-pages-site.sh
55
+ # Safe to re-run any number of times — every run fully overwrites its
56
+ # output files from the current wiki content (no partial/incremental state).
57
+
58
+ set -euo pipefail
59
+
60
+ # shellcheck source=lib/resolve-project-dir.sh disable=SC1091
61
+ source "$(git rev-parse --show-toplevel)/lib/resolve-project-dir.sh"
62
+
63
+ REPO_ROOT="$JENGA_PROJECT_DIR"
64
+ WIKI_DIR="$REPO_ROOT/project/.wiki"
65
+ DOCS_DIR="$REPO_ROOT/docs"
66
+ DOC_MD="$WIKI_DIR/documentation.md"
67
+ INTRO_MD="$WIKI_DIR/intro-guide.md"
68
+ CONCEPTS_DIR="$WIKI_DIR/concepts"
69
+
70
+ for f in "$DOC_MD" "$INTRO_MD"; do
71
+ if [ ! -f "$f" ]; then
72
+ echo "build-pages-site.sh: required source file not found: $f" >&2
73
+ exit 1
74
+ fi
75
+ done
76
+ if [ ! -d "$CONCEPTS_DIR" ]; then
77
+ echo "build-pages-site.sh: required source directory not found: $CONCEPTS_DIR" >&2
78
+ exit 1
79
+ fi
80
+
81
+ WORK="$(mktemp -d)"
82
+ trap 'rm -rf "$WORK"' EXIT
83
+
84
+ # --- 1. Split documentation.md into per-section scratch files -------------
85
+ # A line can only belong to one target section at a time; the generic
86
+ # "any other ## heading" rule resets `section` to "" so unrelated top-level
87
+ # sections (e.g. "## Table of Contents") are dropped rather than bleeding
88
+ # into whichever named section preceded them.
89
+ awk -v work="$WORK" '
90
+ /^## Agents$/ { section = "agents"; next }
91
+ /^## Skills$/ { section = "skills"; next }
92
+ /^## MCP Tools$/ { section = "mcp-tools"; next }
93
+ /^## Hooks$/ { section = "hooks"; next }
94
+ /^## Directory Structure$/ { section = "directory-structure"; next }
95
+ /^## Agent Communication Contract$/ { section = "contract"; next }
96
+ /^## / { section = "" }
97
+ {
98
+ if (section != "") {
99
+ print >> (work "/section-" section ".md")
100
+ }
101
+ }
102
+ ' "$DOC_MD"
103
+
104
+ for s in agents skills mcp-tools hooks directory-structure contract; do
105
+ [ -f "$WORK/section-$s.md" ] || touch "$WORK/section-$s.md"
106
+ done
107
+
108
+ write_page() {
109
+ # write_page <output-path> <title> <permalink> <body-file>
110
+ local out="$1" title="$2" permalink="$3" body="$4"
111
+ {
112
+ printf -- '---\n'
113
+ printf 'layout: page\n'
114
+ printf 'title: %s\n' "$title"
115
+ printf 'permalink: %s\n' "$permalink"
116
+ printf -- '---\n\n'
117
+ cat "$body"
118
+ } > "$out"
119
+ }
120
+
121
+ # --- 2. docs/agents.md, docs/hooks.md, docs/mcp-tools.md -------------------
122
+ # Straight ports — no internal markdown links were found in these sections
123
+ # of documentation.md, so no link rewriting is needed beyond the front
124
+ # matter wrapper.
125
+ write_page "$DOCS_DIR/agents.md" "Agents" "/agents.html" "$WORK/section-agents.md"
126
+ write_page "$DOCS_DIR/hooks.md" "Hooks" "/hooks.html" "$WORK/section-hooks.md"
127
+ write_page "$DOCS_DIR/mcp-tools.md" "MCP Tools" "/mcp-tools.html" "$WORK/section-mcp-tools.md"
128
+
129
+ # --- 3. docs/skills.md ------------------------------------------------------
130
+ # Single reference page (matches the story's own suggested top-level
131
+ # category list: Getting Started / Concepts / Skills reference / Agents /
132
+ # Hooks / MCP Tools — "Skills reference" is one page, not one page per
133
+ # skill). Category (### ) and per-skill (#### ) structure is preserved
134
+ # as-is from the source.
135
+ write_page "$DOCS_DIR/skills.md" "Skills Reference" "/skills.html" "$WORK/section-skills.md"
136
+
137
+ # --- 4. docs/reference.md ---------------------------------------------------
138
+ # Combines the two remaining documentation.md sections (Directory Structure,
139
+ # Agent Communication Contract) behind a short hand-authored index blurb
140
+ # that links out to the pages built above — this replaces documentation.md's
141
+ # original role as the single "full reference" entry point, now that its
142
+ # content is split across multiple pages.
143
+ {
144
+ cat <<'EOF'
145
+ ## Full Reference Index
146
+
147
+ This page is the entry point into the full reference material, split across
148
+ several pages so nothing requires scrolling through one giant file:
149
+
150
+ - **[Skills Reference](./skills.md)** — every skill, grouped by Setup &
151
+ Planning, Execution, Status & Review, and Committing & Maintenance.
152
+ - **[Agents](./agents.md)** — Scrum Master, Developer, Tester: roles,
153
+ ownership, and responsibilities.
154
+ - **[Hooks](./hooks.md)** — session lifecycle hooks and what they run.
155
+ - **[MCP Tools](./mcp-tools.md)** — Model Context Protocol tools available
156
+ in a Claude Code session.
157
+
158
+ The rest of this page covers the repo's directory structure and the
159
+ inter-agent communication contract (the typed "sender object" every agent
160
+ call carries).
161
+
162
+ ---
163
+
164
+ EOF
165
+ cat "$WORK/section-directory-structure.md"
166
+ printf '\n---\n\n'
167
+ cat "$WORK/section-contract.md"
168
+ } > "$WORK/reference-body.md"
169
+ write_page "$DOCS_DIR/reference.md" "Reference" "/reference.html" "$WORK/reference-body.md"
170
+
171
+ # --- 5. docs/getting-started.md ---------------------------------------------
172
+ # Port of intro-guide.md verbatim (drop the leading H1 — front matter
173
+ # supplies the page title instead), with its two links into the
174
+ # now-restructured reference/concepts pages rewritten.
175
+ tail -n +2 "$INTRO_MD" > "$WORK/intro-body.md"
176
+ # NOTE on delimiter choice: sed's `s<delim>pattern<delim>replacement<delim>`
177
+ # breaks if the replacement text itself contains the delimiter character.
178
+ # Several replacements below contain a literal "#" (anchor fragments), so
179
+ # "#" cannot be used as the delimiter here — "|" is used instead, since
180
+ # none of these paths/anchors contain a literal "|".
181
+ sed -i.bak \
182
+ -e 's|\[documentation\.md\](\./documentation\.md)|[reference.md](./reference.md)|g' \
183
+ -e 's|(\./concepts/role-separation\.md)|(./concepts.md#role-separation)|g' \
184
+ -e 's|(\./concepts/board-hierarchy\.md)|(./concepts.md#board-hierarchy)|g' \
185
+ -e 's|(\./concepts/session-continuity\.md)|(./concepts.md#session-continuity)|g' \
186
+ -e 's|(\./concepts/first-feature\.md)|(./concepts.md#your-first-feature)|g' \
187
+ -e 's|(\./concepts/multi-session-work\.md)|(./concepts.md#working-across-sessions)|g' \
188
+ -e 's|(\./concepts/mid-flow-capture\.md)|(./concepts.md#capturing-mid-flow-ideas)|g' \
189
+ -e 's|(\./concepts/parallel-tasks\.md)|(./concepts.md#parallel-tasks)|g' \
190
+ "$WORK/intro-body.md"
191
+ rm -f "$WORK/intro-body.md.bak"
192
+ write_page "$DOCS_DIR/getting-started.md" "Getting Started" "/getting-started.html" "$WORK/intro-body.md"
193
+
194
+ # --- 6. docs/concepts.md -----------------------------------------------------
195
+ # Concatenates all 7 project/.wiki/concepts/*.md files into one page:
196
+ # - each file's H1 becomes an H2 section heading (slug listed below must
197
+ # stay in sync with each file's actual title text — GitHub Pages/kramdown
198
+ # slugifies headings to lowercase-hyphenated automatically)
199
+ # - all other headings are demoted one level (## -> ###, ### -> ####)
200
+ # - sibling/parent links are rewritten to point within the merged page
201
+ # and at the sibling getting-started.md / reference.md pages
202
+ #
203
+ {
204
+ cat <<'EOF'
205
+ ## Concepts
206
+
207
+ The ideas behind Jenga AI's structure, and the how-tos for using it day to
208
+ day. Jump to any section:
209
+
210
+ - [Role Separation](#role-separation)
211
+ - [Board Hierarchy](#board-hierarchy)
212
+ - [Session Continuity](#session-continuity)
213
+ - [Your First Feature](#your-first-feature)
214
+ - [Working Across Sessions](#working-across-sessions)
215
+ - [Capturing Mid-Flow Ideas](#capturing-mid-flow-ideas)
216
+ - [Parallel Tasks](#parallel-tasks)
217
+
218
+ ---
219
+
220
+ EOF
221
+ } > "$WORK/concepts-body.md"
222
+
223
+ append_concept() {
224
+ # append_concept <filename-stem> <section-title>
225
+ local stem="$1"
226
+ local title="$2"
227
+ local src="$CONCEPTS_DIR/$stem.md"
228
+ {
229
+ printf '## %s\n\n' "$title"
230
+ # NOTE on delimiter choice: same reasoning as the getting-started block
231
+ # above — replacements here contain a literal "#" anchor character, so
232
+ # "|" is used as the sed delimiter instead of "#".
233
+ #
234
+ # NOTE on heading demotion: a naive two-pass "## -> ###" then
235
+ # "### -> ####" sed would double-demote lines that were originally
236
+ # "## " (they'd match the first rule, becoming "### ", and then ALSO
237
+ # match the second rule on the same pass, becoming "#### " — wrong).
238
+ # The single extended-regex rule below captures the existing run of
239
+ # 2-3 "#" characters and prepends exactly one more, so each line is
240
+ # demoted exactly once regardless of its original level.
241
+ tail -n +2 "$src" \
242
+ | sed \
243
+ -e 's|\[documentation\.md\](\.\./documentation\.md)|[reference.md](./reference.md)|g' \
244
+ -e 's|(\.\./intro-guide\.md)|(./getting-started.md)|g' \
245
+ -e 's|(\.\./documentation\.md)|(./reference.md)|g' \
246
+ -e 's|(\./role-separation\.md)|(#role-separation)|g' \
247
+ -e 's|(\./board-hierarchy\.md)|(#board-hierarchy)|g' \
248
+ -e 's|(\./session-continuity\.md)|(#session-continuity)|g' \
249
+ -e 's|(\./first-feature\.md)|(#your-first-feature)|g' \
250
+ -e 's|(\./multi-session-work\.md)|(#working-across-sessions)|g' \
251
+ -e 's|(\./mid-flow-capture\.md)|(#capturing-mid-flow-ideas)|g' \
252
+ -e 's|(\./parallel-tasks\.md)|(#parallel-tasks)|g' \
253
+ | sed -E 's/^(#{2,3}) /#\1 /'
254
+ printf '\n---\n\n'
255
+ } >> "$WORK/concepts-body.md"
256
+ }
257
+
258
+ append_concept "role-separation" "Role Separation"
259
+ append_concept "board-hierarchy" "Board Hierarchy"
260
+ append_concept "session-continuity" "Session Continuity"
261
+ append_concept "first-feature" "Your First Feature"
262
+ append_concept "multi-session-work" "Working Across Sessions"
263
+ append_concept "mid-flow-capture" "Capturing Mid-Flow Ideas"
264
+ append_concept "parallel-tasks" "Parallel Tasks"
265
+
266
+ write_page "$DOCS_DIR/concepts.md" "Concepts" "/concepts.html" "$WORK/concepts-body.md"
267
+
268
+ echo "build-pages-site.sh: regenerated docs/{getting-started,concepts,skills,agents,hooks,mcp-tools,reference}.md from project/.wiki/*"
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/generate-legacy-shipped-paths.js — legacy shipped-path list generator (E26_S08_T03)
4
+ *
5
+ * Why this exists
6
+ * ────────────────
7
+ * `lib/postinstall-manifest.js`'s delete reconciliation (E26_S08_T01) is purely forward-looking:
8
+ * a consumer already installed before any manifest existed can never have their pre-existing
9
+ * orphans cleaned up, because the first manifest a fixed version ever writes for them records
10
+ * only what THAT run mirrored. This script produces the static list this package ships so the
11
+ * FIRST manifest a consumer ever gets can instead be *seeded* with paths known to have shipped in
12
+ * some real prior published version — see `seedFromLegacyPaths` in `lib/postinstall-manifest.js`
13
+ * and `scripts/postinstall.js`'s `no-prior-manifest` branch for how the seed is consumed.
14
+ *
15
+ * Why not git tags
16
+ * ─────────────────
17
+ * The task's design note allows deriving the list "from git tags" as an alternative to
18
+ * publish-time derivation. Checked and rejected for this repo specifically: this repo's local
19
+ * tags (`v0.0.1`, `v55.0.1`, `last-self-sync`) do not correspond to the real npm publish history
20
+ * at all — `npm view @jenga-ai/agent versions --json` shows the real, disconnected sequence
21
+ * (1.0.0 through 3.0.0 as of this writing). Reconstructing shipped paths from local git tags in
22
+ * this repo would silently produce a list bearing no relation to what was actually published.
23
+ *
24
+ * Two modes
25
+ * ─────────
26
+ * --bootstrap Fetches EVERY version `npm view <package> versions --json` currently lists from
27
+ * the real registry, `npm pack`s each one into a throwaway temp dir, and unions
28
+ * the `skills/`+`agents/` entries inside every tarball. Network-dependent. This is
29
+ * how the real historical shipped-path set is captured — including versions whose
30
+ * git history is not reliably reconstructable locally (verified true here). Meant
31
+ * to be run manually / rarely — a one-off backfill, or an occasional resync — NOT
32
+ * wired into the automatic per-publish flow (see incremental mode below for that).
33
+ *
34
+ * (default) Incremental, no network: reads whatever is already at the output path (if any)
35
+ * and unions it with the paths CURRENTLY on disk under this repo's own `skills/`
36
+ * and `agents/` directories — i.e. "what this release is about to ship" folds into
37
+ * the running cumulative record. This is the mode wired into the publish pipeline
38
+ * (`skills/publish/scripts/npm_pipeline.sh` / `npm_ci_pipeline.sh`, via the
39
+ * `generate:legacy-paths` npm script) so every future publish keeps the list
40
+ * current with zero network dependency and zero publish-time registry flakiness.
41
+ *
42
+ * IMPORTANT — this mode's first-ever run is NOT a substitute for --bootstrap: if
43
+ * the output file doesn't exist yet, incremental mode unions an EMPTY existing set
44
+ * with whatever's on disk in THIS repo's tree right now. That happens to currently
45
+ * equal the real historical union (this repo's working tree is a superset of every
46
+ * published version's file list, as of the 2026-09-07 verification below) — but
47
+ * that is a coincidence of this repo's current state, not a guarantee the mode
48
+ * itself provides. The very first generation of the shipped artifact MUST use
49
+ * --bootstrap so the baseline is verified against the real registry, not assumed.
50
+ *
51
+ * Output
52
+ * ──────
53
+ * `lib/legacy-shipped-paths.json` (ships automatically — `lib/` is already in package.json's
54
+ * `files` allow-list, no change needed there):
55
+ *
56
+ * {
57
+ * "generated_at": "<ISO 8601>",
58
+ * "package": "@jenga-ai/agent",
59
+ * "source": "bootstrap-from-registry+incremental" | "incremental",
60
+ * "paths": ["agents/developer.md", "skills/do/SKILL.md", ...]
61
+ * }
62
+ *
63
+ * `paths` are relative to a mirror root, POSIX-separated, deduped and sorted — matching the same
64
+ * shape convention `lib/postinstall-manifest.js`'s own manifest uses, for consistency.
65
+ *
66
+ * The generation step is regenerated automatically as part of the publish flow (incremental mode),
67
+ * per this task's AC — it is NOT hand-maintained, so it cannot silently go stale across releases.
68
+ *
69
+ * ESM, Node built-ins only — matches lib/postinstall-manifest.js and lib/mirror.js. `npm` itself is
70
+ * shelled out to (via `execFileSync`) only in `--bootstrap` mode.
71
+ */
72
+
73
+ import fs from 'node:fs';
74
+ import os from 'node:os';
75
+ import path from 'node:path';
76
+ import { execFileSync } from 'node:child_process';
77
+ import { fileURLToPath } from 'node:url';
78
+
79
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
80
+ const REPO_ROOT = path.join(__dirname, '..');
81
+
82
+ export const DEFAULT_OUTPUT_PATH = path.join(REPO_ROOT, 'lib', 'legacy-shipped-paths.json');
83
+ export const DEFAULT_PACKAGE_NAME = '@jenga-ai/agent';
84
+
85
+ /** Discovery-bound directories mirrored into a consumer's .agents/ and .claude/ (see docs/distribution.md §1). */
86
+ const COPY_SET = ['skills', 'agents'];
87
+
88
+ // ── walk a real directory tree ──────────────────────────────────────────────
89
+
90
+ function walkDir(base, dir, out) {
91
+ let entries;
92
+ try {
93
+ entries = fs.readdirSync(dir, { withFileTypes: true });
94
+ } catch (_) {
95
+ return;
96
+ }
97
+ for (const entry of entries) {
98
+ const abs = path.join(dir, entry.name);
99
+ if (entry.isDirectory()) {
100
+ walkDir(base, abs, out);
101
+ } else if (entry.isFile()) {
102
+ out.push(path.relative(base, abs).split(path.sep).join('/'));
103
+ }
104
+ // symlinks intentionally ignored — matches lib/mirror.js's own walk behavior.
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Relative POSIX paths this repo's CURRENT `skills/` + `agents/` trees would ship, i.e. exactly
110
+ * what a fresh install of the version about to be published would mirror.
111
+ *
112
+ * @param {string} repoRoot
113
+ * @returns {string[]} sorted, deduped
114
+ */
115
+ export function currentShippedPaths(repoRoot = REPO_ROOT) {
116
+ const out = [];
117
+ for (const entry of COPY_SET) {
118
+ const dir = path.join(repoRoot, entry);
119
+ if (fs.existsSync(dir)) walkDir(repoRoot, dir, out);
120
+ }
121
+ return [...new Set(out)].sort();
122
+ }
123
+
124
+ // ── read existing output (if any) ───────────────────────────────────────────
125
+
126
+ function readExistingPaths(outputPath) {
127
+ try {
128
+ const parsed = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
129
+ return Array.isArray(parsed.paths) ? parsed.paths.filter((p) => typeof p === 'string') : [];
130
+ } catch (_) {
131
+ return []; // absent, unreadable, or corrupt — start from an empty cumulative set
132
+ }
133
+ }
134
+
135
+ // ── bootstrap from the real npm registry ────────────────────────────────────
136
+
137
+ /**
138
+ * Fetch every currently-listed published version of `packageName`, `npm pack` each into a
139
+ * throwaway temp dir, and union the `skills/`+`agents/` entries found inside every tarball.
140
+ * Network-dependent — intended for manual/rare use (a one-off backfill or occasional resync),
141
+ * never called by the automatic per-publish (incremental) path.
142
+ *
143
+ * @param {string} packageName
144
+ * @returns {string[]} sorted, deduped relative POSIX paths
145
+ */
146
+ export function bootstrapFromRegistry(packageName = DEFAULT_PACKAGE_NAME) {
147
+ const versionsRaw = execFileSync('npm', ['view', packageName, 'versions', '--json'], {
148
+ encoding: 'utf8',
149
+ });
150
+ const versions = JSON.parse(versionsRaw);
151
+ const all = new Set();
152
+
153
+ for (const version of versions) {
154
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'jenga-legacy-bootstrap-'));
155
+ try {
156
+ execFileSync('npm', ['pack', `${packageName}@${version}`, '--silent'], { cwd: tmp, stdio: 'ignore' });
157
+ const tarball = fs.readdirSync(tmp).find((f) => f.endsWith('.tgz'));
158
+ if (!tarball) continue;
159
+ const listing = execFileSync('tar', ['-tzf', path.join(tmp, tarball)], { encoding: 'utf8' });
160
+ for (const line of listing.split('\n')) {
161
+ const m = line.match(/^package\/(skills|agents)\/(.+)$/);
162
+ if (m && !line.endsWith('/')) all.add(`${m[1]}/${m[2]}`);
163
+ }
164
+ } finally {
165
+ fs.rmSync(tmp, { recursive: true, force: true });
166
+ }
167
+ }
168
+
169
+ return [...all].sort();
170
+ }
171
+
172
+ // ── generate ─────────────────────────────────────────────────────────────────
173
+
174
+ /**
175
+ * @param {object} [opts]
176
+ * @param {string} [opts.outputPath] Default: lib/legacy-shipped-paths.json
177
+ * @param {string} [opts.packageName] Default: @jenga-ai/agent
178
+ * @param {boolean} [opts.bootstrap] Default: false (incremental, no network)
179
+ * @param {string} [opts.repoRoot] Default: this repo's own root
180
+ * @returns {{written: boolean, path: string, count: number, source: string}}
181
+ */
182
+ export function generate({
183
+ outputPath = DEFAULT_OUTPUT_PATH,
184
+ packageName = DEFAULT_PACKAGE_NAME,
185
+ bootstrap = false,
186
+ repoRoot = REPO_ROOT,
187
+ } = {}) {
188
+ const existing = readExistingPaths(outputPath);
189
+ let paths;
190
+ let source;
191
+
192
+ if (bootstrap) {
193
+ const registryPaths = bootstrapFromRegistry(packageName);
194
+ paths = [...new Set([...registryPaths, ...existing])].sort();
195
+ source = 'bootstrap-from-registry+incremental';
196
+ } else {
197
+ const current = currentShippedPaths(repoRoot);
198
+ paths = [...new Set([...existing, ...current])].sort();
199
+ source = 'incremental';
200
+ }
201
+
202
+ const artifact = {
203
+ generated_at: new Date().toISOString(),
204
+ package: packageName,
205
+ source,
206
+ paths,
207
+ };
208
+
209
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
210
+ fs.writeFileSync(outputPath, JSON.stringify(artifact, null, 2) + '\n', 'utf8');
211
+
212
+ return { written: true, path: outputPath, count: paths.length, source };
213
+ }
214
+
215
+ /**
216
+ * Read the shipped legacy-paths artifact's `paths` array. Fail-toward-doing-nothing: any read or
217
+ * parse failure returns `[]` rather than throwing — a missing/corrupt legacy-paths file must
218
+ * never abort or degrade an unattended `npm install`, mirroring `readManifest`'s own posture in
219
+ * `lib/postinstall-manifest.js`.
220
+ *
221
+ * @param {string} artifactPath
222
+ * @returns {string[]}
223
+ */
224
+ export function readLegacyShippedPaths(artifactPath = DEFAULT_OUTPUT_PATH) {
225
+ try {
226
+ const parsed = JSON.parse(fs.readFileSync(artifactPath, 'utf8'));
227
+ if (!Array.isArray(parsed.paths)) return [];
228
+ return parsed.paths.filter((p) => typeof p === 'string' && p.length > 0);
229
+ } catch (_) {
230
+ return [];
231
+ }
232
+ }
233
+
234
+ // ── CLI guard ────────────────────────────────────────────────────────────────
235
+ // node scripts/generate-legacy-shipped-paths.js [--bootstrap] [--package <name>] [outputPath]
236
+
237
+ const invokedPath = process.argv[1] ? fs.realpathSync(process.argv[1]) : null;
238
+ if (invokedPath === fileURLToPath(import.meta.url)) {
239
+ const argv = process.argv.slice(2);
240
+ const bootstrap = argv.includes('--bootstrap');
241
+ const pkgFlagIndex = argv.indexOf('--package');
242
+ const packageName = pkgFlagIndex !== -1 ? argv[pkgFlagIndex + 1] : DEFAULT_PACKAGE_NAME;
243
+ const positional = argv.filter((a, i) => a !== '--bootstrap' && i !== pkgFlagIndex && i !== pkgFlagIndex + 1 && !a.startsWith('--'));
244
+ const outputPath = positional[0] || DEFAULT_OUTPUT_PATH;
245
+
246
+ const result = generate({ outputPath, packageName, bootstrap });
247
+ console.log(`✓ ${result.path} (${result.count} paths, ${result.source})`);
248
+ }