@henols/vice-mcp 0.1.9 → 0.1.11
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 +15 -0
- package/THIRD-PARTY-NOTICES.md +113 -0
- package/backend-detect.mts +595 -0
- package/build.ts +1 -0
- package/disasm-decoder.ts +248 -0
- package/disasm-opcodes.ts +464 -0
- package/disasm-renderer.ts +306 -0
- package/package.json +27 -2
- package/refresh-manifest.ts +23 -2
- package/resources/backend-detect.mjs +396 -0
- package/resources/broker-control.mjs +110 -8
- package/resources/broker-kill.mjs +114 -103
- package/resources/broker-launch.mjs +334 -19
- package/resources/broker-state.mjs +11 -0
- package/resources/vice-broker.mjs +185 -14
- package/stock-address.ts +219 -0
- package/stock-checkpoints.ts +794 -0
- package/stock-condition.ts +636 -0
- package/stock-connect.ts +427 -0
- package/stock-derived.ts +122 -0
- package/stock-disassemble.ts +252 -0
- package/stock-dispatch.ts +640 -0
- package/stock-execution.ts +327 -0
- package/stock-handler.ts +175 -0
- package/stock-input.ts +274 -0
- package/stock-machine.ts +357 -0
- package/stock-memory.ts +323 -0
- package/stock-paths.ts +191 -0
- package/stock-petscii.ts +143 -0
- package/stock-protocol.ts +2057 -0
- package/stock-registers.ts +324 -0
- package/stock-runstate.ts +104 -0
- package/tools-manifest.json +1 -9
- package/tools-manifest.stock.json +841 -0
- package/vice-broker-client.ts +233 -7
- package/vice-proxy.ts +202 -17
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
// disasm-renderer.ts
|
|
2
|
+
//
|
|
3
|
+
// The pure `render(instructions, opts) -> string` / `renderLine(instruction,
|
|
4
|
+
// opts) -> string` pair that turns decoded 6510 instructions (04-03's
|
|
5
|
+
// `Instruction[]`) into ACME-ready `!cpu 6510` source. D-05's standalone
|
|
6
|
+
// module: no protocol, no symbol-store, no emulator, no `node:` builtin.
|
|
7
|
+
// `stock-disassemble.ts` (04-05) wires the real symbol resolver in via
|
|
8
|
+
// `RenderOptions.symbolFor`; this file never imports `stock-address.ts`
|
|
9
|
+
// itself, so it stays importable by anything that only has an
|
|
10
|
+
// `Instruction[]` in hand (Phase 5's backtrace, Phase 6's CPU-history decode
|
|
11
|
+
// -- neither has a live symbol store).
|
|
12
|
+
//
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// WHY THIS FILE EXISTS
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// 04-06 feeds this module's `render()` output to a real `acme` process and
|
|
17
|
+
// asserts the reassembled bytes match the original stream exactly (the
|
|
18
|
+
// byte-exact round-trip criterion 4 requires). 04-05's `stock_disassemble`
|
|
19
|
+
// tool answer returns the same string in its `listing` field. Both consumers
|
|
20
|
+
// depend on the two invariants this file exists to enforce:
|
|
21
|
+
// - D-09: every opcode ACME's `!cpu 6510` cannot express goes out as
|
|
22
|
+
// `!byte` with all its bytes, never as a mnemonic ACME would reject.
|
|
23
|
+
// - D-11: the rendered operand's width always equals the decoded
|
|
24
|
+
// instruction's width, forced explicitly (`mnemonic+2`) wherever ACME
|
|
25
|
+
// would otherwise re-encode a small absolute address to zero page.
|
|
26
|
+
//
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// THE `+2` SPELLING IS AN ASSUMPTION, NOT A VERIFIED FACT (see the plan)
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// ACME's documented size-forcing postfix syntax is `mnemonic+1` / `+2` /
|
|
31
|
+
// `+3`. ACME is not installed in this execution environment, so nothing here
|
|
32
|
+
// proves that spelling against a real assembler. **04-06's real-ACME
|
|
33
|
+
// round-trip is the proof** -- if ACME rejects `+2`, 04-06 corrects this
|
|
34
|
+
// file (it lists `disasm-renderer.ts` in its own `files_modified` for
|
|
35
|
+
// exactly this reason). Do not invent a different mechanism (padding with
|
|
36
|
+
// extra `!byte`, etc.) to dodge this uncertainty; `+2` is the one spelling
|
|
37
|
+
// this module emits everywhere the width invariant requires forcing.
|
|
38
|
+
//
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// WHAT NOT TO DO
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// - Never import `stock-*.ts`/`vice*.ts`/any `node:` builtin -- in
|
|
43
|
+
// particular never `stock-address.ts`. The symbol lookup arrives only as
|
|
44
|
+
// `opts.symbolFor`, an injected function (D-05); 04-05 wires the real
|
|
45
|
+
// resolver in at the tool layer, not here.
|
|
46
|
+
// - Never emit a mnemonic for an instruction whose `acmeExpressible` is
|
|
47
|
+
// `false`. That is the exclusion-list approach D-09 rejected -- it ships
|
|
48
|
+
// output that provably does not reassemble. Render every one of its
|
|
49
|
+
// bytes as `!byte` instead, with the mnemonic moved into a comment.
|
|
50
|
+
// - Never substitute a symbol into an immediate operand (the `#<`/`#>`
|
|
51
|
+
// high/low-byte ambiguity, D-11) or into any zeropage-family operand (a
|
|
52
|
+
// symbol whose value resolves `>= $0100` would silently widen the
|
|
53
|
+
// instruction).
|
|
54
|
+
// - Never emit a substituted symbol name without also emitting its own
|
|
55
|
+
// `name = $XXXX` definition line in the listing header -- criterion 4's
|
|
56
|
+
// "reassembles with zero external declarations" depends on it.
|
|
57
|
+
|
|
58
|
+
import type { DisasmNote, Instruction } from "./disasm-decoder.ts";
|
|
59
|
+
import type { AddressingMode } from "./disasm-opcodes.ts";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Options controlling `render()`/`renderLine()`. `symbolFor` is DISASM-06's
|
|
63
|
+
* injected resolver -- 04-05 wires `stock-address.ts`'s real symbol store
|
|
64
|
+
* behind it; this module never imports that store itself (D-05).
|
|
65
|
+
*/
|
|
66
|
+
export interface RenderOptions {
|
|
67
|
+
showSymbols?: boolean;
|
|
68
|
+
symbolFor?: (address: number) => string | undefined;
|
|
69
|
+
origin?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Resolved, fully-defaulted options this module's internals actually work
|
|
73
|
+
* against -- computed once per `render()`/`renderLine()` call. */
|
|
74
|
+
interface ResolvedOptions {
|
|
75
|
+
showSymbols: boolean;
|
|
76
|
+
symbolFor?: (address: number) => string | undefined;
|
|
77
|
+
origin?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** True iff `value` is a well-formed, generic JSON object -- not null, not
|
|
81
|
+
* an array. Matches this module tree's own isPlainObject() convention
|
|
82
|
+
* (stock-schema-check.ts, disasm-decoder.ts et al. -- the one-line predicate
|
|
83
|
+
* is repeated per file, never centrally imported). */
|
|
84
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
85
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** True iff `value` is a representable, non-negative whole number -- the
|
|
89
|
+
* same narrowing `disasm-decoder.ts` uses for its own numeric options. */
|
|
90
|
+
function isNonNegativeSafeInteger(value: unknown): value is number {
|
|
91
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveOptions(opts: RenderOptions | undefined): ResolvedOptions {
|
|
95
|
+
const o = isPlainObject(opts) ? opts : {};
|
|
96
|
+
return {
|
|
97
|
+
showSymbols: o.showSymbols === true,
|
|
98
|
+
symbolFor: typeof o.symbolFor === "function" ? (o.symbolFor as (address: number) => string | undefined) : undefined,
|
|
99
|
+
origin: isNonNegativeSafeInteger(o.origin) ? o.origin : undefined,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Renders an 8-bit value as ACME hex syntax, e.g. `$0f`. */
|
|
104
|
+
function hex2(value: number): string {
|
|
105
|
+
return `$${(value & 0xff).toString(16).padStart(2, "0")}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Renders a 16-bit value as ACME hex syntax, e.g. `$d020`. */
|
|
109
|
+
function hex4(value: number): string {
|
|
110
|
+
return `$${(value & 0xffff).toString(16).padStart(4, "0")}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** D-10's fixed note-text vocabulary. Every note renders through this table
|
|
114
|
+
* -- never a note's own bare string literal -- so the wording stays in one
|
|
115
|
+
* place. */
|
|
116
|
+
const NOTE_TEXT: Readonly<Record<DisasmNote, string>> = {
|
|
117
|
+
"nmos-page-wrap": "NMOS page-wrap: the high byte is fetched from $xx00, not the next page",
|
|
118
|
+
truncated: "truncated -- partial instruction at the end of the requested range",
|
|
119
|
+
"acme-unassemblable": "not expressible in ACME !cpu 6510",
|
|
120
|
+
"illegal-opcode": "illegal opcode",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** Joins every note on an instruction into one `;`-comment body, in the
|
|
124
|
+
* decoder's own fixed note order, `" | "`-separated (D-10). Returns `""`
|
|
125
|
+
* when there are no notes -- callers must check for that before adding a
|
|
126
|
+
* leading `; `. */
|
|
127
|
+
function formatNotesComment(notes: DisasmNote[]): string {
|
|
128
|
+
if (notes.length === 0) return "";
|
|
129
|
+
return notes.map((note) => NOTE_TEXT[note]).join(" | ");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* DISASM-06's substitution gate. Returns a resolved symbol only when
|
|
134
|
+
* `opts.showSymbols` is `true`, `opts.symbolFor` is supplied, and it returns
|
|
135
|
+
* a name for `address`. Callers decide, by operand role, whether they are
|
|
136
|
+
* even allowed to call this at all -- immediate and zeropage-family operand
|
|
137
|
+
* renderers never call it (D-11's own table).
|
|
138
|
+
*/
|
|
139
|
+
function resolveSymbol(address: number, opts: ResolvedOptions): { name: string; address: number } | undefined {
|
|
140
|
+
if (!opts.showSymbols || !opts.symbolFor) return undefined;
|
|
141
|
+
const name = opts.symbolFor(address);
|
|
142
|
+
return name !== undefined ? { name, address } : undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The suffix ACME syntax needs for an absolute-family addressing mode.
|
|
146
|
+
* Typed against `disasm-opcodes.ts`'s own `AddressingMode` -- the opcode
|
|
147
|
+
* table's shared vocabulary, not a locally re-derived one. */
|
|
148
|
+
function absoluteSuffix(mode: AddressingMode): "" | ",x" | ",y" {
|
|
149
|
+
if (mode === "absolute_x") return ",x";
|
|
150
|
+
if (mode === "absolute_y") return ",y";
|
|
151
|
+
return "";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Renders the "mnemonic + operand" text for an expressible instruction --
|
|
156
|
+
* the part that, for a D-09 `!byte` substitution, moves into the trailing
|
|
157
|
+
* comment instead of being emitted as real ACME source. Never called for a
|
|
158
|
+
* truncated instruction (operand unknown by construction).
|
|
159
|
+
*/
|
|
160
|
+
function renderMnemonicOperand(instr: Instruction, opts: ResolvedOptions): { text: string; symbol?: { name: string; address: number } } {
|
|
161
|
+
const m = instr.mnemonic;
|
|
162
|
+
|
|
163
|
+
switch (instr.mode) {
|
|
164
|
+
case "implicit":
|
|
165
|
+
case "accumulator":
|
|
166
|
+
return { text: m };
|
|
167
|
+
|
|
168
|
+
case "immediate":
|
|
169
|
+
// D-11: never substituted -- the `#<`/`#>` high/low-byte ambiguity.
|
|
170
|
+
return { text: `${m} #${hex2(instr.operand!.value)}` };
|
|
171
|
+
|
|
172
|
+
case "zeropage":
|
|
173
|
+
// D-11: never substituted -- a symbol >= $0100 would widen this.
|
|
174
|
+
return { text: `${m} ${hex2(instr.operand!.value)}` };
|
|
175
|
+
|
|
176
|
+
case "zeropage_x":
|
|
177
|
+
return { text: `${m} ${hex2(instr.operand!.value)},x` };
|
|
178
|
+
|
|
179
|
+
case "zeropage_y":
|
|
180
|
+
return { text: `${m} ${hex2(instr.operand!.value)},y` };
|
|
181
|
+
|
|
182
|
+
case "indirect_x":
|
|
183
|
+
return { text: `${m} (${hex2(instr.operand!.value)},x)` };
|
|
184
|
+
|
|
185
|
+
case "indirect_y":
|
|
186
|
+
return { text: `${m} (${hex2(instr.operand!.value)}),y` };
|
|
187
|
+
|
|
188
|
+
case "indirect": {
|
|
189
|
+
// `jmp ($xxxx)` has exactly one encoding -- substitution is safe, no
|
|
190
|
+
// width force is ever needed.
|
|
191
|
+
const value = instr.operand!.value;
|
|
192
|
+
const symbol = resolveSymbol(value, opts);
|
|
193
|
+
const addrText = symbol?.name ?? hex4(value);
|
|
194
|
+
return { text: `${m} (${addrText})`, ...(symbol !== undefined ? { symbol } : {}) };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
case "relative": {
|
|
198
|
+
// A branch is always 2 bytes; ACME computes the offset from the
|
|
199
|
+
// label, so substitution can never change the encoding.
|
|
200
|
+
const target = instr.resolvedTarget!;
|
|
201
|
+
const symbol = resolveSymbol(target, opts);
|
|
202
|
+
const addrText = symbol?.name ?? hex4(target);
|
|
203
|
+
return { text: `${m} ${addrText}`, ...(symbol !== undefined ? { symbol } : {}) };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
case "absolute":
|
|
207
|
+
case "absolute_x":
|
|
208
|
+
case "absolute_y": {
|
|
209
|
+
// D-11's width invariant: a value below $0100 renders with ACME's
|
|
210
|
+
// `+2` size-forcing postfix, whether or not a symbol is substituted --
|
|
211
|
+
// otherwise ACME would re-encode it to zero page and shrink the
|
|
212
|
+
// instruction.
|
|
213
|
+
const value = instr.operand!.value;
|
|
214
|
+
const symbol = resolveSymbol(value, opts);
|
|
215
|
+
const addrText = symbol?.name ?? hex4(value);
|
|
216
|
+
const forceSize = value < 0x100;
|
|
217
|
+
const mnemonic = forceSize ? `${m}+2` : m;
|
|
218
|
+
const suffix = absoluteSuffix(instr.mode);
|
|
219
|
+
return { text: `${mnemonic} ${addrText}${suffix}`, ...(symbol !== undefined ? { symbol } : {}) };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** ACME source indent for instruction/directive lines -- purely cosmetic,
|
|
225
|
+
* comments cannot affect assembly. */
|
|
226
|
+
const INDENT = " ";
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Renders one `Instruction` to its listing line(s), applying D-09's `!byte`
|
|
230
|
+
* substitution for anything ACME cannot express (or that decoded as
|
|
231
|
+
* truncated) and D-10's note-comment vocabulary. Returns the substituted
|
|
232
|
+
* symbol, if any, so `render()` can collect it into the header's symbol
|
|
233
|
+
* definitions.
|
|
234
|
+
*/
|
|
235
|
+
function renderInstructionLine(instr: Instruction, opts: ResolvedOptions): { text: string; symbol?: { name: string; address: number } } {
|
|
236
|
+
const notesText = formatNotesComment(instr.notes);
|
|
237
|
+
|
|
238
|
+
if (instr.notes.includes("truncated")) {
|
|
239
|
+
// DISASM-05: the operand is unknown -- never render a mnemonic. Emit
|
|
240
|
+
// every byte that exists so the following instruction (if any) is never
|
|
241
|
+
// reached; there is none, since a truncated instruction is always last.
|
|
242
|
+
const bytesHex = instr.bytes.map(hex2).join(", ");
|
|
243
|
+
return { text: `${INDENT}!byte ${bytesHex} ; ${notesText}` };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!instr.acmeExpressible) {
|
|
247
|
+
// D-09: every byte goes out as `!byte`, keeping the following
|
|
248
|
+
// instruction at the correct address. The mnemonic and operand a human
|
|
249
|
+
// reader needs move into the trailing comment instead.
|
|
250
|
+
const { text: mnemonicOperand, symbol } = renderMnemonicOperand(instr, opts);
|
|
251
|
+
const bytesHex = instr.bytes.map(hex2).join(", ");
|
|
252
|
+
const comment = notesText ? `${mnemonicOperand} [${notesText}]` : mnemonicOperand;
|
|
253
|
+
return { text: `${INDENT}!byte ${bytesHex} ; ${comment}`, ...(symbol !== undefined ? { symbol } : {}) };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const { text: mnemonicOperand, symbol } = renderMnemonicOperand(instr, opts);
|
|
257
|
+
const text = notesText ? `${INDENT}${mnemonicOperand} ; ${notesText}` : `${INDENT}${mnemonicOperand}`;
|
|
258
|
+
return { text, ...(symbol !== undefined ? { symbol } : {}) };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Renders a single instruction to one listing line -- no `!cpu 6510`
|
|
263
|
+
* header, no origin, no symbol definitions. `render()` is what produces a
|
|
264
|
+
* self-contained listing; this is the per-instruction primitive it and
|
|
265
|
+
* standalone callers (e.g. Phase 5's backtrace, when it wants one rendered
|
|
266
|
+
* line without a whole listing) share.
|
|
267
|
+
*/
|
|
268
|
+
export function renderLine(instruction: Instruction, opts?: RenderOptions): string {
|
|
269
|
+
const resolved = resolveOptions(opts);
|
|
270
|
+
return renderInstructionLine(instruction, resolved).text;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Renders a full, self-contained ACME `!cpu 6510` listing: the header
|
|
275
|
+
* (`!cpu 6510`, one `name = $XXXX` definition per substituted symbol sorted
|
|
276
|
+
* by address, then `* = $XXXX`), followed by one line per instruction. This
|
|
277
|
+
* is what 04-06 feeds to a real `acme` process and what 04-05's tool answer
|
|
278
|
+
* returns as `listing`.
|
|
279
|
+
*/
|
|
280
|
+
export function render(instructions: Instruction[], opts?: RenderOptions): string {
|
|
281
|
+
const resolved = resolveOptions(opts);
|
|
282
|
+
const list = Array.isArray(instructions) ? instructions : [];
|
|
283
|
+
|
|
284
|
+
const symbols = new Map<string, number>();
|
|
285
|
+
const instructionLines: string[] = [];
|
|
286
|
+
|
|
287
|
+
for (const instr of list) {
|
|
288
|
+
const { text, symbol } = renderInstructionLine(instr, resolved);
|
|
289
|
+
instructionLines.push(text);
|
|
290
|
+
if (symbol !== undefined) symbols.set(symbol.name, symbol.address);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const lines: string[] = ["!cpu 6510"];
|
|
294
|
+
|
|
295
|
+
const sortedSymbols = [...symbols.entries()].sort((a, b) => a[1] - b[1]);
|
|
296
|
+
for (const [name, address] of sortedSymbols) {
|
|
297
|
+
lines.push(`${name} = ${hex4(address)}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const origin = resolved.origin ?? list[0]?.address ?? 0;
|
|
301
|
+
lines.push(`* = ${hex4(origin)}`);
|
|
302
|
+
|
|
303
|
+
lines.push(...instructionLines);
|
|
304
|
+
|
|
305
|
+
return lines.join("\n");
|
|
306
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henols/vice-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "VICE emulator MCP server for C64 reverse-engineering: a stdio MCP server that proxies vice tools to a host VICE MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
"vice-sync.ts",
|
|
14
14
|
"vice-probe.ts",
|
|
15
15
|
"vice-broker-client.ts",
|
|
16
|
+
"stock-protocol.ts",
|
|
17
|
+
"stock-connect.ts",
|
|
16
18
|
"containerpath.ts",
|
|
17
19
|
"hostpath.ts",
|
|
18
20
|
"repo-root.ts",
|
|
@@ -21,9 +23,30 @@
|
|
|
21
23
|
"refresh-manifest.ts",
|
|
22
24
|
"build.ts",
|
|
23
25
|
"container-guard.mts",
|
|
26
|
+
"backend-detect.mts",
|
|
27
|
+
"stock-dispatch.ts",
|
|
28
|
+
"stock-derived.ts",
|
|
29
|
+
"stock-handler.ts",
|
|
30
|
+
"stock-runstate.ts",
|
|
31
|
+
"stock-address.ts",
|
|
32
|
+
"stock-paths.ts",
|
|
33
|
+
"stock-machine.ts",
|
|
34
|
+
"stock-petscii.ts",
|
|
35
|
+
"stock-input.ts",
|
|
36
|
+
"stock-memory.ts",
|
|
37
|
+
"stock-registers.ts",
|
|
38
|
+
"stock-checkpoints.ts",
|
|
39
|
+
"stock-execution.ts",
|
|
40
|
+
"stock-condition.ts",
|
|
41
|
+
"stock-disassemble.ts",
|
|
42
|
+
"disasm-opcodes.ts",
|
|
43
|
+
"disasm-decoder.ts",
|
|
44
|
+
"disasm-renderer.ts",
|
|
24
45
|
"resources",
|
|
25
46
|
"tools-manifest.json",
|
|
26
|
-
"
|
|
47
|
+
"tools-manifest.stock.json",
|
|
48
|
+
"README.md",
|
|
49
|
+
"THIRD-PARTY-NOTICES.md"
|
|
27
50
|
],
|
|
28
51
|
"engines": {
|
|
29
52
|
"node": ">=22.18.0"
|
|
@@ -56,6 +79,8 @@
|
|
|
56
79
|
],
|
|
57
80
|
"scripts": {
|
|
58
81
|
"test": "node --test '*.test.*'",
|
|
82
|
+
"test:automated": "node test-gate.mjs",
|
|
83
|
+
"test:manual": "node test-gate.mjs --manual",
|
|
59
84
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
60
85
|
"build": "node build.ts",
|
|
61
86
|
"smoke": "node smoke.mjs"
|
package/refresh-manifest.ts
CHANGED
|
@@ -11,11 +11,17 @@
|
|
|
11
11
|
import { serverInfo, activeInstance, type ServerInfoPayload, type ToolInfo } from "./vice.ts";
|
|
12
12
|
import { writeFileSync, chmodSync, renameSync } from "node:fs";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
|
-
import { dirname, join, resolve } from "node:path";
|
|
14
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
15
15
|
|
|
16
16
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
const DEFAULT_MANIFEST_PATH = join(HERE, "tools-manifest.json");
|
|
18
18
|
|
|
19
|
+
// The hand-authored stock surface's own filename (stock-dispatch.ts's
|
|
20
|
+
// tools-manifest.stock.json, plan 02-09) -- named here ONLY so
|
|
21
|
+
// writeManifestAtomic() below can refuse to ever target it, never as
|
|
22
|
+
// something this file writes.
|
|
23
|
+
const STOCK_MANIFEST_BASENAME = "tools-manifest.stock.json";
|
|
24
|
+
|
|
19
25
|
function manifestPath(): string {
|
|
20
26
|
return process.env.VICE_TOOLS_MANIFEST
|
|
21
27
|
? resolve(process.env.VICE_TOOLS_MANIFEST)
|
|
@@ -39,8 +45,23 @@ export interface ToolsManifest {
|
|
|
39
45
|
* a crash, not only against a rejected handshake -- the rejected-handshake
|
|
40
46
|
* half of that guarantee already held (the early return below, unchanged
|
|
41
47
|
* from the original), but a crash between "open the file" and "write the
|
|
42
|
-
* content" could previously still truncate a good manifest to nothing.
|
|
48
|
+
* content" could previously still truncate a good manifest to nothing.
|
|
49
|
+
*
|
|
50
|
+
* T-02-31 (02-09): this function regenerates the manifest from a LIVE fork
|
|
51
|
+
* host's tools/list, so its output path must NEVER be
|
|
52
|
+
* tools-manifest.stock.json -- that file is the hand-authored, separately
|
|
53
|
+
* committed stock surface (D-07/D-09), and this refresh path overwriting it
|
|
54
|
+
* with the fork's full tool list would silently destroy the trimming that
|
|
55
|
+
* surface exists to enforce. This assertion makes that impossible by
|
|
56
|
+
* accident; a future `--stock` flag pointing this generator at the stock
|
|
57
|
+
* file has to edit this line deliberately, not merely change a default. */
|
|
43
58
|
function writeManifestAtomic(path: string, manifest: ToolsManifest): void {
|
|
59
|
+
if (basename(path) === STOCK_MANIFEST_BASENAME) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`refresh-manifest: refusing to write ${STOCK_MANIFEST_BASENAME} -- this generator regenerates the fork's ` +
|
|
62
|
+
`manifest from a live host and must never overwrite the hand-authored stock surface (D-07/D-09)`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
44
65
|
const tmpPath = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
45
66
|
writeFileSync(tmpPath, "");
|
|
46
67
|
chmodSync(tmpPath, 0o600);
|