@henols/vice-mcp 0.2.0 → 0.2.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/THIRD-PARTY-NOTICES.md +23 -0
- package/package.json +15 -1
- package/r2000-acme-ident.ts +97 -0
- package/r2000-cli.ts +1103 -0
- package/r2000-confidence.ts +233 -0
- package/r2000-d64.ts +310 -0
- package/r2000-enum-gen.ts +574 -0
- package/r2000-launch.ts +357 -0
- package/r2000-mcp-client.ts +596 -0
- package/r2000-memmap-render.ts +531 -0
- package/r2000-project.ts +190 -0
- package/r2000-regbits-gen.ts +416 -0
- package/r2000-regbits.json +1370 -0
- package/r2000-symbols.ts +388 -0
- package/r2000-tools.ts +914 -0
- package/r2000-verify.ts +184 -0
- package/stock-symbols.ts +33 -9
- package/vice-proxy.ts +146 -0
package/r2000-verify.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// r2000-verify.ts -- the ONE place that interprets regenerator2000's
|
|
3
|
+
// `--verify` output.
|
|
4
|
+
//
|
|
5
|
+
// WHY PARSING IS REQUIRED AT ALL (D-10, the concrete incident): with ACME
|
|
6
|
+
// absent from PATH and ca65 present, a real `regenerator2000 0.9.20 --verify`
|
|
7
|
+
// run on this host printed
|
|
8
|
+
//
|
|
9
|
+
// ✗ ACME — ACME not found in PATH (skipped)
|
|
10
|
+
// ✓ All roundtrip verifications passed.
|
|
11
|
+
// EXIT=0
|
|
12
|
+
//
|
|
13
|
+
// -- exit 0, and a summary line that reads as a full pass, while the one
|
|
14
|
+
// assembler this project actually cares about (`!cpu 6510`, ACME 0.97) never
|
|
15
|
+
// ran at all. Trusting the exit code here would let ACME be silently
|
|
16
|
+
// skipped and still report success. This is the WHAT-NOT-TO-DO for any
|
|
17
|
+
// future edit to this file: never derive `ok` from `status`, ever, no
|
|
18
|
+
// matter how tempting a bare zero-exit-status check looks. The verdict this
|
|
19
|
+
// module produces comes ONLY from parsing the per-assembler result lines
|
|
20
|
+
// and reading ACME's own line -- never from the process exit code, and
|
|
21
|
+
// never from the aggregate "All roundtrip verifications passed." summary
|
|
22
|
+
// line, which is itself the thing that lied in the transcript above.
|
|
23
|
+
//
|
|
24
|
+
// Both captured transcripts (the honest pass and this exact false-pass trap)
|
|
25
|
+
// are pinned verbatim as fixtures in r2000-verify.test.ts, so a future
|
|
26
|
+
// "simplification" back to an exit-code check fails a unit test immediately.
|
|
27
|
+
//
|
|
28
|
+
// WR-04 (10-REVIEW.md, fixed in plan 11-01): the verdict must also never
|
|
29
|
+
// trust just the FIRST ACME result line. `acmeVerdict()` used to select
|
|
30
|
+
// ACME's line with a bare array .find() over the first matching entry, so
|
|
31
|
+
// a transcript containing both a
|
|
32
|
+
// passing and a failing ACME line (a shape --verify has never printed as of
|
|
33
|
+
// 0.9.20, but one this defensive parser is explicitly meant to survive)
|
|
34
|
+
// reported `ok: true` from the first (passing) line while discarding the
|
|
35
|
+
// later failure -- exactly the "misleading success" this module exists to
|
|
36
|
+
// refuse. The fix requires UNANIMITY: every parsed ACME line must be `ok`,
|
|
37
|
+
// the first non-ok line (if any) drives the verdict, and if more than one
|
|
38
|
+
// ACME line is present after passing that check, the module refuses to
|
|
39
|
+
// guess which one is authoritative rather than picking one arbitrarily.
|
|
40
|
+
// Both the mixed-transcript case and the too-many-ACME-lines case are pinned
|
|
41
|
+
// verbatim as fixtures in r2000-verify.test.ts.
|
|
42
|
+
//
|
|
43
|
+
// Import nothing from `hostpath.ts`/`containerpath.ts` -- plan 10-01's
|
|
44
|
+
// absence assertion in `hostpath-consumers.test.ts` already names this file.
|
|
45
|
+
|
|
46
|
+
import { buildVerifyArgs, runR2000 } from "./r2000-launch.ts";
|
|
47
|
+
|
|
48
|
+
/** The three possible outcomes for a single per-assembler `--verify` result
|
|
49
|
+
* line. `"skipped"` and `"ok"` are DIFFERENT outcomes and must never be
|
|
50
|
+
* conflated -- a skipped assembler did not run, so it proves nothing, while
|
|
51
|
+
* an `ok` assembler was actually invoked and its output byte-diffed. */
|
|
52
|
+
export type AssemblerOutcome = "ok" | "skipped" | "failed";
|
|
53
|
+
|
|
54
|
+
export interface VerifyLine {
|
|
55
|
+
assembler: string;
|
|
56
|
+
outcome: AssemblerOutcome;
|
|
57
|
+
detail: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Matches exactly a per-assembler result line, e.g.:
|
|
61
|
+
// ✓ ACME — byte-identical (44 bytes)
|
|
62
|
+
// ✗ 64tass — 64tass not found in PATH (skipped)
|
|
63
|
+
// Tolerates an em-dash (U+2014), en-dash (U+2013) or plain hyphen as the
|
|
64
|
+
// separator, since only the ACME verdict is load-bearing here and the exact
|
|
65
|
+
// glyph regenerator2000 prints is an upstream formatting detail, not
|
|
66
|
+
// something this parser should be brittle against. Deliberately does NOT
|
|
67
|
+
// match the aggregate "✓ All roundtrip verifications passed." summary line
|
|
68
|
+
// (no separator token present there) or the "EXIT=N" line some transcripts
|
|
69
|
+
// carry -- both are excluded from the returned array by construction, not
|
|
70
|
+
// by a special-cased skip: they simply never match this shape.
|
|
71
|
+
const VERIFY_LINE_PATTERN = /^[✓✗]\s+(.+?)\s+[—–-]\s+(.+)$/;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parses `regenerator2000 --verify`'s stdout into one `VerifyLine` per
|
|
75
|
+
* per-assembler result line. The aggregate `✓ All roundtrip verifications
|
|
76
|
+
* passed.` line is a summary, not an assembler line -- it never matches
|
|
77
|
+
* `VERIFY_LINE_PATTERN` (no `—`/`-` separator), so it is excluded from the
|
|
78
|
+
* result by construction rather than filtered out after the fact.
|
|
79
|
+
*/
|
|
80
|
+
export function parseVerifyOutput(stdout: string): VerifyLine[] {
|
|
81
|
+
const lines: VerifyLine[] = [];
|
|
82
|
+
for (const rawLine of stdout.split(/\r?\n/)) {
|
|
83
|
+
const trimmed = rawLine.trim();
|
|
84
|
+
if (!trimmed) continue;
|
|
85
|
+
const match = trimmed.match(VERIFY_LINE_PATTERN);
|
|
86
|
+
if (!match) continue;
|
|
87
|
+
const [, assemblerRaw, detailRaw] = match;
|
|
88
|
+
const assembler = assemblerRaw!.trim();
|
|
89
|
+
const detail = detailRaw!.trim();
|
|
90
|
+
// A "(skipped)" suffix always means skipped, regardless of the leading
|
|
91
|
+
// glyph. Anything else takes its outcome from the leading glyph: a
|
|
92
|
+
// leading ✓ is "ok", any other non-skipped result line is "failed" --
|
|
93
|
+
// this project has never observed a real ACME/ca65 failure transcript,
|
|
94
|
+
// but a future one must not silently parse as "ok".
|
|
95
|
+
const outcome: AssemblerOutcome = /\(skipped\)\s*$/i.test(detail)
|
|
96
|
+
? "skipped"
|
|
97
|
+
: trimmed.startsWith("✓")
|
|
98
|
+
? "ok"
|
|
99
|
+
: "failed";
|
|
100
|
+
lines.push({ assembler, outcome, detail });
|
|
101
|
+
}
|
|
102
|
+
return lines;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Derives the ACME-specific verdict from a parsed line set. `ok` only when
|
|
107
|
+
* at least one ACME line exists, EVERY parsed ACME line has outcome `"ok"`
|
|
108
|
+
* (unanimity -- WR-04), AND exactly one ACME line is present. A missing
|
|
109
|
+
* ACME line, any ACME line with outcome `"skipped"` or `"failed"`, and more
|
|
110
|
+
* than one `"ok"` ACME line each return `ok: false` with a distinct,
|
|
111
|
+
* quotable reason -- never conflate "skipped" with "passed", never let a
|
|
112
|
+
* passing line hide a later failing one, and never fall back to the summary
|
|
113
|
+
* line (which this module never even parses as a VerifyLine, see
|
|
114
|
+
* `parseVerifyOutput`).
|
|
115
|
+
*/
|
|
116
|
+
export function acmeVerdict(lines: VerifyLine[]): { ok: boolean; reason: string } {
|
|
117
|
+
const acmeLines = lines.filter((l) => l.assembler.toLowerCase() === "acme");
|
|
118
|
+
|
|
119
|
+
if (acmeLines.length === 0) {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
reason:
|
|
123
|
+
"no ACME line found in --verify output -- ACME was never invoked at all, which is a failure, " +
|
|
124
|
+
"not an absence of evidence",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// First non-ok line wins -- so a passing ACME line earlier in the
|
|
129
|
+
// transcript can never hide a failing one later in it (WR-04).
|
|
130
|
+
const bad = acmeLines.find((l) => l.outcome !== "ok");
|
|
131
|
+
|
|
132
|
+
if (bad?.outcome === "skipped") {
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
reason:
|
|
136
|
+
`ACME was skipped, not run -- "${bad.detail}". A skipped ACME is a failure, never a pass ` +
|
|
137
|
+
`(D-10): --verify can print "✓ All roundtrip verifications passed." and exit 0 even when ` +
|
|
138
|
+
`ACME never ran at all -- exactly the false pass observed live on this host with ACME absent ` +
|
|
139
|
+
`and ca65 present.`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (bad?.outcome === "failed") {
|
|
144
|
+
return { ok: false, reason: `ACME reported a failure: "${bad.detail}"` };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Every ACME line is ok at this point. Still refuse to guess which one is
|
|
148
|
+
// authoritative if more than one was printed (WR-04) -- unanimous is not
|
|
149
|
+
// the same as unambiguous.
|
|
150
|
+
if (acmeLines.length > 1) {
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
reason: `--verify printed ${acmeLines.length} ACME result lines -- refusing to guess which one is the verdict`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { ok: true, reason: `ACME reported: ${acmeLines[0]!.detail}` };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface VerifyProjectResult {
|
|
161
|
+
ok: boolean;
|
|
162
|
+
reason: string;
|
|
163
|
+
lines: VerifyLine[];
|
|
164
|
+
status: number | null;
|
|
165
|
+
stdout: string;
|
|
166
|
+
stderr: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Runs `regenerator2000 --verify` against `projectPath` (via
|
|
171
|
+
* `buildVerifyArgs()`/`runR2000()`, so the `--vice` scan applies here too),
|
|
172
|
+
* parses stdout, and derives `ok` from `acmeVerdict()` -- deliberately NOT
|
|
173
|
+
* from `status`. Returns the raw status and streams so a caller can print
|
|
174
|
+
* them for diagnostics, but no code path in this function ever lets a zero
|
|
175
|
+
* exit status alone make `ok` true. A warning on stderr is never treated as
|
|
176
|
+
* a failure -- only the parsed ACME result line decides the verdict.
|
|
177
|
+
*/
|
|
178
|
+
export function verifyProject(projectPath: string): VerifyProjectResult {
|
|
179
|
+
const argv = buildVerifyArgs({ projectPath });
|
|
180
|
+
const { status, stdout, stderr } = runR2000(argv);
|
|
181
|
+
const lines = parseVerifyOutput(stdout);
|
|
182
|
+
const verdict = acmeVerdict(lines);
|
|
183
|
+
return { ok: verdict.ok, reason: verdict.reason, lines, status, stdout, stderr };
|
|
184
|
+
}
|
package/stock-symbols.ts
CHANGED
|
@@ -26,11 +26,17 @@
|
|
|
26
26
|
// The confirmed input format is a VICE label file, one `al C:xxxx .Name`
|
|
27
27
|
// line per symbol, verified against ACME's `--vicelabels` output via
|
|
28
28
|
// acme-build/scripts/acme.mjs's own parser (curateLabels(),
|
|
29
|
-
// `/^al\s+C:[0-9a-f]+\s+\.(\S+)/i`).
|
|
30
|
-
// regenerator2000's `--export_lbl`
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
29
|
+
// `/^al\s+C:[0-9a-f]+\s+\.(\S+)/i`). VERIFIED (Phase 9, R2000-16(c)):
|
|
30
|
+
// regenerator2000 0.9.20's `--export_lbl` was run against the
|
|
31
|
+
// probe-illegal.prg-derived fixture and emitted `al C:0810 .init_screen`,
|
|
32
|
+
// which matches this module's own VICE_LABEL_LINE_RE
|
|
33
|
+
// (`/^al\s+C:([0-9a-fA-F]{1,4})\s+\.(\S+)/`) exactly. This claim is SCOPED to
|
|
34
|
+
// regenerator2000 0.9.20 and that fixture -- not to all inputs forever (the
|
|
35
|
+
// same scoping caveat ROADMAP.md applies to Phase 9's criterion 3(3) `pass`).
|
|
36
|
+
// The parser below still SKIPS unrecognised lines rather than refusing the
|
|
37
|
+
// whole file: a future regenerator2000 version, a hand-edited label file, or
|
|
38
|
+
// a different exporter entirely can still produce lines this format should
|
|
39
|
+
// tolerate rather than reject outright.
|
|
34
40
|
//
|
|
35
41
|
// WHAT NOT TO DO:
|
|
36
42
|
// - Never add a second resolver holder or call setSymbolResolver() from
|
|
@@ -75,8 +81,12 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
75
81
|
const VICE_LABEL_LINE_RE = /^al\s+C:([0-9a-fA-F]{1,4})\s+\.(\S+)/;
|
|
76
82
|
|
|
77
83
|
/** T-05-02-03: three independent resource ceilings, each refusing with both
|
|
78
|
-
* the observed value and the limit named.
|
|
79
|
-
|
|
84
|
+
* the observed value and the limit named. `MAX_LABEL_FILE_BYTES` is exported
|
|
85
|
+
* (11-08, Rule A20) so `r2000-symbols.ts`'s `exportLabels()`/`importLabels()`
|
|
86
|
+
* can apply the SAME byte ceiling to a regenerator2000-produced/-consumed
|
|
87
|
+
* `.lbl` file before ever calling `parseViceLabelFile()` below -- never a
|
|
88
|
+
* second hand-copied number. */
|
|
89
|
+
export const MAX_LABEL_FILE_BYTES = 2 * 1024 * 1024;
|
|
80
90
|
const MAX_LABEL_FILE_LINES = 50000;
|
|
81
91
|
const MAX_SYMBOLS = 20000;
|
|
82
92
|
|
|
@@ -98,7 +108,11 @@ export class StockSymbolsError extends ViceError {
|
|
|
98
108
|
}
|
|
99
109
|
}
|
|
100
110
|
|
|
101
|
-
|
|
111
|
+
/** Exported alongside `parseViceLabelFile()` (11-08, Rule A20) purely so a
|
|
112
|
+
* cross-module caller can name this shape in its own type annotations --
|
|
113
|
+
* this module's own internal state (`loadedTable` below) still never leaves
|
|
114
|
+
* this file. */
|
|
115
|
+
export interface SymbolTable {
|
|
102
116
|
byName: Map<string, number>;
|
|
103
117
|
byAddress: Map<number, string>;
|
|
104
118
|
}
|
|
@@ -186,7 +200,17 @@ function resolveLabelFilePath(pathArg: unknown): string {
|
|
|
186
200
|
// counted, never a whole-file refusal.
|
|
187
201
|
// ---------------------------------------------------------------------------
|
|
188
202
|
|
|
189
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Exported (11-08, Rule A20) so `r2000-symbols.ts` can validate a
|
|
205
|
+
* regenerator2000-produced `.lbl` file (or check a caller-supplied one
|
|
206
|
+
* BEFORE it is ever handed to a spawned regenerator2000 child) through THIS
|
|
207
|
+
* parser -- the ONE `al C:xxxx .Name` reader in this repo -- rather than
|
|
208
|
+
* adding a second copy of `VICE_LABEL_LINE_RE`. Ceiling violations
|
|
209
|
+
* (`MAX_LABEL_FILE_LINES`/`MAX_SYMBOLS`) throw `StockSymbolsError` exactly as
|
|
210
|
+
* they do for `handleSymbolsLoad` below; a caller across the module boundary
|
|
211
|
+
* is expected to surface that error verbatim, never re-wrap it.
|
|
212
|
+
*/
|
|
213
|
+
export function parseViceLabelFile(text: string): {
|
|
190
214
|
table: SymbolTable;
|
|
191
215
|
symbolCount: number;
|
|
192
216
|
skippedLines: number;
|
package/vice-proxy.ts
CHANGED
|
@@ -185,6 +185,126 @@ import * as stockDispatch from "./stock-dispatch.ts";
|
|
|
185
185
|
// below, strictly after the DENY_LIST check -- see the comment at that call
|
|
186
186
|
// site for why the ordering is load-bearing.
|
|
187
187
|
import { capabilityRefusalMessage } from "./capability-registry.ts";
|
|
188
|
+
// Plan 11-05: the curated r2000_* tool surface's DEFINITIONS, imported
|
|
189
|
+
// STATICALLY -- registration below happens synchronously at module scope, so
|
|
190
|
+
// a dynamic import cannot serve it. This costs no child process and no
|
|
191
|
+
// socket: the heavy part (the MCP client, the spawned regenerator2000 child)
|
|
192
|
+
// stays behind r2000-tools.ts's own `await import("./r2000-mcp-client.ts")`
|
|
193
|
+
// inside runR2000Tool() itself, reached only when a tool is actually called.
|
|
194
|
+
import { R2000_TOOL_DEFINITIONS, runR2000Tool } from "./r2000-tools.ts";
|
|
195
|
+
|
|
196
|
+
// ------------------------------------------------------------ r2000 subcommand
|
|
197
|
+
//
|
|
198
|
+
// D-06 / RESEARCH.md Open Question #1 (plan 10-04): `vice-mcp r2000 <verb>` is
|
|
199
|
+
// the ONLY surface that resolves identically across the Claude Code plugin
|
|
200
|
+
// route and both npm-installer routes -- `installer/bin/cli.mjs`'s
|
|
201
|
+
// `viceServerEntry()` always launches this server via `npx` in BOTH
|
|
202
|
+
// npm-installer modes (`--vendor` only pre-resolves the package; it never
|
|
203
|
+
// places `.claude/mcp/vice/*.ts` as plain files inside a consuming project),
|
|
204
|
+
// so any design resolving a filesystem path to the seam would silently fail
|
|
205
|
+
// to resolve for npm-installed users. This bin is the one surface proven to
|
|
206
|
+
// work in all three routes.
|
|
207
|
+
//
|
|
208
|
+
// This branch runs as the first executable statement of the module body,
|
|
209
|
+
// deliberately ABOVE `ACTIVE_BACKEND`'s backend probe (which shells out to a
|
|
210
|
+
// binary's `--help`), above the manifest read, and above
|
|
211
|
+
// `new MCPServer(...)`/`server.startStdio()` far below -- a CLI invocation
|
|
212
|
+
// must never open a socket, never probe a binary, and never write a byte of
|
|
213
|
+
// JSON-RPC to stdout. WHAT NOT TO DO: never let this branch fall through
|
|
214
|
+
// into the server path, and never print anything on stdout on the server
|
|
215
|
+
// path that a CLI caller could confuse for `r2000` output.
|
|
216
|
+
//
|
|
217
|
+
// Ending the process here is deliberate and is NOT a violation of this
|
|
218
|
+
// file's standing "never end the process from a teardown handler" rule (see
|
|
219
|
+
// that handler's own comment further down): that rule protects the
|
|
220
|
+
// long-lived server's lease-release path, and this branch ends the process
|
|
221
|
+
// before any lease, socket or handler exists. A dynamic import is used
|
|
222
|
+
// (not a static one) so the CLI module is not part of the server's startup
|
|
223
|
+
// cost on the normal, non-`r2000` path.
|
|
224
|
+
//
|
|
225
|
+
// IN-01 (10-REVIEW.md; 11.1-CONTEXT.md AUDIT-01, D-11.1-04): `console.log`/
|
|
226
|
+
// `console.error` writes to `process.stdout`/`process.stderr` are
|
|
227
|
+
// ASYNCHRONOUS on POSIX once the fd is a pipe (Node opens pipe/socket fds
|
|
228
|
+
// non-blocking, unlike a TTY or a regular file), so a bare `process.exit()`
|
|
229
|
+
// immediately after can discard whatever write has not yet drained --
|
|
230
|
+
// measured at a 128 KiB truncation point on this host's Node for a single
|
|
231
|
+
// write exceeding the OS pipe's capacity. The reachable trigger is
|
|
232
|
+
// `cmdExportAsm`'s `console.error(result.stderr)` in r2000-cli.ts, which can
|
|
233
|
+
// carry a large diagnostic from the spawned regenerator2000 child: the one
|
|
234
|
+
// case where the user most needs the diagnostic is exactly the case a piped
|
|
235
|
+
// invocation could silently lose it in. `drainStdio()` below explicitly
|
|
236
|
+
// awaits both streams' own pending writes (a `write("", cb)`-style
|
|
237
|
+
// zero-length write's callback fires only once every prior queued write has
|
|
238
|
+
// actually flushed) before the terminating `process.exit(code)`.
|
|
239
|
+
//
|
|
240
|
+
// T-11.1-EXITHANG: the drain is BOUNDED to `R2000_CLI_DRAIN_TIMEOUT_MS`. An
|
|
241
|
+
// exit that hangs forever waiting on a pipe nobody reads is worse than a
|
|
242
|
+
// truncated diagnostic -- this project's standing rule is that a teardown
|
|
243
|
+
// path never becomes a hang (see the "never end the process from a teardown
|
|
244
|
+
// handler" comment above; a BOUNDED drain here does not violate that rule
|
|
245
|
+
// for the same reason the original unbounded `process.exit()` did not: this
|
|
246
|
+
// still runs before any lease, socket or handler exists, and now also can
|
|
247
|
+
// never block indefinitely).
|
|
248
|
+
const R2000_CLI_DRAIN_TIMEOUT_MS = 300;
|
|
249
|
+
|
|
250
|
+
/** Resolves once `stream`'s own pending writes have flushed, or after
|
|
251
|
+
* `timeoutMs`, whichever comes first. A zero-length `write("", cb)`'s
|
|
252
|
+
* callback fires strictly after every write queued ahead of it on the same
|
|
253
|
+
* stream has completed -- so this is a genuine drain barrier, not a fixed
|
|
254
|
+
* sleep. Guarded so a stream that is not writable (already closed/ended,
|
|
255
|
+
* e.g. under `> /dev/null` teardown races) resolves immediately rather than
|
|
256
|
+
* calling `write()` on it. */
|
|
257
|
+
function drainStdio(stream: NodeJS.WriteStream): Promise<void> {
|
|
258
|
+
return new Promise((resolve) => {
|
|
259
|
+
if (!stream.writable) {
|
|
260
|
+
resolve();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const timer = setTimeout(resolve, R2000_CLI_DRAIN_TIMEOUT_MS);
|
|
264
|
+
stream.write("", () => {
|
|
265
|
+
clearTimeout(timer);
|
|
266
|
+
resolve();
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (process.argv[2] === "r2000") {
|
|
272
|
+
// A broken pipe (the reader closing early, e.g. `| head`) makes
|
|
273
|
+
// `stream.write()` fail with EPIPE. Awaiting `drainStdio()` below gives
|
|
274
|
+
// that failure the chance to actually surface as Node's stream 'error'
|
|
275
|
+
// event -- which throws UNCAUGHT and crashes the process if nothing is
|
|
276
|
+
// listening, converting what used to be a silent (bare `process.exit()`
|
|
277
|
+
// outran the async error) success into a stack-trace crash. This
|
|
278
|
+
// mirrors the SAME EPIPE class this file already guards against for the
|
|
279
|
+
// server path further down (see that handler's own
|
|
280
|
+
// modelcontextprotocol/typescript-sdk#1564 citation) -- registered here
|
|
281
|
+
// too because this branch exits long before reaching that one.
|
|
282
|
+
process.stdout.on("error", () => {});
|
|
283
|
+
process.stderr.on("error", () => {});
|
|
284
|
+
|
|
285
|
+
// Test-only escape hatch, never documented to end users and inert unless
|
|
286
|
+
// this exact env var is set: writes a deterministic filler payload
|
|
287
|
+
// through this SAME drained-exit path, so vice-proxy.test.ts can measure
|
|
288
|
+
// an exact byte count well above any OS pipe capacity without needing a
|
|
289
|
+
// real regenerator2000 project (empirically, neither `--help`'s ~5.6 KB
|
|
290
|
+
// USAGE text nor a synthesized/garbage `.regen2000proj` fed to
|
|
291
|
+
// export-asm's error path scales anywhere near 128 KiB on this host's
|
|
292
|
+
// regenerator2000 0.9.20 -- both were measured before this hatch was
|
|
293
|
+
// added; see 11.1-05-SUMMARY.md for the measurements). Never reachable
|
|
294
|
+
// from a real `r2000 <verb>` invocation: the check is against a specific,
|
|
295
|
+
// unambiguous env var name no real caller would ever set.
|
|
296
|
+
const testFillBytes = process.env.VICE_TEST_R2000_CLI_STDOUT_FILL_BYTES;
|
|
297
|
+
if (testFillBytes) {
|
|
298
|
+
process.stdout.write("x".repeat(Number(testFillBytes)));
|
|
299
|
+
await Promise.all([drainStdio(process.stdout), drainStdio(process.stderr)]);
|
|
300
|
+
process.exit(0);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const { runR2000Cli } = await import("./r2000-cli.ts");
|
|
304
|
+
const code = await runR2000Cli(process.argv.slice(3));
|
|
305
|
+
await Promise.all([drainStdio(process.stdout), drainStdio(process.stderr)]);
|
|
306
|
+
process.exit(code);
|
|
307
|
+
}
|
|
188
308
|
|
|
189
309
|
const HERE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
190
310
|
|
|
@@ -3220,6 +3340,32 @@ tools[RESULT_CONTINUE_TOOL.name] = buildViceTool(RESULT_CONTINUE_TOOL, (args) =>
|
|
|
3220
3340
|
// entry in `tools/list`.
|
|
3221
3341
|
tools[RECYCLE_TOOL.name] = buildBackendAwareTool(stockDispatch.resolveAdvertisedToolDefinition(RECYCLE_TOOL, ACTIVE_BACKEND.backend, manifestTools), (args) => handleRecycle(args));
|
|
3222
3342
|
tools[DIAGNOSE_TOOL.name] = buildBackendAwareTool(stockDispatch.resolveAdvertisedToolDefinition(DIAGNOSE_TOOL, ACTIVE_BACKEND.backend, manifestTools), (args) => handleDiagnose(args));
|
|
3343
|
+
// Backend-INDEPENDENT by construction (plan 11-05): the r2000_* family never
|
|
3344
|
+
// touches VICE at all -- regenerator2000 is a separate, static-analysis child
|
|
3345
|
+
// process, so there is no fork/stock distinction to make and
|
|
3346
|
+
// buildBackendAwareTool() would be flatly wrong here (there is nothing for it
|
|
3347
|
+
// to dispatch to on either backend). The family is in NEITHER
|
|
3348
|
+
// tools-manifest.json NOR tools-manifest.stock.json: both are regenerated by
|
|
3349
|
+
// refresh-manifest.ts from a live HOST VICE server's own tools/list, and an
|
|
3350
|
+
// r2000 child process is never that host -- a hand-added entry in either
|
|
3351
|
+
// manifest would be silently wiped on the next refresh. Registered here via
|
|
3352
|
+
// buildViceTool() directly (the SAME exception RESULT_CONTINUE_TOOL above
|
|
3353
|
+
// is), so no r2000_* runner can ever reach forwardToVice(), call(), or
|
|
3354
|
+
// ensureViceSession() -- CLAUDE.md's "derived tools must be intercepted
|
|
3355
|
+
// before forwardToVice()" constraint is satisfied by construction for this
|
|
3356
|
+
// family, not by an interception, because the runner is never wired to
|
|
3357
|
+
// forwardToVice() in the first place.
|
|
3358
|
+
// Deliberately NOT named `def` (the manifest loop's own loop variable,
|
|
3359
|
+
// above): `stock-dispatch.test.ts`'s `proxyToolRegistrations()` regex-scans
|
|
3360
|
+
// this file's own `tools[...] = ...;` lines and keys each one by its raw
|
|
3361
|
+
// captured text, so an identically-named loop variable here would make this
|
|
3362
|
+
// registration textually indistinguishable from the manifest loop's -- a
|
|
3363
|
+
// distinct name (`r2000Def`) keeps the r2000 family's own exemption from
|
|
3364
|
+
// `buildBackendAwareTool()` from ever being confused with, or accidentally
|
|
3365
|
+
// widened to cover, the manifest loop's registration.
|
|
3366
|
+
for (const r2000Def of R2000_TOOL_DEFINITIONS) {
|
|
3367
|
+
tools[r2000Def.name] = buildViceTool(r2000Def, (args) => runR2000Tool(r2000Def.name, args));
|
|
3368
|
+
}
|
|
3223
3369
|
|
|
3224
3370
|
const server = new MCPServer({ name: "vice", version: PROXY_VERSION, tools });
|
|
3225
3371
|
await server.startStdio();
|