@henols/c64-re-tools 1.1.0 → 2.0.0-rc.3

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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +138 -37
  3. package/distribution/package.ts +36 -0
  4. package/distribution/plugin.ts +120 -0
  5. package/package.json +34 -40
  6. package/skills/c64-assembler/SKILL.md +73 -0
  7. package/skills/c64-assembler/scripts/assemble.ts +103 -0
  8. package/skills/c64-basic/SKILL.md +67 -0
  9. package/skills/c64-basic/scripts/basic.ts +46 -0
  10. package/skills/c64-disk/SKILL.md +79 -0
  11. package/skills/c64-disk/scripts/disk.ts +65 -0
  12. package/skills/c64-emulator/SKILL.md +124 -0
  13. package/skills/c64-knowledge/SKILL.md +102 -0
  14. package/skills/c64-knowledge/scripts/knowledge.ts +257 -0
  15. package/skills/c64-memory-map/SKILL.md +76 -0
  16. package/skills/c64-memory-map/scripts/memmap.ts +81 -0
  17. package/skills/c64-memory-map/scripts/platform.ts +424 -0
  18. package/skills/c64-provenance/SKILL.md +76 -0
  19. package/skills/c64-provenance/scripts/compare.ts +92 -0
  20. package/skills/c64-provenance/scripts/provenance.ts +83 -0
  21. package/skills/c64-reverse-engineering/SKILL.md +90 -0
  22. package/skills/c64-static-analysis/SKILL.md +96 -0
  23. package/skills/c64-static-analysis/scripts/analyze.ts +175 -0
  24. package/skills/c64-static-analysis/scripts/findings.ts +73 -0
  25. package/skills/c64-testing/SKILL.md +79 -0
  26. package/skills/c64-testing/references/scenario-format.md +76 -0
  27. package/skills/c64-testing/scripts/checklist.ts +51 -0
  28. package/skills/c64-testing/scripts/scenario.ts +473 -0
  29. package/skills/c64-testing/scripts/test.ts +55 -0
  30. package/skills/c64-unpacker/SKILL.md +82 -0
  31. package/skills/c64-unpacker/scripts/evidence.ts +135 -0
  32. package/skills/c64-unpacker/scripts/unpack.ts +216 -0
  33. package/src/c64.ts +281 -0
  34. package/src/cli/ap-sdk.ts +44 -0
  35. package/src/cli/cleanup.ts +194 -0
  36. package/src/cli/main.ts +122 -0
  37. package/src/cli/status.ts +34 -0
  38. package/src/host/main.ts +106 -0
  39. package/src/host/server.ts +376 -0
  40. package/src/host/tools/c1541.ts +255 -0
  41. package/src/host/tools/index.ts +31 -0
  42. package/src/host/tools/petcat.ts +70 -0
  43. package/src/host/tools/status.ts +30 -0
  44. package/src/host/tools/version.ts +41 -0
  45. package/src/host/vice/adapter.ts +761 -0
  46. package/src/host/vice/binary-monitor.ts +389 -0
  47. package/src/host/vice/chips.ts +108 -0
  48. package/src/host/vice/connect.ts +34 -0
  49. package/src/host/vice/process.ts +402 -0
  50. package/src/host/vice/screen.ts +183 -0
  51. package/src/host/vice/session.ts +1316 -0
  52. package/src/host/vice/text-monitor.ts +223 -0
  53. package/src/host-client/connect.ts +238 -0
  54. package/src/host-client/tools.ts +89 -0
  55. package/src/host-client/transfer.ts +100 -0
  56. package/src/host-client/vice-session.ts +254 -0
  57. package/src/knowledge/database.ts +132 -0
  58. package/src/knowledge/history.ts +128 -0
  59. package/src/knowledge/import.ts +344 -0
  60. package/src/knowledge/read.ts +191 -0
  61. package/src/knowledge/schema.ts +140 -0
  62. package/src/knowledge/write.ts +348 -0
  63. package/src/mcp/main.ts +106 -0
  64. package/src/mcp/server.ts +307 -0
  65. package/src/mcp/tools/debug.ts +237 -0
  66. package/src/mcp/tools/execution.ts +116 -0
  67. package/src/mcp/tools/input.ts +69 -0
  68. package/src/mcp/tools/machine.ts +97 -0
  69. package/src/mcp/tools/media.ts +106 -0
  70. package/src/mcp/tools/memory.ts +203 -0
  71. package/src/mcp/tools/video.ts +260 -0
  72. package/src/native/acme.ts +116 -0
  73. package/src/native/discover.ts +141 -0
  74. package/src/native/dxa.ts +166 -0
  75. package/src/native/ghidra/analyze.ts +86 -0
  76. package/src/native/ghidra/index.ts +157 -0
  77. package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
  78. package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
  79. package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
  80. package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
  81. package/src/native/ghidra/scripts/C64Export.java +242 -0
  82. package/src/native/ghidra/scripts/C64Prepare.java +60 -0
  83. package/src/native/local.ts +34 -0
  84. package/src/native/processes.ts +350 -0
  85. package/src/native/run.ts +172 -0
  86. package/src/native/staging.ts +83 -0
  87. package/src/native/status.ts +101 -0
  88. package/src/native/types.ts +228 -0
  89. package/src/native/watchdog.ts +78 -0
  90. package/src/project.ts +39 -0
  91. package/src/protocol/framing.ts +132 -0
  92. package/src/protocol/messages.ts +243 -0
  93. package/src/protocol/tools.ts +247 -0
  94. package/src/protocol/vice.ts +1235 -0
  95. package/src/script.ts +59 -0
  96. package/src/time.ts +15 -0
  97. package/src/trace.ts +162 -0
  98. package/THIRD-PARTY-NOTICES.md +0 -26
  99. package/bin/cli.mjs +0 -283
  100. package/skills/acme-build/SKILL.md +0 -260
  101. package/skills/acme-build/scripts/acme.mjs +0 -341
  102. package/skills/acme-build/template.a +0 -53
  103. package/skills/c64-disk-access/SKILL.md +0 -159
  104. package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
  105. package/skills/c64-memory-mapping/SKILL.md +0 -631
  106. package/skills/c64-memory-mapping/memmap.json +0 -8800
  107. package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
  108. package/skills/c64-petcat/SKILL.md +0 -87
  109. package/skills/c64-petcat/scripts/petcat.mjs +0 -221
  110. package/skills/c64-program-recon/SKILL.md +0 -706
  111. package/skills/c64-program-recon/references/control-flow.md +0 -180
  112. package/skills/c64-program-recon/references/graphics.md +0 -79
  113. package/skills/c64-program-recon/references/observation-hazards.md +0 -143
  114. package/skills/c64-program-recon/references/reconstruction.md +0 -133
  115. package/skills/c64-program-recon/references/sound-and-input.md +0 -74
  116. package/skills/c64-program-recon/references/tool-selection.md +0 -78
  117. package/skills/c64-program-recon/scripts/derive.mjs +0 -364
  118. package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
  119. package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
  120. package/skills/c64-provenance-diff/SKILL.md +0 -292
  121. package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
  122. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
  123. package/skills/c64-ram-capture/RELEASES.json.example +0 -17
  124. package/skills/c64-ram-capture/SKILL.md +0 -474
  125. package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
  126. package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
  127. package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
  128. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
  129. package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
  130. package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
  131. package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
  132. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
  133. package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
  134. package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
  135. package/skills/c64-ram-capture/transients/README.md +0 -137
  136. package/skills/routine-queue-walker/SKILL.md +0 -394
  137. package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
  138. package/skills/vice-wedge-triage/SKILL.md +0 -269
