@henols/vice-mcp 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/THIRD-PARTY-NOTICES.md +1 -24
- package/{r2000-acme-ident.ts → anno-acme-ident.ts} +13 -13
- package/anno-cli.ts +1465 -0
- package/{r2000-confidence.ts → anno-confidence.ts} +22 -22
- package/anno-coverage.ts +2465 -0
- package/{r2000-d64.ts → anno-d64.ts} +5 -5
- package/anno-derive.ts +590 -0
- package/anno-details.ts +169 -0
- package/anno-enum-gen.ts +533 -0
- package/anno-export-asm.ts +1310 -0
- package/anno-index.ts +150 -0
- package/{r2000-memmap-render.ts → anno-memmap-render.ts} +236 -95
- package/{r2000-regbits-gen.ts → anno-regbits-gen.ts} +20 -15
- package/{r2000-regbits.json → anno-regbits.json} +2 -2
- package/anno-register.ts +240 -0
- package/anno-store.ts +3486 -0
- package/anno-symbols.ts +266 -0
- package/anno-tools.ts +2111 -0
- package/anno-types.ts +1636 -0
- package/block-class.ts +201 -0
- package/build.ts +1 -1
- package/capability-registry.ts +3 -1
- package/disasm-decoder.ts +14 -14
- package/disasm-opcodes.ts +4 -4
- package/disasm-renderer.ts +2 -2
- package/hostpath.ts +1 -1
- package/install-resources.ts +1 -1
- package/package.json +23 -17
- package/prg-image.ts +119 -0
- package/repo-root.ts +20 -5
- package/resources/broker-launch.mjs +8 -4
- package/resources/vice-launcher.sh +3 -3
- package/stock-address.ts +5 -5
- package/stock-cia.ts +2 -2
- package/stock-condition.ts +7 -7
- package/stock-connect.ts +1 -1
- package/stock-dispatch.ts +35 -5
- package/stock-execution.ts +5 -3
- package/stock-input.ts +9 -9
- package/stock-machine.ts +17 -6
- package/stock-protocol.ts +16 -11
- package/stock-registers.ts +54 -29
- package/stock-sprites.ts +3 -3
- package/stock-symbols.ts +9 -9
- package/stock-timing.ts +1 -1
- package/stock-vicii.ts +1 -1
- package/version.ts +1 -1
- package/vice-proxy.ts +68 -46
- package/r2000-cli.ts +0 -1103
- package/r2000-enum-gen.ts +0 -574
- package/r2000-launch.ts +0 -357
- package/r2000-mcp-client.ts +0 -596
- package/r2000-project.ts +0 -190
- package/r2000-symbols.ts +0 -388
- package/r2000-tools.ts +0 -914
- package/r2000-verify.ts +0 -184
package/r2000-cli.ts
DELETED
|
@@ -1,1103 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// r2000-cli.ts -- the thin CLI ergonomics layer over the guarded regenerator2000
|
|
3
|
-
// seam (D-06). Reached as `vice-mcp r2000 <verb>` because that bin is the only
|
|
4
|
-
// surface that resolves identically across the Claude Code plugin route and
|
|
5
|
-
// both npm-installer routes: `installer/bin/cli.mjs`'s `viceServerEntry()`
|
|
6
|
-
// always launches this server via `npx` in BOTH npm-installer modes, and
|
|
7
|
-
// neither route places `.claude/mcp/vice/*.ts` as plain files inside a
|
|
8
|
-
// consuming project for some other filesystem-path-resolving design to find.
|
|
9
|
-
//
|
|
10
|
-
// WHAT NOT TO DO, named concretely:
|
|
11
|
-
// - Never accept a caller-supplied passthrough of extra flags to
|
|
12
|
-
// regenerator2000 (D-07). Every child-process argv is built only by
|
|
13
|
-
// `r2000-launch.ts`'s fixed builders (`buildExportAsmArgs()`,
|
|
14
|
-
// `buildVerifyArgs()`) -- this file adds exactly two options of its own
|
|
15
|
-
// (`--entry`, `--out`), neither of which reaches the child process argv.
|
|
16
|
-
// - Never auto-pick a `.d64` entry when the caller does not name one (D-02).
|
|
17
|
-
// A silent auto-pick would happily analyse a cracktro or loader stub's
|
|
18
|
-
// bytes instead of the actual game -- precisely the failure
|
|
19
|
-
// `c64-provenance-diff` exists to prevent elsewhere in this project. Zero
|
|
20
|
-
// `--entry` means: print the directory listing, tell the user to re-run
|
|
21
|
-
// with `--entry NAME`, and exit 2. Never guess.
|
|
22
|
-
// - Never dispatch a flat `.raw`/`.bin` capture by its BYTE LENGTH alone
|
|
23
|
-
// (WR-07). The incident: a 4096-byte `capture.raw` fell through to the
|
|
24
|
-
// `bytes.length === 65536` branch's `else`, which is `parsePrg()` --
|
|
25
|
-
// whose first two bytes become the load address -- so a truncated
|
|
26
|
-
// capture silently "bootstrapped" with origin `$62c5` (its own first two
|
|
27
|
-
// payload bytes read backwards) and exit 0, with every downstream
|
|
28
|
-
// address wrong and no diagnostic. That is exactly the "silently guess"
|
|
29
|
-
// behaviour D-02 exists to forbid, just for a different input shape.
|
|
30
|
-
// `.raw`/`.bin` inputs are now dispatched by EXTENSION, before the
|
|
31
|
-
// length check, so `flatImageOrigin()`'s own named refusal (any length
|
|
32
|
-
// other than exactly 65536) is always reachable for those two
|
|
33
|
-
// extensions.
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
// `runR2000Cli()` returns an exit code and never terminates the process
|
|
37
|
-
// itself, so it is testable in-process as well as from the bin (the bin,
|
|
38
|
-
// `vice-proxy.ts`, is the only place that ends the process with this
|
|
39
|
-
// function's return value). All output goes to stdout/stderr via
|
|
40
|
-
// `console.log`/`console.error` -- never a thrown stack trace for an
|
|
41
|
-
// expected, user-facing failure (missing file, unknown `.d64` entry, `.vsf`
|
|
42
|
-
// input): each of those produces a single actionable line instead.
|
|
43
|
-
//
|
|
44
|
-
// Import nothing from `hostpath.ts` or `containerpath.ts`. Every path this
|
|
45
|
-
// CLI handles is already container-side: regenerator2000 runs on the MCP
|
|
46
|
-
// proxy's side of the boundary (D-R4), and translating any of these
|
|
47
|
-
// arguments would be the mirror image of the DERIV-07 screenshot-path trap,
|
|
48
|
-
// where a client-side-derived path was wrongly translated a second time.
|
|
49
|
-
// This absence is asserted structurally by `hostpath-consumers.test.ts`
|
|
50
|
-
// (D-08), not merely stated here.
|
|
51
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
52
|
-
import { tmpdir } from "node:os";
|
|
53
|
-
import { dirname, extname, join } from "node:path";
|
|
54
|
-
|
|
55
|
-
import { buildExportAsmArgs, runR2000, R2000ViceFlagError } from "./r2000-launch.ts";
|
|
56
|
-
import { synthesizeProject, parsePrg, flatImageOrigin } from "./r2000-project.ts";
|
|
57
|
-
import { listEntries, extractEntry, assertPlainImage } from "./r2000-d64.ts";
|
|
58
|
-
import { verifyProject } from "./r2000-verify.ts";
|
|
59
|
-
import { generateEnums } from "./r2000-enum-gen.ts";
|
|
60
|
-
import { exportLabels, importLabels } from "./r2000-symbols.ts";
|
|
61
|
-
import { renderMemoryMap, checkRenderedMemoryMap } from "./r2000-memmap-render.ts";
|
|
62
|
-
|
|
63
|
-
const NPX_INVOCATION = "npx -y @henols/vice-mcp r2000 <verb>";
|
|
64
|
-
const PLUGIN_INVOCATION = "node <plugin-root>/.claude/mcp/vice/vice-proxy.ts r2000 <verb>";
|
|
65
|
-
|
|
66
|
-
// `verify` is the criterion-4 (R2000-06) proof route: it runs
|
|
67
|
-
// regenerator2000's own `--verify` and reports the verdict `r2000-verify.ts`'s
|
|
68
|
-
// `acmeVerdict()` derives from the PARSED ACME result line -- never from the
|
|
69
|
-
// child process's exit code (D-10). A skipped ACME reads as a failure here,
|
|
70
|
-
// not as an absence, even on a transcript whose own exit code and summary
|
|
71
|
-
// line both say "passed" -- see r2000-verify.ts's header comment and
|
|
72
|
-
// r2000-verify.test.ts's pinned trap transcript for the live incident this
|
|
73
|
-
// guards against. A future maintainer reading only this file must not
|
|
74
|
-
// reintroduce a `result.status === 0` shortcut anywhere in `cmdVerify()`.
|
|
75
|
-
//
|
|
76
|
-
// FLOW-02 (D-11.1-01): the `.vsf` paragraph below used to end by naming a
|
|
77
|
-
// specific numbered phase as the eventual owner of closing that gap. That
|
|
78
|
-
// phase shipped and never touched `.vsf` bootstrap, so the sentence told a
|
|
79
|
-
// user to wait on a remediation that would never arrive. A phase number is
|
|
80
|
-
// a planning artifact -- it has no place in a shipped diagnostic. Corrected
|
|
81
|
-
// to name the backlog file instead; see the matching fix in
|
|
82
|
-
// `bootstrapProject()` below and `docs-dangling-refs.test.ts`'s guard
|
|
83
|
-
// against this defect class recurring anywhere in this file's string
|
|
84
|
-
// literals.
|
|
85
|
-
const USAGE = `usage (npm install): ${NPX_INVOCATION}
|
|
86
|
-
usage (plugin/in-repo): ${PLUGIN_INVOCATION}
|
|
87
|
-
|
|
88
|
-
verbs:
|
|
89
|
-
bootstrap <input> [--entry NAME] [--out PROJECT] [--force]
|
|
90
|
-
Accepts a .prg, a .d64 (pick an entry with --entry), or a flat 64K
|
|
91
|
-
.raw/.bin capture. Writes a .regen2000proj to --out (default: the
|
|
92
|
-
input path with its extension replaced by .regen2000proj). Refuses to
|
|
93
|
-
overwrite an existing file at that path unless --force is given. A
|
|
94
|
-
.regen2000proj input is refused outright -- bootstrap never re-reads
|
|
95
|
-
one; use export-asm or verify on it directly.
|
|
96
|
-
|
|
97
|
-
export-asm <input-or-project> [--entry NAME] [--out FILE] [--force]
|
|
98
|
-
If given a .regen2000proj it is used directly; otherwise this bootstraps
|
|
99
|
-
the input to a temporary project first, so a bare .prg becomes ACME
|
|
100
|
-
source in one command with no human interaction (--entry is forwarded
|
|
101
|
-
to that bootstrap step, and is required the same way it is for
|
|
102
|
-
bootstrap itself when the input is a .d64 -- IN-06/D-11.1-04: this line
|
|
103
|
-
used to omit --entry even though the verb already accepted and used
|
|
104
|
-
it, which is corrected here alongside closing the sibling defect of
|
|
105
|
-
accepting-but-silently-dropping an option). Writes ACME source to
|
|
106
|
-
--out (default: the input stem plus .a). Refuses to overwrite an
|
|
107
|
-
existing file at that path unless --force is given.
|
|
108
|
-
|
|
109
|
-
verify <input-or-project> [--entry NAME]
|
|
110
|
-
The criterion-4 (R2000-06) reassembly proof: exports and reassembles
|
|
111
|
-
through regenerator2000's own --verify, and prints each assembler's
|
|
112
|
-
parsed result line. Exits 0 only when ACME's own line reports success --
|
|
113
|
-
a skipped ACME is a failure here, never a pass, regardless of the child
|
|
114
|
-
process's own exit code or its "All roundtrip verifications passed."
|
|
115
|
-
summary line (D-10). Accepts a .regen2000proj directly, or bootstraps a
|
|
116
|
-
bare .prg/.d64/flat-64K input to a temporary project first, exactly
|
|
117
|
-
like export-asm.
|
|
118
|
-
|
|
119
|
-
gen-enums <project> [--max-results N]
|
|
120
|
-
Generates program-specific enums from the register writes an existing
|
|
121
|
-
.regen2000proj's disassembly already contains (D-20/D-22/D-23,
|
|
122
|
-
R2000-13) -- one variant per DISTINCT value actually written, named
|
|
123
|
-
from the curated bit-name table (r2000-regbits.json), applied at every
|
|
124
|
-
matching immediate-load address, and saved. Prints total/paired/
|
|
125
|
-
unpaired register-store counts and, per created/updated enum, its name
|
|
126
|
-
and variant count. Requires an EXISTING .regen2000proj (this verb does
|
|
127
|
-
not bootstrap from a raw input -- run bootstrap first). --max-results
|
|
128
|
-
overrides the 10000-row ceiling on each of the two search passes this
|
|
129
|
-
verb runs internally; exits non-zero, printing the reason, when either
|
|
130
|
-
pass returns exactly its own ceiling (coverage may be incomplete) or
|
|
131
|
-
when generateEnums() itself refuses (an illegal generated identifier,
|
|
132
|
-
or an enum install that failed outright).
|
|
133
|
-
|
|
134
|
-
export-lbl <project> [--out FILE]
|
|
135
|
-
Exports the project's user-defined labels to a VICE label file
|
|
136
|
-
(R2000-14): "al C:xxxx .Name" lines that stock-symbols.ts's existing
|
|
137
|
-
parser accepts. The written file is read back and validated through
|
|
138
|
-
that same parser before this verb reports success -- a regenerator2000
|
|
139
|
-
exit code has lied before (r2000-verify.ts's D-10 founding incident),
|
|
140
|
-
so the exit code alone is never trusted. Defaults --out to the project
|
|
141
|
-
path's stem plus .lbl. Prints the written path and the symbol count
|
|
142
|
-
parsed back from the file. Requires an EXISTING .regen2000proj (this
|
|
143
|
-
verb does not bootstrap from a raw input).
|
|
144
|
-
|
|
145
|
-
import-lbl <project> <lbl>
|
|
146
|
-
Imports a VICE label file into the project (R2000-15, the D-28 path):
|
|
147
|
-
--import_lbl paired with --mcp-server-stdio plus an explicit
|
|
148
|
-
r2000_save_project -- the only combination that actually persists the
|
|
149
|
-
import (--import_lbl alone under --headless silently discards it,
|
|
150
|
-
main.rs:800-806). Prints the names imported, then an explicit line
|
|
151
|
-
naming the fact that the import was persisted by an explicit save
|
|
152
|
-
(proven by re-reading the project from disk in a fresh process, never
|
|
153
|
-
trusted from the child's own success text alone). Exits non-zero,
|
|
154
|
-
printing the reason, when the result is not disk-verified, or when the
|
|
155
|
-
given .lbl fails stock-symbols.ts's own size/line/symbol ceilings.
|
|
156
|
-
Requires an EXISTING .regen2000proj (this verb does not bootstrap from
|
|
157
|
-
a raw input).
|
|
158
|
-
|
|
159
|
-
render-memmap <project> --provenance FILE [--out FILE] [--check]
|
|
160
|
-
Generates the Markdown memory map from the project's store plus a
|
|
161
|
-
validated provenance sidecar (D-24: the store is canonical, this
|
|
162
|
-
output is a GENERATED VIEW -- never hand-edit it). Without --check,
|
|
163
|
-
writes --out (default: memory-map.md beside the project) and prints
|
|
164
|
-
the row count, the number of [unknown]-graded rows, and the render
|
|
165
|
-
digest. With --check, re-renders in memory and compares against the
|
|
166
|
-
file at --out: prints "in sync" and exits 0 when they match, prints
|
|
167
|
-
the first differing line and exits non-zero on drift (from either a
|
|
168
|
-
hand edit OR a store-side change since the file was last rendered),
|
|
169
|
-
or prints "missing" and exits non-zero when --out does not exist yet.
|
|
170
|
-
--check is how a hand edit to the generated file is caught. Requires
|
|
171
|
-
an EXISTING .regen2000proj and an EXISTING --provenance sidecar (this
|
|
172
|
-
verb does not bootstrap from a raw input).
|
|
173
|
-
|
|
174
|
-
.d64 input with no --entry named prints the directory listing and exits 2 --
|
|
175
|
-
this CLI never guesses which entry to use (D-02).
|
|
176
|
-
|
|
177
|
-
.vsf input is not supported by any verb -- regenerator2000's auto-detected
|
|
178
|
-
machine-type field is correct only by coincidence for C64 snapshots, and no
|
|
179
|
-
R2000-* requirement covers .vsf as a bootstrap input (D-34). The idea is
|
|
180
|
-
recorded as backlog at
|
|
181
|
-
.planning/todos/pending/2026-08-20-vsf-as-a-bootstrap-input.md. Convert to
|
|
182
|
-
.prg, .d64 or a flat 64K capture.
|
|
183
|
-
`;
|
|
184
|
-
|
|
185
|
-
function errMsg(err: unknown): string {
|
|
186
|
-
return err instanceof Error ? err.message : String(err);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
interface ParsedArgs {
|
|
190
|
-
positional: string[];
|
|
191
|
-
entry?: string;
|
|
192
|
-
out?: string;
|
|
193
|
-
force?: boolean;
|
|
194
|
-
entryMissingValue?: boolean;
|
|
195
|
-
outMissingValue?: boolean;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/** Fixed, closed option set -- exactly `--entry`, `--out` and `--force`. No
|
|
199
|
-
* rest field, no passthrough: an unrecognised flag is treated as a
|
|
200
|
-
* positional token (and will surface as "input file not found" rather than
|
|
201
|
-
* silently reaching regenerator2000, since nothing here ever forwards raw
|
|
202
|
-
* argv to the child process).
|
|
203
|
-
*
|
|
204
|
-
* WR-08: a value that is absent or itself `--`-shaped is a refusal
|
|
205
|
-
* (`entryMissingValue`/`outMissingValue`), never silently taken as the
|
|
206
|
-
* option's value -- the same posture `parseExportLblArgs()` below already
|
|
207
|
-
* has for `--out`. Before this fix, `entry = rest[++i]` and `out =
|
|
208
|
-
* rest[++i]` took the NEXT token unconditionally, so `bootstrap x.prg --out
|
|
209
|
-
* --entry FOO` parsed as `{ out: "--entry", entry: undefined }` and
|
|
210
|
-
* `bootstrapProject()` went on to `writeFileSync()` a project literally
|
|
211
|
-
* named `--entry` -- a dash-prefixed filename that is a shell-glob hazard
|
|
212
|
-
* for whatever later reads that directory. Callers check the two
|
|
213
|
-
* `*MissingValue` flags and refuse with a one-line message naming the
|
|
214
|
-
* option that was actually short a value (WR-09's lesson: `--out --entry
|
|
215
|
-
* FOO` must say `--out` is missing a value, never that `--entry` is a bad
|
|
216
|
-
* path) -- never throw, so `bootstrapProject()`'s never-throw contract
|
|
217
|
-
* holds for every caller of this parser too. */
|
|
218
|
-
function parseArgs(rest: string[]): ParsedArgs {
|
|
219
|
-
const positional: string[] = [];
|
|
220
|
-
let entry: string | undefined;
|
|
221
|
-
let out: string | undefined;
|
|
222
|
-
let force = false;
|
|
223
|
-
let entryMissingValue = false;
|
|
224
|
-
let outMissingValue = false;
|
|
225
|
-
for (let i = 0; i < rest.length; i++) {
|
|
226
|
-
const a = rest[i]!;
|
|
227
|
-
if (a === "--entry") {
|
|
228
|
-
const value = rest[i + 1];
|
|
229
|
-
if (value === undefined || value.startsWith("--")) {
|
|
230
|
-
entryMissingValue = true;
|
|
231
|
-
} else {
|
|
232
|
-
entry = value;
|
|
233
|
-
i++;
|
|
234
|
-
}
|
|
235
|
-
} else if (a === "--out") {
|
|
236
|
-
const value = rest[i + 1];
|
|
237
|
-
if (value === undefined || value.startsWith("--")) {
|
|
238
|
-
outMissingValue = true;
|
|
239
|
-
} else {
|
|
240
|
-
out = value;
|
|
241
|
-
i++;
|
|
242
|
-
}
|
|
243
|
-
} else if (a === "--force") {
|
|
244
|
-
force = true;
|
|
245
|
-
} else {
|
|
246
|
-
positional.push(a);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
return { positional, entry, out, force, entryMissingValue, outMissingValue };
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* IN-06 (D-11.1-04): the ONE declared verb-to-accepted-options fact in this
|
|
254
|
-
* file. Every option every verb's own code actually reads is listed here --
|
|
255
|
-
* ground truth, not merely what USAGE happens to say (see the export-asm
|
|
256
|
-
* comment above: USAGE previously under-documented a real, working
|
|
257
|
-
* `--entry` forward, which this map's own test caught and which USAGE was
|
|
258
|
-
* corrected to match). `verify`'s entry is the fix for IN-06 itself:
|
|
259
|
-
* `parseArgs()` parses `--out` for every verb because it is a single shared
|
|
260
|
-
* parser, but `cmdVerify()` never reads the resulting `out` field -- so a
|
|
261
|
-
* caller who passed `--out` to `verify` got no error and no effect. Listing
|
|
262
|
-
* only `--entry` here means `checkAcceptedOptions()` below refuses `--out`
|
|
263
|
-
* (and `--force`) before `cmdVerify()` ever runs.
|
|
264
|
-
*/
|
|
265
|
-
export const VERB_OPTIONS: Readonly<Record<string, readonly string[]>> = Object.freeze({
|
|
266
|
-
bootstrap: ["--entry", "--out", "--force"],
|
|
267
|
-
"export-asm": ["--entry", "--out", "--force"],
|
|
268
|
-
verify: ["--entry"],
|
|
269
|
-
"gen-enums": ["--max-results"],
|
|
270
|
-
"export-lbl": ["--out"],
|
|
271
|
-
"import-lbl": [],
|
|
272
|
-
"render-memmap": ["--provenance", "--out", "--check"],
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* The one shared refusal check IN-06 generalises to every verb (WR-08's
|
|
277
|
-
* closed-option-set posture, applied uniformly rather than verb by verb).
|
|
278
|
-
* Scans `rest` for any `--flag`-shaped token not in `verb`'s accepted set
|
|
279
|
-
* from `VERB_OPTIONS` and returns a one-line refusal naming the flag and the
|
|
280
|
-
* accepted set; returns `undefined` when every flag-shaped token is
|
|
281
|
-
* accepted (or when `verb` is not a key in the map at all, so an unknown
|
|
282
|
-
* verb still falls through to `runR2000Cli()`'s own "unknown verb"
|
|
283
|
-
* message). Never throws -- this file's never-throw posture applies here
|
|
284
|
-
* too.
|
|
285
|
-
*/
|
|
286
|
-
export function checkAcceptedOptions(verb: string, rest: string[]): string | undefined {
|
|
287
|
-
const accepted = VERB_OPTIONS[verb];
|
|
288
|
-
if (!accepted) return undefined;
|
|
289
|
-
for (const token of rest) {
|
|
290
|
-
if (token.startsWith("--") && !accepted.includes(token)) {
|
|
291
|
-
const acceptedList = accepted.length > 0 ? accepted.join(", ") : "none";
|
|
292
|
-
return `${verb}: unknown option "${token}" -- not accepted by this verb (accepted: ${acceptedList})`;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
return undefined;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Refuses to overwrite an existing file at `outPath` unless the caller
|
|
300
|
-
* passed `--force`. Shared by every verb that writes an output file
|
|
301
|
-
* (`bootstrap`, `export-asm`) so overwrite safety stays uniform rather than
|
|
302
|
-
* one verb accreting a check the others lack (CR-01/CR-02).
|
|
303
|
-
*/
|
|
304
|
-
function refuseOverwrite(outPath: string, force: boolean | undefined, verbLabel: string, extraHint = ""): boolean {
|
|
305
|
-
if (force || !existsSync(outPath)) return true;
|
|
306
|
-
console.error(
|
|
307
|
-
`${verbLabel}: refusing to overwrite the existing file ${outPath}${extraHint} -- ` +
|
|
308
|
-
`pass --force to overwrite it deliberately.`,
|
|
309
|
-
);
|
|
310
|
-
return false;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
interface BootstrapOutcome {
|
|
314
|
-
code: number;
|
|
315
|
-
path?: string;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Shared bootstrap logic used both by the `bootstrap` verb directly and by
|
|
320
|
-
* `export-asm` when handed a bare input rather than an existing
|
|
321
|
-
* `.regen2000proj`. Never throws for an expected, user-facing failure --
|
|
322
|
-
* every branch below returns a `{ code }` result with a one-line message
|
|
323
|
-
* already printed to stderr/stdout, so callers never see a stack trace for a
|
|
324
|
-
* missing file, an unknown `.d64` entry, or a `.vsf` input.
|
|
325
|
-
*/
|
|
326
|
-
function bootstrapProject(
|
|
327
|
-
input: string,
|
|
328
|
-
opts: { entry?: string; outPath?: string; force?: boolean },
|
|
329
|
-
): BootstrapOutcome {
|
|
330
|
-
if (!existsSync(input)) {
|
|
331
|
-
console.error(`bootstrap: input file not found: ${input}`);
|
|
332
|
-
return { code: 1 };
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const ext = extname(input).toLowerCase();
|
|
336
|
-
if (ext === ".regen2000proj") {
|
|
337
|
-
// CR-02: without this branch, a .regen2000proj input fell through to
|
|
338
|
-
// parsePrg(), which happily "parsed" the JSON text as a .prg (its first
|
|
339
|
-
// two bytes read as a bogus little-endian load address), and the
|
|
340
|
-
// derived out-path was then byte-identical to the input path --
|
|
341
|
-
// clobbering the project with garbage synthesised from its own text.
|
|
342
|
-
// Decision (see commit message): refuse rather than silently pass it
|
|
343
|
-
// through unchanged -- bootstrap's whole job is synthesising a project
|
|
344
|
-
// from RAW input, and a caller who names an existing .regen2000proj as
|
|
345
|
-
// bootstrap's input almost certainly meant export-asm or verify, which
|
|
346
|
-
// already accept a .regen2000proj directly and do the right thing.
|
|
347
|
-
console.error(
|
|
348
|
-
`bootstrap: ${input} is already a .regen2000proj -- bootstrap synthesises project files from raw ` +
|
|
349
|
-
"input (a .prg, a .d64 entry, or a flat 64K capture), it never re-reads one. Use export-asm or " +
|
|
350
|
-
"verify on it directly.",
|
|
351
|
-
);
|
|
352
|
-
return { code: 1 };
|
|
353
|
-
}
|
|
354
|
-
if (ext === ".vsf") {
|
|
355
|
-
// FLOW-02 (D-11.1-01): this refusal used to end by naming a specific
|
|
356
|
-
// numbered phase as the eventual owner of closing that gap. That phase
|
|
357
|
-
// shipped and never touched `.vsf` bootstrap -- the sentence pointed a
|
|
358
|
-
// user at a remediation path that would never exist. Name the backlog
|
|
359
|
-
// file instead; the WHY (the machine-type coincidence) stays, since
|
|
360
|
-
// that is the reason a user actually needs.
|
|
361
|
-
console.error(
|
|
362
|
-
"bootstrap: .vsf input is not supported -- regenerator2000's auto-detected machine-type field only " +
|
|
363
|
-
'reads correctly by coincidence ("C64SC" falls through to its own default, matching none of its ' +
|
|
364
|
-
"literal System arms). Filed as backlog, not covered by any R2000-* requirement (D-34): see " +
|
|
365
|
-
".planning/todos/pending/2026-08-20-vsf-as-a-bootstrap-input.md. Convert to .prg, .d64 or a flat " +
|
|
366
|
-
"64K capture instead.",
|
|
367
|
-
);
|
|
368
|
-
return { code: 1 };
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
let bytes: Uint8Array;
|
|
372
|
-
try {
|
|
373
|
-
bytes = readFileSync(input);
|
|
374
|
-
} catch (err) {
|
|
375
|
-
console.error(`bootstrap: could not read ${input}: ${errMsg(err)}`);
|
|
376
|
-
return { code: 1 };
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
let origin: number;
|
|
380
|
-
let body: Uint8Array;
|
|
381
|
-
|
|
382
|
-
if (ext === ".d64") {
|
|
383
|
-
try {
|
|
384
|
-
assertPlainImage(bytes);
|
|
385
|
-
} catch (err) {
|
|
386
|
-
console.error(`bootstrap: ${errMsg(err)}`);
|
|
387
|
-
return { code: 1 };
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
if (!opts.entry) {
|
|
391
|
-
// D-02's fail-loud contract, implemented here and nowhere else: never
|
|
392
|
-
// pick an entry. A silent auto-pick would happily analyse a cracktro
|
|
393
|
-
// or loader stub instead of the game.
|
|
394
|
-
let entries;
|
|
395
|
-
try {
|
|
396
|
-
entries = listEntries(bytes);
|
|
397
|
-
} catch (err) {
|
|
398
|
-
console.error(`bootstrap: ${errMsg(err)}`);
|
|
399
|
-
return { code: 1 };
|
|
400
|
-
}
|
|
401
|
-
console.log(`bootstrap: ${input} is a .d64 image with no --entry given -- directory listing:`);
|
|
402
|
-
if (entries.length === 0) {
|
|
403
|
-
console.log(" (no entries)");
|
|
404
|
-
} else {
|
|
405
|
-
for (const e of entries) {
|
|
406
|
-
console.log(` ${e.name}\t${e.type}\t${e.sizeBlocks} block(s)`);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
console.log("Re-run with --entry NAME to choose one. This CLI never guesses (D-02).");
|
|
410
|
-
return { code: 2 };
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
let extracted: Uint8Array;
|
|
414
|
-
try {
|
|
415
|
-
extracted = extractEntry(bytes, opts.entry);
|
|
416
|
-
} catch (err) {
|
|
417
|
-
console.error(`bootstrap: ${errMsg(err)}`);
|
|
418
|
-
return { code: 1 };
|
|
419
|
-
}
|
|
420
|
-
try {
|
|
421
|
-
({ origin, body } = parsePrg(extracted));
|
|
422
|
-
} catch {
|
|
423
|
-
// WR-09 (D-11.1-04): parsePrg() throws only when its input is under 3
|
|
424
|
-
// bytes (a .prg needs a 2-byte load address plus at least 1 payload
|
|
425
|
-
// byte). Every sibling branch below already wraps its own parsePrg()
|
|
426
|
-
// call -- this was the one gap, and its unwrapped throw escaped to
|
|
427
|
-
// runR2000Cli()'s last-resort net, surfacing as `r2000: parsePrg:
|
|
428
|
-
// input is N byte(s)` -- a message naming an internal function to a
|
|
429
|
-
// user who only ever supplied a disk image. Reworded entirely in the
|
|
430
|
-
// caller's own vocabulary (the entry name, never `parsePrg`) so the
|
|
431
|
-
// never-throw contract holds on this branch too.
|
|
432
|
-
console.error(
|
|
433
|
-
`bootstrap: entry "${opts.entry}" holds ${extracted.length} byte(s) -- not a loadable program ` +
|
|
434
|
-
"(a .prg needs at least 3 bytes: a 2-byte load address plus at least 1 payload byte). Choose a " +
|
|
435
|
-
"different --entry, or use a different input.",
|
|
436
|
-
);
|
|
437
|
-
return { code: 1 };
|
|
438
|
-
}
|
|
439
|
-
} else if (ext === ".raw" || ext === ".bin") {
|
|
440
|
-
// WR-07: dispatch by extension, not by byte length, so a truncated or
|
|
441
|
-
// oversized flat capture hits flatImageOrigin()'s own named refusal
|
|
442
|
-
// instead of falling through to parsePrg() and being silently
|
|
443
|
-
// reinterpreted as a .prg (see the header comment above).
|
|
444
|
-
try {
|
|
445
|
-
origin = flatImageOrigin(bytes);
|
|
446
|
-
} catch (err) {
|
|
447
|
-
console.error(`bootstrap: ${errMsg(err)}`);
|
|
448
|
-
return { code: 1 };
|
|
449
|
-
}
|
|
450
|
-
body = bytes;
|
|
451
|
-
} else if (bytes.length === 65536) {
|
|
452
|
-
// Extension-less flat capture (no `.raw`/`.bin` suffix): the only
|
|
453
|
-
// remaining route by which a 65536-byte flat image reaches
|
|
454
|
-
// flatImageOrigin() is this length check, kept for that case alone.
|
|
455
|
-
origin = flatImageOrigin(bytes);
|
|
456
|
-
body = bytes;
|
|
457
|
-
} else {
|
|
458
|
-
try {
|
|
459
|
-
({ origin, body } = parsePrg(bytes));
|
|
460
|
-
} catch (err) {
|
|
461
|
-
console.error(`bootstrap: ${errMsg(err)}`);
|
|
462
|
-
return { code: 1 };
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
const outPath = opts.outPath ?? input.replace(/\.[^./\\]+$/, "") + ".regen2000proj";
|
|
467
|
-
if (!refuseOverwrite(outPath, opts.force, "bootstrap")) {
|
|
468
|
-
return { code: 1 };
|
|
469
|
-
}
|
|
470
|
-
let projectJson: string;
|
|
471
|
-
try {
|
|
472
|
-
projectJson = synthesizeProject(body, { origin });
|
|
473
|
-
} catch (err) {
|
|
474
|
-
console.error(`bootstrap: ${errMsg(err)}`);
|
|
475
|
-
return { code: 1 };
|
|
476
|
-
}
|
|
477
|
-
try {
|
|
478
|
-
writeFileSync(outPath, projectJson);
|
|
479
|
-
} catch (err) {
|
|
480
|
-
// WR-09 (D-11.1-04): an ENOENT (missing parent directory), EACCES or
|
|
481
|
-
// ENOSPC here is an ordinary, expected failure -- refuseOverwrite()
|
|
482
|
-
// above only handles the exists-case; this is the everything-else case,
|
|
483
|
-
// and it must not throw past this function's own never-throw contract.
|
|
484
|
-
console.error(`bootstrap: could not write ${outPath}: ${errMsg(err)}`);
|
|
485
|
-
return { code: 1 };
|
|
486
|
-
}
|
|
487
|
-
console.log(`bootstrap: wrote ${outPath} (origin $${origin.toString(16).padStart(4, "0")})`);
|
|
488
|
-
return { code: 0, path: outPath };
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function cmdBootstrap(rest: string[]): number {
|
|
492
|
-
const { positional, entry, out, force, entryMissingValue, outMissingValue } = parseArgs(rest);
|
|
493
|
-
// WR-08: refused before any file is touched -- a flag-shaped or missing
|
|
494
|
-
// value for either option must never reach bootstrapProject() as if it
|
|
495
|
-
// were a real value.
|
|
496
|
-
if (entryMissingValue) {
|
|
497
|
-
console.error("bootstrap: --entry requires a value\n");
|
|
498
|
-
console.log(USAGE);
|
|
499
|
-
return 1;
|
|
500
|
-
}
|
|
501
|
-
if (outMissingValue) {
|
|
502
|
-
console.error("bootstrap: --out requires a value\n");
|
|
503
|
-
console.log(USAGE);
|
|
504
|
-
return 1;
|
|
505
|
-
}
|
|
506
|
-
const input = positional[0];
|
|
507
|
-
if (!input) {
|
|
508
|
-
console.error("bootstrap: usage: bootstrap <input> [--entry NAME] [--out PROJECT] [--force]");
|
|
509
|
-
return 1;
|
|
510
|
-
}
|
|
511
|
-
return bootstrapProject(input, { entry, outPath: out, force }).code;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
function cmdExportAsm(rest: string[]): number {
|
|
515
|
-
const { positional, entry, out, force, entryMissingValue, outMissingValue } = parseArgs(rest);
|
|
516
|
-
// WR-08, same posture as cmdBootstrap() above: refuse before any temp
|
|
517
|
-
// directory or file is created.
|
|
518
|
-
if (entryMissingValue) {
|
|
519
|
-
console.error("export-asm: --entry requires a value\n");
|
|
520
|
-
console.log(USAGE);
|
|
521
|
-
return 1;
|
|
522
|
-
}
|
|
523
|
-
if (outMissingValue) {
|
|
524
|
-
console.error("export-asm: --out requires a value\n");
|
|
525
|
-
console.log(USAGE);
|
|
526
|
-
return 1;
|
|
527
|
-
}
|
|
528
|
-
const input = positional[0];
|
|
529
|
-
if (!input) {
|
|
530
|
-
console.error("export-asm: usage: export-asm <input-or-project> [--out FILE] [--force]");
|
|
531
|
-
return 1;
|
|
532
|
-
}
|
|
533
|
-
if (!existsSync(input)) {
|
|
534
|
-
console.error(`export-asm: input file not found: ${input}`);
|
|
535
|
-
return 1;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
const ext = extname(input).toLowerCase();
|
|
539
|
-
let projectPath: string;
|
|
540
|
-
let tmpDir: string | undefined;
|
|
541
|
-
|
|
542
|
-
try {
|
|
543
|
-
if (ext === ".regen2000proj") {
|
|
544
|
-
projectPath = input;
|
|
545
|
-
} else {
|
|
546
|
-
// Bootstrap to a temp project first, so a bare .prg (or .d64/.raw)
|
|
547
|
-
// becomes ACME source in one command with no human interaction.
|
|
548
|
-
tmpDir = mkdtempSync(join(tmpdir(), "r2000-cli-"));
|
|
549
|
-
const tmpProjectPath = join(tmpDir, "bootstrap.regen2000proj");
|
|
550
|
-
const outcome = bootstrapProject(input, { entry, outPath: tmpProjectPath });
|
|
551
|
-
if (outcome.code !== 0) return outcome.code;
|
|
552
|
-
projectPath = outcome.path!;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
const outPath = out ?? input.replace(/\.[^./\\]+$/, "") + ".a";
|
|
556
|
-
if (
|
|
557
|
-
!refuseOverwrite(
|
|
558
|
-
outPath,
|
|
559
|
-
force,
|
|
560
|
-
"export-asm",
|
|
561
|
-
" with generated source -- acme-build's own convention pairs <stem>.a with <stem>.prg, so " +
|
|
562
|
-
"the default target is very often hand-written source",
|
|
563
|
-
)
|
|
564
|
-
) {
|
|
565
|
-
return 1;
|
|
566
|
-
}
|
|
567
|
-
const argv = buildExportAsmArgs({ projectPath, outPath });
|
|
568
|
-
const result = runR2000(argv);
|
|
569
|
-
if (result.status !== 0) {
|
|
570
|
-
console.error(`export-asm: regenerator2000 exited ${result.status}`);
|
|
571
|
-
if (result.stderr) console.error(result.stderr);
|
|
572
|
-
return result.status ?? 1;
|
|
573
|
-
}
|
|
574
|
-
console.log(`export-asm: wrote ${outPath}`);
|
|
575
|
-
return 0;
|
|
576
|
-
} finally {
|
|
577
|
-
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
/**
|
|
582
|
-
* `verify <input-or-project>` -- the criterion-4 (R2000-06) reassembly
|
|
583
|
-
* proof. Accepts a `.regen2000proj` directly, or bootstraps a bare input to
|
|
584
|
-
* a temporary project first, exactly like `cmdExportAsm()` above. Prints
|
|
585
|
-
* every parsed assembler result line, then the verdict's own reason. The
|
|
586
|
-
* exit code comes from `verifyProject()`'s `ok` field -- which
|
|
587
|
-
* `r2000-verify.ts`'s `acmeVerdict()` derives ONLY from the parsed ACME
|
|
588
|
-
* result line, never from regenerator2000's own process exit code (D-10).
|
|
589
|
-
* A skipped ACME therefore prints and exits as a failure here, reading as
|
|
590
|
-
* exactly that -- never as a silent absence -- even though the underlying
|
|
591
|
-
* `--verify` process may itself have exited 0 with a summary line claiming
|
|
592
|
-
* success (r2000-verify.test.ts's pinned trap transcript is the live
|
|
593
|
-
* incident this guards against).
|
|
594
|
-
*/
|
|
595
|
-
function cmdVerify(rest: string[]): number {
|
|
596
|
-
const { positional, entry, entryMissingValue } = parseArgs(rest);
|
|
597
|
-
// WR-08: `--out` is not in verify's own accepted set (VERB_OPTIONS above),
|
|
598
|
-
// so `checkAcceptedOptions()` already refuses it before this function ever
|
|
599
|
-
// runs -- only `--entry`'s missing-value case is reachable here.
|
|
600
|
-
if (entryMissingValue) {
|
|
601
|
-
console.error("verify: --entry requires a value\n");
|
|
602
|
-
console.log(USAGE);
|
|
603
|
-
return 1;
|
|
604
|
-
}
|
|
605
|
-
const input = positional[0];
|
|
606
|
-
if (!input) {
|
|
607
|
-
console.error("verify: usage: verify <input-or-project> [--entry NAME]");
|
|
608
|
-
return 1;
|
|
609
|
-
}
|
|
610
|
-
if (!existsSync(input)) {
|
|
611
|
-
console.error(`verify: input file not found: ${input}`);
|
|
612
|
-
return 1;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
const ext = extname(input).toLowerCase();
|
|
616
|
-
let projectPath: string;
|
|
617
|
-
let tmpDir: string | undefined;
|
|
618
|
-
|
|
619
|
-
try {
|
|
620
|
-
if (ext === ".regen2000proj") {
|
|
621
|
-
projectPath = input;
|
|
622
|
-
} else {
|
|
623
|
-
// Bootstrap to a temp project first, so a bare .prg (or .d64/.raw)
|
|
624
|
-
// can be verified in one command with no human interaction.
|
|
625
|
-
tmpDir = mkdtempSync(join(tmpdir(), "r2000-cli-"));
|
|
626
|
-
const tmpProjectPath = join(tmpDir, "bootstrap.regen2000proj");
|
|
627
|
-
const outcome = bootstrapProject(input, { entry, outPath: tmpProjectPath });
|
|
628
|
-
if (outcome.code !== 0) return outcome.code;
|
|
629
|
-
projectPath = outcome.path!;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
const result = verifyProject(projectPath);
|
|
633
|
-
for (const line of result.lines) {
|
|
634
|
-
const glyph = line.outcome === "ok" ? "✓" : "✗";
|
|
635
|
-
console.log(` ${glyph} ${line.assembler} — ${line.detail}`);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
if (!result.ok) {
|
|
639
|
-
// Print the reason verbatim -- especially a "skipped" reason, which
|
|
640
|
-
// must read as a failure and not as an absence (D-10).
|
|
641
|
-
console.error(`verify: ${result.reason}`);
|
|
642
|
-
return 1;
|
|
643
|
-
}
|
|
644
|
-
console.log(`verify: ${result.reason}`);
|
|
645
|
-
return 0;
|
|
646
|
-
} finally {
|
|
647
|
-
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
interface GenEnumsParsedArgs {
|
|
652
|
-
positional: string[];
|
|
653
|
-
maxResults?: number;
|
|
654
|
-
maxResultsRaw?: string;
|
|
655
|
-
unknownOption?: string;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
/** Fixed, closed option set for gen-enums -- exactly `--max-results`. Per
|
|
659
|
-
* WR-08's posture (do not silently accept a flag a verb does not
|
|
660
|
-
* implement), any OTHER `--flag`-shaped token is recorded as `unknownOption`
|
|
661
|
-
* and refused by the caller, rather than silently treated as a positional
|
|
662
|
-
* argument the way the other verbs' `parseArgs()` does. */
|
|
663
|
-
function parseGenEnumsArgs(rest: string[]): GenEnumsParsedArgs {
|
|
664
|
-
const positional: string[] = [];
|
|
665
|
-
let maxResults: number | undefined;
|
|
666
|
-
let maxResultsRaw: string | undefined;
|
|
667
|
-
let unknownOption: string | undefined;
|
|
668
|
-
for (let i = 0; i < rest.length; i++) {
|
|
669
|
-
const a = rest[i]!;
|
|
670
|
-
if (a === "--max-results") {
|
|
671
|
-
maxResultsRaw = rest[++i];
|
|
672
|
-
maxResults = maxResultsRaw !== undefined ? Number.parseInt(maxResultsRaw, 10) : Number.NaN;
|
|
673
|
-
} else if (a.startsWith("--")) {
|
|
674
|
-
unknownOption ??= a;
|
|
675
|
-
} else {
|
|
676
|
-
positional.push(a);
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
return { positional, maxResults, maxResultsRaw, unknownOption };
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
/**
|
|
683
|
-
* `gen-enums <project> [--max-results N]` -- D-20/D-22/D-23's whole pass,
|
|
684
|
-
* driven through `generateEnums()` (`r2000-enum-gen.ts`, Task 2). Prints the
|
|
685
|
-
* coverage report's own summary lines (total/paired/unpaired counts, one
|
|
686
|
-
* line per created/updated enum) and returns non-zero when: an unknown
|
|
687
|
-
* option was given (WR-08); no project path was given; the project file
|
|
688
|
-
* does not exist; `--max-results` did not parse as a positive integer;
|
|
689
|
-
* either search pass returned exactly its own ceiling (a possible-
|
|
690
|
-
* truncation signal, D-23's "no silent caps" applied at the CLI's own exit
|
|
691
|
-
* code); or `generateEnums()` itself threw (an illegal generated
|
|
692
|
-
* identifier, or an enum install/apply call that failed outright).
|
|
693
|
-
*/
|
|
694
|
-
async function cmdGenEnums(rest: string[]): Promise<number> {
|
|
695
|
-
const { positional, maxResults, maxResultsRaw, unknownOption } = parseGenEnumsArgs(rest);
|
|
696
|
-
if (unknownOption) {
|
|
697
|
-
console.error(`gen-enums: unknown option "${unknownOption}"\n`);
|
|
698
|
-
console.log(USAGE);
|
|
699
|
-
return 1;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
const project = positional[0];
|
|
703
|
-
if (!project) {
|
|
704
|
-
console.error("gen-enums: usage: gen-enums <project> [--max-results N]");
|
|
705
|
-
return 1;
|
|
706
|
-
}
|
|
707
|
-
if (!existsSync(project)) {
|
|
708
|
-
console.error(`gen-enums: input file not found: ${project}`);
|
|
709
|
-
return 1;
|
|
710
|
-
}
|
|
711
|
-
if (maxResults !== undefined && (!Number.isInteger(maxResults) || maxResults <= 0)) {
|
|
712
|
-
console.error(`gen-enums: --max-results must be a positive integer, got "${maxResultsRaw}"`);
|
|
713
|
-
return 1;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
let report: Awaited<ReturnType<typeof generateEnums>>;
|
|
717
|
-
try {
|
|
718
|
-
report = maxResults !== undefined ? await generateEnums({ projectPath: project, maxResults }) : await generateEnums({ projectPath: project });
|
|
719
|
-
} catch (err) {
|
|
720
|
-
console.error(`gen-enums: ${errMsg(err)}`);
|
|
721
|
-
return 1;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
for (const line of report.summaryLines) {
|
|
725
|
-
console.log(` ${line}`);
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
if (report.pass1Truncated || report.pass2Truncated) {
|
|
729
|
-
console.error(
|
|
730
|
-
"gen-enums: a search pass returned exactly its own max_results ceiling -- coverage may be incomplete; " +
|
|
731
|
-
"re-run with a higher --max-results",
|
|
732
|
-
);
|
|
733
|
-
return 1;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
console.log(`gen-enums: ${report.enums.length} enum(s) created/updated`);
|
|
737
|
-
return 0;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
interface ExportLblParsedArgs {
|
|
741
|
-
positional: string[];
|
|
742
|
-
out?: string;
|
|
743
|
-
outMissingValue?: boolean;
|
|
744
|
-
unknownOption?: string;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
/** Fixed, closed option set for export-lbl -- exactly `--out`. Per WR-08's
|
|
748
|
-
* posture (do not silently accept a flag a verb does not implement, or a
|
|
749
|
-
* flag missing its value), any OTHER `--flag`-shaped token is refused as
|
|
750
|
-
* `unknownOption`, and `--out` with no value (or a flag-shaped "value") is
|
|
751
|
-
* refused as `outMissingValue`, rather than silently treated the way
|
|
752
|
-
* bootstrap/export-asm's own looser `parseArgs()` does. */
|
|
753
|
-
function parseExportLblArgs(rest: string[]): ExportLblParsedArgs {
|
|
754
|
-
const positional: string[] = [];
|
|
755
|
-
let out: string | undefined;
|
|
756
|
-
let outMissingValue = false;
|
|
757
|
-
let unknownOption: string | undefined;
|
|
758
|
-
for (let i = 0; i < rest.length; i++) {
|
|
759
|
-
const a = rest[i]!;
|
|
760
|
-
if (a === "--out") {
|
|
761
|
-
const value = rest[i + 1];
|
|
762
|
-
if (value === undefined || value.startsWith("--")) {
|
|
763
|
-
outMissingValue = true;
|
|
764
|
-
} else {
|
|
765
|
-
out = value;
|
|
766
|
-
i++;
|
|
767
|
-
}
|
|
768
|
-
} else if (a.startsWith("--")) {
|
|
769
|
-
unknownOption ??= a;
|
|
770
|
-
} else {
|
|
771
|
-
positional.push(a);
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
return { positional, out, outMissingValue, unknownOption };
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
interface ImportLblParsedArgs {
|
|
778
|
-
positional: string[];
|
|
779
|
-
unknownOption?: string;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
/** import-lbl takes no options at all besides its two positional arguments
|
|
783
|
-
* -- any `--flag`-shaped token is refused as `unknownOption` (WR-08
|
|
784
|
-
* posture), never silently treated as a positional. */
|
|
785
|
-
function parseImportLblArgs(rest: string[]): ImportLblParsedArgs {
|
|
786
|
-
const positional: string[] = [];
|
|
787
|
-
let unknownOption: string | undefined;
|
|
788
|
-
for (const a of rest) {
|
|
789
|
-
if (a.startsWith("--")) unknownOption ??= a;
|
|
790
|
-
else positional.push(a);
|
|
791
|
-
}
|
|
792
|
-
return { positional, unknownOption };
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
/**
|
|
796
|
-
* `export-lbl <project> [--out FILE]` -- the R2000-14 export leg, via
|
|
797
|
-
* `r2000-symbols.ts`'s `exportLabels()`. Prints the written path and the
|
|
798
|
-
* symbol count parsed back from the file (never the raw regenerator2000
|
|
799
|
-
* exit code alone -- `exportLabels()` itself already re-reads and validates
|
|
800
|
-
* the file through stock-symbols.ts's parser before returning).
|
|
801
|
-
*/
|
|
802
|
-
async function cmdExportLbl(rest: string[]): Promise<number> {
|
|
803
|
-
const { positional, out, outMissingValue, unknownOption } = parseExportLblArgs(rest);
|
|
804
|
-
if (unknownOption) {
|
|
805
|
-
console.error(`export-lbl: unknown option "${unknownOption}"\n`);
|
|
806
|
-
console.log(USAGE);
|
|
807
|
-
return 1;
|
|
808
|
-
}
|
|
809
|
-
if (outMissingValue) {
|
|
810
|
-
console.error("export-lbl: --out requires a value\n");
|
|
811
|
-
console.log(USAGE);
|
|
812
|
-
return 1;
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
const project = positional[0];
|
|
816
|
-
if (!project) {
|
|
817
|
-
console.error("export-lbl: usage: export-lbl <project> [--out FILE]");
|
|
818
|
-
return 1;
|
|
819
|
-
}
|
|
820
|
-
if (!existsSync(project)) {
|
|
821
|
-
console.error(`export-lbl: project file not found: ${project}`);
|
|
822
|
-
return 1;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
const outPath = out ?? project.replace(/\.[^./\\]+$/, "") + ".lbl";
|
|
826
|
-
let result: Awaited<ReturnType<typeof exportLabels>>;
|
|
827
|
-
try {
|
|
828
|
-
result = await exportLabels({ projectPath: project, outPath });
|
|
829
|
-
} catch (err) {
|
|
830
|
-
console.error(`export-lbl: ${errMsg(err)}`);
|
|
831
|
-
return 1;
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
console.log(`export-lbl: wrote ${outPath} (${result.symbolCount} symbol(s))`);
|
|
835
|
-
return 0;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
/**
|
|
839
|
-
* `import-lbl <project> <lbl>` -- the R2000-15/D-28 import leg, via
|
|
840
|
-
* `r2000-symbols.ts`'s `importLabels()`. Prints the names imported, then an
|
|
841
|
-
* explicit line naming that persistence was proven by an independent disk
|
|
842
|
-
* re-read (never left implicit, so the transcript itself shows the D-28
|
|
843
|
-
* trap was avoided). Exits non-zero -- naming the reason, which includes
|
|
844
|
-
* stock-symbols.ts's own ceiling messages verbatim when the given `.lbl`
|
|
845
|
-
* fails them -- whenever the result is not disk-verified.
|
|
846
|
-
*/
|
|
847
|
-
async function cmdImportLbl(rest: string[]): Promise<number> {
|
|
848
|
-
const { positional, unknownOption } = parseImportLblArgs(rest);
|
|
849
|
-
if (unknownOption) {
|
|
850
|
-
console.error(`import-lbl: unknown option "${unknownOption}"\n`);
|
|
851
|
-
console.log(USAGE);
|
|
852
|
-
return 1;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
const [project, lbl] = positional;
|
|
856
|
-
if (!project || !lbl) {
|
|
857
|
-
console.error("import-lbl: usage: import-lbl <project> <lbl>");
|
|
858
|
-
return 1;
|
|
859
|
-
}
|
|
860
|
-
if (!existsSync(project)) {
|
|
861
|
-
console.error(`import-lbl: project file not found: ${project}`);
|
|
862
|
-
return 1;
|
|
863
|
-
}
|
|
864
|
-
if (!existsSync(lbl)) {
|
|
865
|
-
console.error(`import-lbl: label file not found: ${lbl}`);
|
|
866
|
-
return 1;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
let result: Awaited<ReturnType<typeof importLabels>>;
|
|
870
|
-
try {
|
|
871
|
-
result = await importLabels({ projectPath: project, lblPath: lbl });
|
|
872
|
-
} catch (err) {
|
|
873
|
-
console.error(`import-lbl: ${errMsg(err)}`);
|
|
874
|
-
return 1;
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
console.log(`import-lbl: imported ${result.importedNames.length} name(s): ${result.importedNames.join(", ")}`);
|
|
878
|
-
if (!result.diskVerified) {
|
|
879
|
-
console.error(`import-lbl: ${result.reason}`);
|
|
880
|
-
return 1;
|
|
881
|
-
}
|
|
882
|
-
console.log(
|
|
883
|
-
"import-lbl: persisted by an explicit r2000_save_project call over the same --mcp-server-stdio session " +
|
|
884
|
-
"(D-28) -- verified by re-reading the project from disk in a fresh process, not merely trusted from the " +
|
|
885
|
-
"child's own success text.",
|
|
886
|
-
);
|
|
887
|
-
return 0;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
interface RenderMemmapParsedArgs {
|
|
891
|
-
positional: string[];
|
|
892
|
-
provenance?: string;
|
|
893
|
-
provenanceMissingValue?: boolean;
|
|
894
|
-
out?: string;
|
|
895
|
-
outMissingValue?: boolean;
|
|
896
|
-
check?: boolean;
|
|
897
|
-
unknownOption?: string;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
/** Fixed, closed option set for render-memmap -- exactly `--provenance`,
|
|
901
|
-
* `--out` and `--check`. Per WR-08's posture (do not silently accept a flag
|
|
902
|
-
* a verb does not implement, or a flag missing its value), any OTHER
|
|
903
|
-
* `--flag`-shaped token is refused as `unknownOption`, and `--provenance`/
|
|
904
|
-
* `--out` with no value (or a flag-shaped "value") is refused via their own
|
|
905
|
-
* `*MissingValue` fields. */
|
|
906
|
-
function parseRenderMemmapArgs(rest: string[]): RenderMemmapParsedArgs {
|
|
907
|
-
const positional: string[] = [];
|
|
908
|
-
let provenance: string | undefined;
|
|
909
|
-
let provenanceMissingValue = false;
|
|
910
|
-
let out: string | undefined;
|
|
911
|
-
let outMissingValue = false;
|
|
912
|
-
let check = false;
|
|
913
|
-
let unknownOption: string | undefined;
|
|
914
|
-
for (let i = 0; i < rest.length; i++) {
|
|
915
|
-
const a = rest[i]!;
|
|
916
|
-
if (a === "--provenance") {
|
|
917
|
-
const value = rest[i + 1];
|
|
918
|
-
if (value === undefined || value.startsWith("--")) {
|
|
919
|
-
provenanceMissingValue = true;
|
|
920
|
-
} else {
|
|
921
|
-
provenance = value;
|
|
922
|
-
i++;
|
|
923
|
-
}
|
|
924
|
-
} else if (a === "--out") {
|
|
925
|
-
const value = rest[i + 1];
|
|
926
|
-
if (value === undefined || value.startsWith("--")) {
|
|
927
|
-
outMissingValue = true;
|
|
928
|
-
} else {
|
|
929
|
-
out = value;
|
|
930
|
-
i++;
|
|
931
|
-
}
|
|
932
|
-
} else if (a === "--check") {
|
|
933
|
-
check = true;
|
|
934
|
-
} else if (a.startsWith("--")) {
|
|
935
|
-
unknownOption ??= a;
|
|
936
|
-
} else {
|
|
937
|
-
positional.push(a);
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
return { positional, provenance, provenanceMissingValue, out, outMissingValue, check, unknownOption };
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
/**
|
|
944
|
-
* `render-memmap <project> --provenance FILE [--out FILE] [--check]` --
|
|
945
|
-
* D-24's generated-view verb, via `r2000-memmap-render.ts`'s
|
|
946
|
-
* `renderMemoryMap()`/`checkRenderedMemoryMap()`. Never writes a file when
|
|
947
|
-
* `--check` is given -- that mode only reads and reports.
|
|
948
|
-
*/
|
|
949
|
-
async function cmdRenderMemmap(rest: string[]): Promise<number> {
|
|
950
|
-
const {
|
|
951
|
-
positional,
|
|
952
|
-
provenance,
|
|
953
|
-
provenanceMissingValue,
|
|
954
|
-
out,
|
|
955
|
-
outMissingValue,
|
|
956
|
-
check,
|
|
957
|
-
unknownOption,
|
|
958
|
-
} = parseRenderMemmapArgs(rest);
|
|
959
|
-
|
|
960
|
-
if (unknownOption) {
|
|
961
|
-
console.error(`render-memmap: unknown option "${unknownOption}"\n`);
|
|
962
|
-
console.log(USAGE);
|
|
963
|
-
return 1;
|
|
964
|
-
}
|
|
965
|
-
if (provenanceMissingValue) {
|
|
966
|
-
console.error("render-memmap: --provenance requires a value\n");
|
|
967
|
-
console.log(USAGE);
|
|
968
|
-
return 1;
|
|
969
|
-
}
|
|
970
|
-
if (outMissingValue) {
|
|
971
|
-
console.error("render-memmap: --out requires a value\n");
|
|
972
|
-
console.log(USAGE);
|
|
973
|
-
return 1;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
const project = positional[0];
|
|
977
|
-
if (!project) {
|
|
978
|
-
console.error("render-memmap: usage: render-memmap <project> --provenance FILE [--out FILE] [--check]");
|
|
979
|
-
return 1;
|
|
980
|
-
}
|
|
981
|
-
if (!existsSync(project)) {
|
|
982
|
-
console.error(`render-memmap: project file not found: ${project}`);
|
|
983
|
-
return 1;
|
|
984
|
-
}
|
|
985
|
-
if (!provenance) {
|
|
986
|
-
console.error("render-memmap: --provenance FILE is required\n");
|
|
987
|
-
console.log(USAGE);
|
|
988
|
-
return 1;
|
|
989
|
-
}
|
|
990
|
-
if (!existsSync(provenance)) {
|
|
991
|
-
console.error(`render-memmap: provenance sidecar not found: ${provenance}`);
|
|
992
|
-
return 1;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
const outPath = out ?? join(dirname(project), "memory-map.md");
|
|
996
|
-
|
|
997
|
-
if (check) {
|
|
998
|
-
let result: Awaited<ReturnType<typeof checkRenderedMemoryMap>>;
|
|
999
|
-
try {
|
|
1000
|
-
result = await checkRenderedMemoryMap({ projectPath: project, provenancePath: provenance, renderedPath: outPath });
|
|
1001
|
-
} catch (err) {
|
|
1002
|
-
console.error(`render-memmap: ${errMsg(err)}`);
|
|
1003
|
-
return 1;
|
|
1004
|
-
}
|
|
1005
|
-
if (result.status === "in-sync") {
|
|
1006
|
-
console.log(`render-memmap: in sync (${outPath})`);
|
|
1007
|
-
return 0;
|
|
1008
|
-
}
|
|
1009
|
-
if (result.status === "missing") {
|
|
1010
|
-
console.error(`render-memmap: missing -- ${outPath} does not exist yet. Run render-memmap without --check first.`);
|
|
1011
|
-
return 1;
|
|
1012
|
-
}
|
|
1013
|
-
console.error(`render-memmap: drifted at line ${result.line}`);
|
|
1014
|
-
console.error(` expected: ${result.expected}`);
|
|
1015
|
-
console.error(` actual: ${result.actual}`);
|
|
1016
|
-
return 1;
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
let rendered: Awaited<ReturnType<typeof renderMemoryMap>>;
|
|
1020
|
-
try {
|
|
1021
|
-
rendered = await renderMemoryMap({ projectPath: project, provenancePath: provenance });
|
|
1022
|
-
} catch (err) {
|
|
1023
|
-
console.error(`render-memmap: ${errMsg(err)}`);
|
|
1024
|
-
return 1;
|
|
1025
|
-
}
|
|
1026
|
-
try {
|
|
1027
|
-
writeFileSync(outPath, rendered.markdown);
|
|
1028
|
-
} catch (err) {
|
|
1029
|
-
// WR-09 (D-11.1-04): the same shape as bootstrapProject()'s write above,
|
|
1030
|
-
// one verb over -- an ordinary write failure (missing parent directory,
|
|
1031
|
-
// permissions, full disk) must not throw past this verb's own
|
|
1032
|
-
// never-throw contract.
|
|
1033
|
-
console.error(`render-memmap: could not write ${outPath}: ${errMsg(err)}`);
|
|
1034
|
-
return 1;
|
|
1035
|
-
}
|
|
1036
|
-
console.log(
|
|
1037
|
-
`render-memmap: wrote ${outPath} (${rendered.rowCount} row(s), ${rendered.unknownCount} [unknown], digest ${rendered.renderDigest})`,
|
|
1038
|
-
);
|
|
1039
|
-
return 0;
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
/**
|
|
1043
|
-
* Entry point for the `r2000` subcommand. Returns an exit code; never calls
|
|
1044
|
-
* exit the process directly (the bin does that). Handles `--help`/no verb/unknown
|
|
1045
|
-
* verb per `acme.mjs`'s own dispatch convention (`.claude/skills/acme-build/
|
|
1046
|
-
* scripts/acme.mjs`), with one deliberate difference: an explicit `--help`
|
|
1047
|
-
* returns 0 (a no-op invocation with no verb also returns 0), while an
|
|
1048
|
-
* unrecognised verb returns 1.
|
|
1049
|
-
*/
|
|
1050
|
-
export async function runR2000Cli(argv: string[]): Promise<number> {
|
|
1051
|
-
const [verb, ...rest] = argv;
|
|
1052
|
-
|
|
1053
|
-
if (!verb || verb === "--help" || verb === "-h") {
|
|
1054
|
-
console.log(USAGE);
|
|
1055
|
-
return 0;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
// IN-06 (D-11.1-04): the single call site for the shared verb-options
|
|
1059
|
-
// check, run BEFORE dispatch so a refused option never reaches any cmd*
|
|
1060
|
-
// function -- one place enforces the closed option set for every verb,
|
|
1061
|
-
// rather than seven places each doing (or, as `verify` proved, NOT doing)
|
|
1062
|
-
// it themselves.
|
|
1063
|
-
const optionError = checkAcceptedOptions(verb, rest);
|
|
1064
|
-
if (optionError) {
|
|
1065
|
-
console.error(optionError);
|
|
1066
|
-
console.log(USAGE);
|
|
1067
|
-
return 1;
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
try {
|
|
1071
|
-
switch (verb) {
|
|
1072
|
-
case "bootstrap":
|
|
1073
|
-
return cmdBootstrap(rest);
|
|
1074
|
-
case "export-asm":
|
|
1075
|
-
return cmdExportAsm(rest);
|
|
1076
|
-
case "verify":
|
|
1077
|
-
return cmdVerify(rest);
|
|
1078
|
-
case "gen-enums":
|
|
1079
|
-
return await cmdGenEnums(rest);
|
|
1080
|
-
case "export-lbl":
|
|
1081
|
-
return await cmdExportLbl(rest);
|
|
1082
|
-
case "import-lbl":
|
|
1083
|
-
return await cmdImportLbl(rest);
|
|
1084
|
-
case "render-memmap":
|
|
1085
|
-
return await cmdRenderMemmap(rest);
|
|
1086
|
-
default:
|
|
1087
|
-
console.error(`r2000: unknown verb "${verb}"\n`);
|
|
1088
|
-
console.log(USAGE);
|
|
1089
|
-
return 1;
|
|
1090
|
-
}
|
|
1091
|
-
} catch (err) {
|
|
1092
|
-
// An R2000ViceFlagError from the seam (or any other unexpected throw)
|
|
1093
|
-
// must be re-thrown or reported verbatim, never swallowed -- the loud
|
|
1094
|
-
// failure is the point (D-07). This is a last-resort net: every expected
|
|
1095
|
-
// failure path above already returns its own code with its own message.
|
|
1096
|
-
if (err instanceof R2000ViceFlagError) {
|
|
1097
|
-
console.error(err.message);
|
|
1098
|
-
return 1;
|
|
1099
|
-
}
|
|
1100
|
-
console.error(`r2000: ${errMsg(err)}`);
|
|
1101
|
-
return 1;
|
|
1102
|
-
}
|
|
1103
|
-
}
|