@henols/c64-re-tools 0.2.1 → 0.2.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.
- package/README.md +1 -1
- package/THIRD-PARTY-NOTICES.md +26 -0
- package/bin/cli.mjs +18 -7
- package/package.json +6 -4
- package/skills/acme-build/SKILL.md +83 -33
- package/skills/acme-build/scripts/acme.mjs +159 -64
- package/skills/acme-build/template.a +1 -1
- package/skills/c64-disk-access/SKILL.md +156 -0
- package/skills/c64-disk-access/scripts/c1541.mjs +569 -0
- package/skills/c64-memory-mapping/SKILL.md +419 -23
- package/skills/c64-memory-mapping/scripts/driver.mjs +1 -1
- package/skills/c64-petcat/SKILL.md +87 -0
- package/skills/c64-petcat/scripts/petcat.mjs +221 -0
- package/skills/c64-program-recon/SKILL.md +497 -92
- package/skills/c64-program-recon/references/control-flow.md +12 -15
- package/skills/c64-program-recon/references/graphics.md +1 -1
- package/skills/c64-program-recon/references/observation-hazards.md +18 -16
- package/skills/c64-program-recon/references/reconstruction.md +11 -6
- package/skills/c64-program-recon/references/sound-and-input.md +6 -8
- package/skills/c64-program-recon/references/tool-selection.md +37 -18
- package/skills/c64-program-recon/scripts/packer-finding.mjs +709 -0
- package/skills/c64-program-recon/templates/memory-map.template.md +27 -13
- package/skills/c64-provenance-diff/SKILL.md +43 -8
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +9 -6
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +20 -8
- package/skills/c64-ram-capture/RELEASES.json.example +17 -0
- package/skills/c64-ram-capture/SKILL.md +147 -46
- package/skills/c64-ram-capture/scripts/compare.mjs +2 -2
- package/skills/c64-ram-capture/scripts/derive-transients.mjs +575 -0
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +3 -3
- package/skills/c64-ram-capture/scripts/mcp-module.mjs +174 -0
- package/skills/c64-ram-capture/scripts/project-paths.mjs +1 -1
- package/skills/c64-ram-capture/scripts/releases.mjs +1 -1
- package/skills/c64-ram-capture/scripts/vsf-slice.mjs +147 -0
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +19 -13
- package/skills/c64-ram-capture/templates/capture-record.template.md +44 -4
- package/skills/c64-ram-capture/transients/README.md +136 -0
- package/skills/routine-queue-walker/SKILL.md +365 -0
- package/skills/routine-queue-walker/scripts/completeness-report.mjs +463 -0
- package/skills/vice-wedge-triage/SKILL.md +104 -97
- package/skills/c64-provenance-diff/scripts/diff-images.test.mjs +0 -665
- package/skills/c64-ram-capture/scripts/d64-parse.mjs +0 -243
- package/skills/c64-ram-capture/scripts/d64-parse.test.mjs +0 -243
- package/skills/c64-ram-capture/scripts/dump-artifacts.test.mjs +0 -133
- package/skills/c64-ram-capture/scripts/test-corpus.mjs +0 -75
- package/skills/c64-ram-capture/scripts/watch-loads.test.mjs +0 -339
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// petcat -> BASIC detokenization + SYS-handover driver. One capability:
|
|
3
|
+
// decode a BASIC program's stub to readable text and resolve the numeric
|
|
4
|
+
// address it hands over to machine code, when that address is a literal.
|
|
5
|
+
//
|
|
6
|
+
// Reached ONLY through the host-tool execution seam -- the project owner's
|
|
7
|
+
// rule of 2026-08-28 is that this script runs container-side, `petcat` lives
|
|
8
|
+
// host-side, and there is no container PATH to find it on. This file never spawns `petcat` itself; it constructs a
|
|
9
|
+
// TYPED request (`petcat.decode`) and reads the produced listing file back
|
|
10
|
+
// off the shared workspace tree, mirroring
|
|
11
|
+
// src/skills/c64-disk-access/scripts/c1541.mjs's own invokeSeam() shape
|
|
12
|
+
// verbatim.
|
|
13
|
+
//
|
|
14
|
+
// WHAT NOT TO DO:
|
|
15
|
+
// - Never spawn the conversion binary (`petcat`) directly from this
|
|
16
|
+
// script, even as a "just this once" fallback. A direct call works on
|
|
17
|
+
// the developer's own host and silently fails inside a container -- the
|
|
18
|
+
// exact failure this seam exists to remove.
|
|
19
|
+
// - Never guess an entry point when the seam reports a null one. A
|
|
20
|
+
// guessed address is spent on a disassembler downstream, and a wrong
|
|
21
|
+
// one is expensive there -- report the decline and its reason exactly
|
|
22
|
+
// as the seam gave them, never a fallback value.
|
|
23
|
+
import { dirname, relative, isAbsolute, resolve, sep } from "node:path";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { spawn } from "node:child_process";
|
|
26
|
+
|
|
27
|
+
import { resolveMcpModule, refusalMessage } from "../../c64-ram-capture/scripts/mcp-module.mjs";
|
|
28
|
+
|
|
29
|
+
const SELF = fileURLToPath(import.meta.url);
|
|
30
|
+
const HERE = dirname(SELF);
|
|
31
|
+
|
|
32
|
+
/** The MCP-side module this script reaches -- never imported statically
|
|
33
|
+
* (cross-package: this file ships in `@henols/c64-re-tools`, the seam client
|
|
34
|
+
* ships in `@henols/vice-mcp`), only located via the ladder and invoked with
|
|
35
|
+
* `process.execPath`, the interpreter already running this script, on an
|
|
36
|
+
* in-tree module -- not an external host binary. */
|
|
37
|
+
const HOST_TOOL_CLIENT_FILE = "host-tool-client.ts";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Invokes the host-tool execution seam for `tool`/`args`, rooted at
|
|
41
|
+
* `repoRoot` for THIS invocation's workspace-relative path resolution.
|
|
42
|
+
* Never rejects: a resolution failure, a spawn failure, or unparseable
|
|
43
|
+
* output all resolve to `{ ok: false, message }` -- the same shape a tool's
|
|
44
|
+
* own refusal uses, so a caller never needs a try/catch. Copied verbatim
|
|
45
|
+
* from acme.mjs's own invokeSeam() -- see this file's own header for why a
|
|
46
|
+
* shared import is not possible across the two npm packages.
|
|
47
|
+
*/
|
|
48
|
+
function invokeSeam(tool, args, repoRoot) {
|
|
49
|
+
return new Promise((resolvePromise) => {
|
|
50
|
+
const resolved = resolveMcpModule(HOST_TOOL_CLIENT_FILE);
|
|
51
|
+
if (!resolved.ok) {
|
|
52
|
+
resolvePromise({ ok: false, message: refusalMessage(HOST_TOOL_CLIENT_FILE, resolved.rungs) });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const cliArgs = [resolved.path, "run", "--tool", tool, "--args", JSON.stringify(args), "--repo-root", repoRoot];
|
|
57
|
+
let child;
|
|
58
|
+
try {
|
|
59
|
+
child = spawn(process.execPath, cliArgs, { stdio: ["ignore", "pipe", "pipe"] });
|
|
60
|
+
} catch (e) {
|
|
61
|
+
resolvePromise({ ok: false, message: e instanceof Error ? e.message : String(e) });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let stdout = "";
|
|
66
|
+
let stderr = "";
|
|
67
|
+
child.stdout.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
|
|
68
|
+
child.stderr.on("data", (chunk) => { stderr += chunk.toString("utf8"); });
|
|
69
|
+
child.on("error", (err) => resolvePromise({ ok: false, message: err.message }));
|
|
70
|
+
child.on("close", () => {
|
|
71
|
+
const lines = stdout.split("\n").filter((line) => line.trim() !== "");
|
|
72
|
+
const last = lines[lines.length - 1];
|
|
73
|
+
if (last === undefined) {
|
|
74
|
+
resolvePromise({ ok: false, message: `host-tool-client.ts produced no output${stderr ? ` (stderr: ${stderr})` : ""}` });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
resolvePromise(JSON.parse(last));
|
|
79
|
+
} catch {
|
|
80
|
+
resolvePromise({ ok: false, message: `host-tool-client.ts produced non-JSON output: ${last}` });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The smallest common ancestor directory of two absolute paths -- computed,
|
|
87
|
+
* never a fixed guess, so the request's `--repo-root` for THIS invocation is
|
|
88
|
+
* always exactly big enough to contain both the image and the output
|
|
89
|
+
* directory, and no bigger. Copied verbatim from acme.mjs's own
|
|
90
|
+
* commonAncestorDir() -- see this file's own header for why a shared import
|
|
91
|
+
* is not possible. */
|
|
92
|
+
function commonAncestorDir(a, b) {
|
|
93
|
+
const partsA = resolve(a).split(sep);
|
|
94
|
+
const partsB = resolve(b).split(sep);
|
|
95
|
+
const common = [];
|
|
96
|
+
for (let i = 0; i < Math.min(partsA.length, partsB.length); i++) {
|
|
97
|
+
if (partsA[i] === partsB[i]) common.push(partsA[i]);
|
|
98
|
+
else break;
|
|
99
|
+
}
|
|
100
|
+
const joined = common.join(sep);
|
|
101
|
+
return joined === "" ? sep : joined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** `path.relative()`, except the "same directory" case yields `"."` rather
|
|
105
|
+
* than `""` -- the seam's `resolveWorkspacePath()` refuses an empty string,
|
|
106
|
+
* but accepts `"."` as a no-op relative reference to its own root. */
|
|
107
|
+
function toRel(root, abs) {
|
|
108
|
+
const r = relative(root, abs);
|
|
109
|
+
return r === "" ? "." : r;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// How to refer to this script in hints, from wherever we were run.
|
|
113
|
+
function selfPath() {
|
|
114
|
+
const r = relative(process.cwd(), SELF);
|
|
115
|
+
return !r || r.startsWith("..") || isAbsolute(r) ? SELF : r;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const die = (m) => { console.error(`error: ${m}`); process.exit(1); };
|
|
119
|
+
|
|
120
|
+
// ------------------------------------------------------------- capabilities
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Detokenizes `--image` (required) to readable BASIC text and resolves its
|
|
124
|
+
* `SYS` handover point, when it has one. `--out-dir` is optional, defaulting
|
|
125
|
+
* to the seam's own dirname(image) default exactly as acme.build's own
|
|
126
|
+
* outDir default does. Prints the seam's response verbatim as one line of
|
|
127
|
+
* JSON when `--json` is given -- the response IS the reportable shape
|
|
128
|
+
* (`{ ok, tool, exitStatus, results, stderrTail, entrypoint,
|
|
129
|
+
* entrypointReason }` / `{ ok: false, message }`), so no reshaping happens
|
|
130
|
+
* here.
|
|
131
|
+
*/
|
|
132
|
+
async function runDecode(argv) {
|
|
133
|
+
const o = parseOpts(argv);
|
|
134
|
+
if (!o.image) die(`usage: decode --image <path.prg> [--out-dir <dir>] [--json]`);
|
|
135
|
+
|
|
136
|
+
const imageAbs = resolve(o.image);
|
|
137
|
+
const outDirAbs = o.outDir ? resolve(o.outDir) : dirname(imageAbs);
|
|
138
|
+
|
|
139
|
+
// Workspace-relative request construction (mirrors acme.mjs's own build()):
|
|
140
|
+
// the root for THIS invocation is the smallest ancestor containing both
|
|
141
|
+
// the image and the output directory.
|
|
142
|
+
const repoRoot = commonAncestorDir(dirname(imageAbs), outDirAbs);
|
|
143
|
+
const args = { image: toRel(repoRoot, imageAbs) };
|
|
144
|
+
if (o.outDir) args.outDir = toRel(repoRoot, outDirAbs);
|
|
145
|
+
|
|
146
|
+
const response = await invokeSeam("petcat.decode", args, repoRoot);
|
|
147
|
+
report(response, o);
|
|
148
|
+
process.exit(response.ok ? 0 : 1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function report(response, { json }) {
|
|
152
|
+
if (json) {
|
|
153
|
+
console.log(JSON.stringify(response));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
console.error(`petcat call FAILED: ${response.message}`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// WHAT NOT TO DO (this file's own header): never guess an entry point --
|
|
161
|
+
// print exactly what the seam reported, decline included.
|
|
162
|
+
if (response.entrypoint !== null) {
|
|
163
|
+
console.log(`entry point: ${response.entrypoint} (${response.entrypointReason})`);
|
|
164
|
+
} else {
|
|
165
|
+
console.log(`entry point not resolved: ${response.entrypointReason}`);
|
|
166
|
+
}
|
|
167
|
+
for (const r of response.results ?? []) {
|
|
168
|
+
console.log(`${r.path} (${r.byteLength} bytes, sha256 ${r.sha256})`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ------------------------------------------------------------------ options
|
|
173
|
+
|
|
174
|
+
// Exported so petcat.test.mjs can cover this file's
|
|
175
|
+
// own CLI-option parsing as a pure unit, mirroring c1541.mjs's own exported
|
|
176
|
+
// parsers -- never spawns anything, never needs petcat installed.
|
|
177
|
+
export function parseOpts(argv) {
|
|
178
|
+
const o = { json: false };
|
|
179
|
+
for (let i = 0; i < argv.length; i++) {
|
|
180
|
+
const a = argv[i];
|
|
181
|
+
if (a === "--json") o.json = true;
|
|
182
|
+
else if (a === "--image") o.image = argv[++i];
|
|
183
|
+
else if (a === "--out-dir") o.outDir = argv[++i];
|
|
184
|
+
}
|
|
185
|
+
return o;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// --------------------------------------------------------------------- main
|
|
189
|
+
//
|
|
190
|
+
// The CLI dispatch below MUST be guarded to run only
|
|
191
|
+
// when this file is the actual entry point, not merely imported -- mirrors
|
|
192
|
+
// c1541.mjs's own entry-point guard verbatim (the "Rule 3
|
|
193
|
+
// fix, discovered mid-execution"), added there after an unguarded dispatch
|
|
194
|
+
// ran with the TEST RUNNER's own process.argv on every import of
|
|
195
|
+
// c1541.test.mjs, printing the usage banner and calling process.exit(0)
|
|
196
|
+
// before a single test() call ever registered. Nothing imports petcat.mjs as
|
|
197
|
+
// a module today (confirmed by grep across src/ and scripts/), so this was
|
|
198
|
+
// latent rather than live here -- but the next petcat.test.mjs that imports
|
|
199
|
+
// a pure helper from this file would reintroduce the exact bug c1541.mjs
|
|
200
|
+
// already found and fixed once. Same shape
|
|
201
|
+
// (`resolve(process.argv[1]) === fileURLToPath(import.meta.url)`), never a
|
|
202
|
+
// second guard shape invented for this sibling script.
|
|
203
|
+
|
|
204
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
205
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
206
|
+
const VERBS = {
|
|
207
|
+
decode: (argv) => runDecode(argv),
|
|
208
|
+
};
|
|
209
|
+
if (!cmd || !VERBS[cmd]) {
|
|
210
|
+
console.log(`usage: node ${selfPath()} <command> [options]
|
|
211
|
+
|
|
212
|
+
decode --image <path.prg> [--out-dir <dir>] [--json] detokenize a BASIC program and resolve its SYS handover point
|
|
213
|
+
|
|
214
|
+
Never invokes petcat directly and never guesses an entry point -- a computed
|
|
215
|
+
SYS argument is reported as a named decline, never an address.
|
|
216
|
+
|
|
217
|
+
options: --image PATH --out-dir DIR --json`);
|
|
218
|
+
process.exit(cmd ? 1 : 0);
|
|
219
|
+
}
|
|
220
|
+
await VERBS[cmd](rest);
|
|
221
|
+
}
|