@@ -0,0 +1,255 @@
1
+ // The c1541 adapter. c1541 does all image access: it recognises the
2
+ // image, reports free blocks and the BAM, follows sector chains and writes raw
3
+ // blocks. File names never go to c1541: its name arguments and its directory
4
+ // listing change letter case, so the adapter reads the raw header and
5
+ // directory blocks and matches names by their exact PETSCII bytes.
6
+ //
7
+ // c1541 exits 0 after most errors, so success is decided from its output and
8
+ // from the output files it was asked to write.
9
+
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+
13
+ import { petsciiNameToText, textToPetsciiName } from "../../c64.ts";
14
+ import { WireFailure } from "../../protocol/messages.ts";
15
+ import { CBM_FILE_TYPES, type C1541Params, type C1541Result, type CbmFileType, type DiskFile, type DiskSector } from "../../protocol/tools.ts";
16
+ import { Workspace } from "../../native/staging.ts";
17
+ import { C1541, findTool } from "../../native/discover.ts";
18
+ import { requireMinimumVersion } from "./version.ts";
19
+ import { runToolOrFail, type ToolContext } from "../../native/run.ts";
20
+
21
+ const TIMEOUT_MS = 30_000;
22
+ const BLOCK_BYTES = 256;
23
+ /** Block reads in one c1541 call, so that its command line stays far below the Windows limit of 32767 characters. */
24
+ export const READS_PER_CALL = 256;
25
+
26
+ /** Where a drive format keeps its header block and the header's fields. */
27
+ interface Layout {
28
+ header: DiskSector;
29
+ name: number;
30
+ id: number;
31
+ dos: number;
32
+ }
33
+
34
+ const LAYOUTS: Record<string, Layout> = {
35
+ "1541": { header: { track: 18, sector: 0 }, name: 0x90, id: 0xa2, dos: 0xa5 },
36
+ "1571": { header: { track: 18, sector: 0 }, name: 0x90, id: 0xa2, dos: 0xa5 },
37
+ "1581": { header: { track: 40, sector: 0 }, name: 0x04, id: 0x16, dos: 0x19 },
38
+ };
39
+
40
+ export interface DiskEntry extends DiskFile {
41
+ /** The name's PETSCII bytes, up to the first shifted space ($a0). */
42
+ nameBytes: Uint8Array;
43
+ startTrack: number;
44
+ startSector: number;
45
+ }
46
+
47
+ const damaged = (message: string) => new WireFailure("media-error", `The disk image is damaged: ${message}`);
48
+
49
+ /** The drive format and track count from c1541's info command. */
50
+ export function parseInfo(output: string): { format: string; tracks: number } {
51
+ const format = /^disk format\s*:\s*(\S+)/m.exec(output);
52
+ const tracks = /^track count\s*:\s*(\d+)/m.exec(output);
53
+ if (format === null || tracks === null) {
54
+ throw new WireFailure("media-error", "c1541 does not recognise the disk image. Check that the file is a complete disk image of the given type.");
55
+ }
56
+ return { format: format[1]!, tracks: Number(tracks[1]) };
57
+ }
58
+
59
+ export function parseFreeBlocks(output: string): number {
60
+ const match = /^(\d+) blocks free\.$/m.exec(output);
61
+ if (match === null) throw new WireFailure("operation-failed", "c1541 did not report the free blocks of the disk.");
62
+ return Number(match[1]);
63
+ }
64
+
65
+ /** A sector chain as c1541's chain command prints it: "(17, 1) -> (17,11) -> 207". */
66
+ export function parseChain(output: string, start: DiskSector): DiskSector[] {
67
+ const where = `the sector chain from track ${start.track} sector ${start.sector}`;
68
+ if (/cyclic reference found/.test(output)) throw damaged(`${where} loops.`);
69
+ const line = /^\(\s*\d+,\s*\d+\).*$/m.exec(output)?.[0];
70
+ if (line === undefined) throw damaged(`${where} cannot be read.`);
71
+ const parts = line.split(" -> ");
72
+ const end = parts.pop()!;
73
+ if (!/^\d+$/.test(end.trim())) throw damaged(`${where} leaves the disk.`);
74
+ return parts.map((part) => {
75
+ const match = /^\(\s*(\d+),\s*(\d+)\)$/.exec(part.trim());
76
+ if (match === null) throw new WireFailure("operation-failed", "c1541 printed a sector chain in an unknown form.");
77
+ return { track: Number(match[1]), sector: Number(match[2]) };
78
+ });
79
+ }
80
+
81
+ /** The BAM grid c1541 prints: one row per track, "*" for a used sector, "." for a free one. */
82
+ export function parseBam(output: string, tracks: number): Array<{ track: number; freeSectors: number[]; usedSectors: number[] }> {
83
+ const rows: Array<{ track: number; freeSectors: number[]; usedSectors: number[] }> = [];
84
+ for (const line of output.split(/\r?\n/)) {
85
+ const match = /^\s*(\d+)\s+([.*]{1,8}(?: [.*]{1,8})*)\s*$/.exec(line);
86
+ if (match === null) continue;
87
+ const row = { track: Number(match[1]), freeSectors: [] as number[], usedSectors: [] as number[] };
88
+ [...match[2]!.replace(/ /g, "")].forEach((mark, sector) => (mark === "*" ? row.usedSectors : row.freeSectors).push(sector));
89
+ rows.push(row);
90
+ }
91
+ const complete = rows.length === tracks && rows.every((row, index) => row.track === index + 1);
92
+ if (!complete) throw new WireFailure("operation-failed", `c1541 printed a BAM that does not cover the ${tracks} tracks of the disk.`);
93
+ return rows;
94
+ }
95
+
96
+ /** The bytes of a name field up to the first shifted space ($a0). */
97
+ function nameField(block: Buffer, offset: number, length: number): Uint8Array {
98
+ const field = block.subarray(offset, offset + length);
99
+ const end = field.indexOf(0xa0);
100
+ return Uint8Array.from(end < 0 ? field : field.subarray(0, end));
101
+ }
102
+
103
+ function fileType(typeByte: number): CbmFileType {
104
+ return CBM_FILE_TYPES[typeByte & 0x07] ?? "unknown";
105
+ }
106
+
107
+ /** The disk header and the directory entries, from the raw header block and the directory blocks after it. */
108
+ export function parseDirectory(blocks: Buffer[], layout: Layout): { diskName: string; diskId: string; dosType: string; entries: DiskEntry[] } {
109
+ const [header, ...directory] = blocks;
110
+ if (header === undefined) throw new WireFailure("operation-failed", "c1541 returned no header block.");
111
+ const entries: DiskEntry[] = [];
112
+ for (const block of directory) {
113
+ for (let offset = 0; offset < BLOCK_BYTES; offset += 32) {
114
+ const typeByte = block.readUInt8(offset + 2);
115
+ // Type 0 is an empty or scratched slot; the DOS lists no file there.
116
+ if (typeByte === 0) continue;
117
+ const nameBytes = nameField(block, offset + 5, 16);
118
+ entries.push({
119
+ name: petsciiNameToText(nameBytes),
120
+ nameBytes,
121
+ type: fileType(typeByte),
122
+ blocks: block.readUInt16LE(offset + 30),
123
+ closed: (typeByte & 0x80) !== 0,
124
+ locked: (typeByte & 0x40) !== 0,
125
+ startTrack: block.readUInt8(offset + 3),
126
+ startSector: block.readUInt8(offset + 4),
127
+ });
128
+ }
129
+ }
130
+ return {
131
+ diskName: petsciiNameToText(nameField(header, layout.name, 16)),
132
+ diskId: petsciiNameToText(header.subarray(layout.id, layout.id + 2)),
133
+ dosType: petsciiNameToText(header.subarray(layout.dos, layout.dos + 2)),
134
+ entries,
135
+ };
136
+ }
137
+
138
+ /** A file's data from its blocks in chain order: 254 bytes per block, the last block up to its end index. */
139
+ export function fileData(blocks: Buffer[], chain: DiskSector[]): Buffer {
140
+ const parts: Buffer[] = [];
141
+ blocks.forEach((block, index) => {
142
+ const next = chain[index + 1];
143
+ if (next === undefined) {
144
+ if (block[0] !== 0) throw new WireFailure("operation-failed", "c1541 returned a last file block that links further.");
145
+ parts.push(block.subarray(2, Math.max(2, block.readUInt8(1) + 1)));
146
+ } else {
147
+ if (block[0] !== next.track || block[1] !== next.sector) throw new WireFailure("operation-failed", "c1541 returned file blocks that do not match the sector chain.");
148
+ parts.push(block.subarray(2));
149
+ }
150
+ });
151
+ return Buffer.concat(parts);
152
+ }
153
+
154
+ const toFile = (entry: DiskEntry): DiskFile => ({ name: entry.name, type: entry.type, blocks: entry.blocks, closed: entry.closed, locked: entry.locked });
155
+
156
+ const sameBytes = (a: Uint8Array, b: Uint8Array) => a.length === b.length && a.every((byte, index) => byte === b[index]);
157
+
158
+ /** Runs c1541 commands on the staged image and returns all its output. */
159
+ export class C1541Session {
160
+ readonly #executable: string;
161
+ readonly #workspace: Workspace;
162
+ readonly #image: string;
163
+ readonly #context: ToolContext;
164
+ #reads = 0;
165
+
166
+ constructor(executable: string, workspace: Workspace, image: string, context: ToolContext) {
167
+ this.#executable = executable;
168
+ this.#workspace = workspace;
169
+ this.#image = image;
170
+ this.#context = context;
171
+ }
172
+
173
+ async run(commands: string[]): Promise<string> {
174
+ const run = await runToolOrFail(
175
+ "c1541",
176
+ "The disk inspection",
177
+ {
178
+ argv: [this.#executable, "-attach", this.#image, ...commands],
179
+ cwd: this.#workspace.root,
180
+ timeoutMs: TIMEOUT_MS,
181
+ truncated: "c1541 printed more output than the host accepts.",
182
+ },
183
+ this.#context,
184
+ );
185
+ return `${run.stdout}\n${run.stderr}`;
186
+ }
187
+
188
+ async chain(start: DiskSector): Promise<DiskSector[]> {
189
+ return parseChain(await this.run(["-chain", String(start.track), String(start.sector)]), start);
190
+ }
191
+
192
+ /** Raw blocks, in the given order, through c1541's block read: at most READS_PER_CALL blocks in each c1541 call. */
193
+ async blocks(sectors: DiskSector[]): Promise<Buffer[]> {
194
+ const blocks: Buffer[] = [];
195
+ for (let first = 0; first < sectors.length; first += READS_PER_CALL) {
196
+ blocks.push(...(await this.#readBlocks(sectors.slice(first, first + READS_PER_CALL))));
197
+ }
198
+ return blocks;
199
+ }
200
+
201
+ async #readBlocks(sectors: DiskSector[]): Promise<Buffer[]> {
202
+ const directory = `blocks-${this.#reads++}`;
203
+ const full = this.#workspace.directory(directory);
204
+ const reads = sectors.map((at, index) => ({ at, file: `${index}.bin` }));
205
+ const output = await this.run(reads.flatMap(({ at, file }) => ["-bread", join(directory, file), String(at.track), String(at.sector)]));
206
+ return reads.map(({ at, file }) => {
207
+ const path = join(full, file);
208
+ const block = existsSync(path) ? readFileSync(path) : undefined;
209
+ if (block?.length === BLOCK_BYTES) return block;
210
+ if (/cannot read track|out of bounds/.test(output)) throw damaged(`track ${at.track} sector ${at.sector} cannot be read.`);
211
+ throw new WireFailure("operation-failed", `c1541 did not write track ${at.track} sector ${at.sector}.`);
212
+ });
213
+ }
214
+ }
215
+
216
+ export async function inspect(params: C1541Params, image: Buffer, context: ToolContext): Promise<{ result: C1541Result; attachments?: Buffer[] }> {
217
+ const executable = findTool(C1541, context.env);
218
+ await requireMinimumVersion(C1541, executable, context);
219
+ const workspace = Workspace.create(context.supervisor);
220
+ try {
221
+ const file = `image.${params.imageType}`;
222
+ workspace.materialize("input", [{ path: file, size: image.length }], [image]);
223
+ const c1541 = new C1541Session(executable, workspace, `input/${file}`, context);
224
+ const overview = await c1541.run(["-info", "-dir"]);
225
+ const info = parseInfo(overview);
226
+ const layout = LAYOUTS[info.format];
227
+ if (layout === undefined) throw new WireFailure("media-error", `The disk image has the drive format ${info.format}; only 1541, 1571 and 1581 disks are supported.`);
228
+
229
+ if (params.action === "bam") {
230
+ return { result: { action: "bam", tracks: parseBam(await c1541.run(["-bam"]), info.tracks) } };
231
+ }
232
+ const headerChain = await c1541.chain(layout.header);
233
+ const directory = parseDirectory(await c1541.blocks(headerChain), layout);
234
+ if (params.action === "directory") {
235
+ const { diskName, diskId, dosType, entries } = directory;
236
+ return { result: { action: "directory", diskName, diskId, dosType, freeBlocks: parseFreeBlocks(overview), entries: entries.map(toFile) } };
237
+ }
238
+
239
+ if (params.name === undefined) throw new WireFailure("invalid-input", `action ${params.action} needs a file name`);
240
+ const wanted = textToPetsciiName(params.name);
241
+ const entry = directory.entries.find((candidate) => sameBytes(candidate.nameBytes, wanted));
242
+ if (entry === undefined) return { result: { action: params.action, found: false } };
243
+ if (params.action === "entry") {
244
+ return { result: { action: "entry", found: true, entry: { ...toFile(entry), startTrack: entry.startTrack, startSector: entry.startSector } } };
245
+ }
246
+ // A file without blocks (a DEL entry, for example) has no chain.
247
+ const start = { track: entry.startTrack, sector: entry.startSector };
248
+ const chain = start.track === 0 ? [] : await c1541.chain(start);
249
+ if (params.action === "chain") return { result: { action: "chain", found: true, sectors: chain } };
250
+ const data = fileData(await c1541.blocks(chain), chain);
251
+ return { result: { action: "read", found: true, name: entry.name, bytes: data.length }, attachments: [data] };
252
+ } finally {
253
+ workspace.remove();
254
+ }
255
+ }
@@ -0,0 +1,31 @@
1
+ // The typed tool dispatcher of the Host Runtime. The host runs only
2
+ // VICE and the tools that come with it (c1541, petcat); skill scripts run
3
+ // ACME, DXA and Ghidra themselves. One entry per operation, never a
4
+ // generic executable route.
5
+
6
+ import type { ProcessSupervisor } from "../../native/processes.ts";
7
+ import type { ToolContext } from "../../native/run.ts";
8
+ import type { ToolOperation, ToolOperations } from "../../protocol/tools.ts";
9
+ import type { ToolDispatcher } from "../server.ts";
10
+ import { inspect } from "./c1541.ts";
11
+ import { decode } from "./petcat.ts";
12
+ import { hostStatus } from "./status.ts";
13
+
14
+ /** Runs one operation with its validated parameters and attachments. */
15
+ type Adapter<O extends ToolOperation> = (
16
+ params: ToolOperations[O]["params"],
17
+ attachments: Buffer[],
18
+ context: ToolContext,
19
+ ) => Promise<{ result: ToolOperations[O]["result"]; attachments?: Buffer[] }>;
20
+
21
+ const ADAPTERS: { [O in ToolOperation]: Adapter<O> } = {
22
+ "host.status": (_params, _attachments, context) => hostStatus(context),
23
+ "c1541.inspect": (params, attachments, context) => inspect(params, attachments[0]!, context),
24
+ "petcat.decode": (_params, attachments, context) => decode(attachments[0]!, context),
25
+ };
26
+
27
+ export function createToolDispatcher(options: { supervisor: ProcessSupervisor; env?: NodeJS.ProcessEnv; log?: (line: string) => void }): ToolDispatcher {
28
+ return (op, params, attachments, signal) => {
29
+ return ADAPTERS[op](params, attachments, { supervisor: options.supervisor, signal, env: options.env, log: options.log });
30
+ };
31
+ }
@@ -0,0 +1,70 @@
1
+ // The petcat adapter. petcat makes the C64 BASIC V2 listing; the
2
+ // adapter parses the tokenized program itself to check that listing and to
3
+ // find the machine-code handoffs, because petcat's exit status proves nothing
4
+ // (it prints any bytes as text).
5
+
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+
9
+ import { findHandoffs, parseTokenized } from "../../c64.ts";
10
+ import { WireFailure } from "../../protocol/messages.ts";
11
+ import { type PetcatResult } from "../../protocol/tools.ts";
12
+ import { Workspace } from "../../native/staging.ts";
13
+ import { findTool, PETCAT } from "../../native/discover.ts";
14
+ import { requireMinimumVersion } from "./version.ts";
15
+ import { runToolOrFail, type ToolContext } from "../../native/run.ts";
16
+
17
+ const TIMEOUT_MS = 30_000;
18
+ const LISTING = "listing.txt";
19
+ /** petcat's listing lines (" 10 sys2061"), as number and text. */
20
+ export function parseListing(listing: string): Array<{ number: number; text: string }> {
21
+ const lines: Array<{ number: number; text: string }> = [];
22
+ for (const line of listing.split(/\r?\n/)) {
23
+ if (line.trim() === "") continue;
24
+ const match = /^\s*(\d+) ?(.*)$/.exec(line);
25
+ if (match === null) throw new WireFailure("operation-failed", "petcat printed a listing line without a line number.");
26
+ lines.push({ number: Number(match[1]), text: match[2]! });
27
+ }
28
+ return lines;
29
+ }
30
+
31
+ export async function decode(
32
+ program: Buffer,
33
+ context: ToolContext,
34
+ ): Promise<{ result: PetcatResult }> {
35
+ const tokenized = parseTokenized(program);
36
+ if ("reason" in tokenized) return { result: { decoded: false, reason: tokenized.reason } };
37
+ const executable = findTool(PETCAT, context.env);
38
+ await requireMinimumVersion(PETCAT, executable, context);
39
+ const workspace = Workspace.create(context.supervisor);
40
+ try {
41
+ workspace.materialize("input", [{ path: "program.prg", size: program.length }], [program]);
42
+ const listingPath = join(workspace.directory("out"), LISTING);
43
+ const run = await runToolOrFail(
44
+ "petcat",
45
+ "The BASIC decode",
46
+ // -2: C64 BASIC V2 keywords. -nh: no header. The input is never read as an option.
47
+ { argv: [executable, "-2", "-nh", "-o", listingPath, "--", "input/program.prg"], cwd: workspace.root, timeoutMs: TIMEOUT_MS },
48
+ context,
49
+ );
50
+ if (run.code !== 0 || !existsSync(listingPath)) {
51
+ context.log?.(`petcat exited ${run.code}:\n${run.stdout}\n${run.stderr}`);
52
+ throw new WireFailure("operation-failed", "petcat could not decode the program.");
53
+ }
54
+ const lines = parseListing(readFileSync(listingPath, "latin1"));
55
+ const agrees = lines.length === tokenized.lines.length && lines.every((line, index) => line.number === tokenized.lines[index]!.number);
56
+ if (!agrees) throw new WireFailure("operation-failed", "petcat's listing does not match the lines of the program.");
57
+ return {
58
+ result: {
59
+ decoded: true,
60
+ loadAddress: tokenized.loadAddress,
61
+ basicEnd: tokenized.basicEnd,
62
+ listing: lines.map((line) => `${line.number} ${line.text}`).join("\n"),
63
+ lines,
64
+ handoffs: findHandoffs(tokenized.lines),
65
+ },
66
+ };
67
+ } finally {
68
+ workspace.remove();
69
+ }
70
+ }
@@ -0,0 +1,30 @@
1
+ // host.status: VICE and the tools that come with it, each found the way its
2
+ // operations find it and run once with a version option. A tool from a
3
+ // VICE older than MIN_VICE is reported as not working, as its operations refuse it.
4
+
5
+ import type { ToolStatus } from "../../protocol/tools.ts";
6
+ import { C1541, findTool, isOlderThanMinimum, MIN_VICE, PETCAT, VICE, viceVersionOf, type ToolSpec } from "../../native/discover.ts";
7
+ import type { ToolContext } from "../../native/run.ts";
8
+ import { probeTools } from "../../native/status.ts";
9
+ import { findVice } from "../vice/process.ts";
10
+
11
+ function requireMinimum(status: ToolStatus, tool: ToolSpec): ToolStatus {
12
+ if (!status.runs) return status;
13
+ const version = viceVersionOf(status.version ?? "");
14
+ if (version !== undefined && !isOlderThanMinimum(version)) return status;
15
+ const found = version === undefined ? "It does not tell its VICE version" : `It comes from VICE ${version.major}.${version.minor}`;
16
+ return { ...status, runs: false, problem: `${found}, and c64-re-tools needs VICE ${MIN_VICE.major}.${MIN_VICE.minor} or later. ${tool.remedy}` };
17
+ }
18
+
19
+ export async function hostStatus(context: ToolContext): Promise<{ result: { tools: ToolStatus[] } }> {
20
+ const specs = [VICE, C1541, PETCAT];
21
+ const tools = await probeTools(
22
+ [
23
+ { name: VICE.name, find: findVice, args: ["-version"], pattern: /^x64sc \(VICE \d/ },
24
+ { name: C1541.name, find: (env) => findTool(C1541, env), args: ["-version"], pattern: /^c1541 \(VICE \d/ },
25
+ { name: PETCAT.name, find: (env) => findTool(PETCAT, env), args: ["-version"], pattern: /^petcat \(VICE \d/ },
26
+ ],
27
+ context,
28
+ );
29
+ return { result: { tools: tools.map((status, index) => requireMinimum(status, specs[index]!)) } };
30
+ }
@@ -0,0 +1,41 @@
1
+ // The VICE tools on the host must come from a VICE that c64-re-tools works
2
+ // with. Each executable is asked once for its version; a version that
3
+ // cannot be read is refused, as is one that is too old.
4
+
5
+ import { tmpdir } from "node:os";
6
+
7
+ import { WireFailure } from "../../protocol/messages.ts";
8
+ import { isOlderThanMinimum, MIN_VICE, viceVersionOf, type ToolSpec } from "../../native/discover.ts";
9
+ import { runToolOrFail, type ToolContext } from "../../native/run.ts";
10
+
11
+ const VERSION_TIMEOUT_MS = 30_000;
12
+ const checked = new Map<string, Promise<void>>();
13
+
14
+ /** Refuses a tool from a VICE older than MIN_VICE, or one whose VICE version cannot be read, by name and with the remedy. */
15
+ export function requireMinimumVersion(tool: ToolSpec, executable: string, context: ToolContext): Promise<void> {
16
+ let check = checked.get(executable);
17
+ if (check === undefined) {
18
+ // Every caller waits for the same check, so no caller's cancel may stop it; its own time limit bounds it.
19
+ const shared: ToolContext = { ...context, signal: new AbortController().signal };
20
+ check = (async () => {
21
+ const run = await runToolOrFail(tool.name, `The ${tool.name} version check`, { argv: [executable, "-version"], cwd: tmpdir(), timeoutMs: VERSION_TIMEOUT_MS }, shared);
22
+ const version = viceVersionOf(`${run.stdout}\n${run.stderr}`);
23
+ if (version === undefined) {
24
+ throw new WireFailure(
25
+ "installation-incomplete",
26
+ `${tool.name} does not tell its VICE version, and c64-re-tools needs VICE ${MIN_VICE.major}.${MIN_VICE.minor} or later. ${tool.remedy}`,
27
+ );
28
+ }
29
+ if (isOlderThanMinimum(version)) {
30
+ throw new WireFailure(
31
+ "installation-incomplete",
32
+ `${tool.name} comes from VICE ${version.major}.${version.minor}, and c64-re-tools needs VICE ${MIN_VICE.major}.${MIN_VICE.minor} or later. ${tool.remedy}`,
33
+ );
34
+ }
35
+ })();
36
+ // A refusal or a failed check is asked again next time: the user can install a newer VICE meanwhile.
37
+ check.catch(() => checked.delete(executable));
38
+ checked.set(executable, check);
39
+ }
40
+ return check;
41
+ }