@henols/c64-re-tools 1.0.0 → 2.0.0-rc.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/LICENSE +21 -0
- package/README.md +190 -37
- package/distribution/package.ts +36 -0
- package/distribution/plugin.ts +120 -0
- package/package.json +34 -40
- package/skills/c64-assembler/SKILL.md +73 -0
- package/skills/c64-assembler/scripts/assemble.ts +103 -0
- package/skills/c64-basic/SKILL.md +67 -0
- package/skills/c64-basic/scripts/basic.ts +46 -0
- package/skills/c64-disk/SKILL.md +79 -0
- package/skills/c64-disk/scripts/disk.ts +65 -0
- package/skills/c64-emulator/SKILL.md +124 -0
- package/skills/c64-knowledge/SKILL.md +102 -0
- package/skills/c64-knowledge/scripts/knowledge.ts +257 -0
- package/skills/c64-memory-map/SKILL.md +76 -0
- package/skills/c64-memory-map/scripts/memmap.ts +81 -0
- package/skills/c64-memory-map/scripts/platform.ts +424 -0
- package/skills/c64-provenance/SKILL.md +76 -0
- package/skills/c64-provenance/scripts/compare.ts +92 -0
- package/skills/c64-provenance/scripts/provenance.ts +83 -0
- package/skills/c64-reverse-engineering/SKILL.md +90 -0
- package/skills/c64-static-analysis/SKILL.md +96 -0
- package/skills/c64-static-analysis/scripts/analyze.ts +175 -0
- package/skills/c64-static-analysis/scripts/findings.ts +73 -0
- package/skills/c64-testing/SKILL.md +79 -0
- package/skills/c64-testing/references/scenario-format.md +76 -0
- package/skills/c64-testing/scripts/checklist.ts +51 -0
- package/skills/c64-testing/scripts/scenario.ts +473 -0
- package/skills/c64-testing/scripts/test.ts +55 -0
- package/skills/c64-unpacker/SKILL.md +82 -0
- package/skills/c64-unpacker/scripts/evidence.ts +135 -0
- package/skills/c64-unpacker/scripts/unpack.ts +216 -0
- package/src/c64.ts +281 -0
- package/src/cli/ap-sdk.ts +44 -0
- package/src/cli/cleanup.ts +194 -0
- package/src/cli/main.ts +122 -0
- package/src/cli/status.ts +34 -0
- package/src/host/main.ts +121 -0
- package/src/host/server.ts +409 -0
- package/src/host/tools/c1541.ts +255 -0
- package/src/host/tools/index.ts +31 -0
- package/src/host/tools/petcat.ts +70 -0
- package/src/host/tools/status.ts +30 -0
- package/src/host/tools/version.ts +41 -0
- package/src/host/vice/adapter.ts +761 -0
- package/src/host/vice/binary-monitor.ts +389 -0
- package/src/host/vice/chips.ts +108 -0
- package/src/host/vice/connect.ts +34 -0
- package/src/host/vice/process.ts +402 -0
- package/src/host/vice/screen.ts +183 -0
- package/src/host/vice/session.ts +1316 -0
- package/src/host/vice/text-monitor.ts +223 -0
- package/src/host-client/connect.ts +241 -0
- package/src/host-client/tools.ts +89 -0
- package/src/host-client/transfer.ts +100 -0
- package/src/host-client/vice-session.ts +254 -0
- package/src/knowledge/database.ts +132 -0
- package/src/knowledge/history.ts +128 -0
- package/src/knowledge/import.ts +344 -0
- package/src/knowledge/read.ts +191 -0
- package/src/knowledge/schema.ts +140 -0
- package/src/knowledge/write.ts +348 -0
- package/src/mcp/main.ts +109 -0
- package/src/mcp/server.ts +307 -0
- package/src/mcp/tools/debug.ts +237 -0
- package/src/mcp/tools/execution.ts +116 -0
- package/src/mcp/tools/input.ts +69 -0
- package/src/mcp/tools/machine.ts +97 -0
- package/src/mcp/tools/media.ts +106 -0
- package/src/mcp/tools/memory.ts +203 -0
- package/src/mcp/tools/video.ts +260 -0
- package/src/native/acme.ts +116 -0
- package/src/native/discover.ts +141 -0
- package/src/native/dxa.ts +166 -0
- package/src/native/ghidra/analyze.ts +86 -0
- package/src/native/ghidra/index.ts +157 -0
- package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
- package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
- package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
- package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
- package/src/native/ghidra/scripts/C64Export.java +242 -0
- package/src/native/ghidra/scripts/C64Prepare.java +60 -0
- package/src/native/local.ts +34 -0
- package/src/native/processes.ts +350 -0
- package/src/native/run.ts +172 -0
- package/src/native/staging.ts +83 -0
- package/src/native/status.ts +101 -0
- package/src/native/types.ts +228 -0
- package/src/native/watchdog.ts +78 -0
- package/src/project.ts +39 -0
- package/src/protocol/framing.ts +132 -0
- package/src/protocol/messages.ts +252 -0
- package/src/protocol/tools.ts +247 -0
- package/src/protocol/vice.ts +1235 -0
- package/src/script.ts +59 -0
- package/src/time.ts +15 -0
- package/src/trace.ts +162 -0
- package/THIRD-PARTY-NOTICES.md +0 -26
- package/bin/cli.mjs +0 -283
- package/skills/acme-build/SKILL.md +0 -260
- package/skills/acme-build/scripts/acme.mjs +0 -341
- package/skills/acme-build/template.a +0 -53
- package/skills/c64-disk-access/SKILL.md +0 -159
- package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
- package/skills/c64-memory-mapping/SKILL.md +0 -631
- package/skills/c64-memory-mapping/memmap.json +0 -8800
- package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
- package/skills/c64-petcat/SKILL.md +0 -87
- package/skills/c64-petcat/scripts/petcat.mjs +0 -221
- package/skills/c64-program-recon/SKILL.md +0 -706
- package/skills/c64-program-recon/references/control-flow.md +0 -180
- package/skills/c64-program-recon/references/graphics.md +0 -79
- package/skills/c64-program-recon/references/observation-hazards.md +0 -143
- package/skills/c64-program-recon/references/reconstruction.md +0 -133
- package/skills/c64-program-recon/references/sound-and-input.md +0 -74
- package/skills/c64-program-recon/references/tool-selection.md +0 -78
- package/skills/c64-program-recon/scripts/derive.mjs +0 -364
- package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
- package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
- package/skills/c64-provenance-diff/SKILL.md +0 -292
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
- package/skills/c64-ram-capture/RELEASES.json.example +0 -17
- package/skills/c64-ram-capture/SKILL.md +0 -474
- package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
- package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
- package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
- package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
- package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
- package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
- package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
- package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
- package/skills/c64-ram-capture/transients/README.md +0 -137
- package/skills/routine-queue-walker/SKILL.md +0 -394
- package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
- package/skills/vice-wedge-triage/SKILL.md +0 -269
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// The artifact renderer (01-04 Task 1, Part B). This module exists because
|
|
3
|
-
// of a structural fact worth stating up front: the executing agent can
|
|
4
|
-
// write text, not binary, so the only shape a committable 65536-byte image
|
|
5
|
-
// can take under the one permitted route to the emulator is *the agent
|
|
6
|
-
// serialises what it fetched via mcp__plugin_c64-re-tools_vice__* tool calls, and a pure
|
|
7
|
-
// function renders it*. Every function below takes already-fetched data as
|
|
8
|
-
// an argument -- nothing here contacts the emulator.
|
|
9
|
-
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
10
|
-
import { createHash } from "node:crypto";
|
|
11
|
-
import { fileURLToPath } from "node:url";
|
|
12
|
-
import { dirname, join, resolve, relative } from "node:path";
|
|
13
|
-
|
|
14
|
-
import { releaseDir } from "./releases.mjs";
|
|
15
|
-
import { projectRoot } from "./project-paths.mjs";
|
|
16
|
-
import { addrNum, hex4 } from "./watch-loads.mjs";
|
|
17
|
-
|
|
18
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
19
|
-
const REPO_ROOT = projectRoot();
|
|
20
|
-
|
|
21
|
-
const die = (m) => { console.error(`error: ${m}`); process.exit(1); };
|
|
22
|
-
|
|
23
|
-
function rel(p) {
|
|
24
|
-
return relative(REPO_ROOT, p);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// ---------------------------------------------------------------- assembleImage
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Assemble the ordered `{ address, hex }` chunk records the agent wrote
|
|
31
|
-
* after its `mcp__plugin_c64-re-tools_vice__vice_memory_read` calls into a 65536-byte buffer.
|
|
32
|
-
* Asserts contiguity from $0000 with no gap and no overlap and a total of
|
|
33
|
-
* exactly 65536 bytes, naming the offending address in every failure.
|
|
34
|
-
*/
|
|
35
|
-
export function assembleImage(chunks) {
|
|
36
|
-
const sorted = [...chunks].sort((a, b) => addrNum(a.address) - addrNum(b.address));
|
|
37
|
-
const bufs = [];
|
|
38
|
-
let expected = 0;
|
|
39
|
-
for (const c of sorted) {
|
|
40
|
-
const addr = addrNum(c.address);
|
|
41
|
-
const buf = Buffer.from(c.hex, "hex");
|
|
42
|
-
if (addr > expected) {
|
|
43
|
-
throw new Error(`assembleImage: gap before address ${hex4(expected)} -- next chunk starts at ${hex4(addr)}`);
|
|
44
|
-
}
|
|
45
|
-
if (addr < expected) {
|
|
46
|
-
throw new Error(`assembleImage: overlap at address ${hex4(addr)} -- a previous chunk already covered up to ${hex4(expected - 1)}`);
|
|
47
|
-
}
|
|
48
|
-
bufs.push(buf);
|
|
49
|
-
expected += buf.length;
|
|
50
|
-
}
|
|
51
|
-
const total = Buffer.concat(bufs);
|
|
52
|
-
if (total.length !== 65536) {
|
|
53
|
-
throw new Error(`assembleImage: assembled ${total.length} bytes ending at ${hex4(expected)}, expected exactly 65536`);
|
|
54
|
-
}
|
|
55
|
-
return total;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** SHA-256 of a buffer, hex-encoded. `node:crypto` only -- no package added. */
|
|
59
|
-
export function sha256Buffer(buf) {
|
|
60
|
-
return createHash("sha256").update(buf).digest("hex");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ----------------------------------------------------------------- vicBank
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* VIC-II bank number (0-3) from CIA2 port A ($DD00)'s low two bits. The
|
|
67
|
-
* stored value is the INVERSE of the bank number (c64-memory-mapping skill
|
|
68
|
-
* memmap: raw value %00 = "Bank #3" $C000-$FFFF ... %11 = "Bank #0"
|
|
69
|
-
* $0000-$3FFF), so bank = 3 - (raw & 3). Verified against
|
|
70
|
-
* a committed chip-state sidecar's own recorded
|
|
71
|
-
* dd00_raw=193 (0xC1, low bits %01) -> vic_bank=2, which this formula
|
|
72
|
-
* reproduces exactly.
|
|
73
|
-
*/
|
|
74
|
-
export function vicBank(dd00Raw) {
|
|
75
|
-
return 3 - (dd00Raw & 3);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// --------------------------------------------------------------- screenBase
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Screen memory base address, derived from $D018's bits 4-7 (screen pointer,
|
|
82
|
-
* in 1024-byte units relative to the VIC bank) added to the VIC bank's own
|
|
83
|
-
* base (bank * 16384). Verified against the same committed sidecar:
|
|
84
|
-
* dd00_raw=193, d018_raw=49 (0x31) -> screen_base=35840, reproduced exactly.
|
|
85
|
-
*/
|
|
86
|
-
export function screenBase(d018Raw, dd00Raw) {
|
|
87
|
-
const bank = vicBank(dd00Raw);
|
|
88
|
-
const bankBase = bank * 16384;
|
|
89
|
-
const screenOffset = ((d018Raw >> 4) & 0xf) * 1024;
|
|
90
|
-
return bankBase + screenOffset;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Character memory base: $D018 bits 1-3, in 2048-byte units relative to the VIC bank. */
|
|
94
|
-
function charsetBase(d018Raw, dd00Raw) {
|
|
95
|
-
const bank = vicBank(dd00Raw);
|
|
96
|
-
const bankBase = bank * 16384;
|
|
97
|
-
const charsetOffset = ((d018Raw >> 1) & 0x7) * 2048;
|
|
98
|
-
return bankBase + charsetOffset;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// -------------------------------------------------------------- buildChipState
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Build the chip-state sidecar in the exact shape the committed
|
|
105
|
-
* primary sidecars already use (same top-level keys, same `derived` field
|
|
106
|
-
* set), from the register/state readings the agent recorded. `raw` carries
|
|
107
|
-
* whatever the agent fetched via vice_registers_get / vice_sprite_get /
|
|
108
|
-
* vice_memory_read, keyed to match: `registers`, `sprites`, `cpu` pass
|
|
109
|
-
* through verbatim; `dd00_raw`, `d018_raw`, `port01_raw` and
|
|
110
|
-
* `sprite_pointers` (the bytes read from the sprite-pointer table at
|
|
111
|
-
* screen_base+$3F8..$3FF) feed the derivation.
|
|
112
|
-
*/
|
|
113
|
-
export function buildChipState(raw) {
|
|
114
|
-
const dd00 = raw.dd00_raw;
|
|
115
|
-
const d018 = raw.d018_raw;
|
|
116
|
-
const bank = vicBank(dd00);
|
|
117
|
-
const bankBase = bank * 16384;
|
|
118
|
-
const screenBaseAddr = screenBase(d018, dd00);
|
|
119
|
-
const charsetBaseAddr = charsetBase(d018, dd00);
|
|
120
|
-
const port01raw = raw.port01_raw;
|
|
121
|
-
const spritePointers = raw.sprite_pointers ?? [];
|
|
122
|
-
const spriteDataAddresses = spritePointers.map((p) => bankBase + p * 64);
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
schema_version: 1,
|
|
126
|
-
release: raw.release,
|
|
127
|
-
label: raw.label,
|
|
128
|
-
snapshot_name: raw.snapshot_name ?? null,
|
|
129
|
-
registers: raw.registers,
|
|
130
|
-
sprites: raw.sprites,
|
|
131
|
-
cpu: raw.cpu,
|
|
132
|
-
derived: {
|
|
133
|
-
port01: {
|
|
134
|
-
raw: port01raw,
|
|
135
|
-
loram: !!(port01raw & 1),
|
|
136
|
-
hiram: !!(port01raw & 2),
|
|
137
|
-
charen: !!(port01raw & 4),
|
|
138
|
-
},
|
|
139
|
-
dd00_raw: dd00,
|
|
140
|
-
dd00_direct_read: raw.dd00_direct_read ?? dd00,
|
|
141
|
-
vic_bank: bank,
|
|
142
|
-
d018_raw: d018,
|
|
143
|
-
screen_base: screenBaseAddr,
|
|
144
|
-
charset_base: charsetBaseAddr,
|
|
145
|
-
sprite_pointers: spritePointers,
|
|
146
|
-
sprite_data_addresses: spriteDataAddresses,
|
|
147
|
-
},
|
|
148
|
-
captured_at: raw.captured_at ?? new Date().toISOString(),
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ---------------------------------------------------------- buildRangeManifest
|
|
153
|
-
|
|
154
|
-
const IO_START = 0xd000;
|
|
155
|
-
const IO_END = 0xdfff;
|
|
156
|
-
|
|
157
|
-
function powerOnRunLength(image, start) {
|
|
158
|
-
const b = image[start];
|
|
159
|
-
if (b !== 0x00 && b !== 0xff) return 0;
|
|
160
|
-
let end = start;
|
|
161
|
-
while (end < image.length && image[end] === b) end++;
|
|
162
|
-
return end - start;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Emit the range manifest in the committed shape: ranges whose union
|
|
167
|
-
* covers $0000-$FFFF with no gap and no overlap, a contiguous power-on-
|
|
168
|
-
* pattern run of at least 16 bytes marked kind `unused`, the I/O window
|
|
169
|
-
* ($D000-$DFFF) marked `io`, everything else `unclassified`, and
|
|
170
|
-
* `classification_state` set to the same transient `ranges-only` state the
|
|
171
|
-
* committed primary manifests carry.
|
|
172
|
-
*/
|
|
173
|
-
export function buildRangeManifest(image, meta = {}) {
|
|
174
|
-
if (image.length !== 65536) {
|
|
175
|
-
throw new Error(`buildRangeManifest: image must be exactly 65536 bytes, got ${image.length}`);
|
|
176
|
-
}
|
|
177
|
-
const ranges = [];
|
|
178
|
-
let i = 0;
|
|
179
|
-
while (i < 65536) {
|
|
180
|
-
if (i >= IO_START && i <= IO_END) {
|
|
181
|
-
ranges.push({ start: i, end: IO_END, kind: "io", source: "capture", note: "VIC-II/SID/CIA/color-RAM I/O window" });
|
|
182
|
-
i = IO_END + 1;
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
const runLen = powerOnRunLength(image, i);
|
|
186
|
-
if (runLen >= 16) {
|
|
187
|
-
const end = i + runLen - 1;
|
|
188
|
-
ranges.push({ start: i, end, kind: "unused", source: "capture", note: "contiguous $00/$FF power-on-pattern run of at least 16 bytes" });
|
|
189
|
-
i = end + 1;
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
let j = i;
|
|
193
|
-
while (j < 65536 && !(j >= IO_START && j <= IO_END) && powerOnRunLength(image, j) < 16) {
|
|
194
|
-
j++;
|
|
195
|
-
}
|
|
196
|
-
ranges.push({ start: i, end: j - 1, kind: "unclassified", source: "capture", note: "awaiting the loader/cracktro/game three-bucket partition" });
|
|
197
|
-
i = j;
|
|
198
|
-
}
|
|
199
|
-
return {
|
|
200
|
-
schema_version: 1,
|
|
201
|
-
release: meta.release,
|
|
202
|
-
label: meta.label,
|
|
203
|
-
snapshot_name: meta.snapshot_name ?? null,
|
|
204
|
-
image_bytes: image.length,
|
|
205
|
-
offset_equals_address: true,
|
|
206
|
-
classification_state: "ranges-only",
|
|
207
|
-
ranges,
|
|
208
|
-
note: meta.note ?? "",
|
|
209
|
-
generated_at: meta.generated_at ?? new Date().toISOString(),
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// -------------------------------------------------------------- writeDumpSet
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Render and write the four-file dump set from a committed chunk file's
|
|
217
|
-
* contents plus the chip-state raw readings. Returns the written paths and
|
|
218
|
-
* the image digest, so a caller can register a `dumps[]` entry.
|
|
219
|
-
*/
|
|
220
|
-
export function writeDumpSet({ releaseId, label, chunks, chipStateRaw, meta = {}, captureExtra = {} }) {
|
|
221
|
-
const image = assembleImage(chunks);
|
|
222
|
-
const digest = sha256Buffer(image);
|
|
223
|
-
const dumpsDir = join(releaseDir(releaseId), "dumps");
|
|
224
|
-
mkdirSync(dumpsDir, { recursive: true });
|
|
225
|
-
|
|
226
|
-
const binPath = join(dumpsDir, `${releaseId}-${label}.bin`);
|
|
227
|
-
writeFileSync(binPath, image);
|
|
228
|
-
|
|
229
|
-
const stateOut = buildChipState({ ...chipStateRaw, release: releaseId, label });
|
|
230
|
-
const statePath = join(dumpsDir, `${releaseId}-${label}.state.json`);
|
|
231
|
-
writeFileSync(statePath, JSON.stringify(stateOut, null, 2) + "\n");
|
|
232
|
-
|
|
233
|
-
const manifestOut = buildRangeManifest(image, { release: releaseId, label, ...meta });
|
|
234
|
-
const mapPath = join(dumpsDir, `${releaseId}-${label}.map.json`);
|
|
235
|
-
writeFileSync(mapPath, JSON.stringify(manifestOut, null, 2) + "\n");
|
|
236
|
-
|
|
237
|
-
const captureOut = {
|
|
238
|
-
release: releaseId,
|
|
239
|
-
label,
|
|
240
|
-
sha256: digest,
|
|
241
|
-
bytes: image.length,
|
|
242
|
-
...captureExtra,
|
|
243
|
-
};
|
|
244
|
-
const capturePath = join(dumpsDir, `${releaseId}-${label}.capture.json`);
|
|
245
|
-
writeFileSync(capturePath, JSON.stringify(captureOut, null, 2) + "\n");
|
|
246
|
-
|
|
247
|
-
return {
|
|
248
|
-
bin: rel(binPath),
|
|
249
|
-
state: rel(statePath),
|
|
250
|
-
map: rel(mapPath),
|
|
251
|
-
capture: rel(capturePath),
|
|
252
|
-
sha256: digest,
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// -------------------------------------------------------------------- CLI
|
|
257
|
-
|
|
258
|
-
function optValue(rest, name) {
|
|
259
|
-
const i = rest.indexOf(`--${name}`);
|
|
260
|
-
return i === -1 ? undefined : rest[i + 1];
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function readJsonArg(rest, name) {
|
|
264
|
-
const p = optValue(rest, name);
|
|
265
|
-
if (!p) return undefined;
|
|
266
|
-
return JSON.parse(readFileSync(resolve(p), "utf8"));
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const VERBS = {
|
|
270
|
-
assemble(rest) {
|
|
271
|
-
const chunks = readJsonArg(rest, "chunks");
|
|
272
|
-
if (!chunks) die("usage: assemble --chunks <chunks.json> [--json]");
|
|
273
|
-
const image = assembleImage(chunks);
|
|
274
|
-
const digest = sha256Buffer(image);
|
|
275
|
-
const result = { bytes: image.length, sha256: digest };
|
|
276
|
-
console.log(rest.includes("--json") ? JSON.stringify(result, null, 2) : `${result.bytes} bytes, sha256 ${result.sha256}`);
|
|
277
|
-
},
|
|
278
|
-
|
|
279
|
-
"chip-state"(rest) {
|
|
280
|
-
const raw = readJsonArg(rest, "raw");
|
|
281
|
-
if (!raw) die("usage: chip-state --raw <raw.json> [--json]");
|
|
282
|
-
const result = buildChipState(raw);
|
|
283
|
-
console.log(JSON.stringify(result, null, 2));
|
|
284
|
-
},
|
|
285
|
-
|
|
286
|
-
manifest(rest) {
|
|
287
|
-
const chunks = readJsonArg(rest, "chunks");
|
|
288
|
-
const metaArg = readJsonArg(rest, "meta") ?? {};
|
|
289
|
-
if (!chunks) die("usage: manifest --chunks <chunks.json> [--meta <meta.json>] [--json]");
|
|
290
|
-
const image = assembleImage(chunks);
|
|
291
|
-
const result = buildRangeManifest(image, metaArg);
|
|
292
|
-
console.log(JSON.stringify(result, null, 2));
|
|
293
|
-
},
|
|
294
|
-
|
|
295
|
-
"write-set"(rest) {
|
|
296
|
-
const releaseId = optValue(rest, "release");
|
|
297
|
-
const label = optValue(rest, "label");
|
|
298
|
-
const chunks = readJsonArg(rest, "chunks");
|
|
299
|
-
const chipStateRaw = readJsonArg(rest, "raw");
|
|
300
|
-
const metaArg = readJsonArg(rest, "meta") ?? {};
|
|
301
|
-
if (!releaseId || !label || !chunks || !chipStateRaw) {
|
|
302
|
-
die("usage: write-set --release <id> --label <label> --chunks <chunks.json> --raw <raw.json> [--meta <meta.json>] [--json]");
|
|
303
|
-
}
|
|
304
|
-
const result = writeDumpSet({ releaseId, label, chunks, chipStateRaw, meta: metaArg });
|
|
305
|
-
console.log(JSON.stringify(result, null, 2));
|
|
306
|
-
},
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
310
|
-
const [cmd, ...rest] = process.argv.slice(2);
|
|
311
|
-
if (!cmd || !VERBS[cmd]) {
|
|
312
|
-
console.log(`usage: node ${fileURLToPath(import.meta.url)} <assemble|chip-state|manifest|write-set> [--json]`);
|
|
313
|
-
process.exitCode = cmd ? 1 : 0;
|
|
314
|
-
} else {
|
|
315
|
-
VERBS[cmd](rest);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// mcp-module.mjs -- the ONE resolution ladder a skill script uses to reach a
|
|
3
|
-
// module that lives in the OTHER package (`@henols/vice-mcp`'s `src/mcp/vice/`
|
|
4
|
-
// tree, as opposed to this file's own `@henols/c64-re-tools` skills tree).
|
|
5
|
-
//
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// WHY THIS FILE EXISTS
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// Before this file existed, `vsf-slice.mjs`
|
|
10
|
-
// carried its own copy of this exact three-rung ladder (`ladder()` /
|
|
11
|
-
// `resolveTarget()`), and TWO MORE skill scripts (`acme.mjs`,
|
|
12
|
-
// `packer-finding.mjs`) were about to migrate onto the host-tool execution
|
|
13
|
-
// seam and were each going to need the identical lookup to reach
|
|
14
|
-
// `host-tool-client.ts`. Three production consumers were about to carry
|
|
15
|
-
// three copies of one ladder -- so it is extracted here instead, and
|
|
16
|
-
// `vsf-slice.mjs` is rewritten to import it rather than keep its own.
|
|
17
|
-
//
|
|
18
|
-
// The underlying constraint this ladder exists to answer is
|
|
19
|
-
// `vsf-slice.mjs`'s own recorded finding: the MCP server ships as
|
|
20
|
-
// `@henols/vice-mcp`, whose `files[]` lists only `src/mcp/vice/` contents,
|
|
21
|
-
// while `src/skills/**` ships in a DIFFERENT package (`@henols/c64-re-tools`).
|
|
22
|
-
// A plain `import` from a skill script into the MCP tree resolves on NEITHER
|
|
23
|
-
// npm-installer route, so a skill script that needs an MCP-side module must
|
|
24
|
-
// LOCATE it on disk and invoke it as a subprocess (`process.execPath
|
|
25
|
-
// <resolved-path> ...argv`) -- never a static `import`.
|
|
26
|
-
//
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
// WHAT THIS IS THE ONE AUTHORITATIVE PLACE FOR
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// - the three-rung resolution order (`$VICE_MCP_DIR`, the in-repo relative
|
|
31
|
-
// path, then `require.resolve()` against the published package) for
|
|
32
|
-
// locating ANY named file inside `src/mcp/vice/` from a skill's own
|
|
33
|
-
// `scripts/` directory;
|
|
34
|
-
// - refusing BY NAME, listing every rung tried, when none resolves
|
|
35
|
-
// (`refusalMessage()`);
|
|
36
|
-
// - computing the in-repo hop count from `import.meta.url` rather than a
|
|
37
|
-
// fixed `".."` count -- `hostpath.ts` and `containerpath.ts` both record
|
|
38
|
-
// dropping a hard-coded four-level hop when they moved directories once
|
|
39
|
-
// already, and this project has ALSO shipped a stale hardcoded snapshot
|
|
40
|
-
// offset before (see `vsf-slice.mjs`'s own header) -- a fixed `".."`
|
|
41
|
-
// count is a mistake this project keeps making, not a one-off.
|
|
42
|
-
//
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// WHAT NOT TO DO
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
// - Never carry a second copy of these three rungs, their ordering, or
|
|
47
|
-
// their refusal text anywhere else in this repository. A future
|
|
48
|
-
// skill-side consumer of an MCP-side module imports `resolveMcpModule`
|
|
49
|
-
// from HERE.
|
|
50
|
-
// - Never let a resolution failure become a local re-implementation of
|
|
51
|
-
// whatever was being resolved -- refuse by name instead. A silent
|
|
52
|
-
// fallback that reimplements the target module's own logic is exactly
|
|
53
|
-
// the divergence this ladder exists to prevent (see `vsf-slice.mjs`'s own
|
|
54
|
-
// header on the stale-offset incident this project has already had).
|
|
55
|
-
// - `process.execPath` spawned on a resolved `.ts`/`.mjs` file inside this
|
|
56
|
-
// project's OWN tree reaches NO external host binary -- it is the
|
|
57
|
-
// interpreter already running the calling script, pointed at an in-tree
|
|
58
|
-
// module. This is NOT what the host-tool execution seam (`host-tool.mts`
|
|
59
|
-
// / `host-tool-client.ts`) exists for, and must never be confused with
|
|
60
|
-
// it: the seam is for binaries that live on the HOST, outside any
|
|
61
|
-
// container; this ladder is for locating a file inside this project's own
|
|
62
|
-
// two npm packages.
|
|
63
|
-
import { existsSync } from "node:fs";
|
|
64
|
-
import { createRequire } from "node:module";
|
|
65
|
-
import { fileURLToPath } from "node:url";
|
|
66
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
67
|
-
|
|
68
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
69
|
-
|
|
70
|
-
/** The package the MCP-side module ships in, for the npm-installer rung. */
|
|
71
|
-
export const TARGET_PACKAGE = "@henols/vice-mcp";
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Walks UP from `startDir` until a directory literally named `name` is
|
|
75
|
-
* found, returning its absolute path -- or `null` if the filesystem root is
|
|
76
|
-
* reached first without a match. This is what lets the in-repo hop count be
|
|
77
|
-
* COMPUTED from `import.meta.url` instead of a fixed `".."` count: whichever
|
|
78
|
-
* skill's `scripts/` directory this module is imported from, walking up to
|
|
79
|
-
* the `skills/` ancestor and back down to `mcp/vice/` is the same operation.
|
|
80
|
-
*/
|
|
81
|
-
function findAncestorDir(startDir, name) {
|
|
82
|
-
let dir = startDir;
|
|
83
|
-
for (;;) {
|
|
84
|
-
if (basename(dir) === name) return dir;
|
|
85
|
-
const parent = dirname(dir);
|
|
86
|
-
if (parent === dir) return null; // reached the filesystem root, no match
|
|
87
|
-
dir = parent;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Computed once, at module load: HERE is .../src/skills/<skill>/scripts/, so
|
|
92
|
-
// walking up to the "skills" ancestor and taking ITS parent ("src") gives the
|
|
93
|
-
// root from which "mcp/vice" is a sibling of "skills" -- with no fixed hop
|
|
94
|
-
// count written down anywhere in this file.
|
|
95
|
-
const SKILLS_DIR = findAncestorDir(HERE, "skills");
|
|
96
|
-
const IN_REPO_MCP_VICE_DIR = SKILLS_DIR ? join(dirname(SKILLS_DIR), "mcp", "vice") : null;
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* The resolution ladder for `fileName`, in order, as `{ rung, path, note }`
|
|
100
|
-
* records -- returned IN FULL (never short-circuited), so a refusal message
|
|
101
|
-
* can name every path tried, which is the whole point of refusing rather
|
|
102
|
-
* than guessing:
|
|
103
|
-
*
|
|
104
|
-
* 1. `$VICE_MCP_DIR` -- an explicit override, checked first so a caller can
|
|
105
|
-
* always point this ladder at a known-good tree.
|
|
106
|
-
* 2. The in-repo relative path (see `findAncestorDir()` above). The plugin
|
|
107
|
-
* distribution keeps both trees in one checkout, so this is the rung
|
|
108
|
-
* that resolves during development and in CI.
|
|
109
|
-
* 3. `require.resolve()` against the published package, for the
|
|
110
|
-
* npm-installer route where the two packages are installed separately.
|
|
111
|
-
*/
|
|
112
|
-
function ladder(fileName) {
|
|
113
|
-
const rungs = [];
|
|
114
|
-
|
|
115
|
-
const override = process.env.VICE_MCP_DIR;
|
|
116
|
-
rungs.push({
|
|
117
|
-
rung: "VICE_MCP_DIR",
|
|
118
|
-
path: override ? join(resolve(override), fileName) : null,
|
|
119
|
-
note: override ? null : "VICE_MCP_DIR is not set",
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
rungs.push({
|
|
123
|
-
rung: "in-repo relative path",
|
|
124
|
-
path: IN_REPO_MCP_VICE_DIR ? join(IN_REPO_MCP_VICE_DIR, fileName) : null,
|
|
125
|
-
note: IN_REPO_MCP_VICE_DIR ? null : `could not locate an ancestor "skills" directory from ${HERE}`,
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
let resolved = null;
|
|
129
|
-
let note = null;
|
|
130
|
-
try {
|
|
131
|
-
resolved = createRequire(import.meta.url).resolve(`${TARGET_PACKAGE}/${fileName}`);
|
|
132
|
-
} catch (err) {
|
|
133
|
-
note = `${TARGET_PACKAGE} is not resolvable from here (${err.code ?? err.message})`;
|
|
134
|
-
}
|
|
135
|
-
rungs.push({ rung: `${TARGET_PACKAGE} package`, path: resolved, note });
|
|
136
|
-
|
|
137
|
-
return rungs;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Builds the refusal text naming every rung tried plus the instruction to
|
|
142
|
-
* set `VICE_MCP_DIR` -- the ONE place this message is worded, so callers
|
|
143
|
-
* that construct their own additional context around it (e.g.
|
|
144
|
-
* `vsf-slice.mjs`'s own domain-specific rationale) do not restate it.
|
|
145
|
-
*/
|
|
146
|
-
export function refusalMessage(fileName, rungs) {
|
|
147
|
-
const tried = rungs
|
|
148
|
-
.map((r) => ` ${r.rung}: ${r.path ?? "(no path)"}${r.note ? ` -- ${r.note}` : ""}`)
|
|
149
|
-
.join("\n");
|
|
150
|
-
return (
|
|
151
|
-
`could not resolve ${fileName}.\n` +
|
|
152
|
-
`Tried, in order:\n${tried}\n` +
|
|
153
|
-
`Set VICE_MCP_DIR to the directory holding ${fileName}.`
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Resolves `fileName` (e.g. `"vsf-slice.ts"`, `"host-tool-client.ts"`) against
|
|
159
|
-
* the three-rung ladder above, with NO per-file special-casing -- every
|
|
160
|
-
* caller gets the same three rungs in the same order.
|
|
161
|
-
*
|
|
162
|
-
* Returns `{ ok: true, rung, path }` for the first rung whose path exists on
|
|
163
|
-
* disk, or `{ ok: false, rungs, message }` when none resolves -- `rungs`
|
|
164
|
-
* carries the full attempt list (so a caller can build its own message from
|
|
165
|
-
* the raw records instead of `refusalMessage()`, if it wants extra context),
|
|
166
|
-
* and `message` is `refusalMessage(fileName, rungs)` already computed. Never
|
|
167
|
-
* throws.
|
|
168
|
-
*/
|
|
169
|
-
export function resolveMcpModule(fileName) {
|
|
170
|
-
const rungs = ladder(fileName);
|
|
171
|
-
const hit = rungs.find((r) => r.path && existsSync(r.path));
|
|
172
|
-
if (hit) return { ok: true, rung: hit.rung, path: hit.path };
|
|
173
|
-
return { ok: false, rungs, message: refusalMessage(fileName, rungs) };
|
|
174
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Where this toolkit's data lives, resolved portably.
|
|
3
|
-
//
|
|
4
|
-
// These modules ship as a bundled skill toolkit and may be installed at any
|
|
5
|
-
// depth in any project, so nothing here counts directory hops. Two rules:
|
|
6
|
-
//
|
|
7
|
-
// 1. The project root is found by walking UP for a `.git` marker. Counting
|
|
8
|
-
// hops from `import.meta.url` breaks the moment the toolkit is installed
|
|
9
|
-
// somewhere other than `src/skills/<skill>/scripts/`, and it breaks
|
|
10
|
-
// silently -- paths resolve to a plausible wrong place rather than erroring.
|
|
11
|
-
// 2. Every data location is overridable by environment variable, so a project
|
|
12
|
-
// that does not use this repo's `recovery/` + `disks/` layout can point the
|
|
13
|
-
// toolkit at its own without editing any module.
|
|
14
|
-
//
|
|
15
|
-
// Pure path arithmetic over the filesystem. Contacts nothing.
|
|
16
|
-
import { existsSync } from "node:fs";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
18
|
-
import { dirname, join, resolve, parse } from "node:path";
|
|
19
|
-
|
|
20
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Nearest ancestor directory containing a `.git` entry, starting from this
|
|
24
|
-
* file. Falls back to `C64RE_PROJECT_ROOT` when set, which also covers the
|
|
25
|
-
* case of running from an export with no git metadata at all.
|
|
26
|
-
*/
|
|
27
|
-
export function projectRoot() {
|
|
28
|
-
if (process.env.C64RE_PROJECT_ROOT) return resolve(process.env.C64RE_PROJECT_ROOT);
|
|
29
|
-
let dir = HERE;
|
|
30
|
-
const { root } = parse(dir);
|
|
31
|
-
while (true) {
|
|
32
|
-
if (existsSync(join(dir, ".git"))) return dir;
|
|
33
|
-
if (dir === root) break;
|
|
34
|
-
dir = dirname(dir);
|
|
35
|
-
}
|
|
36
|
-
throw new Error(
|
|
37
|
-
"project-paths: could not locate the project root -- no `.git` found above " +
|
|
38
|
-
`${HERE}. Set C64RE_PROJECT_ROOT to the directory that holds your data dirs.`,
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Directory holding the release registry and the per-release dump directories.
|
|
44
|
-
* Defaults to `<project root>/recovery`; override with `C64RE_DATA_DIR`.
|
|
45
|
-
*/
|
|
46
|
-
export function dataRoot() {
|
|
47
|
-
return process.env.C64RE_DATA_DIR
|
|
48
|
-
? resolve(process.env.C64RE_DATA_DIR)
|
|
49
|
-
: join(projectRoot(), "recovery");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Directory holding the disk images a registry entry's `disk_image` is
|
|
54
|
-
* relative to. Defaults to the project root itself, because registry entries
|
|
55
|
-
* record project-relative paths like `disks/foo.d64`; override with
|
|
56
|
-
* `C64RE_DISKS_ROOT` when they are relative to something else.
|
|
57
|
-
*/
|
|
58
|
-
export function disksRoot() {
|
|
59
|
-
return process.env.C64RE_DISKS_ROOT
|
|
60
|
-
? resolve(process.env.C64RE_DISKS_ROOT)
|
|
61
|
-
: projectRoot();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** The release registry file. Override the whole path with `C64RE_REGISTRY`. */
|
|
65
|
-
export function registryFile() {
|
|
66
|
-
return process.env.C64RE_REGISTRY
|
|
67
|
-
? resolve(process.env.C64RE_REGISTRY)
|
|
68
|
-
: join(dataRoot(), "RELEASES.json");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** One release's own data directory. */
|
|
72
|
-
export function releaseDataDir(id) {
|
|
73
|
-
return join(dataRoot(), id);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
77
|
-
console.log(`project root: ${projectRoot()}`);
|
|
78
|
-
console.log(`data root: ${dataRoot()}`);
|
|
79
|
-
console.log(`disks root: ${disksRoot()}`);
|
|
80
|
-
console.log(`registry: ${registryFile()}`);
|
|
81
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// N-way release registry accessor. This is the only module that reads a release
|
|
3
|
-
// identifier out of the registry -- every other module takes the id as an
|
|
4
|
-
// argument and never touches the registry file directly. `release` is the
|
|
5
|
-
// primary noun rather than "the canonical image": there are N releases, each
|
|
6
|
-
// owning a set of dumps, with `canonical` demoted to a boolean on one entry.
|
|
7
|
-
//
|
|
8
|
-
// Portable: the registry's location comes from `project-paths.mjs`, so a project
|
|
9
|
-
// with a different data layout points the toolkit at its own via
|
|
10
|
-
// `C64RE_DATA_DIR` / `C64RE_REGISTRY` rather than editing this file.
|
|
11
|
-
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
12
|
-
import { fileURLToPath } from "node:url";
|
|
13
|
-
import { resolve } from "node:path";
|
|
14
|
-
|
|
15
|
-
import { registryFile, releaseDataDir } from "./project-paths.mjs";
|
|
16
|
-
|
|
17
|
-
export const registryPath = registryFile();
|
|
18
|
-
|
|
19
|
-
const die = (m) => { console.error(`error: ${m}`); process.exit(1); };
|
|
20
|
-
|
|
21
|
-
export function loadRegistry() {
|
|
22
|
-
if (!existsSync(registryPath)) {
|
|
23
|
-
throw new Error(`no registry at ${registryPath}`);
|
|
24
|
-
}
|
|
25
|
-
return JSON.parse(readFileSync(registryPath, "utf8"));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Persist the registry. JSON.stringify preserves each object's insertion
|
|
30
|
-
* (key) order, so a read-modify-write via upsertRelease keeps a stable key
|
|
31
|
-
* order automatically -- callers should not rebuild release objects from
|
|
32
|
-
* scratch with a different key order.
|
|
33
|
-
*/
|
|
34
|
-
function saveRegistry(reg) {
|
|
35
|
-
writeFileSync(registryPath, JSON.stringify(reg, null, 2) + "\n");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** The full entry for `id`, or throws with the known-id list on a miss. */
|
|
39
|
-
export function release(id) {
|
|
40
|
-
const reg = loadRegistry();
|
|
41
|
-
const r = reg.releases.find((r) => r.id === id);
|
|
42
|
-
if (!r) assertKnownRelease(id, reg);
|
|
43
|
-
return r;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* The registry's own N-readiness documentation:
|
|
48
|
-
* a top-level `schema_notes` string, sibling to `schema_version` and
|
|
49
|
-
* `releases`, stating the mechanical claim that adding a release is one
|
|
50
|
-
* `releases[]` entry plus one invocation of `tools/recover.mjs`. Kept as a
|
|
51
|
-
* plain top-level field rather than a JSON comment (JSON has none) or a
|
|
52
|
-
* per-release field (it describes the registry's shape, not any one
|
|
53
|
-
* release). Rehearsed against the real validator in
|
|
54
|
-
* a release's own NOTES.md.
|
|
55
|
-
*/
|
|
56
|
-
export function schemaNotes() {
|
|
57
|
-
return loadRegistry().schema_notes ?? null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function releaseDir(id) {
|
|
61
|
-
const reg = loadRegistry();
|
|
62
|
-
assertKnownRelease(id, reg);
|
|
63
|
-
return releaseDataDir(id);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Dies with the list of known ids on a miss -- see plan Layer 2. */
|
|
67
|
-
export function assertKnownRelease(id, reg) {
|
|
68
|
-
const registry = reg || loadRegistry();
|
|
69
|
-
const known = registry.releases.map((r) => r.id);
|
|
70
|
-
if (!known.includes(id)) {
|
|
71
|
-
throw new Error(`unknown release "${id}" -- known releases: ${known.join(", ")}`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Read-modify-write: `fn` receives a shallow copy of the release entry and
|
|
77
|
-
* returns the replacement; the whole registry is then re-persisted with
|
|
78
|
-
* stable key order. This is the only sanctioned way to mutate an entry.
|
|
79
|
-
*/
|
|
80
|
-
export function upsertRelease(id, fn) {
|
|
81
|
-
const reg = loadRegistry();
|
|
82
|
-
const idx = reg.releases.findIndex((r) => r.id === id);
|
|
83
|
-
if (idx === -1) {
|
|
84
|
-
throw new Error(`unknown release "${id}" -- known releases: ${reg.releases.map((r) => r.id).join(", ")}`);
|
|
85
|
-
}
|
|
86
|
-
reg.releases[idx] = fn({ ...reg.releases[idx] });
|
|
87
|
-
saveRegistry(reg);
|
|
88
|
-
return reg.releases[idx];
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// -------------------------------------------------------------------- CLI
|
|
92
|
-
|
|
93
|
-
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
94
|
-
const [cmd, ...rest] = process.argv.slice(2);
|
|
95
|
-
if (cmd === "list") {
|
|
96
|
-
const reg = loadRegistry();
|
|
97
|
-
for (const r of reg.releases) {
|
|
98
|
-
console.log(`${r.id} canonical=${r.canonical} disk_image=${r.disk_image} dumps=${r.dumps.length}`);
|
|
99
|
-
}
|
|
100
|
-
} else if (cmd === "show") {
|
|
101
|
-
if (!rest[0]) die("usage: show <release-id>");
|
|
102
|
-
console.log(JSON.stringify(release(rest[0]), null, 2));
|
|
103
|
-
} else if (cmd === "schema-notes") {
|
|
104
|
-
console.log(schemaNotes() ?? "(no schema_notes field set)");
|
|
105
|
-
} else {
|
|
106
|
-
console.log(`usage: node ${fileURLToPath(import.meta.url)} <list|show <id>|schema-notes>`);
|
|
107
|
-
process.exit(cmd ? 1 : 0);
|
|
108
|
-
}
|
|
109
|
-
}
|