@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
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ you pass `--force`.
|
|
|
43
43
|
## Requirements
|
|
44
44
|
|
|
45
45
|
- **This installer** runs on Node ≥ 18.
|
|
46
|
-
- **The VICE MCP server** it wires up requires **Node ≥
|
|
46
|
+
- **The VICE MCP server** it wires up requires **Node ≥ 24**, and a
|
|
47
47
|
host with VICE (`x64sc`) reachable from the MCP client. See the
|
|
48
48
|
[`@henols/vice-mcp`](https://www.npmjs.com/package/@henols/vice-mcp) readme.
|
|
49
49
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Third-Party Notices for `@henols/c64-re-tools`
|
|
2
|
+
|
|
3
|
+
This package is MIT-licensed, copyright Henrik Olsson (see `LICENSE` at the
|
|
4
|
+
repository root). It ships the C64 reverse-engineering **skill playbooks**
|
|
5
|
+
under `skills/` plus the installer CLI under `bin/`, and it declares one
|
|
6
|
+
runtime dependency, `@henols/vice-mcp`, which carries its own notices file.
|
|
7
|
+
|
|
8
|
+
This document covers only the third-party material that ships **in this
|
|
9
|
+
package**. It is deliberately not a second, competing inventory: the canonical
|
|
10
|
+
third-party notices for the whole repository — including the cc65 opcode-table
|
|
11
|
+
transcription and the ACME/VICE not-incorporated scope notes, none of which
|
|
12
|
+
ship here — live in the `@henols/vice-mcp` package at
|
|
13
|
+
`src/mcp/vice/THIRD-PARTY-NOTICES.md` in the repository, and inside that
|
|
14
|
+
package's own tarball as `THIRD-PARTY-NOTICES.md`. Read that file for the full
|
|
15
|
+
inventory; read this one for what `@henols/c64-re-tools` itself carries.
|
|
16
|
+
|
|
17
|
+
## Incorporated material
|
|
18
|
+
|
|
19
|
+
None. The skill playbooks under `skills/` are this project's own prose,
|
|
20
|
+
written against this project's own curated tool surface.
|
|
21
|
+
|
|
22
|
+
## Runtime dependency
|
|
23
|
+
|
|
24
|
+
- **`@henols/vice-mcp`** — this repository's own MCP server package, MIT. Its
|
|
25
|
+
own third-party notices ship inside it as `THIRD-PARTY-NOTICES.md`, and that
|
|
26
|
+
file is the canonical inventory referred to at the top of this document.
|
package/bin/cli.mjs
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
import { spawnSync } from "node:child_process";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { dirname, join, resolve } from "node:path";
|
|
16
|
+
// Entry-point dispatch guard (see the bottom of this file): the same idiom
|
|
17
|
+
// `src/skills/c64-memory-mapping/scripts/driver.mjs` already uses, so a bare
|
|
18
|
+
// `import` from a test file does not also execute the CLI.
|
|
16
19
|
import {
|
|
17
20
|
existsSync,
|
|
18
21
|
readFileSync,
|
|
@@ -27,11 +30,11 @@ const PKG_ROOT = dirname(HERE);
|
|
|
27
30
|
const SKILLS_SRC = join(PKG_ROOT, "skills");
|
|
28
31
|
|
|
29
32
|
// The single version-resolution seam this repo maintains is
|
|
30
|
-
//
|
|
33
|
+
// `src/mcp/vice/version.ts` (quick-260819-tsz, D-5). This package
|
|
31
34
|
// deliberately does NOT import it: it ships without the seam file (its
|
|
32
35
|
// `files[]` is `bin/`, `skills/`, `README.md`) and targets node >= 18, which
|
|
33
36
|
// cannot type-strip the seam's `.ts` the way the vice-mcp package's own
|
|
34
|
-
// node >=
|
|
37
|
+
// node >= 24 runtime can. What follows is exactly the seam's own
|
|
35
38
|
// precedence step 1 -- "read my own package.json's `.version`, trust it
|
|
36
39
|
// when it is a real published number" -- not a second, independent
|
|
37
40
|
// implementation of the resolution algorithm; there is no template/`-`
|
|
@@ -45,11 +48,11 @@ const SELF_VERSION = typeof SELF.version === "string" ? SELF.version : "0.0.0";
|
|
|
45
48
|
const MCP_PKG = "@henols/vice-mcp";
|
|
46
49
|
// The dev placeholder every derived, publishable version string carries in
|
|
47
50
|
// the working tree (R-2, quick-260819-tsz). Defined authoritatively as
|
|
48
|
-
// `DEV_PLACEHOLDER` in
|
|
51
|
+
// `DEV_PLACEHOLDER` in `src/mcp/vice/version.ts` -- repeated here as a
|
|
49
52
|
// literal, NOT imported, because this package deliberately ships without
|
|
50
53
|
// that seam file (see the comment above) and targets node >= 18, which
|
|
51
54
|
// cannot type-strip a `.ts` import the way the vice-mcp package's own
|
|
52
|
-
// node >=
|
|
55
|
+
// node >= 24 runtime can. This is the same disclosed divergence as
|
|
53
56
|
// `SELF_VERSION` above: one literal, kept in sync by hand, documented here
|
|
54
57
|
// so a future edit to the seam's placeholder is not missed.
|
|
55
58
|
const MCP_DEV_PLACEHOLDER = "0.0.0-dev";
|
|
@@ -124,7 +127,7 @@ What it does:
|
|
|
124
127
|
1. Copies bundled skills into <target>/.claude/skills/
|
|
125
128
|
2. Adds a 'vice' server to <target>/.mcp.json (other servers are preserved)
|
|
126
129
|
|
|
127
|
-
Requires Node >=
|
|
130
|
+
Requires Node >= 24 to RUN the vice MCP server (this installer runs on Node >= 18).`;
|
|
128
131
|
|
|
129
132
|
function viceServerEntry(vendor) {
|
|
130
133
|
return {
|
|
@@ -262,8 +265,16 @@ function main() {
|
|
|
262
265
|
console.error("Dry run -- nothing was written.");
|
|
263
266
|
} else {
|
|
264
267
|
console.error("Done. Restart Claude Code in this project so it picks up the skills and MCP server.");
|
|
265
|
-
console.error("Note: running the vice MCP server requires Node >=
|
|
268
|
+
console.error("Note: running the vice MCP server requires Node >= 24.");
|
|
266
269
|
}
|
|
267
270
|
}
|
|
268
271
|
|
|
269
|
-
|
|
272
|
+
// Only dispatch when this module is the process entry point -- i.e. run as
|
|
273
|
+
// `node bin/cli.mjs ...` or via the `c64-re-tools` bin shim -- not when
|
|
274
|
+
// imported by a test (installer/wire-mcp.test.mjs imports `wireMcp`/`readJson`
|
|
275
|
+
// below and must not trigger a real install as a side effect).
|
|
276
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
277
|
+
main();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export { wireMcp, readJson };
|
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": {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|
|
11
11
|
"skills/",
|
|
12
|
-
"README.md"
|
|
12
|
+
"README.md",
|
|
13
|
+
"THIRD-PARTY-NOTICES.md"
|
|
13
14
|
],
|
|
14
15
|
"engines": {
|
|
15
16
|
"node": ">=18"
|
|
@@ -44,10 +45,11 @@
|
|
|
44
45
|
"installer"
|
|
45
46
|
],
|
|
46
47
|
"dependencies": {
|
|
47
|
-
"@henols/vice-mcp": "0.2.
|
|
48
|
+
"@henols/vice-mcp": "0.2.3"
|
|
48
49
|
},
|
|
49
50
|
"scripts": {
|
|
50
51
|
"sync-skills": "node scripts/sync-skills.mjs",
|
|
51
|
-
"prepack": "node scripts/sync-skills.mjs"
|
|
52
|
+
"prepack": "node scripts/sync-skills.mjs",
|
|
53
|
+
"test": "node --test '*.test.mjs'"
|
|
52
54
|
}
|
|
53
55
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: acme-build
|
|
3
|
-
description: Assemble Commodore 64 6510 assembly with the ACME cross assembler. Use when asked to assemble, build, compile or link .a/.asm 6502/6510 source, produce a C64 .prg, scaffold a new C64 program, or list
|
|
3
|
+
description: Assemble Commodore 64 6510 assembly with the ACME cross assembler. Use when asked to assemble, build, compile or link .a/.asm 6502/6510 source, produce a C64 .prg, scaffold a new C64 program, or list which symbols an assembled build actually used from its symbol file.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Assembling C64 source with ACME
|
|
@@ -8,7 +8,7 @@ description: Assemble Commodore 64 6510 assembly with the ACME cross assembler.
|
|
|
8
8
|
Source in, `.prg` out. Everything goes through one script:
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
A
|
|
11
|
+
A=src/skills/acme-build/scripts/acme.mjs # from the repo root
|
|
12
12
|
|
|
13
13
|
node $A new game.asm # scaffold a C64 program
|
|
14
14
|
node $A build game.asm # assemble -> .prg .sym .vs .rep
|
|
@@ -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
|
|
@@ -132,38 +139,81 @@ you also assemble by hand, so these stay recognised as mnemonics.
|
|
|
132
139
|
|
|
133
140
|
## Disassembly
|
|
134
141
|
|
|
135
|
-
This skill does not disassemble.
|
|
136
|
-
|
|
137
|
-
|
|
142
|
+
This skill does not disassemble. The route that does is `anno export-asm`, and
|
|
143
|
+
it lives in the `anno` CLI rather than here.
|
|
144
|
+
|
|
145
|
+
**Dated withdrawal 2026-08-29, dated return 2026-08-31 — both halves are kept,
|
|
146
|
+
because the withdrawal is the record of why the route is shaped the way it is.**
|
|
147
|
+
Whole-program static disassembly was WITHDRAWN on 2026-08-29 rather than left standing on an unverified reassembly claim: the removed route
|
|
148
|
+
settled correctness with a transcript parser, and the recorded false pass that
|
|
149
|
+
discipline exists against read `ACME not found in PATH (skipped)` / `All
|
|
150
|
+
roundtrip verifications passed.` / `EXIT=0` — exit zero, an aggregate line
|
|
151
|
+
reading as a full pass, and the one assembler this project cares about never
|
|
152
|
+
having run. On **2026-08-31 the route returned**, rebuilt over the annotation
|
|
153
|
+
store as `anno export-asm`. It is not a rename of what was removed: its
|
|
154
|
+
correctness is settled by **assembling the output with a real ACME and diffing
|
|
155
|
+
the bytes against the input** — never by an exit code, and never by a string
|
|
156
|
+
match on the exporter's own output.
|
|
157
|
+
|
|
158
|
+
**The live invocation:**
|
|
138
159
|
|
|
139
160
|
```bash
|
|
140
|
-
npx -y @henols/vice-mcp
|
|
141
|
-
node <plugin-root
|
|
161
|
+
npx -y @henols/vice-mcp anno export-asm game.prg --store game.annostore --out game-src
|
|
162
|
+
node <plugin-root>/src/mcp/vice/vice-proxy.ts anno export-asm game.prg --store game.annostore
|
|
142
163
|
```
|
|
143
164
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
165
|
+
`<image>` and `--store` are **two separate arguments and neither is derived from
|
|
166
|
+
the other** — the image supplies the bytes, the store supplies the names, ranges,
|
|
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`.
|
|
152
174
|
|
|
153
|
-
|
|
175
|
+
**The verb writes source text and runs no assembler.** Its own second output line
|
|
176
|
+
says so, in as many words:
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
export-asm: this file has NOT been assembled -- this command writes source text and runs no assembler.
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The real-ACME byte-diff oracle is **test-only** — it lives in
|
|
183
|
+
`src/mcp/vice/acme-verify.ts`, is exercised by `acme-verify.test.ts` (hard-failed
|
|
184
|
+
in CI with `VICE_REQUIRE_ACME=1`), and is deliberately absent from the published
|
|
185
|
+
package, so nothing on the runtime path can reach it. A clean `export-asm` run is
|
|
186
|
+
therefore evidence that source was written and nothing more; it is not an
|
|
187
|
+
assembler verdict. If you need to know the emitted source reassembles, assemble
|
|
188
|
+
it yourself — that is what this skill's own build route is for.
|
|
154
189
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
190
|
+
**Reading one range at a time is still the right move for a single routine.**
|
|
191
|
+
`anno_read_region` and `anno_disassemble` render an explicit inclusive range out
|
|
192
|
+
of the image on demand, capped at 4096 bytes per call and REFUSED by name above
|
|
193
|
+
the cap rather than truncated. `c64-program-recon` documents that route and this
|
|
194
|
+
history together; it is not restated there.
|
|
195
|
+
|
|
196
|
+
## Setup
|
|
160
197
|
|
|
161
|
-
`$
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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.
|
|
167
217
|
|
|
168
218
|
Re-checked against ACME release 0.97 "Zem" (31 Jan 2021). CI now assembles
|
|
169
219
|
the shipped scaffold on every build with `$ACME` cleared (the "Assemble the
|
|
@@ -171,7 +221,7 @@ acme-build scaffold (library-free)" step in `.github/workflows/ci.yml`), so
|
|
|
171
221
|
this claim is re-checkable rather than a one-machine observation.
|
|
172
222
|
|
|
173
223
|
Copy `acme.mjs` into any project's `.claude/skills/acme-build/scripts/`, and
|
|
174
|
-
`template.a` into `.claude/skills/acme-build/`, to use this elsewhere.
|
|
224
|
+
`template.a` into `.claude/skills/acme-build/`, to use this elsewhere (the path Claude Code auto-discovers and `installSkills()` deploys to).
|
|
175
225
|
|
|
176
226
|
## Which skill does what
|
|
177
227
|
|
|
@@ -182,7 +232,7 @@ This one turns source into bytes. It does not restate what the others carry.
|
|
|
182
232
|
| Where to start on an unknown program, and which address to read next | `c64-program-recon` |
|
|
183
233
|
| What a specific address or bit means, or annotating a listing | `c64-memory-mapping` — `node … lookup '$D018'` |
|
|
184
234
|
| A verified 64K image, or comparing two captures | `c64-ram-capture` |
|
|
185
|
-
| Static disassembly of a `.prg` or flat image |
|
|
235
|
+
| Static disassembly of a `.prg` or flat image | **`anno export-asm`** — withdrawn 2026-08-29, returned 2026-08-31 behind a real-ACME byte-diff oracle that is test-only, so the verb writes source and assembles nothing (see Disassembly above) |
|
|
186
236
|
| **Source in, `.prg` out** | here |
|
|
187
237
|
|
|
188
238
|
## References
|
|
@@ -192,10 +242,10 @@ This one turns source into bytes. It does not restate what the others carry.
|
|
|
192
242
|
| `scripts/acme.mjs` | The driver. Its comments are the contract for every flag above |
|
|
193
243
|
| `template.a` | The scaffold `new` writes: BASIC stub with a computed `SYS`, five local hardware constants (no library needed), no `!to` |
|
|
194
244
|
|
|
195
|
-
|
|
196
|
-
find them**, graded with `Evidence:` and `Confidence:`. Promote
|
|
197
|
-
the new evidence, never by editing
|
|
198
|
-
|
|
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.
|
|
199
249
|
|
|
200
250
|
## Troubleshooting
|
|
201
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
|