@henols/c64-re-tools 0.2.2 → 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/package.json +2 -2
- package/skills/acme-build/SKILL.md +39 -23
- 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 +30 -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 +93 -39
- 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 +1 -2
- package/skills/c64-program-recon/references/sound-and-input.md +6 -8
- package/skills/c64-program-recon/references/tool-selection.md +36 -17
- package/skills/c64-program-recon/scripts/packer-finding.mjs +165 -87
- package/skills/c64-program-recon/templates/memory-map.template.md +2 -2
- package/skills/c64-provenance-diff/SKILL.md +40 -5
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +8 -8
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +7 -5
- package/skills/c64-ram-capture/SKILL.md +112 -44
- 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/releases.mjs +1 -1
- package/skills/c64-ram-capture/scripts/vsf-slice.mjs +147 -0
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +15 -15
- 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 +114 -22
- package/skills/routine-queue-walker/scripts/completeness-report.mjs +463 -0
- package/skills/vice-wedge-triage/SKILL.md +96 -89
- package/skills/c64-ram-capture/scripts/d64-parse.mjs +0 -243
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henols/c64-re-tools",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Installer that adds the C64 reverse-engineering skills and the VICE emulator MCP server (@henols/vice-mcp) to a project.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"installer"
|
|
46
46
|
],
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@henols/vice-mcp": "0.2.
|
|
48
|
+
"@henols/vice-mcp": "0.2.3"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"sync-skills": "node scripts/sync-skills.mjs",
|
|
@@ -22,6 +22,13 @@ absent `run` verb is not an omission). It contacts nothing.
|
|
|
22
22
|
Options: `-o FILE` `--out-dir DIR` `-f FORMAT` `--setpc ADDR` `-DSYM=VAL`
|
|
23
23
|
`-I DIR` `--no-report` `--json`.
|
|
24
24
|
|
|
25
|
+
`-I DIR` is resolved **workspace-relative** to the project root the host
|
|
26
|
+
broker was launched with — the same resolution `source`/`--out-dir` already
|
|
27
|
+
go through — before it ever reaches the assembler. An absolute or escaping
|
|
28
|
+
`-I` directory is refused by the seam rather than passed to ACME; this is a
|
|
29
|
+
documented contract change from earlier releases, when an absolute include
|
|
30
|
+
reached the assembler unchecked.
|
|
31
|
+
|
|
25
32
|
## Build
|
|
26
33
|
|
|
27
34
|
```bash
|
|
@@ -137,8 +144,7 @@ it lives in the `anno` CLI rather than here.
|
|
|
137
144
|
|
|
138
145
|
**Dated withdrawal 2026-08-29, dated return 2026-08-31 — both halves are kept,
|
|
139
146
|
because the withdrawal is the record of why the route is shaped the way it is.**
|
|
140
|
-
Whole-program static disassembly was WITHDRAWN on 2026-08-29
|
|
141
|
-
rather than left standing on an unverified reassembly claim: the removed route
|
|
147
|
+
Whole-program static disassembly was WITHDRAWN on 2026-08-29 rather than left standing on an unverified reassembly claim: the removed route
|
|
142
148
|
settled correctness with a transcript parser, and the recorded false pass that
|
|
143
149
|
discipline exists against read `ACME not found in PATH (skipped)` / `All
|
|
144
150
|
roundtrip verifications passed.` / `EXIT=0` — exit zero, an aggregate line
|
|
@@ -152,16 +158,19 @@ match on the exporter's own output.
|
|
|
152
158
|
**The live invocation:**
|
|
153
159
|
|
|
154
160
|
```bash
|
|
155
|
-
npx -y @henols/vice-mcp anno export-asm game.prg --store game.annostore --out game
|
|
161
|
+
npx -y @henols/vice-mcp anno export-asm game.prg --store game.annostore --out game-src
|
|
156
162
|
node <plugin-root>/src/mcp/vice/vice-proxy.ts anno export-asm game.prg --store game.annostore
|
|
157
163
|
```
|
|
158
164
|
|
|
159
165
|
`<image>` and `--store` are **two separate arguments and neither is derived from
|
|
160
166
|
the other** — the image supplies the bytes, the store supplies the names, ranges,
|
|
161
|
-
typed regions and comments, and naming one does not name the other. `--out`
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
167
|
+
typed regions and comments, and naming one does not name the other. `--out` names
|
|
168
|
+
a **directory** the whole export is written into, defaulting to the image's
|
|
169
|
+
basename stem beside the **store**, not beside the image, because the export is a
|
|
170
|
+
generated view of the annotations. The directory holds a root file that sources
|
|
171
|
+
the rest, one file per annotation scope, and an `unscoped.a` for anything inside
|
|
172
|
+
no scope. A non-empty destination is refused rather than overwritten unless you
|
|
173
|
+
pass `--force`.
|
|
165
174
|
|
|
166
175
|
**The verb writes source text and runs no assembler.** Its own second output line
|
|
167
176
|
says so, in as many words:
|
|
@@ -186,18 +195,25 @@ history together; it is not restated there.
|
|
|
186
195
|
|
|
187
196
|
## Setup
|
|
188
197
|
|
|
189
|
-
`acme` on `$PATH`
|
|
190
|
-
|
|
191
|
-
that's deliberate: neither a
|
|
192
|
-
|
|
193
|
-
to assemble on a fresh
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
`acme` on `$PATH` — **on the host**, never inside a container — is the **only**
|
|
199
|
+
requirement. The scaffold that `new` writes assembles against a bare install
|
|
200
|
+
with no standard hardware-register library — that's deliberate: neither a
|
|
201
|
+
plain `~/.local/bin/acme` build nor the Debian trixie `apt` candidate ships
|
|
202
|
+
one, so a scaffold that depended on it would fail to assemble on a fresh
|
|
203
|
+
install.
|
|
204
|
+
|
|
205
|
+
**Route:** `scripts/acme.mjs` never spawns `acme` itself
|
|
206
|
+
and never probes a container PATH for it — a container has no such PATH to
|
|
207
|
+
probe (the project owner's rule of 2026-08-28). The script reaches the
|
|
208
|
+
assembler only through the host-tool execution seam
|
|
209
|
+
(`src/mcp/vice/host-tool.mts`'s `acme.build` allowlist entry), which runs on
|
|
210
|
+
the HOST and probes the library there — the same handful of conventional
|
|
211
|
+
install locations `acme.mts`'s own `findAcmeLib()` names, none of them
|
|
212
|
+
documented a second time here. `$ACME` still matters for **your own** sources
|
|
213
|
+
that use angle-bracket includes (see "Writing source" above), not for the
|
|
214
|
+
scaffold — but set it in the environment the **host** broker process sees, not
|
|
215
|
+
this script's own environment, since the probe now runs host-side inside the
|
|
216
|
+
seam's executor. If you don't have that library, the scaffold doesn't need it.
|
|
201
217
|
|
|
202
218
|
Re-checked against ACME release 0.97 "Zem" (31 Jan 2021). CI now assembles
|
|
203
219
|
the shipped scaffold on every build with `$ACME` cleared (the "Assemble the
|
|
@@ -226,10 +242,10 @@ This one turns source into bytes. It does not restate what the others carry.
|
|
|
226
242
|
| `scripts/acme.mjs` | The driver. Its comments are the contract for every flag above |
|
|
227
243
|
| `template.a` | The scaffold `new` writes: BASIC stub with a computed `SYS`, five local hardware constants (no library needed), no `!to` |
|
|
228
244
|
|
|
229
|
-
|
|
230
|
-
find them**, graded with `Evidence:` and `Confidence:`. Promote
|
|
231
|
-
the new evidence, never by editing
|
|
232
|
-
|
|
245
|
+
Record findings that make RE faster in your own project notes **at the moment you
|
|
246
|
+
find them**, graded with `Evidence:` and `Confidence:`. Promote a finding by
|
|
247
|
+
re-logging it with the new evidence, never by editing an old grade in place — the
|
|
248
|
+
grade is only worth anything if it says what was actually known when it was written.
|
|
233
249
|
|
|
234
250
|
## Troubleshooting
|
|
235
251
|
|
|
@@ -2,30 +2,114 @@
|
|
|
2
2
|
// ACME -> C64 assembler driver. Target is fixed: C64, 6510 CPU, cbm output.
|
|
3
3
|
// Scope is assembling only: source in, .prg + symbol files out. Running the
|
|
4
4
|
// result on a C64 belongs to the emulator skill.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
//
|
|
6
|
+
// The assembler is reached ONLY through the host-tool execution seam -- the
|
|
7
|
+
// project owner's rule of 2026-08-28 is that this script runs container-side,
|
|
8
|
+
// `acme` lives host-side, and there is no container PATH to find it on. This file used to spawn `acme` directly (a synchronous
|
|
9
|
+
// `spawnSync("acme", args, { env })`) and probed FOUR fixed HOST paths
|
|
10
|
+
// (`/usr/local/share/acme`, `/usr/share/acme`, `/usr/lib/acme`, `~/.acme`)
|
|
11
|
+
// for its `<...>`-include library -- both are exactly what the owner's rule
|
|
12
|
+
// says cannot work from inside a container. The spawn and the library probe
|
|
13
|
+
// both moved to `src/mcp/vice/host-tool.mts`'s `acme.build` allowlist entry;
|
|
14
|
+
// this file now only constructs a TYPED request and reads the produced files
|
|
15
|
+
// back off the shared workspace tree.
|
|
16
|
+
//
|
|
17
|
+
// WHAT NOT TO DO: never reintroduce a local child-process call to the
|
|
18
|
+
// assembler as a fallback when the seam is unreachable -- a fallback that
|
|
19
|
+
// works on the developer's own host and silently fails inside a container is
|
|
20
|
+
// the exact failure this seam exists to remove. A seam refusal is reported
|
|
21
|
+
// and the build fails; it is never retried by spawning `acme` here.
|
|
22
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from "node:fs";
|
|
23
|
+
import { dirname, join, basename, relative, isAbsolute, resolve, sep } from "node:path";
|
|
8
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";
|
|
9
28
|
|
|
10
29
|
const SELF = fileURLToPath(import.meta.url);
|
|
11
30
|
const HERE = dirname(SELF);
|
|
12
31
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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.
|
|
45
|
+
*/
|
|
46
|
+
function invokeSeam(tool, args, repoRoot) {
|
|
47
|
+
return new Promise((resolvePromise) => {
|
|
48
|
+
const resolved = resolveMcpModule(HOST_TOOL_CLIENT_FILE);
|
|
49
|
+
if (!resolved.ok) {
|
|
50
|
+
resolvePromise({ ok: false, message: refusalMessage(HOST_TOOL_CLIENT_FILE, resolved.rungs) });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const cliArgs = [resolved.path, "run", "--tool", tool, "--args", JSON.stringify(args), "--repo-root", repoRoot];
|
|
55
|
+
let child;
|
|
56
|
+
try {
|
|
57
|
+
child = spawn(process.execPath, cliArgs, { stdio: ["ignore", "pipe", "pipe"] });
|
|
58
|
+
} catch (e) {
|
|
59
|
+
resolvePromise({ ok: false, message: e instanceof Error ? e.message : String(e) });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let stdout = "";
|
|
64
|
+
let stderr = "";
|
|
65
|
+
child.stdout.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
|
|
66
|
+
child.stderr.on("data", (chunk) => { stderr += chunk.toString("utf8"); });
|
|
67
|
+
child.on("error", (err) => resolvePromise({ ok: false, message: err.message }));
|
|
68
|
+
child.on("close", () => {
|
|
69
|
+
const lines = stdout.split("\n").filter((line) => line.trim() !== "");
|
|
70
|
+
const last = lines[lines.length - 1];
|
|
71
|
+
if (last === undefined) {
|
|
72
|
+
resolvePromise({ ok: false, message: `host-tool-client.ts produced no output${stderr ? ` (stderr: ${stderr})` : ""}` });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
resolvePromise(JSON.parse(last));
|
|
77
|
+
} catch {
|
|
78
|
+
resolvePromise({ ok: false, message: `host-tool-client.ts produced non-JSON output: ${last}` });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The smallest common ancestor directory of two absolute paths -- computed,
|
|
85
|
+
* never a fixed guess, so the request's `--repo-root` for THIS invocation is
|
|
86
|
+
* always exactly big enough to contain both the source and the output
|
|
87
|
+
* directory, and no bigger. This is what keeps a build entirely outside this
|
|
88
|
+
* project's own tree (this repo's `skill-acme-build-cli.test.ts`'s own
|
|
89
|
+
* scratch directories under the SYSTEM temp dir, and CI's own
|
|
90
|
+
* `RUNNER_TEMP`-rooted scaffold check) working after the migration: the
|
|
91
|
+
* seam's `resolveWorkspacePath()` refuses any path outside its given root, so
|
|
92
|
+
* the root for one invocation is chosen to be wherever that invocation's own
|
|
93
|
+
* files actually live, never a client-supplied absolute path sent as-is. */
|
|
94
|
+
function commonAncestorDir(a, b) {
|
|
95
|
+
const partsA = resolve(a).split(sep);
|
|
96
|
+
const partsB = resolve(b).split(sep);
|
|
97
|
+
const common = [];
|
|
98
|
+
for (let i = 0; i < Math.min(partsA.length, partsB.length); i++) {
|
|
99
|
+
if (partsA[i] === partsB[i]) common.push(partsA[i]);
|
|
100
|
+
else break;
|
|
25
101
|
}
|
|
26
|
-
|
|
102
|
+
const joined = common.join(sep);
|
|
103
|
+
return joined === "" ? sep : joined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** `path.relative()`, except the "same directory" case yields `"."` rather
|
|
107
|
+
* than `""` -- the seam's `resolveWorkspacePath()` refuses an empty string,
|
|
108
|
+
* but accepts `"."` as a no-op relative reference to its own root. */
|
|
109
|
+
function toRel(root, abs) {
|
|
110
|
+
const r = relative(root, abs);
|
|
111
|
+
return r === "" ? "." : r;
|
|
27
112
|
}
|
|
28
|
-
const ACME_LIB = findAcmeLib();
|
|
29
113
|
|
|
30
114
|
// How to refer to this script in hints, from wherever we were run.
|
|
31
115
|
function selfPath() {
|
|
@@ -90,54 +174,65 @@ function curateLabels(vsPath, symbols) {
|
|
|
90
174
|
return { kept: kept.length, dropped };
|
|
91
175
|
}
|
|
92
176
|
|
|
93
|
-
function build(src, opts) {
|
|
177
|
+
async function build(src, opts) {
|
|
94
178
|
if (!existsSync(src)) die(`no such source file: ${src}`);
|
|
179
|
+
|
|
180
|
+
const srcAbs = resolve(src);
|
|
95
181
|
// Side files follow the .prg, not the source: two -DVARIANT builds of one
|
|
96
182
|
// source must not overwrite each other's symbol tables.
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
args
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const r = spawnSync("acme", args, { encoding: "utf8", env });
|
|
125
|
-
if (r.error) {
|
|
126
|
-
die(r.error.code === "ENOENT"
|
|
127
|
-
? "install the ACME cross assembler and put `acme` on PATH"
|
|
128
|
-
: String(r.error));
|
|
183
|
+
const desiredPrg = opts.out
|
|
184
|
+
? resolve(opts.out)
|
|
185
|
+
: join(resolve(opts.outDir || dirname(src)), basename(src).replace(/\.(a|asm|s)$/i, "") + ".prg");
|
|
186
|
+
const desiredOutDirAbs = dirname(desiredPrg);
|
|
187
|
+
if (!existsSync(desiredOutDirAbs)) mkdirSync(desiredOutDirAbs, { recursive: true });
|
|
188
|
+
const desiredStem = desiredPrg.replace(/\.prg$/i, "");
|
|
189
|
+
|
|
190
|
+
// Workspace-relative request construction (A-03): the root for THIS
|
|
191
|
+
// invocation is the smallest ancestor containing both the source and the
|
|
192
|
+
// output directory -- see commonAncestorDir()'s own header.
|
|
193
|
+
const repoRoot = commonAncestorDir(dirname(srcAbs), desiredOutDirAbs);
|
|
194
|
+
const autoOutDirAbs = dirname(srcAbs); // the executor's own default when outDir is omitted
|
|
195
|
+
|
|
196
|
+
const args = { source: toRel(repoRoot, srcAbs) };
|
|
197
|
+
if (desiredOutDirAbs !== autoOutDirAbs) args.outDir = toRel(repoRoot, desiredOutDirAbs);
|
|
198
|
+
if (opts.format) args.format = opts.format;
|
|
199
|
+
if (opts.setpc) args.setpc = opts.setpc;
|
|
200
|
+
if (opts.defines && opts.defines.length) args.defines = opts.defines;
|
|
201
|
+
if (opts.includes && opts.includes.length) args.includes = opts.includes;
|
|
202
|
+
if (opts.noReport) args.noReport = true;
|
|
203
|
+
|
|
204
|
+
const response = await invokeSeam("acme.build", args, repoRoot);
|
|
205
|
+
|
|
206
|
+
if (!response.ok) {
|
|
207
|
+
// A seam-level refusal (unresolvable seam, unreachable broker, a bad
|
|
208
|
+
// request) -- never a local fallback that spawns the assembler itself.
|
|
209
|
+
die(response.message);
|
|
129
210
|
}
|
|
130
211
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
212
|
+
// The executor always names outputs after the SOURCE's own basename (never
|
|
213
|
+
// a caller-chosen stem) -- see host-tool.mts's buildHostToolArgv(). When
|
|
214
|
+
// `-o`/`--out-dir` asked for a DIFFERENT stem (a rename, not just a
|
|
215
|
+
// different directory), the produced files are moved here to the exact
|
|
216
|
+
// requested names -- a workspace file operation, not a second copy of
|
|
217
|
+
// argv construction.
|
|
218
|
+
const autoStem = join(desiredOutDirAbs, basename(srcAbs).replace(/\.(a|asm|s)$/i, ""));
|
|
219
|
+
if (autoStem !== desiredStem) {
|
|
220
|
+
for (const ext of [".prg", ".sym", ".vs", ".rep"]) {
|
|
221
|
+
const from = `${autoStem}${ext}`;
|
|
222
|
+
const to = `${desiredStem}${ext}`;
|
|
223
|
+
if (existsSync(from) && from !== to) {
|
|
224
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
225
|
+
renameSync(from, to);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
138
228
|
}
|
|
229
|
+
|
|
230
|
+
const stem = desiredStem;
|
|
231
|
+
const prg = desiredPrg;
|
|
232
|
+
|
|
233
|
+
const diags = parseDiagnostics((response.stderrTail || "").trim());
|
|
139
234
|
const errors = diags.filter((d) => d.severity.endsWith("error"));
|
|
140
|
-
const ok =
|
|
235
|
+
const ok = response.exitStatus === 0 && existsSync(prg);
|
|
141
236
|
|
|
142
237
|
let range = null, size = null, symbols = [], labels = null;
|
|
143
238
|
if (ok) {
|
|
@@ -176,16 +271,16 @@ function reportBuild(res, { json }) {
|
|
|
176
271
|
|
|
177
272
|
// -------------------------------------------------------------------- verbs
|
|
178
273
|
|
|
179
|
-
function cmdBuild(argv) {
|
|
274
|
+
async function cmdBuild(argv) {
|
|
180
275
|
const o = parseOpts(argv);
|
|
181
|
-
const res = build(o.src, o);
|
|
276
|
+
const res = await build(o.src, o);
|
|
182
277
|
reportBuild(res, o);
|
|
183
278
|
process.exit(res.ok ? 0 : 1);
|
|
184
279
|
}
|
|
185
280
|
|
|
186
|
-
function cmdSym(argv) {
|
|
281
|
+
async function cmdSym(argv) {
|
|
187
282
|
const o = parseOpts(argv);
|
|
188
|
-
const res = build(o.src, { ...o, noReport: true });
|
|
283
|
+
const res = await build(o.src, { ...o, noReport: true });
|
|
189
284
|
if (!res.ok) { reportBuild(res, o); process.exit(1); }
|
|
190
285
|
const used = res.symbols.filter((s) => s.used).sort((a, b) => a.name.localeCompare(b.name));
|
|
191
286
|
if (o.json) { console.log(JSON.stringify(used, null, 2)); return; }
|
|
@@ -199,7 +294,7 @@ function cmdNew(argv) {
|
|
|
199
294
|
if (!path) die("usage: new <file.a>");
|
|
200
295
|
if (existsSync(path)) die(`${path} already exists`);
|
|
201
296
|
// template.a lives at the skill root, one level up from scripts/, by
|
|
202
|
-
// decision
|
|
297
|
+
// decision: only .mjs modules move into scripts/.
|
|
203
298
|
writeFileSync(path, readFileSync(join(HERE, "..", "template.a"), "utf8"));
|
|
204
299
|
console.log(`wrote ${path}`);
|
|
205
300
|
console.log(`next: node ${selfPath()} build ${path}`);
|
|
@@ -243,4 +338,4 @@ options: -o FILE --out-dir DIR -f FORMAT --setpc ADDR -DSYM=VAL -I DIR
|
|
|
243
338
|
--no-report --json`);
|
|
244
339
|
process.exit(cmd ? 1 : 0);
|
|
245
340
|
}
|
|
246
|
-
VERBS[cmd](rest);
|
|
341
|
+
await VERBS[cmd](rest);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
; No angle-bracket library includes here on purpose. Neither documented ACME
|
|
7
7
|
; provisioning route ships the standard hardware-register library - a bare
|
|
8
8
|
; `~/.local/bin/acme` install and the Debian trixie `apt` candidate were both
|
|
9
|
-
; verified to lack it
|
|
9
|
+
; verified to lack it - so a scaffold that depends on
|
|
10
10
|
; it fails to assemble on a fresh install. The five constants below are
|
|
11
11
|
; exactly what this scaffold's body uses, defined locally instead. If you
|
|
12
12
|
; want the full library for your own sources' angle-bracket includes, set
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: c64-disk-access
|
|
3
|
+
description: Read a Commodore .d64 disk image's directory, block allocation map, a named file's sector chain, or a named file's raw bytes, using VICE's own c1541 disk tool as the reference implementation, and audit its directory for fabricated or corrupted entries. Use when asked to list what a disk image contains, check a disk's free blocks or block allocation map, trace which sectors a named file occupies on disk, extract a named file's raw bytes from a .d64, or check whether a disk's directory entries are genuine (a cracker-fabricated filename, a corrupted first track/sector, or a cyclic directory chain).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Reading C64 disk images with c1541
|
|
7
|
+
|
|
8
|
+
Read-only. Six capabilities, one script, one binary (`c1541`) reached only
|
|
9
|
+
through the host-tool execution seam:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
S=src/skills/c64-disk-access/scripts/c1541.mjs # from the repo root
|
|
13
|
+
|
|
14
|
+
node $S bam --image path/to/image.d64 # block allocation map
|
|
15
|
+
node $S dir --image path/to/image.d64 # what's on the disk
|
|
16
|
+
node $S entry --image path/to/image.d64 --name FILENAME # one directory entry's raw fields
|
|
17
|
+
node $S chain --image path/to/image.d64 --name FILENAME # a named file's sector chain
|
|
18
|
+
node $S read --image path/to/image.d64 --name FILENAME # extract a named file's bytes
|
|
19
|
+
node $S audit --image path/to/image.d64 # find fabricated/corrupted directory entries
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The script wraps `c1541` and nothing else — **read-only**. `bam`/`dir`/`audit`
|
|
23
|
+
need only `--image`; `entry`/`chain`/`read` also need `--name`, a CBM
|
|
24
|
+
filename or glob pattern (never a path). No `-format`, `-write`, `-bwrite`
|
|
25
|
+
or `-delete` verb is reachable from here, deliberately: this skill only
|
|
26
|
+
ever reads a disk image, never mutates one.
|
|
27
|
+
|
|
28
|
+
Options: `--image PATH` `--name CBM-NAME` `--out-dir DIR` `--json`.
|
|
29
|
+
|
|
30
|
+
`--out-dir` defaults to the image's own directory, exactly like
|
|
31
|
+
`acme-build`'s own `--out-dir` default. Both `--image` and `--out-dir` are
|
|
32
|
+
resolved **workspace-relative** to the smallest ancestor directory
|
|
33
|
+
containing both, before the request ever reaches the seam — the same
|
|
34
|
+
resolution `acme-build`'s `source`/`--out-dir` already go through. `--name`
|
|
35
|
+
is passed straight through, never resolved as a path; a value beginning
|
|
36
|
+
with `-` is refused by the seam before any child process is spawned (it
|
|
37
|
+
would otherwise be read as a flag by `c1541`'s own CLI).
|
|
38
|
+
|
|
39
|
+
## Directory listing
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
node $S dir --image game.d64 --json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Prints the seam's response verbatim as one line of JSON:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{"ok":true,"tool":"c1541.dir","exitStatus":0,"results":[{"path":"/abs/path/game.dir.txt","sha256":"...","byteLength":123}],"stderrTail":""}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`results[0].path` names the listing file the seam wrote — `c1541`'s own
|
|
52
|
+
`-dir` output, captured and digested.
|
|
53
|
+
|
|
54
|
+
## Block allocation map
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
node $S bam --image game.d64 --json
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Same response shape as `dir`; `results[0].path` names a file carrying one
|
|
61
|
+
per-sector allocation row per track (a run of `*`/`.` characters, `*` for
|
|
62
|
+
an allocated sector).
|
|
63
|
+
|
|
64
|
+
## One directory entry's raw fields
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
node $S entry --image game.d64 --name FILENAME --json
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`results[0].path` names a file carrying the entry's raw 32-byte directory
|
|
71
|
+
record (as a hex dump) followed by its `T/S: <t>/<s>, <n> blocks` summary
|
|
72
|
+
line. This script ALSO parses that line back and adds `firstTrack`/
|
|
73
|
+
`firstSector` as numeric fields on the JSON response, purely for display —
|
|
74
|
+
the file itself is still the authoritative source.
|
|
75
|
+
|
|
76
|
+
## A named file's sector chain
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
node $S chain --image game.d64 --name FILENAME --json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`results[0].path` names a file listing every `(track,sector)` hop the file
|
|
83
|
+
occupies, in order. A single-sector file's chain shows one hop with no
|
|
84
|
+
second tuple on the arrow's right side; a multi-sector file's chain repeats
|
|
85
|
+
the tuple at every hop.
|
|
86
|
+
|
|
87
|
+
## Extracting a named file's bytes
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
node $S read --image game.d64 --name FILENAME --out-dir /scratch --json
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Writes the file's raw bytes (unlike the other four capabilities, this one
|
|
94
|
+
is NOT a captured-stdout listing — `c1541` writes the output file itself).
|
|
95
|
+
`results[0]` carries that file's `path`/`sha256`/`byteLength`.
|
|
96
|
+
|
|
97
|
+
## Auditing for fabricated or corrupted entries
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
node $S audit --image game.d64 --json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Composes `dir` (names and block counts), `bam` (the per-sector allocation
|
|
104
|
+
map), and one `entry` call per name (each file's own claimed first track/
|
|
105
|
+
sector, and its directory sector's "next directory" pointer) into a ported,
|
|
106
|
+
read-only detector — no `-format`/`-write`/mutating verb, and no seventh
|
|
107
|
+
`host_tool` id; this is three existing capabilities composed client-side.
|
|
108
|
+
|
|
109
|
+
A directory entry is flagged `suspicious`, with **named reasons, never a
|
|
110
|
+
bare boolean**, on any of:
|
|
111
|
+
|
|
112
|
+
1. its block count is `0`;
|
|
113
|
+
2. its first track/sector lies outside the image's own geometry (there is
|
|
114
|
+
no such track, or no such sector on that track);
|
|
115
|
+
3. its first **sector** — not merely its whole track — is reported free by
|
|
116
|
+
the allocation map, meaning the file cannot really start there. This is
|
|
117
|
+
sharper than checking only whether the whole track is free, because the
|
|
118
|
+
per-sector map is available.
|
|
119
|
+
|
|
120
|
+
A cyclic or self-referential directory chain (two entries claiming the same
|
|
121
|
+
first track/sector, or a "next directory" pointer that refers back to a
|
|
122
|
+
sector already seen — including the directory's own starting sector) stops
|
|
123
|
+
being treated as new information and is reported as a top-level
|
|
124
|
+
`chain_error` naming the repeated pointer, rather than looping. Every
|
|
125
|
+
remaining entry is still audited afterward — a chain error on one entry
|
|
126
|
+
never hides another entry's own independent flag.
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{"entries":[{"name":"basicstub","blocks":1,"first_track":17,"first_sector":0,"suspicious":false,"suspicious_reasons":[]}],"chain_error":null}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**A flag is a signal to investigate, not a verdict.** A directory entry a
|
|
133
|
+
cracker fabricated for a file never actually written, a genuinely corrupted
|
|
134
|
+
image, and (rarely) an unusual-but-legitimate disk layout can all produce a
|
|
135
|
+
flag; this command reports what it finds, named, and leaves the
|
|
136
|
+
interpretation to whoever is looking at the disk.
|
|
137
|
+
|
|
138
|
+
## Failure shape
|
|
139
|
+
|
|
140
|
+
A nonexistent image, a nonexistent named entry, or any other call `c1541`
|
|
141
|
+
cannot service is reported as `{"ok":false,"message":"..."}` with a
|
|
142
|
+
non-zero exit code — **never** a success envelope over an empty or partial
|
|
143
|
+
result. `c1541` itself exits `0` even on a genuine failure (it prints its
|
|
144
|
+
own `Error - ...` lines to stdout instead); the seam's own classifier, not
|
|
145
|
+
the exit code, is what decides success here.
|
|
146
|
+
|
|
147
|
+
## What this skill does NOT do
|
|
148
|
+
|
|
149
|
+
- **No mutating verb.** `-format`/`-write`/`-bwrite`/`-delete` are never
|
|
150
|
+
reachable from this script, on the wire, or anywhere in this skill's tree
|
|
151
|
+
— only the six read-only capabilities above are exposed.
|
|
152
|
+
- **No direct binary spawn.** `c1541` runs host-side; this script only ever
|
|
153
|
+
constructs a typed request and reads the produced files back off the
|
|
154
|
+
shared workspace tree — the host-tool execution seam is the only route.
|
|
155
|
+
- **No emulator dependency.** This skill names no VICE emulator tool at
|
|
156
|
+
all — it works entirely on files, never on a running machine, by construction.
|