@henols/c64-re-tools 1.1.0 → 2.0.0-rc.10
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 +154 -37
- package/distribution/package.ts +21 -0
- package/distribution/plugin.ts +122 -0
- package/package.json +34 -40
- package/skills/c64-assembler/SKILL.md +76 -0
- package/skills/c64-assembler/scripts/assemble.ts +110 -0
- package/skills/c64-assembler/scripts/compare.ts +84 -0
- package/skills/c64-basic/SKILL.md +66 -0
- package/skills/c64-basic/scripts/basic.ts +46 -0
- package/skills/c64-disk/SKILL.md +93 -0
- package/skills/c64-disk/scripts/containers.ts +124 -0
- package/skills/c64-disk/scripts/disk.ts +120 -0
- package/skills/c64-emulator/SKILL.md +134 -0
- package/skills/c64-knowledge/SKILL.md +166 -0
- package/skills/c64-knowledge/scripts/knowledge.ts +492 -0
- package/skills/c64-memory-map/SKILL.md +79 -0
- package/skills/c64-memory-map/scripts/memmap.ts +92 -0
- package/skills/c64-provenance/SKILL.md +110 -0
- package/skills/c64-provenance/scripts/compare.ts +152 -0
- package/skills/c64-provenance/scripts/provenance.ts +334 -0
- package/skills/c64-provenance/scripts/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/separate.ts +140 -0
- package/skills/c64-reverse-engineering/SKILL.md +144 -0
- package/skills/c64-source/SKILL.md +126 -0
- package/skills/c64-source/scripts/generate.ts +490 -0
- package/skills/c64-source/scripts/loader.ts +571 -0
- package/skills/c64-source/scripts/opcodes.ts +61 -0
- package/skills/c64-source/scripts/source.ts +595 -0
- package/skills/c64-static-analysis/SKILL.md +119 -0
- package/skills/c64-static-analysis/scripts/analyze.ts +275 -0
- package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
- package/skills/c64-static-analysis/scripts/findings.ts +75 -0
- package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
- package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
- package/skills/c64-testing/SKILL.md +103 -0
- package/skills/c64-testing/references/scenario-format.md +116 -0
- package/skills/c64-testing/scripts/checklist.ts +51 -0
- package/skills/c64-testing/scripts/run.ts +189 -0
- package/skills/c64-testing/scripts/scenario.ts +493 -0
- package/skills/c64-testing/scripts/test.ts +65 -0
- package/skills/c64-unpacker/SKILL.md +135 -0
- package/skills/c64-unpacker/scripts/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/stages.ts +130 -0
- package/skills/c64-unpacker/scripts/unpack.ts +683 -0
- package/src/c64.ts +411 -0
- package/src/cli/ap-sdk.ts +44 -0
- package/src/cli/cleanup.ts +194 -0
- package/src/cli/main.ts +125 -0
- package/src/cli/status.ts +61 -0
- package/src/host/main.ts +134 -0
- package/src/host/server.ts +392 -0
- package/src/host/tools/c1541.ts +284 -0
- package/src/host/tools/index.ts +35 -0
- package/src/host/tools/petcat.ts +77 -0
- package/src/host/tools/status.ts +47 -0
- package/src/host/tools/version.ts +41 -0
- package/src/host/vice/adapter.ts +804 -0
- package/src/host/vice/binary-monitor.ts +389 -0
- package/src/host/vice/chips.ts +118 -0
- package/src/host/vice/connect.ts +34 -0
- package/src/host/vice/process.ts +413 -0
- package/src/host/vice/screen.ts +184 -0
- package/src/host/vice/session.ts +1426 -0
- package/src/host/vice/text-monitor.ts +223 -0
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +241 -0
- package/src/host-client/coverage.ts +63 -0
- package/src/host-client/steps.ts +170 -0
- package/src/host-client/tools.ts +89 -0
- package/src/host-client/transfer.ts +126 -0
- package/src/host-client/vice-session.ts +316 -0
- package/src/knowledge/database.ts +132 -0
- package/src/knowledge/history.ts +163 -0
- package/src/knowledge/import.ts +391 -0
- package/src/knowledge/read.ts +342 -0
- package/src/knowledge/schema.ts +229 -0
- package/src/knowledge/write.ts +663 -0
- package/src/mcp/main.ts +134 -0
- package/src/mcp/output.ts +32 -0
- package/src/mcp/server.ts +316 -0
- package/src/mcp/tools/debug.ts +327 -0
- package/src/mcp/tools/execution.ts +117 -0
- package/src/mcp/tools/input.ts +72 -0
- package/src/mcp/tools/machine.ts +100 -0
- package/src/mcp/tools/media.ts +129 -0
- package/src/mcp/tools/memory.ts +250 -0
- package/src/mcp/tools/video.ts +271 -0
- package/src/native/acme.ts +116 -0
- package/src/native/discover.ts +179 -0
- package/src/native/dxa.ts +166 -0
- package/src/native/ghidra/analyze.ts +86 -0
- package/src/native/ghidra/index.ts +173 -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 +274 -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 +113 -0
- package/src/native/types.ts +257 -0
- package/src/native/watchdog.ts +78 -0
- package/src/package.ts +21 -0
- package/src/platform.ts +441 -0
- package/src/project.ts +39 -0
- package/src/protocol/framing.ts +132 -0
- package/src/protocol/messages.ts +259 -0
- package/src/protocol/tools.ts +266 -0
- package/src/protocol/vice.ts +1348 -0
- package/src/script.ts +60 -0
- package/src/time.ts +15 -0
- package/src/trace.ts +194 -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
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// The long-lived Host Runtime connection an MCP process owns: one connection,
|
|
2
|
+
// one VICE. Exposes C64-domain operations only.
|
|
3
|
+
|
|
4
|
+
import { ProtocolError, WireFailure, type VideoStandard } from "../protocol/messages.ts";
|
|
5
|
+
import { validateViceResult, type Breakpoint, type BreakpointParams, type PointRemoved, type Watchpoint, type WatchpointParams, type ExecutionParams, type ExecutionResult, type JoystickState, type MachineStatus, type MemmapRange, type MemoryWriteParams, type RegisterValues, type ResetMode, type RunState, type RunTarget, type RunUntilResult, type MemoryReadParams, type MemoryReadResult, type Registers, type Space, type ViceOperation, type ViceOperations } from "../protocol/vice.ts";
|
|
6
|
+
import { HostConnection } from "./connect.ts";
|
|
7
|
+
import { readProjectFile } from "./transfer.ts";
|
|
8
|
+
|
|
9
|
+
const HOST_LOST =
|
|
10
|
+
"The connection to the c64-re-tools host runtime was lost. The emulator and its machine state are gone: memory, registers, breakpoints, snapshots and the loaded program. " +
|
|
11
|
+
"The next call starts a new machine when the Host Runtime (c64-re-tools-host) runs. Load the program again and set the breakpoints again.";
|
|
12
|
+
const SESSION_CLOSED = "The C64 session is closed because the c64-re-tools MCP server stops.";
|
|
13
|
+
const LOST_BEFORE_ANSWER = "The connection to the c64-re-tools host runtime was lost before the emulator answered. No machine state was lost. Do the call again.";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The loss of a session that never answered (D57): it had no machine state,
|
|
17
|
+
* so the caller can do the call again on a new session.
|
|
18
|
+
*/
|
|
19
|
+
export class LostBeforeAnswer extends WireFailure {
|
|
20
|
+
constructor() {
|
|
21
|
+
super("machine-unavailable", LOST_BEFORE_ANSWER);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface Pending {
|
|
26
|
+
op: ViceOperation;
|
|
27
|
+
resolve: (value: unknown, attachments: Buffer[]) => void;
|
|
28
|
+
reject: (error: Error) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class ViceSessionClient {
|
|
32
|
+
/** Settles when the session is gone, whether we closed it or the host went away. */
|
|
33
|
+
readonly closed: Promise<void>;
|
|
34
|
+
readonly #connection: HostConnection;
|
|
35
|
+
readonly #pending = new Map<number, Pending>();
|
|
36
|
+
#nextId = 1;
|
|
37
|
+
#lost: WireFailure | undefined;
|
|
38
|
+
#answered = false;
|
|
39
|
+
#lossReported = false;
|
|
40
|
+
|
|
41
|
+
private constructor(connection: HostConnection) {
|
|
42
|
+
this.#connection = connection;
|
|
43
|
+
connection.onMessage((message, attachments) => {
|
|
44
|
+
// Anything but a reply after the handshake breaks the contract.
|
|
45
|
+
if (message.type !== "reply") return this.#fail();
|
|
46
|
+
const pending = this.#pending.get(message.id);
|
|
47
|
+
if (pending === undefined) return; // not ours; ignore
|
|
48
|
+
this.#pending.delete(message.id);
|
|
49
|
+
this.#answered = true;
|
|
50
|
+
if ("error" in message) {
|
|
51
|
+
pending.reject(new WireFailure(message.error.code, message.error.message));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
pending.resolve(validateViceResult(pending.op, message.result), attachments);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
pending.reject(
|
|
58
|
+
new WireFailure(
|
|
59
|
+
"operation-failed",
|
|
60
|
+
error instanceof ProtocolError ? "The c64-re-tools host runtime sent an invalid reply." : "The c64-re-tools MCP server could not read the reply of the host runtime.",
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
this.#fail();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
this.closed = connection.closed.then(() => {
|
|
67
|
+
this.#lost ??= this.#answered ? new WireFailure("machine-state-lost", HOST_LOST) : new LostBeforeAnswer();
|
|
68
|
+
this.#lossReported = this.#pending.size > 0;
|
|
69
|
+
for (const pending of this.#pending.values()) pending.reject(this.#lost);
|
|
70
|
+
this.#pending.clear();
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Connects and waits until the host has started this session's emulator; `signal` stops the wait at once. */
|
|
75
|
+
static async open(options: { videoStandard: VideoStandard; env?: NodeJS.ProcessEnv; signal?: AbortSignal }): Promise<ViceSessionClient> {
|
|
76
|
+
const connection = await HostConnection.open({
|
|
77
|
+
role: "vice-session",
|
|
78
|
+
videoStandard: options.videoStandard,
|
|
79
|
+
...(options.env === undefined ? {} : { env: options.env }),
|
|
80
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
81
|
+
});
|
|
82
|
+
return new ViceSessionClient(connection);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
status(): Promise<MachineStatus> {
|
|
86
|
+
return this.#request("status", {});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
memoryRead(params: MemoryReadParams): Promise<MemoryReadResult> {
|
|
90
|
+
return this.#request("memoryRead", params);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The I/O registers at $d000-$dfff as the chips hold them, without side effects. */
|
|
94
|
+
ioRead(address: number, size: number): Promise<MemoryReadResult> {
|
|
95
|
+
return this.#request("ioRead", { address, size });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
registersGet(space: Space): Promise<Registers> {
|
|
99
|
+
return this.#request("registersGet", { space });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
memoryWrite(params: MemoryWriteParams): Promise<{ address: number; bytesWritten: number }> {
|
|
103
|
+
return this.#request("memoryWrite", params);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
memorySearch(params: ViceOperations["memorySearch"]["params"]): Promise<{ matches: number[] }> {
|
|
107
|
+
return this.#request("memorySearch", params);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
memoryCompare(params: ViceOperations["memoryCompare"]["params"]): Promise<ViceOperations["memoryCompare"]["result"]> {
|
|
111
|
+
return this.#request("memoryCompare", params);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
disassemble(params: ViceOperations["disassemble"]["params"]): Promise<ViceOperations["disassemble"]["result"]> {
|
|
115
|
+
return this.#request("disassemble", params);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
registersSet(space: Space, values: RegisterValues): Promise<Registers> {
|
|
119
|
+
return this.#request("registersSet", { space, values });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
execution(params: ExecutionParams): Promise<ExecutionResult> {
|
|
123
|
+
return this.#request("execution", params);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
runUntil(params: { target: RunTarget; timeoutFrames: number }): Promise<RunUntilResult> {
|
|
127
|
+
return this.#request("runUntil", params);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Loads a project PRG into memory without reset or start; finishes stopped. */
|
|
131
|
+
async programLoad(params: { path: string; address?: number }): Promise<{ state: RunState; loadAddress: number; size: number }> {
|
|
132
|
+
const file = readProjectFile(params.path);
|
|
133
|
+
return this.#request("programLoad", params.address === undefined ? {} : { address: params.address }, [file.bytes]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Autostarts a project program or image, as VICE's autostart does. */
|
|
137
|
+
async autostart(params: { path: string; index: number; run: boolean }): Promise<{ state: RunState; emulatorFlaw?: string }> {
|
|
138
|
+
const file = readProjectFile(params.path);
|
|
139
|
+
return this.#request("autostart", { type: file.type, index: params.index, run: params.run }, [file.bytes]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Attaches a project disk image to drive 8. */
|
|
143
|
+
async diskAttach(params: { path: string }): Promise<{ attached: boolean }> {
|
|
144
|
+
const file = readProjectFile(params.path);
|
|
145
|
+
return this.#request("diskAttach", { type: file.type }, [file.bytes]);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
reset(params: { mode: ResetMode; run: boolean }): Promise<{ state: RunState; pc?: number }> {
|
|
149
|
+
return this.#request("reset", params);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Queues PETSCII bytes for the C64 keyboard. */
|
|
153
|
+
keyboard(petscii: Uint8Array): Promise<{ queuedBytes: number }> {
|
|
154
|
+
return this.#request("keyboard", { data: Buffer.from(petscii).toString("hex") });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
joystick(state: JoystickState): Promise<JoystickState> {
|
|
158
|
+
return this.#request("joystick", state);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Action list gives all breakpoints, remove gives `{removed, id}`; add, enable and disable give the one breakpoint. */
|
|
162
|
+
breakpoint(params: Extract<BreakpointParams, { action: "list" }>): Promise<{ breakpoints: Breakpoint[] }>;
|
|
163
|
+
breakpoint(params: Extract<BreakpointParams, { action: "remove" }>): Promise<PointRemoved>;
|
|
164
|
+
breakpoint(params: Exclude<BreakpointParams, { action: "list" | "remove" }>): Promise<Breakpoint>;
|
|
165
|
+
breakpoint(params: BreakpointParams): Promise<Breakpoint | PointRemoved | { breakpoints: Breakpoint[] }> {
|
|
166
|
+
return this.#request("breakpoint", params);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Action list gives all watchpoints, remove gives `{removed, id}`; add, enable and disable give the one watchpoint. */
|
|
170
|
+
watchpoint(params: Extract<WatchpointParams, { action: "list" }>): Promise<{ watchpoints: Watchpoint[] }>;
|
|
171
|
+
watchpoint(params: Extract<WatchpointParams, { action: "remove" }>): Promise<PointRemoved>;
|
|
172
|
+
watchpoint(params: Exclude<WatchpointParams, { action: "list" | "remove" }>): Promise<Watchpoint>;
|
|
173
|
+
watchpoint(params: WatchpointParams): Promise<Watchpoint | PointRemoved | { watchpoints: Watchpoint[] }> {
|
|
174
|
+
return this.#request("watchpoint", params);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
cpuHistory(params: ViceOperations["cpuHistory"]["params"]): Promise<ViceOperations["cpuHistory"]["result"]> {
|
|
178
|
+
return this.#request("cpuHistory", params);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
backtrace(params: ViceOperations["backtrace"]["params"]): Promise<ViceOperations["backtrace"]["result"]> {
|
|
182
|
+
return this.#request("backtrace", params);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
timing(action: ViceOperations["timing"]["params"]["action"]): Promise<ViceOperations["timing"]["result"]> {
|
|
186
|
+
return this.#request("timing", { action });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
profile(limit: number): Promise<ViceOperations["profile"]["result"]> {
|
|
190
|
+
return this.#request("profile", { limit });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Action read gives the ranges; action clear gives cleared. */
|
|
194
|
+
memmap(params: Extract<ViceOperations["memmap"]["params"], { action: "read" }>): Promise<{ ranges: MemmapRange[]; next?: number }>;
|
|
195
|
+
memmap(params: { action: "clear" }): Promise<{ cleared: boolean }>;
|
|
196
|
+
memmap(params: ViceOperations["memmap"]["params"]): Promise<ViceOperations["memmap"]["result"]> {
|
|
197
|
+
return this.#request("memmap", params);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
vicii(): Promise<ViceOperations["vicii"]["result"]> {
|
|
201
|
+
return this.#request("vicii", {});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Empty `indexes` means all eight sprites. */
|
|
205
|
+
sprites(indexes: number[]): Promise<ViceOperations["sprites"]["result"]> {
|
|
206
|
+
return this.#request("sprites", { indexes });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
cia(which: ViceOperations["cia"]["params"]["which"]): Promise<ViceOperations["cia"]["result"]> {
|
|
210
|
+
return this.#request("cia", { which });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
sid(): Promise<ViceOperations["sid"]["result"]> {
|
|
214
|
+
return this.#request("sid", {});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
observe(params: ViceOperations["observe"]["params"]): Promise<ViceOperations["observe"]["result"]> {
|
|
218
|
+
return this.#request("observe", params);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The last frame the VIC-II drew, as a base64 PNG with its size. */
|
|
222
|
+
screenCapture(baseline?: string): Promise<ViceOperations["screenCapture"]["result"]> {
|
|
223
|
+
return this.#request("screenCapture", baseline === undefined ? {} : { baseline });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
screenCompare(params: ViceOperations["screenCompare"]["params"]): Promise<ViceOperations["screenCompare"]["result"]> {
|
|
227
|
+
return this.#request("screenCompare", params);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
snapshot(params: ViceOperations["snapshot"]["params"]): Promise<ViceOperations["snapshot"]["result"]> {
|
|
231
|
+
return this.#request("snapshot", params);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** The bytes of a named snapshot, as a .vsf file holds them (D41). */
|
|
235
|
+
async snapshotExport(name: string): Promise<Buffer> {
|
|
236
|
+
const { attachments } = await this.#requestWithAttachments("snapshotFile", { action: "export", name });
|
|
237
|
+
const [bytes] = attachments;
|
|
238
|
+
if (bytes === undefined || attachments.length !== 1) throw new WireFailure("operation-failed", "The c64-re-tools host runtime sent no snapshot file.");
|
|
239
|
+
return bytes;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Restores the machine from the bytes of a .vsf file; the CPU stays stopped. */
|
|
243
|
+
snapshotRestore(bytes: Uint8Array): Promise<ViceOperations["snapshotFile"]["result"]> {
|
|
244
|
+
return this.#request("snapshotFile", { action: "restore" }, [bytes]);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
screenBaselines(): Promise<{ baselines: string[] }> {
|
|
248
|
+
return this.#request("screenBaselines", {});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
screenDiscard(baseline: string): Promise<{ discarded: boolean }> {
|
|
252
|
+
return this.#request("screenDiscard", { baseline });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
warp(enabled: boolean): Promise<{ enabled: boolean }> {
|
|
256
|
+
return this.#request("warp", { enabled });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Moves the machine into a VICE with a window (open) or back into a headless one (close). */
|
|
260
|
+
window(action: ViceOperations["window"]["params"]["action"]): Promise<ViceOperations["window"]["result"]> {
|
|
261
|
+
return this.#request("window", { action });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** True when the connection to the host is gone, whatever the reason. */
|
|
265
|
+
get lost(): boolean {
|
|
266
|
+
return this.#lost !== undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** True when the emulator answered a request: then the session had machine state to lose (D57). */
|
|
270
|
+
get answered(): boolean {
|
|
271
|
+
return this.#answered;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** The loss of a lost session, when no request got it; undefined while the session lives (D57). */
|
|
275
|
+
get unreportedLoss(): WireFailure | undefined {
|
|
276
|
+
return this.#lost?.code === "machine-state-lost" && !this.#lossReported ? this.#lost : undefined;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Ends the session; the host stops its emulator. Open and later requests fail as closed. */
|
|
280
|
+
async close(): Promise<void> {
|
|
281
|
+
this.#lost ??= new WireFailure("machine-unavailable", SESSION_CLOSED);
|
|
282
|
+
await this.#connection.close();
|
|
283
|
+
await this.closed;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async #request<O extends ViceOperation>(
|
|
287
|
+
op: O,
|
|
288
|
+
params: ViceOperations[O]["params"],
|
|
289
|
+
attachments: readonly Uint8Array[] = [],
|
|
290
|
+
): Promise<ViceOperations[O]["result"]> {
|
|
291
|
+
return (await this.#requestWithAttachments(op, params, attachments)).result;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** One request; the reply's result and the attachments that came with it. */
|
|
295
|
+
#requestWithAttachments<O extends ViceOperation>(
|
|
296
|
+
op: O,
|
|
297
|
+
params: ViceOperations[O]["params"],
|
|
298
|
+
attachments: readonly Uint8Array[] = [],
|
|
299
|
+
): Promise<{ result: ViceOperations[O]["result"]; attachments: Buffer[] }> {
|
|
300
|
+
if (this.#lost !== undefined) {
|
|
301
|
+
this.#lossReported = true;
|
|
302
|
+
return Promise.reject(this.#lost);
|
|
303
|
+
}
|
|
304
|
+
const id = this.#nextId;
|
|
305
|
+
this.#nextId = this.#nextId >= 0xffff_ffff ? 1 : this.#nextId + 1;
|
|
306
|
+
return new Promise((resolve, reject) => {
|
|
307
|
+
this.#pending.set(id, { op, resolve: (result, received) => resolve({ result: result as ViceOperations[O]["result"], attachments: received }), reject });
|
|
308
|
+
this.#connection.send({ type: "request", id, op, params }, attachments);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Drops a connection whose host broke the protocol; pending work fails as lost. */
|
|
313
|
+
#fail(): void {
|
|
314
|
+
void this.#connection.close();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// The one place that opens .c64-re-tools/knowledge.db.
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
import { projectRoot } from "../project.ts";
|
|
8
|
+
import { checkVersion, ensureSchema, schemaVersion } from "./schema.ts";
|
|
9
|
+
|
|
10
|
+
export const KNOWLEDGE_DIRECTORY = ".c64-re-tools";
|
|
11
|
+
export const KNOWLEDGE_FILE = "knowledge.db";
|
|
12
|
+
|
|
13
|
+
export const KNOWLEDGE_ERROR_CODES = [
|
|
14
|
+
"invalid-input",
|
|
15
|
+
"not-found",
|
|
16
|
+
"conflict",
|
|
17
|
+
"stale-revision",
|
|
18
|
+
"invalid-database",
|
|
19
|
+
"unsupported-schema",
|
|
20
|
+
] as const;
|
|
21
|
+
export type KnowledgeErrorCode = (typeof KNOWLEDGE_ERROR_CODES)[number];
|
|
22
|
+
|
|
23
|
+
/** A refused knowledge operation, with a stable code and an actionable message. */
|
|
24
|
+
export class KnowledgeError extends Error {
|
|
25
|
+
override name = "KnowledgeError";
|
|
26
|
+
readonly code: KnowledgeErrorCode;
|
|
27
|
+
|
|
28
|
+
constructor(code: KnowledgeErrorCode, message: string) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.code = code;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function knowledgePath(root: string = projectRoot()): string {
|
|
35
|
+
return join(root, KNOWLEDGE_DIRECTORY, KNOWLEDGE_FILE);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function configure(db: DatabaseSync): void {
|
|
39
|
+
// A committed write must be complete in knowledge.db itself: rollback journal, never WAL.
|
|
40
|
+
db.exec("PRAGMA journal_mode = DELETE; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function open(path: string, readOnly: boolean): DatabaseSync {
|
|
44
|
+
let db: DatabaseSync;
|
|
45
|
+
try {
|
|
46
|
+
db = new DatabaseSync(path, { readOnly });
|
|
47
|
+
configure(db);
|
|
48
|
+
db.prepare("SELECT count(*) FROM sqlite_master").get();
|
|
49
|
+
} catch (error) {
|
|
50
|
+
throw new KnowledgeError("invalid-database", `${KNOWLEDGE_DIRECTORY}/${KNOWLEDGE_FILE} is not a readable SQLite database: ${(error as Error).message}`);
|
|
51
|
+
}
|
|
52
|
+
return db;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Opens the project's knowledge database read-only, as it is: a read never
|
|
57
|
+
* migrates and never writes. Returns undefined when the database does not
|
|
58
|
+
* exist or has no schema yet: that reads as empty knowledge, and a read
|
|
59
|
+
* creates nothing. Refuses a schema version other than this build's.
|
|
60
|
+
*/
|
|
61
|
+
export function openForRead(root: string = projectRoot()): DatabaseSync | undefined {
|
|
62
|
+
const path = knowledgePath(root);
|
|
63
|
+
if (!existsSync(path)) return undefined;
|
|
64
|
+
const db = open(path, true);
|
|
65
|
+
try {
|
|
66
|
+
const version = schemaVersion(db);
|
|
67
|
+
if (version === 0) {
|
|
68
|
+
db.close();
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
checkVersion(version);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
db.close();
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
return db;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Opens the project's knowledge database for a write, creating
|
|
81
|
+
* .c64-re-tools/knowledge.db and its schema when needed. Refuses a schema
|
|
82
|
+
* version other than this build's.
|
|
83
|
+
*/
|
|
84
|
+
export function openForWrite(root: string = projectRoot()): DatabaseSync {
|
|
85
|
+
mkdirSync(join(root, KNOWLEDGE_DIRECTORY), { recursive: true });
|
|
86
|
+
const db = open(knowledgePath(root), false);
|
|
87
|
+
try {
|
|
88
|
+
ensureSchema(db);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
db.close();
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
return db;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Runs one write on the project's knowledge database and closes it. When the
|
|
98
|
+
* project has no knowledge.db yet, `work` first runs on an empty database in
|
|
99
|
+
* memory: a write that is refused there creates no .c64-re-tools/. `work`
|
|
100
|
+
* must change nothing outside the database it gets.
|
|
101
|
+
*/
|
|
102
|
+
export function withWrite<T>(work: (db: DatabaseSync) => T, root: string = projectRoot()): T {
|
|
103
|
+
if (!existsSync(knowledgePath(root))) {
|
|
104
|
+
const trial = new DatabaseSync(":memory:");
|
|
105
|
+
try {
|
|
106
|
+
configure(trial);
|
|
107
|
+
ensureSchema(trial);
|
|
108
|
+
work(trial);
|
|
109
|
+
} finally {
|
|
110
|
+
trial.close();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const db = openForWrite(root);
|
|
114
|
+
try {
|
|
115
|
+
return work(db);
|
|
116
|
+
} finally {
|
|
117
|
+
db.close();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Runs `work` in one immediate transaction: all of it commits, or none of it does. */
|
|
122
|
+
export function transaction<T>(db: DatabaseSync, work: () => T): T {
|
|
123
|
+
db.exec("BEGIN IMMEDIATE");
|
|
124
|
+
try {
|
|
125
|
+
const result = work();
|
|
126
|
+
db.exec("COMMIT");
|
|
127
|
+
return result;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
db.exec("ROLLBACK");
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Knowledge history: what used to be known, which revision changed
|
|
2
|
+
// it, who changed it and why. History rows are never deleted.
|
|
3
|
+
|
|
4
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
5
|
+
|
|
6
|
+
import { KnowledgeError } from "./database.ts";
|
|
7
|
+
import {
|
|
8
|
+
COMMENT_COLUMNS,
|
|
9
|
+
MODULE_COLUMNS,
|
|
10
|
+
REFERENCE_COLUMNS,
|
|
11
|
+
REGION_COLUMNS,
|
|
12
|
+
SUPPRESSION_COLUMNS,
|
|
13
|
+
SYMBOL_COLUMNS,
|
|
14
|
+
toComment,
|
|
15
|
+
toModule,
|
|
16
|
+
toReference,
|
|
17
|
+
toRegion,
|
|
18
|
+
toSuppression,
|
|
19
|
+
toSymbol,
|
|
20
|
+
type CommentRow,
|
|
21
|
+
type ModuleRow,
|
|
22
|
+
type ReferenceRow,
|
|
23
|
+
type RegionRow,
|
|
24
|
+
type SuppressionRow,
|
|
25
|
+
type SymbolRow,
|
|
26
|
+
} from "./read.ts";
|
|
27
|
+
import { DEFAULT_SPACE, type EvidenceKind, type Origin, type Space } from "./schema.ts";
|
|
28
|
+
import type { Evidence } from "./write.ts";
|
|
29
|
+
|
|
30
|
+
export interface Revision {
|
|
31
|
+
id: number;
|
|
32
|
+
createdAt: string;
|
|
33
|
+
origin: Origin;
|
|
34
|
+
operation: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
inputHash?: string;
|
|
37
|
+
toolVersion?: string;
|
|
38
|
+
/** What the change rests on (D42). */
|
|
39
|
+
evidence?: Evidence[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** One knowledge row with its validity: from its first revision until the revision that closed it. */
|
|
43
|
+
export type HistoryEntry =
|
|
44
|
+
| { entity: "symbol"; row: SymbolRow; from: number; to?: number }
|
|
45
|
+
| { entity: "region"; row: RegionRow; from: number; to?: number }
|
|
46
|
+
| { entity: "comment"; row: CommentRow; from: number; to?: number }
|
|
47
|
+
| { entity: "reference"; row: ReferenceRow; from: number; to?: number }
|
|
48
|
+
| { entity: "suppression"; row: SuppressionRow; from: number; to?: number }
|
|
49
|
+
| { entity: "module"; row: ModuleRow; from: number; to?: number };
|
|
50
|
+
|
|
51
|
+
type Raw = Record<string, unknown>;
|
|
52
|
+
|
|
53
|
+
function toRevision(row: Raw): Revision {
|
|
54
|
+
const revision: Revision = {
|
|
55
|
+
id: row.id as number,
|
|
56
|
+
createdAt: row.created_at as string,
|
|
57
|
+
origin: row.origin as Origin,
|
|
58
|
+
operation: row.operation as string,
|
|
59
|
+
};
|
|
60
|
+
if (row.description !== null) revision.description = row.description as string;
|
|
61
|
+
if (row.input_hash !== null) revision.inputHash = row.input_hash as string;
|
|
62
|
+
if (row.tool_version !== null) revision.toolVersion = row.tool_version as string;
|
|
63
|
+
return revision;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function entry<E extends HistoryEntry["entity"]>(entity: E, row: unknown, raw: Raw): HistoryEntry {
|
|
67
|
+
const result = { entity, row, from: raw.valid_from_revision as number } as HistoryEntry;
|
|
68
|
+
if (raw.valid_to_revision !== null) result.to = raw.valid_to_revision as number;
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function bySequence(a: HistoryEntry, b: HistoryEntry): number {
|
|
73
|
+
return a.from - b.from || (a.to ?? Infinity) - (b.to ?? Infinity);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Every row, current or closed, that concerns one address of a space, oldest first. */
|
|
77
|
+
export function historyAt(db: DatabaseSync | undefined, address: number, space: Space = DEFAULT_SPACE): HistoryEntry[] {
|
|
78
|
+
if (db === undefined) return [];
|
|
79
|
+
const validity = ", valid_to_revision";
|
|
80
|
+
const entries: HistoryEntry[] = [];
|
|
81
|
+
for (const raw of db.prepare(`SELECT ${SYMBOL_COLUMNS}${validity} FROM symbols WHERE space = ? AND address = ?`).all(space, address) as Raw[]) entries.push(entry("symbol", toSymbol(raw), raw));
|
|
82
|
+
for (const raw of db.prepare(`SELECT ${REGION_COLUMNS}${validity} FROM regions WHERE space = ? AND start_address <= ? AND end_address >= ?`).all(space, address, address) as Raw[]) {
|
|
83
|
+
entries.push(entry("region", toRegion(raw), raw));
|
|
84
|
+
}
|
|
85
|
+
for (const raw of db.prepare(`SELECT ${COMMENT_COLUMNS}${validity} FROM comments WHERE space = ? AND address = ?`).all(space, address) as Raw[]) entries.push(entry("comment", toComment(raw), raw));
|
|
86
|
+
for (const raw of db.prepare(`SELECT ${REFERENCE_COLUMNS}${validity} FROM "references" WHERE space = ? AND (from_address = ? OR to_address = ?)`).all(space, address, address) as Raw[]) {
|
|
87
|
+
entries.push(entry("reference", toReference(raw), raw));
|
|
88
|
+
}
|
|
89
|
+
for (const raw of db
|
|
90
|
+
.prepare(`SELECT ${SUPPRESSION_COLUMNS}${validity} FROM suppressions WHERE space = ? AND ((start_address <= ? AND end_address >= ?) OR to_address = ?)`)
|
|
91
|
+
.all(space, address, address, address) as Raw[]) {
|
|
92
|
+
entries.push(entry("suppression", toSuppression(raw), raw));
|
|
93
|
+
}
|
|
94
|
+
for (const raw of db.prepare(`SELECT ${MODULE_COLUMNS}${validity} FROM modules WHERE space = ? AND start_address = ?`).all(space, address) as Raw[]) entries.push(entry("module", toModule(raw), raw));
|
|
95
|
+
return entries.sort(bySequence);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Every symbol row of a space that ever had this name, oldest first: a name can move between addresses. */
|
|
99
|
+
export function historyNamed(db: DatabaseSync | undefined, name: string, space: Space = DEFAULT_SPACE): HistoryEntry[] {
|
|
100
|
+
if (db === undefined) return [];
|
|
101
|
+
return (db.prepare(`SELECT ${SYMBOL_COLUMNS}, valid_to_revision FROM symbols WHERE space = ? AND name = ?`).all(space, name) as Raw[])
|
|
102
|
+
.map((raw) => entry("symbol", toSymbol(raw), raw))
|
|
103
|
+
.sort(bySequence);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Why a revision was written and what it rests on: its description and its evidence, when it has them. */
|
|
107
|
+
export function revisionNote(db: DatabaseSync | undefined, id: number): { reason?: string; evidence?: Evidence[] } {
|
|
108
|
+
const raw = db?.prepare("SELECT * FROM revisions WHERE id = ?").get(id) as Raw | undefined;
|
|
109
|
+
if (db === undefined || raw === undefined) return {};
|
|
110
|
+
const { description, evidence } = withEvidence(db, toRevision(raw));
|
|
111
|
+
return { ...(description === undefined ? {} : { reason: description }), ...(evidence === undefined ? {} : { evidence }) };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The revision with its evidence, when it has any. */
|
|
115
|
+
function withEvidence(db: DatabaseSync, revision: Revision): Revision {
|
|
116
|
+
const rows = db.prepare("SELECT kind, ref, sha256 FROM evidence WHERE revision = ? ORDER BY id").all(revision.id) as Raw[];
|
|
117
|
+
if (rows.length === 0) return revision;
|
|
118
|
+
const evidence = rows.map((row): Evidence => ({ kind: row.kind as EvidenceKind, ref: row.ref as string, ...(row.sha256 === null ? {} : { sha256: row.sha256 as string }) }));
|
|
119
|
+
return { ...revision, evidence };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Revisions, newest first; with `before`, only revisions older than it. */
|
|
123
|
+
export function revisions(db: DatabaseSync | undefined, options: { limit?: number; before?: number } = {}): Revision[] {
|
|
124
|
+
if (db === undefined) return [];
|
|
125
|
+
return (
|
|
126
|
+
db
|
|
127
|
+
.prepare("SELECT * FROM revisions WHERE id < ? ORDER BY id DESC LIMIT ?")
|
|
128
|
+
.all(options.before ?? Number.MAX_SAFE_INTEGER, options.limit ?? 50) as Raw[]
|
|
129
|
+
).map((row) => withEvidence(db, toRevision(row)));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface RevisionChanges {
|
|
133
|
+
revision: Revision;
|
|
134
|
+
/** Rows this revision made current. */
|
|
135
|
+
added: HistoryEntry[];
|
|
136
|
+
/** Rows this revision closed. */
|
|
137
|
+
closed: HistoryEntry[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** One revision and exactly what it changed. */
|
|
141
|
+
export function revision(db: DatabaseSync | undefined, id: number): RevisionChanges {
|
|
142
|
+
const raw = db?.prepare("SELECT * FROM revisions WHERE id = ?").get(id) as Raw | undefined;
|
|
143
|
+
if (db === undefined || raw === undefined) throw new KnowledgeError("not-found", `There is no revision ${id}.`);
|
|
144
|
+
const tables = [
|
|
145
|
+
["symbol", "symbols", SYMBOL_COLUMNS, toSymbol],
|
|
146
|
+
["region", "regions", REGION_COLUMNS, toRegion],
|
|
147
|
+
["comment", "comments", COMMENT_COLUMNS, toComment],
|
|
148
|
+
["reference", '"references"', REFERENCE_COLUMNS, toReference],
|
|
149
|
+
["suppression", "suppressions", SUPPRESSION_COLUMNS, toSuppression],
|
|
150
|
+
["module", "modules", MODULE_COLUMNS, toModule],
|
|
151
|
+
] as const;
|
|
152
|
+
const added: HistoryEntry[] = [];
|
|
153
|
+
const closed: HistoryEntry[] = [];
|
|
154
|
+
for (const [entity, table, columns, map] of tables) {
|
|
155
|
+
for (const row of db.prepare(`SELECT ${columns}, valid_to_revision FROM ${table} WHERE valid_from_revision = ?`).all(id) as Raw[]) {
|
|
156
|
+
added.push(entry(entity, map(row), row));
|
|
157
|
+
}
|
|
158
|
+
for (const row of db.prepare(`SELECT ${columns}, valid_to_revision FROM ${table} WHERE valid_to_revision = ?`).all(id) as Raw[]) {
|
|
159
|
+
closed.push(entry(entity, map(row), row));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { revision: withEvidence(db, toRevision(raw)), added, closed };
|
|
163
|
+
}
|