@ctxr/skill-llm-wiki 1.0.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.
- package/CHANGELOG.md +134 -0
- package/LICENSE +21 -0
- package/README.md +484 -0
- package/SKILL.md +252 -0
- package/guide/basics/concepts.md +74 -0
- package/guide/basics/index.md +45 -0
- package/guide/basics/schema.md +140 -0
- package/guide/cli.md +256 -0
- package/guide/correctness/index.md +45 -0
- package/guide/correctness/invariants.md +89 -0
- package/guide/correctness/safety.md +96 -0
- package/guide/history/diff.md +110 -0
- package/guide/history/hidden-git.md +130 -0
- package/guide/history/index.md +52 -0
- package/guide/history/remote-sync.md +113 -0
- package/guide/index.md +134 -0
- package/guide/isolation/coexistence.md +134 -0
- package/guide/isolation/index.md +44 -0
- package/guide/isolation/scale.md +251 -0
- package/guide/layout/in-place-mode.md +97 -0
- package/guide/layout/index.md +53 -0
- package/guide/layout/layout-contract.md +131 -0
- package/guide/layout/layout-modes.md +115 -0
- package/guide/operations/index.md +76 -0
- package/guide/operations/ingest/build.md +75 -0
- package/guide/operations/ingest/extend.md +61 -0
- package/guide/operations/ingest/index.md +54 -0
- package/guide/operations/ingest/join.md +65 -0
- package/guide/operations/maintain/fix.md +66 -0
- package/guide/operations/maintain/index.md +47 -0
- package/guide/operations/maintain/rebuild.md +86 -0
- package/guide/operations/validate.md +48 -0
- package/guide/substrate/index.md +47 -0
- package/guide/substrate/operators.md +96 -0
- package/guide/substrate/tiered-ai.md +363 -0
- package/guide/ux/index.md +44 -0
- package/guide/ux/preflight.md +150 -0
- package/guide/ux/user-intent.md +135 -0
- package/package.json +55 -0
- package/scripts/cli.mjs +893 -0
- package/scripts/commands/remote.mjs +93 -0
- package/scripts/commands/review.mjs +253 -0
- package/scripts/commands/sync.mjs +84 -0
- package/scripts/lib/chunk.mjs +421 -0
- package/scripts/lib/cluster-detect.mjs +516 -0
- package/scripts/lib/decision-log.mjs +343 -0
- package/scripts/lib/draft.mjs +158 -0
- package/scripts/lib/embeddings.mjs +366 -0
- package/scripts/lib/frontmatter.mjs +497 -0
- package/scripts/lib/git-commands.mjs +155 -0
- package/scripts/lib/git.mjs +486 -0
- package/scripts/lib/gitignore.mjs +62 -0
- package/scripts/lib/history.mjs +331 -0
- package/scripts/lib/indices.mjs +510 -0
- package/scripts/lib/ingest.mjs +258 -0
- package/scripts/lib/intent.mjs +713 -0
- package/scripts/lib/interactive.mjs +99 -0
- package/scripts/lib/migrate.mjs +126 -0
- package/scripts/lib/nest-applier.mjs +260 -0
- package/scripts/lib/operators.mjs +1365 -0
- package/scripts/lib/orchestrator.mjs +718 -0
- package/scripts/lib/paths.mjs +197 -0
- package/scripts/lib/preflight.mjs +213 -0
- package/scripts/lib/provenance.mjs +672 -0
- package/scripts/lib/quality-metric.mjs +269 -0
- package/scripts/lib/query-fixture.mjs +71 -0
- package/scripts/lib/rollback.mjs +95 -0
- package/scripts/lib/shape-check.mjs +172 -0
- package/scripts/lib/similarity-cache.mjs +126 -0
- package/scripts/lib/similarity.mjs +230 -0
- package/scripts/lib/snapshot.mjs +54 -0
- package/scripts/lib/source-frontmatter.mjs +85 -0
- package/scripts/lib/tier2-protocol.mjs +470 -0
- package/scripts/lib/tiered.mjs +453 -0
- package/scripts/lib/validate.mjs +362 -0
package/scripts/cli.mjs
ADDED
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// skill-llm-wiki CLI — deterministic script helpers Claude invokes
|
|
3
|
+
// while driving the methodology's operations.
|
|
4
|
+
//
|
|
5
|
+
// This is NOT the full operation pipeline. The LLM (Claude, invoking this
|
|
6
|
+
// skill) is the orchestrator: it reads SKILL.md, runs ingest + index-rebuild
|
|
7
|
+
// + validate + shape-check via this CLI, drafts frontmatter itself where
|
|
8
|
+
// heuristics are insufficient, and writes results using the standard Edit
|
|
9
|
+
// and Write tools. This CLI exists so the deterministic phases are fast,
|
|
10
|
+
// cheap, and identical across runs.
|
|
11
|
+
//
|
|
12
|
+
// Subcommands:
|
|
13
|
+
// ingest <source> — walk a source, emit candidate JSON
|
|
14
|
+
// draft-leaf <candidate-json> — deterministic leaf frontmatter draft
|
|
15
|
+
// index-rebuild <wiki> — regenerate all index.md files
|
|
16
|
+
// index-rebuild-one <dir> <wiki> — regenerate a single directory's index
|
|
17
|
+
// validate <wiki> — run hard invariants, print report
|
|
18
|
+
// shape-check <wiki> — detect operator candidates
|
|
19
|
+
// resolve-wiki <source> — print current live wiki path
|
|
20
|
+
// next-version <source> — print next version tag
|
|
21
|
+
// --version — print version
|
|
22
|
+
// --help — print usage
|
|
23
|
+
|
|
24
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
25
|
+
// Runtime preflight guard (defense-in-depth).
|
|
26
|
+
//
|
|
27
|
+
// The primary preflight is a Bash check Claude runs BEFORE invoking this CLI
|
|
28
|
+
// (see SKILL.md "Preflight: verify Node.js is installed"). This guard is the
|
|
29
|
+
// second layer.
|
|
30
|
+
//
|
|
31
|
+
// The inline Node-major check here runs BEFORE any `import` statement so
|
|
32
|
+
// that even on an ancient Node that rejects our modern syntax, we abort
|
|
33
|
+
// cleanly with a short stderr message instead of a cryptic parse error.
|
|
34
|
+
// The richer preflight (full semver, git version, wiki fsck) runs inside
|
|
35
|
+
// main() after imports have resolved — it cannot be earlier without
|
|
36
|
+
// creating a circular dependency between cli.mjs and preflight.mjs.
|
|
37
|
+
//
|
|
38
|
+
// Exit codes used by this CLI are:
|
|
39
|
+
// 0 ok · 1 usage · 2 validation · 3 resolve-wiki miss ·
|
|
40
|
+
// 4 Node too old · 5 git missing/too old · 6 wiki corrupt ·
|
|
41
|
+
// 7 NEEDS_TIER2 (suspend — wiki-runner must resolve pending
|
|
42
|
+
// tier2 requests and re-invoke; NOT a failure path) ·
|
|
43
|
+
// 8 DEPS_MISSING (required runtime dependency missing and the
|
|
44
|
+
// auto-install attempt was either declined or failed)
|
|
45
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
46
|
+
const REQUIRED_NODE_MAJOR = 18;
|
|
47
|
+
const _nodeVersionRaw = (process && process.version) || "";
|
|
48
|
+
const _nodeMajorMatch = /^v(\d+)\./.exec(_nodeVersionRaw);
|
|
49
|
+
const _nodeMajor = _nodeMajorMatch ? Number(_nodeMajorMatch[1]) : NaN;
|
|
50
|
+
if (!Number.isFinite(_nodeMajor) || _nodeMajor < REQUIRED_NODE_MAJOR) {
|
|
51
|
+
process.stderr.write(
|
|
52
|
+
"skill-llm-wiki: Node.js " + (_nodeVersionRaw || "<unknown>") +
|
|
53
|
+
" is below the required minimum (v" + REQUIRED_NODE_MAJOR + ".0.0).\n" +
|
|
54
|
+
"Please upgrade Node.js and retry. See SKILL.md " +
|
|
55
|
+
"'Preflight: verify Node.js is installed' for platform-specific " +
|
|
56
|
+
"install instructions.\n",
|
|
57
|
+
);
|
|
58
|
+
process.exit(4);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
62
|
+
// Dependency preflight (defence-in-depth, runs BEFORE the static imports
|
|
63
|
+
// that would otherwise pull in `gray-matter`).
|
|
64
|
+
//
|
|
65
|
+
// The static import chain below transitively loads `gray-matter` via
|
|
66
|
+
// scripts/lib/source-frontmatter.mjs. If that package is missing from
|
|
67
|
+
// node_modules, the import throws ERR_MODULE_NOT_FOUND with no
|
|
68
|
+
// actionable context. By doing a synchronous resolve + prompt/install
|
|
69
|
+
// loop here — using only Node built-ins — we either fix the install or
|
|
70
|
+
// exit 8 cleanly before the failing import is reached.
|
|
71
|
+
//
|
|
72
|
+
// `--version` and `--help` deliberately bypass this check so an operator
|
|
73
|
+
// debugging a broken install can still sanity-check the binary. They are
|
|
74
|
+
// handled by an early-exit branch a few lines down.
|
|
75
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
76
|
+
import { createRequire as _createRequireDP } from "node:module";
|
|
77
|
+
import { spawnSync as _spawnSyncDP } from "node:child_process";
|
|
78
|
+
import { fileURLToPath as _fileURLToPathDP } from "node:url";
|
|
79
|
+
import { dirname as _dirnameDP, resolve, join as _joinDP } from "node:path";
|
|
80
|
+
import { readSync as _readSyncDP, readFileSync, mkdirSync } from "node:fs";
|
|
81
|
+
|
|
82
|
+
const _SKILL_ROOT_DP = _dirnameDP(_dirnameDP(_fileURLToPathDP(import.meta.url)));
|
|
83
|
+
const _REQUIRED_DEPS_DP = ["gray-matter", "@xenova/transformers"];
|
|
84
|
+
|
|
85
|
+
function _depPreflightCheck() {
|
|
86
|
+
// Test-only override: lets the e2e suite exercise the missing-dep
|
|
87
|
+
// path without renaming files inside the live node_modules tree
|
|
88
|
+
// (which would race with parallel test files sharing the same
|
|
89
|
+
// skill root). The value is a comma-separated list of dep names to
|
|
90
|
+
// pretend are missing.
|
|
91
|
+
const forced = process.env.LLM_WIKI_TEST_FORCE_DEPS_MISSING;
|
|
92
|
+
if (forced) {
|
|
93
|
+
return forced
|
|
94
|
+
.split(",")
|
|
95
|
+
.map((s) => s.trim())
|
|
96
|
+
.filter(Boolean);
|
|
97
|
+
}
|
|
98
|
+
let req;
|
|
99
|
+
try {
|
|
100
|
+
req = _createRequireDP(_joinDP(_SKILL_ROOT_DP, "package.json"));
|
|
101
|
+
} catch {
|
|
102
|
+
return _REQUIRED_DEPS_DP.slice();
|
|
103
|
+
}
|
|
104
|
+
const missing = [];
|
|
105
|
+
for (const d of _REQUIRED_DEPS_DP) {
|
|
106
|
+
try {
|
|
107
|
+
req.resolve(d);
|
|
108
|
+
} catch {
|
|
109
|
+
missing.push(d);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return missing;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function _depPreflightFailMessage(missing) {
|
|
116
|
+
return (
|
|
117
|
+
"skill-llm-wiki: required runtime dependencies are missing:\n" +
|
|
118
|
+
missing.map((d) => ` - ${d}`).join("\n") +
|
|
119
|
+
"\n" +
|
|
120
|
+
"Run `npm install` in the skill directory to install them, or see " +
|
|
121
|
+
"guide/ux/preflight.md Case E.\n"
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Skip the dep check entirely for --version and --help so an operator
|
|
126
|
+
// debugging a broken install can still get version/usage output. Every
|
|
127
|
+
// other invocation (including `--help` placed AFTER another arg, which
|
|
128
|
+
// is a malformed invocation we don't need to coddle) runs the check.
|
|
129
|
+
const _argvDP = process.argv.slice(2);
|
|
130
|
+
const _isVersionOrHelpDP =
|
|
131
|
+
_argvDP[0] === "--version" || _argvDP[0] === "--help" || _argvDP[0] === "-h";
|
|
132
|
+
|
|
133
|
+
if (!_isVersionOrHelpDP) {
|
|
134
|
+
let _missingDP = _depPreflightCheck();
|
|
135
|
+
if (_missingDP.length > 0) {
|
|
136
|
+
process.stderr.write(_depPreflightFailMessage(_missingDP));
|
|
137
|
+
const _interactiveDP =
|
|
138
|
+
Boolean(process.stdin && process.stdin.isTTY) &&
|
|
139
|
+
process.env.LLM_WIKI_NO_PROMPT !== "1";
|
|
140
|
+
let _proceedDP = true;
|
|
141
|
+
if (_interactiveDP) {
|
|
142
|
+
process.stderr.write("Install now? [Y/n] ");
|
|
143
|
+
let _ans = "";
|
|
144
|
+
try {
|
|
145
|
+
const buf = Buffer.alloc(64);
|
|
146
|
+
const n = _readSyncDP(process.stdin.fd, buf, 0, buf.length, null);
|
|
147
|
+
_ans = buf.subarray(0, n).toString("utf8").trim().toLowerCase();
|
|
148
|
+
} catch {
|
|
149
|
+
_ans = "";
|
|
150
|
+
}
|
|
151
|
+
if (_ans === "n" || _ans === "no") {
|
|
152
|
+
process.stderr.write("Cannot proceed without dependencies. Exit.\n");
|
|
153
|
+
process.exit(8);
|
|
154
|
+
}
|
|
155
|
+
_proceedDP = true;
|
|
156
|
+
}
|
|
157
|
+
if (_proceedDP) {
|
|
158
|
+
// Test-only knob: when LLM_WIKI_TEST_NO_AUTOINSTALL=1 is set,
|
|
159
|
+
// we skip the auto-install attempt entirely and exit 8
|
|
160
|
+
// immediately. This lets the e2e test exercise the failure
|
|
161
|
+
// path without ever risking a live npm install against the
|
|
162
|
+
// shared node_modules used by parallel test files.
|
|
163
|
+
if (process.env.LLM_WIKI_TEST_NO_AUTOINSTALL === "1") {
|
|
164
|
+
process.stderr.write(
|
|
165
|
+
"skill-llm-wiki: auto-install disabled by test harness. Exit.\n",
|
|
166
|
+
);
|
|
167
|
+
process.exit(8);
|
|
168
|
+
}
|
|
169
|
+
process.stderr.write(
|
|
170
|
+
`skill-llm-wiki: running \`npm install --silent\` in ${_SKILL_ROOT_DP}\n`,
|
|
171
|
+
);
|
|
172
|
+
const _ins = _spawnSyncDP("npm", ["install", "--silent"], {
|
|
173
|
+
cwd: _SKILL_ROOT_DP,
|
|
174
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
175
|
+
});
|
|
176
|
+
if (_ins.error || _ins.status !== 0) {
|
|
177
|
+
process.stderr.write(
|
|
178
|
+
"skill-llm-wiki: `npm install` failed. Cannot proceed without " +
|
|
179
|
+
"dependencies. Exit.\n",
|
|
180
|
+
);
|
|
181
|
+
process.exit(8);
|
|
182
|
+
}
|
|
183
|
+
_missingDP = _depPreflightCheck();
|
|
184
|
+
if (_missingDP.length > 0) {
|
|
185
|
+
process.stderr.write(_depPreflightFailMessage(_missingDP));
|
|
186
|
+
process.stderr.write(
|
|
187
|
+
"skill-llm-wiki: dependencies are still missing after `npm install`. " +
|
|
188
|
+
"Exit.\n",
|
|
189
|
+
);
|
|
190
|
+
process.exit(8);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// All skill-internal modules are loaded via dynamic `import()` inside
|
|
197
|
+
// `main()` so that the dependency preflight above this line gets a
|
|
198
|
+
// chance to run BEFORE any module that transitively imports
|
|
199
|
+
// `gray-matter` or `@xenova/transformers` is evaluated. ESM static
|
|
200
|
+
// imports are hoisted to the top of the file regardless of source
|
|
201
|
+
// position, so the only way to defer them past the preflight is to use
|
|
202
|
+
// dynamic import. The list of imported names is identical to the
|
|
203
|
+
// previous static block.
|
|
204
|
+
let ingestSource;
|
|
205
|
+
let draftLeafFrontmatter, draftCategory;
|
|
206
|
+
let rebuildAllIndices, rebuildIndex;
|
|
207
|
+
let validateWiki, summariseFindings;
|
|
208
|
+
let runShapeCheck;
|
|
209
|
+
let listVersions, nextVersionTag, resolveLiveWiki, writeCurrentPointer;
|
|
210
|
+
let formatAmbiguityJson, formatAmbiguityText, resolveIntent;
|
|
211
|
+
let rollbackOperation;
|
|
212
|
+
let defaultMigrationTarget, migrateLegacyWiki;
|
|
213
|
+
let NonInteractiveError;
|
|
214
|
+
let NeedsTier2Error, ReviewAbortedError, runOperation, ValidationError;
|
|
215
|
+
let TIER2_EXIT_CODE, listBatches;
|
|
216
|
+
let cmdBlame, cmdDiff, cmdHistory, cmdLog, cmdReflog, cmdShow;
|
|
217
|
+
let cmdRemote, cmdSync;
|
|
218
|
+
|
|
219
|
+
async function loadSkillModules() {
|
|
220
|
+
({ ingestSource } = await import("./lib/ingest.mjs"));
|
|
221
|
+
({ draftLeafFrontmatter, draftCategory } = await import("./lib/draft.mjs"));
|
|
222
|
+
({ rebuildAllIndices, rebuildIndex } = await import("./lib/indices.mjs"));
|
|
223
|
+
({ validateWiki, summariseFindings } = await import("./lib/validate.mjs"));
|
|
224
|
+
({ runShapeCheck } = await import("./lib/shape-check.mjs"));
|
|
225
|
+
({ listVersions, nextVersionTag, resolveLiveWiki, writeCurrentPointer } =
|
|
226
|
+
await import("./lib/paths.mjs"));
|
|
227
|
+
({ formatAmbiguityJson, formatAmbiguityText, resolveIntent } = await import(
|
|
228
|
+
"./lib/intent.mjs"
|
|
229
|
+
));
|
|
230
|
+
({ rollbackOperation } = await import("./lib/rollback.mjs"));
|
|
231
|
+
({ defaultMigrationTarget, migrateLegacyWiki } = await import(
|
|
232
|
+
"./lib/migrate.mjs"
|
|
233
|
+
));
|
|
234
|
+
({ NonInteractiveError } = await import("./lib/interactive.mjs"));
|
|
235
|
+
({ NeedsTier2Error, ReviewAbortedError, runOperation, ValidationError } =
|
|
236
|
+
await import("./lib/orchestrator.mjs"));
|
|
237
|
+
({ TIER2_EXIT_CODE, listBatches } = await import("./lib/tier2-protocol.mjs"));
|
|
238
|
+
({ cmdBlame, cmdDiff, cmdHistory, cmdLog, cmdReflog, cmdShow } = await import(
|
|
239
|
+
"./lib/git-commands.mjs"
|
|
240
|
+
));
|
|
241
|
+
({ cmdRemote } = await import("./commands/remote.mjs"));
|
|
242
|
+
({ cmdSync } = await import("./commands/sync.mjs"));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Read the version from package.json at runtime. Resolved relative to this
|
|
246
|
+
// source file so it works both as a local clone (dev) and as a published
|
|
247
|
+
// npm artifact. @ctxr/kit historically stripped package.json from installed
|
|
248
|
+
// skill artifacts; if that environment is re-encountered we fall through to
|
|
249
|
+
// "unknown" rather than carrying a hand-maintained duplicate of the version
|
|
250
|
+
// string that inevitably drifts.
|
|
251
|
+
function getPackageVersion() {
|
|
252
|
+
try {
|
|
253
|
+
const pkgPath = new URL("../package.json", import.meta.url);
|
|
254
|
+
return JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
255
|
+
} catch {
|
|
256
|
+
return "unknown";
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Write usage to the appropriate stream. `--help` is a success path and
|
|
261
|
+
// must go to stdout so shells can pipe it (e.g. `cli --help | grep ...`);
|
|
262
|
+
// an unknown/malformed invocation is a failure path and goes to stderr.
|
|
263
|
+
function printUsage(stream = process.stdout) {
|
|
264
|
+
stream.write(`skill-llm-wiki CLI v${getPackageVersion()}
|
|
265
|
+
|
|
266
|
+
Usage: node scripts/cli.mjs <subcommand> [args] [flags]
|
|
267
|
+
|
|
268
|
+
Top-level operations:
|
|
269
|
+
build <source> Build a new wiki from a source folder
|
|
270
|
+
extend <wiki> Add new entries from a source
|
|
271
|
+
rebuild <wiki> Optimise structure in place
|
|
272
|
+
fix <wiki> Repair methodology divergences
|
|
273
|
+
join <wiki-a> <wiki-b> Merge two wikis into one
|
|
274
|
+
rollback <wiki> --to <ref> Restore a previous committed state
|
|
275
|
+
migrate <legacy-wiki> Migrate a legacy .llmwiki.v<N> folder
|
|
276
|
+
|
|
277
|
+
Hidden-git plumbing (Claude reads these to reason about history):
|
|
278
|
+
diff <wiki> [--op <id>] [...] Git-style diff (default --find-renames --find-copies)
|
|
279
|
+
log <wiki> [...] git log passthrough (default --oneline --all)
|
|
280
|
+
show <wiki> <ref> [-- <path>] git show passthrough
|
|
281
|
+
blame <wiki> <path> git blame passthrough
|
|
282
|
+
reflog <wiki> git reflog passthrough
|
|
283
|
+
history <wiki> <entry-id> Op-log + git-log walk for one entry
|
|
284
|
+
|
|
285
|
+
Remote mirroring (explicit user-invoked only, never auto-pushes):
|
|
286
|
+
remote <wiki> add <name> <url> Register a remote URL
|
|
287
|
+
remote <wiki> remove <name> Delete a configured remote
|
|
288
|
+
remote <wiki> list List configured remotes
|
|
289
|
+
sync <wiki> [--remote <name>] Fetch + push tag refs explicitly
|
|
290
|
+
|
|
291
|
+
Low-level script helpers (deterministic, called by Claude):
|
|
292
|
+
ingest <source> Walk source, emit candidate JSON
|
|
293
|
+
draft-leaf <candidate-file> Script-first frontmatter draft for one candidate
|
|
294
|
+
draft-category <candidate-file> Deterministic category assignment
|
|
295
|
+
index-rebuild <wiki> Regenerate all index.md files in a wiki
|
|
296
|
+
index-rebuild-one <dir> <wiki> Regenerate one directory's index.md
|
|
297
|
+
validate <wiki> Run hard invariants and print a report
|
|
298
|
+
shape-check <wiki> Detect pending operator candidates
|
|
299
|
+
resolve-wiki <source> Print current live wiki path for a source
|
|
300
|
+
next-version <source> Print next version tag for a source
|
|
301
|
+
list-versions <source> List all existing versions for a source
|
|
302
|
+
set-current <source> <version> Update the current-pointer for a source
|
|
303
|
+
|
|
304
|
+
Layout-mode flags (build/extend/rebuild/fix/join):
|
|
305
|
+
--layout-mode sibling|in-place|hosted
|
|
306
|
+
--target <path> Explicit destination (required for hosted)
|
|
307
|
+
|
|
308
|
+
Tiered-AI flags:
|
|
309
|
+
--quality-mode tiered-fast|claude-first|tier0-only
|
|
310
|
+
Default: tiered-fast (TF-IDF → embeddings
|
|
311
|
+
→ Claude ladder). See guide/tiered-ai.md.
|
|
312
|
+
|
|
313
|
+
UX flags:
|
|
314
|
+
--no-prompt Never prompt; fail loud on ambiguity
|
|
315
|
+
--json-errors Emit ambiguity errors as JSON
|
|
316
|
+
--accept-dirty Operate on a dirty user git repo
|
|
317
|
+
|
|
318
|
+
Rollback flags:
|
|
319
|
+
--to <ref> genesis | <op-id> | pre-<op-id> | HEAD~N
|
|
320
|
+
|
|
321
|
+
Global:
|
|
322
|
+
--version Print CLI version
|
|
323
|
+
--help, -h Show this help
|
|
324
|
+
|
|
325
|
+
Exit codes: 0 ok · 1 usage · 2 ambiguous intent · 3 resolve-wiki miss ·
|
|
326
|
+
4 Node too old · 5 git missing/too old · 6 wiki corrupt ·
|
|
327
|
+
7 NEEDS_TIER2 (wiki-runner must resolve pending requests
|
|
328
|
+
and re-invoke this CLI — see SKILL.md delegation contract) ·
|
|
329
|
+
8 DEPS_MISSING (required runtime dependency missing and the
|
|
330
|
+
install attempt was either declined or failed)
|
|
331
|
+
`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Parse a subcommand's remaining argv into { positionals, flags } for
|
|
335
|
+
// delegation to resolveIntent. Unknown flags bubble up as a structured
|
|
336
|
+
// error so we never silently swallow a typo.
|
|
337
|
+
const FLAG_WITH_VALUE = new Set([
|
|
338
|
+
"--layout-mode",
|
|
339
|
+
"--target",
|
|
340
|
+
"--to",
|
|
341
|
+
"--canonical",
|
|
342
|
+
"--quality-mode",
|
|
343
|
+
]);
|
|
344
|
+
const FLAG_BOOLEAN = new Set([
|
|
345
|
+
"--no-prompt",
|
|
346
|
+
"--json-errors",
|
|
347
|
+
"--accept-dirty",
|
|
348
|
+
"--accept-foreign-target",
|
|
349
|
+
"--review",
|
|
350
|
+
]);
|
|
351
|
+
|
|
352
|
+
function parseSubArgv(raw) {
|
|
353
|
+
const positionals = [];
|
|
354
|
+
const flags = {};
|
|
355
|
+
for (let i = 0; i < raw.length; i++) {
|
|
356
|
+
const tok = raw[i];
|
|
357
|
+
if (!tok.startsWith("--")) {
|
|
358
|
+
positionals.push(tok);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
// Accept both `--flag value` and `--flag=value`.
|
|
362
|
+
let name = tok;
|
|
363
|
+
let inlineValue = null;
|
|
364
|
+
const eq = tok.indexOf("=");
|
|
365
|
+
if (eq !== -1) {
|
|
366
|
+
name = tok.slice(0, eq);
|
|
367
|
+
inlineValue = tok.slice(eq + 1);
|
|
368
|
+
}
|
|
369
|
+
if (FLAG_WITH_VALUE.has(name)) {
|
|
370
|
+
const value = inlineValue !== null ? inlineValue : raw[++i];
|
|
371
|
+
if (value === undefined || value === "" || value.startsWith("--")) {
|
|
372
|
+
return { error: `flag ${name} requires a non-empty value` };
|
|
373
|
+
}
|
|
374
|
+
const key = name.slice(2).replace(/-/g, "_");
|
|
375
|
+
flags[key] = value;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (FLAG_BOOLEAN.has(name)) {
|
|
379
|
+
if (inlineValue !== null) {
|
|
380
|
+
return { error: `flag ${name} does not take a value` };
|
|
381
|
+
}
|
|
382
|
+
const key = name.slice(2).replace(/-/g, "_");
|
|
383
|
+
flags[key] = true;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
return { error: `unknown flag: ${name}` };
|
|
387
|
+
}
|
|
388
|
+
return { positionals, flags };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Emit an ambiguity or parse error through the configured formatter and
|
|
392
|
+
// exit 2. Never throws — returns through process.exit.
|
|
393
|
+
function emitIntentError(error, jsonMode) {
|
|
394
|
+
const body = jsonMode
|
|
395
|
+
? formatAmbiguityJson(error)
|
|
396
|
+
: formatAmbiguityText(error);
|
|
397
|
+
process.stderr.write(body);
|
|
398
|
+
process.exit(2);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Generate a stable op-id for a new top-level operation. Format:
|
|
402
|
+
// <operation>-<YYYYMMDD-HHMMSS>-<random>
|
|
403
|
+
// The wall-clock component is replaced by LLM_WIKI_FIXED_TIMESTAMP when
|
|
404
|
+
// set, so deterministic reruns produce identical op-ids.
|
|
405
|
+
function newOpId(operation) {
|
|
406
|
+
const now = process.env.LLM_WIKI_FIXED_TIMESTAMP
|
|
407
|
+
? new Date(Number(process.env.LLM_WIKI_FIXED_TIMESTAMP) * 1000)
|
|
408
|
+
: new Date();
|
|
409
|
+
const y = now.getUTCFullYear();
|
|
410
|
+
const m = String(now.getUTCMonth() + 1).padStart(2, "0");
|
|
411
|
+
const d = String(now.getUTCDate()).padStart(2, "0");
|
|
412
|
+
const hh = String(now.getUTCHours()).padStart(2, "0");
|
|
413
|
+
const mm = String(now.getUTCMinutes()).padStart(2, "0");
|
|
414
|
+
const ss = String(now.getUTCSeconds()).padStart(2, "0");
|
|
415
|
+
const rand = process.env.LLM_WIKI_FIXED_TIMESTAMP
|
|
416
|
+
? "deterministic"
|
|
417
|
+
: Math.random().toString(36).slice(2, 8);
|
|
418
|
+
return `${operation}-${y}${m}${d}-${hh}${mm}${ss}-${rand}`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function main() {
|
|
422
|
+
const argv = process.argv.slice(2);
|
|
423
|
+
if (argv[0] === "--help" || argv[0] === "-h") {
|
|
424
|
+
printUsage(process.stdout);
|
|
425
|
+
process.exit(0);
|
|
426
|
+
}
|
|
427
|
+
if (argv.length === 0) {
|
|
428
|
+
printUsage(process.stderr);
|
|
429
|
+
process.exit(1);
|
|
430
|
+
}
|
|
431
|
+
if (argv[0] === "--version") {
|
|
432
|
+
// The dependency preflight is intentionally skipped for --version
|
|
433
|
+
// and --help so an operator debugging a broken install can still
|
|
434
|
+
// sanity-check the binary. Every other code path runs the
|
|
435
|
+
// preflight before any deterministic work begins.
|
|
436
|
+
console.log(getPackageVersion());
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// The dependency preflight has already run in the pre-import block
|
|
441
|
+
// at the top of this file. By the time we reach this point, every
|
|
442
|
+
// required runtime dep has been verified or the process has exited
|
|
443
|
+
// 8. See guide/ux/preflight.md Case E.
|
|
444
|
+
//
|
|
445
|
+
// Now load the skill-internal modules. They use `gray-matter` and
|
|
446
|
+
// `@xenova/transformers` transitively, so they MUST be loaded only
|
|
447
|
+
// after the dep preflight has confirmed both packages are
|
|
448
|
+
// resolvable.
|
|
449
|
+
await loadSkillModules();
|
|
450
|
+
|
|
451
|
+
const cmd = argv[0];
|
|
452
|
+
const args = argv.slice(1);
|
|
453
|
+
|
|
454
|
+
// ─── Remote + sync subcommands (Phase 7) ────────────────────────────
|
|
455
|
+
// Both take <wiki> as the first positional. `remote` takes a
|
|
456
|
+
// subcommand (add/remove/list); `sync` accepts --remote <name>
|
|
457
|
+
// and --push-branch <ref> flags.
|
|
458
|
+
if (cmd === "remote") {
|
|
459
|
+
if (args.length < 1) {
|
|
460
|
+
usageError("remote requires <wiki> as its first argument");
|
|
461
|
+
}
|
|
462
|
+
const wiki = resolve(args[0]);
|
|
463
|
+
const subcommand = args[1];
|
|
464
|
+
const subArgs = args.slice(2);
|
|
465
|
+
process.exit(cmdRemote(wiki, { subcommand, args: subArgs }));
|
|
466
|
+
}
|
|
467
|
+
if (cmd === "sync") {
|
|
468
|
+
if (args.length < 1) {
|
|
469
|
+
usageError("sync requires <wiki> as its first argument");
|
|
470
|
+
}
|
|
471
|
+
const wiki = resolve(args[0]);
|
|
472
|
+
// Parse --remote / --push-branch / --skip-fetch / --skip-push.
|
|
473
|
+
// Both `--flag value` and `--flag=value` are accepted to match
|
|
474
|
+
// the rest of the CLI's flag conventions. Empty values and
|
|
475
|
+
// leading-dash values are rejected loudly.
|
|
476
|
+
const rest = args.slice(1);
|
|
477
|
+
const opts = {};
|
|
478
|
+
for (let i = 0; i < rest.length; i++) {
|
|
479
|
+
const tok = rest[i];
|
|
480
|
+
// Accept --flag=value form.
|
|
481
|
+
let name = tok;
|
|
482
|
+
let inlineValue = null;
|
|
483
|
+
const eq = tok.indexOf("=");
|
|
484
|
+
if (tok.startsWith("--") && eq !== -1) {
|
|
485
|
+
name = tok.slice(0, eq);
|
|
486
|
+
inlineValue = tok.slice(eq + 1);
|
|
487
|
+
}
|
|
488
|
+
const readValue = (flagName) => {
|
|
489
|
+
const v = inlineValue !== null ? inlineValue : rest[++i];
|
|
490
|
+
if (v === undefined || v === "" || v.startsWith("--")) {
|
|
491
|
+
usageError(`sync: ${flagName} requires a non-empty value`);
|
|
492
|
+
}
|
|
493
|
+
return v;
|
|
494
|
+
};
|
|
495
|
+
if (name === "--remote") {
|
|
496
|
+
opts.remote = readValue("--remote");
|
|
497
|
+
} else if (name === "--push-branch") {
|
|
498
|
+
opts.pushBranch = readValue("--push-branch");
|
|
499
|
+
} else if (name === "--skip-fetch") {
|
|
500
|
+
if (inlineValue !== null) usageError("sync: --skip-fetch does not take a value");
|
|
501
|
+
opts.skipFetch = true;
|
|
502
|
+
} else if (name === "--skip-push") {
|
|
503
|
+
if (inlineValue !== null) usageError("sync: --skip-push does not take a value");
|
|
504
|
+
opts.skipPush = true;
|
|
505
|
+
} else {
|
|
506
|
+
usageError(`sync: unknown argument "${tok}"`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
process.exit(cmdSync(wiki, opts));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ─── Hidden-git passthrough subcommands ─────────────────────────────
|
|
513
|
+
// These wrap scripts/lib/git.mjs with the full isolation env so
|
|
514
|
+
// Claude (or a user) can inspect history without ever touching the
|
|
515
|
+
// user's own git repo. Every hidden-git command takes <wiki> as its
|
|
516
|
+
// first positional; remaining args pass through to git.
|
|
517
|
+
const HIDDEN_GIT_SUBCOMMANDS = new Set([
|
|
518
|
+
"diff",
|
|
519
|
+
"log",
|
|
520
|
+
"show",
|
|
521
|
+
"blame",
|
|
522
|
+
"reflog",
|
|
523
|
+
"history",
|
|
524
|
+
]);
|
|
525
|
+
if (HIDDEN_GIT_SUBCOMMANDS.has(cmd)) {
|
|
526
|
+
if (args.length < 1) {
|
|
527
|
+
usageError(`${cmd} requires <wiki> as its first argument`);
|
|
528
|
+
}
|
|
529
|
+
const wiki = resolve(args[0]);
|
|
530
|
+
// Parse a minimal set of our own flags; everything else passes
|
|
531
|
+
// through to the underlying git command.
|
|
532
|
+
const rest = args.slice(1);
|
|
533
|
+
const opIdx = rest.indexOf("--op");
|
|
534
|
+
let op = null;
|
|
535
|
+
let passthrough = rest.slice();
|
|
536
|
+
if (opIdx !== -1) {
|
|
537
|
+
op = rest[opIdx + 1];
|
|
538
|
+
passthrough = rest.slice(0, opIdx).concat(rest.slice(opIdx + 2));
|
|
539
|
+
}
|
|
540
|
+
let code = 0;
|
|
541
|
+
switch (cmd) {
|
|
542
|
+
case "diff":
|
|
543
|
+
code = cmdDiff(wiki, { op, args: passthrough });
|
|
544
|
+
break;
|
|
545
|
+
case "log":
|
|
546
|
+
code = cmdLog(wiki, { op, args: passthrough });
|
|
547
|
+
break;
|
|
548
|
+
case "show": {
|
|
549
|
+
const ref = passthrough[0];
|
|
550
|
+
const showArgs = passthrough.slice(1);
|
|
551
|
+
code = cmdShow(wiki, { ref, args: showArgs });
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
case "blame": {
|
|
555
|
+
const path = passthrough[0];
|
|
556
|
+
const blameArgs = passthrough.slice(1);
|
|
557
|
+
code = cmdBlame(wiki, { path: path && resolve(path), args: blameArgs });
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
case "reflog":
|
|
561
|
+
code = cmdReflog(wiki, { args: passthrough });
|
|
562
|
+
break;
|
|
563
|
+
case "history": {
|
|
564
|
+
const entryId = passthrough[0];
|
|
565
|
+
code = cmdHistory(wiki, { entryId });
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
process.exit(code);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// ─── Top-level operations routed through intent.mjs ─────────────────
|
|
573
|
+
// build / extend / rebuild / fix / join share the same intent-
|
|
574
|
+
// resolution → dispatch flow. rollback and migrate have tiny bespoke
|
|
575
|
+
// paths (still routed through intent for the ambiguity surface).
|
|
576
|
+
// Phase 2 wires the plumbing; Phase 3 will extend the handlers with
|
|
577
|
+
// full phased orchestration.
|
|
578
|
+
const INTENT_SUBCOMMANDS = new Set([
|
|
579
|
+
"build",
|
|
580
|
+
"extend",
|
|
581
|
+
"rebuild",
|
|
582
|
+
"fix",
|
|
583
|
+
"join",
|
|
584
|
+
"rollback",
|
|
585
|
+
"migrate",
|
|
586
|
+
]);
|
|
587
|
+
if (INTENT_SUBCOMMANDS.has(cmd)) {
|
|
588
|
+
const parsed = parseSubArgv(args);
|
|
589
|
+
if (parsed.error) {
|
|
590
|
+
const jsonMode = args.includes("--json-errors");
|
|
591
|
+
emitIntentError(
|
|
592
|
+
{
|
|
593
|
+
code: "INT-11",
|
|
594
|
+
message: parsed.error,
|
|
595
|
+
options: [],
|
|
596
|
+
resolving_flag: "correct the flag",
|
|
597
|
+
},
|
|
598
|
+
jsonMode,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
const { positionals, flags } = parsed;
|
|
602
|
+
const jsonMode = Boolean(flags.json_errors);
|
|
603
|
+
|
|
604
|
+
// `migrate` has its own resolution path — the intent resolver would
|
|
605
|
+
// reject the legacy folder shape as ambiguous.
|
|
606
|
+
if (cmd === "migrate") {
|
|
607
|
+
if (positionals.length !== 1) {
|
|
608
|
+
emitIntentError(
|
|
609
|
+
{
|
|
610
|
+
code: "INT-06",
|
|
611
|
+
message: "migrate requires exactly one <legacy-wiki> positional",
|
|
612
|
+
options: [
|
|
613
|
+
{
|
|
614
|
+
description: "specify the legacy wiki",
|
|
615
|
+
flag: "migrate <legacy-path>",
|
|
616
|
+
},
|
|
617
|
+
],
|
|
618
|
+
resolving_flag: "positional legacy path",
|
|
619
|
+
},
|
|
620
|
+
jsonMode,
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
const legacyPath = resolve(positionals[0]);
|
|
624
|
+
const target = flags.target
|
|
625
|
+
? resolve(flags.target)
|
|
626
|
+
: defaultMigrationTarget(legacyPath);
|
|
627
|
+
try {
|
|
628
|
+
const opId = newOpId("migrate");
|
|
629
|
+
const r = migrateLegacyWiki(legacyPath, target, { opId });
|
|
630
|
+
process.stdout.write(
|
|
631
|
+
`migrated ${legacyPath} (v${r.version}) → ${target}\n` +
|
|
632
|
+
` op-id: ${r.opId}\n` +
|
|
633
|
+
` sha: ${r.sha}\n`,
|
|
634
|
+
);
|
|
635
|
+
return;
|
|
636
|
+
} catch (err) {
|
|
637
|
+
if (err && err.message && /already exists/.test(err.message)) {
|
|
638
|
+
emitIntentError(
|
|
639
|
+
{
|
|
640
|
+
code: "INT-01",
|
|
641
|
+
message: err.message,
|
|
642
|
+
options: [
|
|
643
|
+
{
|
|
644
|
+
description: "write to a different target",
|
|
645
|
+
flag: "--target <other-path>",
|
|
646
|
+
},
|
|
647
|
+
],
|
|
648
|
+
resolving_flag: "--target",
|
|
649
|
+
},
|
|
650
|
+
jsonMode,
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
throw err;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const intent = resolveIntent({
|
|
658
|
+
subcommand: cmd,
|
|
659
|
+
args: positionals,
|
|
660
|
+
flags,
|
|
661
|
+
cwd: process.cwd(),
|
|
662
|
+
});
|
|
663
|
+
if (intent.status === "ambiguous") {
|
|
664
|
+
emitIntentError(intent.error, jsonMode);
|
|
665
|
+
}
|
|
666
|
+
const plan = intent.plan;
|
|
667
|
+
|
|
668
|
+
if (cmd === "rollback") {
|
|
669
|
+
const result = rollbackOperation(plan.target, flags.to);
|
|
670
|
+
process.stdout.write(
|
|
671
|
+
`rolled back ${plan.target} to ${result.ref} (${result.sha ?? "n/a"})\n`,
|
|
672
|
+
);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// build / extend / rebuild / fix / join: Phase 3 runs the full
|
|
677
|
+
// phased orchestrator. The orchestrator handles snapshot → ingest
|
|
678
|
+
// → draft-frontmatter → index-generation → validation →
|
|
679
|
+
// commit-finalize, with automatic rollback on validation failure.
|
|
680
|
+
if (plan.is_new_wiki) {
|
|
681
|
+
mkdirSync(plan.target, { recursive: true });
|
|
682
|
+
}
|
|
683
|
+
const opId = newOpId(cmd);
|
|
684
|
+
const startedIso = new Date().toISOString();
|
|
685
|
+
let result;
|
|
686
|
+
try {
|
|
687
|
+
result = await runOperation(plan, {
|
|
688
|
+
opId,
|
|
689
|
+
source: plan.source,
|
|
690
|
+
startedIso,
|
|
691
|
+
});
|
|
692
|
+
} catch (err) {
|
|
693
|
+
if (err instanceof NonInteractiveError) {
|
|
694
|
+
emitIntentError(
|
|
695
|
+
{
|
|
696
|
+
code: "INT-12",
|
|
697
|
+
message: err.message,
|
|
698
|
+
options: [
|
|
699
|
+
{
|
|
700
|
+
description: "run with stdin attached to a TTY",
|
|
701
|
+
flag: "(interactive terminal)",
|
|
702
|
+
},
|
|
703
|
+
],
|
|
704
|
+
resolving_flag: "explicit flag set",
|
|
705
|
+
},
|
|
706
|
+
jsonMode,
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
if (err instanceof ValidationError) {
|
|
710
|
+
process.stderr.write(
|
|
711
|
+
`${cmd}: validation failed — working tree rolled back to pre-op state\n` +
|
|
712
|
+
err.message +
|
|
713
|
+
"\n",
|
|
714
|
+
);
|
|
715
|
+
process.exit(2);
|
|
716
|
+
}
|
|
717
|
+
if (err instanceof NeedsTier2Error) {
|
|
718
|
+
// Exit-7 handshake: a phase accumulated Tier 2 requests
|
|
719
|
+
// that only a wiki-runner sub-agent can resolve. The
|
|
720
|
+
// working tree is NOT rolled back — the partial-converge
|
|
721
|
+
// commits in the private git are preserved so the resume
|
|
722
|
+
// invocation can continue from the last completed
|
|
723
|
+
// iteration. The wiki-runner reads the pending batch,
|
|
724
|
+
// spawns one Agent per request, writes the responses, and
|
|
725
|
+
// re-invokes this CLI with the same op-id (same
|
|
726
|
+
// source/target positional args) so the orchestrator
|
|
727
|
+
// resumes. See SKILL.md "Agent delegation contract" and
|
|
728
|
+
// guide/tiered-ai.md "exit-7 handshake" for details.
|
|
729
|
+
const batches = listBatches(plan.target);
|
|
730
|
+
process.stderr.write(
|
|
731
|
+
`${cmd}: NEEDS_TIER2 — ${err.message}\n` +
|
|
732
|
+
` op-id: ${opId}\n` +
|
|
733
|
+
` pending: ${err.pendingPath ?? "(no path)"}\n` +
|
|
734
|
+
` total batches waiting: ${batches.length}\n` +
|
|
735
|
+
` Wiki-runner: read every pending-*.json under ` +
|
|
736
|
+
`${plan.target}/.work/tier2/, spawn one Agent per request, ` +
|
|
737
|
+
`write responses-*.json next to it, and re-invoke this CLI ` +
|
|
738
|
+
`with the same positional args.\n`,
|
|
739
|
+
);
|
|
740
|
+
process.exit(TIER2_EXIT_CODE);
|
|
741
|
+
}
|
|
742
|
+
if (err instanceof ReviewAbortedError) {
|
|
743
|
+
process.stderr.write(
|
|
744
|
+
`${cmd}: ${err.message}\n` +
|
|
745
|
+
"No changes were committed to the wiki.\n",
|
|
746
|
+
);
|
|
747
|
+
process.exit(2);
|
|
748
|
+
}
|
|
749
|
+
throw err;
|
|
750
|
+
}
|
|
751
|
+
process.stdout.write(
|
|
752
|
+
`${cmd}: complete\n` +
|
|
753
|
+
` target: ${plan.target}\n` +
|
|
754
|
+
` mode: ${plan.layout_mode}\n` +
|
|
755
|
+
` op-id: ${opId}\n` +
|
|
756
|
+
` sha: ${result.final_sha ?? "n/a"}\n` +
|
|
757
|
+
` phases: ${result.phases.length}\n`,
|
|
758
|
+
);
|
|
759
|
+
for (const p of result.phases) {
|
|
760
|
+
process.stdout.write(` • ${p.name}: ${p.summary}\n`);
|
|
761
|
+
}
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
switch (cmd) {
|
|
766
|
+
case "ingest": {
|
|
767
|
+
if (args.length < 1) usageError("ingest requires <source>");
|
|
768
|
+
const result = ingestSource(resolve(args[0]));
|
|
769
|
+
// The CLI-level `ingest` helper exposes both the leaf candidates
|
|
770
|
+
// and the index sources so that downstream tooling (and human
|
|
771
|
+
// inspection via `node scripts/cli.mjs ingest`) sees the full
|
|
772
|
+
// picture now that index inputs are classified separately.
|
|
773
|
+
process.stdout.write(
|
|
774
|
+
JSON.stringify(
|
|
775
|
+
{
|
|
776
|
+
candidates: result.leaves ?? result.candidates ?? [],
|
|
777
|
+
indexSources: result.indexSources ?? [],
|
|
778
|
+
},
|
|
779
|
+
null,
|
|
780
|
+
2,
|
|
781
|
+
) + "\n",
|
|
782
|
+
);
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
case "draft-leaf": {
|
|
786
|
+
if (args.length < 1) usageError("draft-leaf requires <candidate-file>");
|
|
787
|
+
const candidate = JSON.parse(readFileSync(args[0], "utf8"));
|
|
788
|
+
const result = draftLeafFrontmatter(candidate, {
|
|
789
|
+
categoryPath: draftCategory(candidate),
|
|
790
|
+
});
|
|
791
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
792
|
+
break;
|
|
793
|
+
}
|
|
794
|
+
case "draft-category": {
|
|
795
|
+
if (args.length < 1)
|
|
796
|
+
usageError("draft-category requires <candidate-file>");
|
|
797
|
+
const candidate = JSON.parse(readFileSync(args[0], "utf8"));
|
|
798
|
+
process.stdout.write(draftCategory(candidate) + "\n");
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
case "index-rebuild": {
|
|
802
|
+
if (args.length < 1) usageError("index-rebuild requires <wiki>");
|
|
803
|
+
const wiki = resolve(args[0]);
|
|
804
|
+
const out = rebuildAllIndices(wiki);
|
|
805
|
+
process.stdout.write(`rebuilt ${out.length} index.md files\n`);
|
|
806
|
+
break;
|
|
807
|
+
}
|
|
808
|
+
case "index-rebuild-one": {
|
|
809
|
+
if (args.length < 2)
|
|
810
|
+
usageError("index-rebuild-one requires <dir> <wiki>");
|
|
811
|
+
const dir = resolve(args[0]);
|
|
812
|
+
const wiki = resolve(args[1]);
|
|
813
|
+
const out = rebuildIndex(dir, wiki);
|
|
814
|
+
process.stdout.write(`rebuilt ${out.path}\n`);
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
case "validate": {
|
|
818
|
+
if (args.length < 1) usageError("validate requires <wiki>");
|
|
819
|
+
const wiki = resolve(args[0]);
|
|
820
|
+
const findings = validateWiki(wiki);
|
|
821
|
+
const summary = summariseFindings(findings);
|
|
822
|
+
for (const f of findings) {
|
|
823
|
+
const tag =
|
|
824
|
+
f.severity === "error"
|
|
825
|
+
? "ERR "
|
|
826
|
+
: f.severity === "warning"
|
|
827
|
+
? "WARN"
|
|
828
|
+
: "INFO";
|
|
829
|
+
console.log(`[${tag}] ${f.code} ${f.target}`);
|
|
830
|
+
console.log(` ${f.message}`);
|
|
831
|
+
}
|
|
832
|
+
console.log(
|
|
833
|
+
`\n${summary.errors} error(s), ${summary.warnings} warning(s)`,
|
|
834
|
+
);
|
|
835
|
+
process.exit(summary.errors > 0 ? 2 : 0);
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
case "shape-check": {
|
|
839
|
+
if (args.length < 1) usageError("shape-check requires <wiki>");
|
|
840
|
+
const wiki = resolve(args[0]);
|
|
841
|
+
const suggestions = runShapeCheck(wiki);
|
|
842
|
+
console.log(`${suggestions.length} pending shape candidate(s)`);
|
|
843
|
+
for (const s of suggestions) {
|
|
844
|
+
const t = Array.isArray(s.target) ? s.target.join(", ") : s.target;
|
|
845
|
+
console.log(` ${s.operator} ${t}`);
|
|
846
|
+
console.log(` ${s.reason}`);
|
|
847
|
+
}
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
case "resolve-wiki": {
|
|
851
|
+
if (args.length < 1) usageError("resolve-wiki requires <source>");
|
|
852
|
+
const live = resolveLiveWiki(resolve(args[0]));
|
|
853
|
+
if (!live) {
|
|
854
|
+
process.stderr.write("no wiki exists for this source yet\n");
|
|
855
|
+
process.exit(3);
|
|
856
|
+
}
|
|
857
|
+
process.stdout.write(live.path + "\n");
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
case "next-version": {
|
|
861
|
+
if (args.length < 1) usageError("next-version requires <source>");
|
|
862
|
+
process.stdout.write(nextVersionTag(resolve(args[0])) + "\n");
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
case "list-versions": {
|
|
866
|
+
if (args.length < 1) usageError("list-versions requires <source>");
|
|
867
|
+
const versions = listVersions(resolve(args[0]));
|
|
868
|
+
for (const v of versions) process.stdout.write(`${v.tag}\t${v.path}\n`);
|
|
869
|
+
break;
|
|
870
|
+
}
|
|
871
|
+
case "set-current": {
|
|
872
|
+
if (args.length < 2)
|
|
873
|
+
usageError("set-current requires <source> <version>");
|
|
874
|
+
writeCurrentPointer(resolve(args[0]), args[1]);
|
|
875
|
+
process.stdout.write(`current → ${args[1]}\n`);
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
default:
|
|
879
|
+
printUsage();
|
|
880
|
+
process.exit(1);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function usageError(msg) {
|
|
885
|
+
process.stderr.write(`error: ${msg}\n`);
|
|
886
|
+
printUsage(process.stderr);
|
|
887
|
+
process.exit(1);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
main().catch((err) => {
|
|
891
|
+
console.error(`error: ${err.message}`);
|
|
892
|
+
process.exit(1);
|
|
893
|
+
});
|