@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
|
@@ -182,9 +182,9 @@ combined (driver.mjs:262, 268) — so a partially-reachable or partially-changed
|
|
|
182
182
|
source set can silently replace good tracked data with less of it. Rebuild, then
|
|
183
183
|
run `git diff --stat` on `memmap.json` before accepting the result, and
|
|
184
184
|
`git checkout` the file if the diff is not explainable as the correction you
|
|
185
|
-
were expecting.
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
were expecting. **`memmap` mutates tracked files** — unlike `lookup` and
|
|
186
|
+
`annotate`, which are read-only — so run it deliberately, on a clean working
|
|
187
|
+
tree, and review the diff before you commit it.
|
|
188
188
|
|
|
189
189
|
## Feeding the enum generator
|
|
190
190
|
|
|
@@ -224,7 +224,14 @@ for any other enum.** `anno_create_project_enum` defines the variants and `anno_
|
|
|
224
224
|
binds one to the accessing instruction's address — see "Name it" and "Document it" below. That is
|
|
225
225
|
manual where `gen-enums` was bulk, but it writes exactly the same rows into the store.
|
|
226
226
|
|
|
227
|
-
##
|
|
227
|
+
## Reference material for annotating
|
|
228
|
+
|
|
229
|
+
Two deeper jobs feed the annotate/lookup job above, rather than standing apart from it: knowing
|
|
230
|
+
what a whole region actually is (code, or one of eight kinds of data) before it can be annotated
|
|
231
|
+
at all, and knowing what one of a program's own addresses represents when no published table can
|
|
232
|
+
say. Both exist to serve a documented listing; neither is invoked as a job on its own.
|
|
233
|
+
|
|
234
|
+
### Classifying every region of an annotation project
|
|
228
235
|
|
|
229
236
|
Everything above answers *what does this published address mean*. This section
|
|
230
237
|
answers a different question over the same map: **given a loaded binary in an
|
|
@@ -243,7 +250,7 @@ entry point and marks it **Code**. Everything else is **Undefined** — not
|
|
|
243
250
|
"data", just unexplored. The job here is to walk the Undefined regions, work
|
|
244
251
|
out what each one actually is, and set it.
|
|
245
252
|
|
|
246
|
-
|
|
253
|
+
#### The one mistake that matters more than the rest
|
|
247
254
|
|
|
248
255
|
**Never disassemble a region without concrete proof that it executes.** Random
|
|
249
256
|
data routinely disassembles into plausible-looking instruction sequences, and
|
|
@@ -269,7 +276,7 @@ None of those? Leave it **Undefined**, or classify it as data — even when the
|
|
|
269
276
|
bytes disassemble cleanly. "It looked like code" is how a sprite sheet becomes
|
|
270
277
|
four hundred lines of fiction.
|
|
271
278
|
|
|
272
|
-
|
|
279
|
+
#### The order of the passes
|
|
273
280
|
|
|
274
281
|
Work the Undefined blocks in four passes, in this order. Do not interleave
|
|
275
282
|
them; each pass makes the next one cheaper.
|
|
@@ -287,7 +294,7 @@ them; each pass makes the next one cheaper.
|
|
|
287
294
|
**Never** speculatively disassemble in this pass; by definition nothing here
|
|
288
295
|
met the proof bar.
|
|
289
296
|
|
|
290
|
-
|
|
297
|
+
#### Scope, and reading a region
|
|
291
298
|
|
|
292
299
|
1. `anno_get_binary_info` first. Keep `origin`, `size`, `system`, `filename`,
|
|
293
300
|
`description` and `may_contain_undocumented_opcodes`.
|
|
@@ -315,7 +322,7 @@ them; each pass makes the next one cheaper.
|
|
|
315
322
|
classification; a 4096-byte hexdump is more than can be read carefully in
|
|
316
323
|
one pass.
|
|
317
324
|
|
|
318
|
-
|
|
325
|
+
#### Applying the classification
|
|
319
326
|
|
|
320
327
|
- **Code**: `anno_disassemble` from the entry-point address to READ, then
|
|
321
328
|
`anno_set_data_type` with `"code"` to RECORD the range you verified.
|
|
@@ -357,7 +364,7 @@ then: anno_set_data_type start=2049 end=2303 data_type="code"
|
|
|
357
364
|
then: anno_get_blocks max_results=500 # refresh
|
|
358
365
|
```
|
|
359
366
|
|
|
360
|
-
|
|
367
|
+
#### The block types
|
|
361
368
|
|
|
362
369
|
| Block type | `data_type` | When |
|
|
363
370
|
|---|---|---|
|
|
@@ -374,7 +381,7 @@ then: anno_get_blocks max_results=500 # refresh
|
|
|
374
381
|
| External file | `external_file` | Large blobs to export as-is: SID tunes, bitmaps, charsets |
|
|
375
382
|
| Undefined | `undefined` | Reset to unknown. The honest answer for a region you cannot place |
|
|
376
383
|
|
|
377
|
-
|
|
384
|
+
#### Recognising each kind
|
|
378
385
|
|
|
379
386
|
**Byte data** — regular patterns that form no valid instruction sequence;
|
|
380
387
|
addressed by `LDA addr,X` / `LDA addr,Y` table lookups; sprite data in 63-byte
|
|
@@ -413,7 +420,7 @@ in multiples of 64, or a bitmap. Export it rather than annotate it.
|
|
|
413
420
|
it is passed to CHROUT, it is PETSCII. Getting this backwards produces text
|
|
414
421
|
that renders as garbage in exactly one of the two places.
|
|
415
422
|
|
|
416
|
-
|
|
423
|
+
#### The adjacent-table limitation, and how it was closed
|
|
417
424
|
|
|
418
425
|
**Dated limitation, recorded 2026-08-24; CLOSED 2026-08-29 when the store
|
|
419
426
|
changed underneath it.** The old store auto-merged two adjacent regions of the
|
|
@@ -440,7 +447,7 @@ So the working rules are now the ordinary ones:
|
|
|
440
447
|
- **Do not** carry the old over-merge caveat into a report taken from this
|
|
441
448
|
store. It was true of the retired one and is not true here.
|
|
442
449
|
|
|
443
|
-
|
|
450
|
+
#### Labelling, and the report
|
|
444
451
|
|
|
445
452
|
Name what you classified — `anno_set_label_name` on entry points, tables and
|
|
446
453
|
strings — and comment it with `anno_set_comment` (`"line"` above,
|
|
@@ -460,7 +467,7 @@ Then report, and mean it:
|
|
|
460
467
|
own — so quoting the revision is how the report is pinned to an exact store
|
|
461
468
|
state rather than to "after the pass".
|
|
462
469
|
|
|
463
|
-
|
|
470
|
+
#### What goes wrong
|
|
464
471
|
|
|
465
472
|
| Symptom | What it actually is |
|
|
466
473
|
|---|---|
|
|
@@ -471,7 +478,7 @@ Then report, and mean it:
|
|
|
471
478
|
| Two tables you classified separately show up as one block | The adjacent-table limitation above. Not your error. |
|
|
472
479
|
| Text renders as garbage on screen but fine through CHROUT | It is PETSCII, typed as screencode — or the reverse. |
|
|
473
480
|
|
|
474
|
-
|
|
481
|
+
### What a symbol in the store actually represents
|
|
475
482
|
|
|
476
483
|
`lookup` at the top of this page answers what a **published** address means —
|
|
477
484
|
a hardware register, a KERNAL entry point, an OS variable. That answer comes
|
|
@@ -483,7 +490,7 @@ When `lookup` returns a region-only answer — the dominant case for a game's ow
|
|
|
483
490
|
code and variables, as noted above — this is the procedure that gets you a
|
|
484
491
|
name.
|
|
485
492
|
|
|
486
|
-
|
|
493
|
+
#### 1. Target and context
|
|
487
494
|
|
|
488
495
|
- **Always start from an explicit address**, `$XXXX` or its decimal
|
|
489
496
|
equivalent. There is no editor cursor in this project's route, and upstream's
|
|
@@ -497,7 +504,7 @@ name.
|
|
|
497
504
|
remember that `LAX`, `SAX` and `DCP` have real read/write side effects that
|
|
498
505
|
belong in the data-flow picture.
|
|
499
506
|
|
|
500
|
-
|
|
507
|
+
#### 2. Gather the usage
|
|
501
508
|
|
|
502
509
|
`anno_get_cross_references` on the address — naming the `store`, the `image` and
|
|
503
510
|
a REQUIRED `max_results` — returns everywhere it is touched. Read the
|
|
@@ -519,7 +526,7 @@ order of likelihood:
|
|
|
519
526
|
3. It is genuinely **dead**: unused variable, or code no longer reached. Say so
|
|
520
527
|
in the report rather than inventing a purpose.
|
|
521
528
|
|
|
522
|
-
|
|
529
|
+
#### 3. Place it
|
|
523
530
|
|
|
524
531
|
**A hardware register?** `lookup` it. If one of the four tables names it, take
|
|
525
532
|
the published name and the per-bit breakdown with it — that reading rests on
|
|
@@ -557,7 +564,7 @@ define one (`0 = INIT`, `1 = TITLE`, `2 = GAMEPLAY`, `3 = GAME_OVER`) with a
|
|
|
557
564
|
real `description` if none matches, then apply it to every instruction reading
|
|
558
565
|
or writing the variable.
|
|
559
566
|
|
|
560
|
-
|
|
567
|
+
#### 4. Name it
|
|
561
568
|
|
|
562
569
|
| Symbol kind | Convention | Example |
|
|
563
570
|
|---|---|---|
|
|
@@ -578,7 +585,7 @@ or writing the variable.
|
|
|
578
585
|
|
|
579
586
|
Apply it with `anno_set_label_name`.
|
|
580
587
|
|
|
581
|
-
|
|
588
|
+
#### 5. Document it
|
|
582
589
|
|
|
583
590
|
- `anno_set_comment` `"line"` at the definition: the range it occupies, its
|
|
584
591
|
purpose, its bitfield layout if it has one.
|
|
@@ -590,15 +597,15 @@ Apply it with `anno_set_label_name`.
|
|
|
590
597
|
initialised by immediate loads of a target's low and high bytes — `LDA #<target
|
|
591
598
|
/ STA ptr / LDA #>target / STA ptr+1` — upstream calls `set_immediate_format`
|
|
592
599
|
twice to turn both immediates into a single readable symbol reference. **That
|
|
593
|
-
call is not exposed on this project's surface.**
|
|
594
|
-
`JSR`/`JMP` and data reference goes through a symbol, so code can move"
|
|
595
|
-
|
|
600
|
+
call is not exposed on this project's surface.** The rule it would serve — "every branch,
|
|
601
|
+
`JSR`/`JMP` and data reference goes through a symbol, so code can move" — still
|
|
602
|
+
supplies the criterion, and the per-call disposition is
|
|
596
603
|
recorded in the manifest named in the attribution header above. Until then,
|
|
597
604
|
reconstruct the target by hand and put it in a side comment on both
|
|
598
605
|
instructions — `; low byte of ptr_sprite_table ($C240)` — so the pointer is
|
|
599
606
|
still readable even though the store cannot format it.
|
|
600
607
|
|
|
601
|
-
|
|
608
|
+
#### 6. Report
|
|
602
609
|
|
|
603
610
|
- **Address** and its current label.
|
|
604
611
|
- **Classification**: flag, counter, pointer, hardware register, state
|
|
@@ -21,7 +21,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
21
21
|
|
|
22
22
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
23
23
|
// memmap.json lives at the skill root, one level up from scripts/, by decision
|
|
24
|
-
//
|
|
24
|
+
// only .mjs modules move into scripts/, data files stay put.
|
|
25
25
|
const MEMMAP_JSON = join(HERE, "..", "memmap.json");
|
|
26
26
|
|
|
27
27
|
// Reference tables, merged into one address -> meaning index. `kind` selects the
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: c64-petcat
|
|
3
|
+
description: Convert a Commodore .prg between PETSCII and ASCII, detokenize a BASIC program into readable text, and read the machine-code handover address out of its startup line, using VICE's own petcat conversion tool as the reference implementation. Use when asked to detokenize a BASIC listing, list what a BASIC stub does, find where a program hands over to machine code, or convert C64 text between PETSCII and ASCII.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Detokenizing BASIC with petcat
|
|
7
|
+
|
|
8
|
+
One capability, one script, one binary (`petcat`) reached only through the
|
|
9
|
+
host-tool execution seam:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
S=src/skills/c64-petcat/scripts/petcat.mjs # from the repo root
|
|
13
|
+
|
|
14
|
+
node $S decode --image path/to/program.prg # detokenize + resolve the SYS handover
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The script wraps `petcat` and nothing else. `--image` is required; `--out-dir`
|
|
18
|
+
is optional, defaulting to the image's own directory, exactly like
|
|
19
|
+
`acme-build`'s own `--out-dir` default. Both are resolved
|
|
20
|
+
**workspace-relative** to the smallest ancestor directory containing both,
|
|
21
|
+
before the request ever reaches the seam — the same resolution
|
|
22
|
+
`acme-build`/`c64-disk-access` already go through.
|
|
23
|
+
|
|
24
|
+
Options: `--image PATH` `--out-dir DIR` `--json`.
|
|
25
|
+
|
|
26
|
+
## Detokenizing and resolving the handover point
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
node $S decode --image game.prg --json
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Prints the seam's response verbatim as one line of JSON:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{"ok":true,"tool":"petcat.decode","exitStatus":0,"results":[{"path":"/abs/path/game.bas.txt","sha256":"...","byteLength":21}],"stderrTail":"","entrypoint":2064,"entrypointReason":"literal SYS argument on BASIC line 10: sys2064"}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`results[0].path` names the detokenized listing file the seam wrote —
|
|
39
|
+
`petcat`'s own decoded text, captured and digested. The listing itself never
|
|
40
|
+
crosses inline, no matter how short it is.
|
|
41
|
+
|
|
42
|
+
Two fields carry the handover verdict, and BOTH are present on every
|
|
43
|
+
`ok: true` response:
|
|
44
|
+
|
|
45
|
+
- **`entrypoint`** — a number when the program's `SYS` argument is a literal
|
|
46
|
+
decimal value, `null` otherwise.
|
|
47
|
+
- **`entrypointReason`** — always a string. When `entrypoint` is a number, it
|
|
48
|
+
names the BASIC line the value came from. When `entrypoint` is `null`, it
|
|
49
|
+
quotes the unresolved expression verbatim, or states that the listing
|
|
50
|
+
carries no handover instruction at all.
|
|
51
|
+
|
|
52
|
+
A `null` entrypoint is **not a failure** — it is a resolved "no": the tool
|
|
53
|
+
detokenized the program correctly and correctly concluded the entry point is
|
|
54
|
+
not a static address. Reported this way, never as a guessed address: a
|
|
55
|
+
guessed entry point is expensive downstream, spent on a disassembler that
|
|
56
|
+
then has nothing real to work from.
|
|
57
|
+
|
|
58
|
+
Non-JSON mode prints the same information as two lines — the resolved entry
|
|
59
|
+
point (or the decline and its reason) and the listing file's path — never the
|
|
60
|
+
listing's contents inline.
|
|
61
|
+
|
|
62
|
+
## The BASIC dialect
|
|
63
|
+
|
|
64
|
+
Fixed server-side, not a flag on this script or a field on the wire — this
|
|
65
|
+
project's target is fixed to C64 BASIC V2.0 everywhere already, the same
|
|
66
|
+
posture `acme-build` already takes for its own assembler target.
|
|
67
|
+
|
|
68
|
+
## Failure shape
|
|
69
|
+
|
|
70
|
+
A file `petcat` does not recognise as a BASIC program at all — including a
|
|
71
|
+
missing file — is reported as `{"ok":false,"message":"..."}` with a
|
|
72
|
+
non-zero exit code, never a success envelope carrying an empty or guessed
|
|
73
|
+
verdict. `petcat` itself exits `0` even on garbage input; the seam's own
|
|
74
|
+
classifier, not the exit code, is what decides success here.
|
|
75
|
+
|
|
76
|
+
## What this skill does NOT do
|
|
77
|
+
|
|
78
|
+
- **No direct binary spawn.** `petcat` runs host-side; this script only ever
|
|
79
|
+
constructs a typed request and reads the produced listing file back off
|
|
80
|
+
the shared workspace tree — the host-tool execution seam is the only
|
|
81
|
+
route.
|
|
82
|
+
- **No guessed entry point.** A computed or otherwise unresolvable `SYS`
|
|
83
|
+
argument is always reported as a named decline with `entrypoint: null` —
|
|
84
|
+
never a fallback value, never an inline listing scan for "something that
|
|
85
|
+
looks like an address".
|
|
86
|
+
- **No emulator dependency.** This skill names no VICE emulator tool at all
|
|
87
|
+
— it works entirely on files, never on a running machine, by construction.
|
|
@@ -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
|
+
}
|