@henols/c64-re-tools 0.1.4
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 +61 -0
- package/bin/cli.mjs +226 -0
- package/package.json +53 -0
- package/skills/acme-build/SKILL.md +224 -0
- package/skills/acme-build/scripts/acme.mjs +263 -0
- package/skills/acme-build/template.a +39 -0
- package/skills/c64-memory-mapping/SKILL.md +199 -0
- package/skills/c64-memory-mapping/memmap.json +8800 -0
- package/skills/c64-memory-mapping/scripts/driver.mjs +553 -0
- package/skills/c64-program-recon/SKILL.md +172 -0
- package/skills/c64-program-recon/references/control-flow.md +174 -0
- package/skills/c64-program-recon/references/graphics.md +73 -0
- package/skills/c64-program-recon/references/observation-hazards.md +118 -0
- package/skills/c64-program-recon/references/reconstruction.md +128 -0
- package/skills/c64-program-recon/references/sound-and-input.md +68 -0
- package/skills/c64-program-recon/references/tool-selection.md +55 -0
- package/skills/c64-program-recon/scripts/derive.mjs +364 -0
- package/skills/c64-program-recon/templates/memory-map.template.md +62 -0
- package/skills/c64-provenance-diff/SKILL.md +257 -0
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +981 -0
- package/skills/c64-provenance-diff/scripts/diff-images.test.mjs +665 -0
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +383 -0
- package/skills/c64-ram-capture/SKILL.md +306 -0
- package/skills/c64-ram-capture/scripts/compare.mjs +258 -0
- package/skills/c64-ram-capture/scripts/d64-parse.mjs +243 -0
- package/skills/c64-ram-capture/scripts/d64-parse.test.mjs +243 -0
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +317 -0
- package/skills/c64-ram-capture/scripts/dump-artifacts.test.mjs +133 -0
- package/skills/c64-ram-capture/scripts/project-paths.mjs +81 -0
- package/skills/c64-ram-capture/scripts/releases.mjs +109 -0
- package/skills/c64-ram-capture/scripts/test-corpus.mjs +75 -0
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +575 -0
- package/skills/c64-ram-capture/scripts/watch-loads.test.mjs +339 -0
- package/skills/c64-ram-capture/templates/capture-record.template.md +59 -0
- package/skills/vice-wedge-triage/SKILL.md +149 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Where this toolkit's data lives, resolved portably.
|
|
3
|
+
//
|
|
4
|
+
// These modules ship as a bundled skill toolkit and may be installed at any
|
|
5
|
+
// depth in any project, so nothing here counts directory hops. Two rules:
|
|
6
|
+
//
|
|
7
|
+
// 1. The project root is found by walking UP for a `.git` marker. Counting
|
|
8
|
+
// hops from `import.meta.url` breaks the moment the toolkit is installed
|
|
9
|
+
// somewhere other than `.claude/skills/<skill>/scripts/`, and it breaks
|
|
10
|
+
// silently -- paths resolve to a plausible wrong place rather than erroring.
|
|
11
|
+
// 2. Every data location is overridable by environment variable, so a project
|
|
12
|
+
// that does not use this repo's `recovery/` + `disks/` layout can point the
|
|
13
|
+
// toolkit at its own without editing any module.
|
|
14
|
+
//
|
|
15
|
+
// Pure path arithmetic over the filesystem. Contacts nothing.
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { dirname, join, resolve, parse } from "node:path";
|
|
19
|
+
|
|
20
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Nearest ancestor directory containing a `.git` entry, starting from this
|
|
24
|
+
* file. Falls back to `C64RE_PROJECT_ROOT` when set, which also covers the
|
|
25
|
+
* case of running from an export with no git metadata at all.
|
|
26
|
+
*/
|
|
27
|
+
export function projectRoot() {
|
|
28
|
+
if (process.env.C64RE_PROJECT_ROOT) return resolve(process.env.C64RE_PROJECT_ROOT);
|
|
29
|
+
let dir = HERE;
|
|
30
|
+
const { root } = parse(dir);
|
|
31
|
+
while (true) {
|
|
32
|
+
if (existsSync(join(dir, ".git"))) return dir;
|
|
33
|
+
if (dir === root) break;
|
|
34
|
+
dir = dirname(dir);
|
|
35
|
+
}
|
|
36
|
+
throw new Error(
|
|
37
|
+
"project-paths: could not locate the project root -- no `.git` found above " +
|
|
38
|
+
`${HERE}. Set C64RE_PROJECT_ROOT to the directory that holds your data dirs.`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Directory holding the release registry and the per-release dump directories.
|
|
44
|
+
* Defaults to `<project root>/recovery`; override with `C64RE_DATA_DIR`.
|
|
45
|
+
*/
|
|
46
|
+
export function dataRoot() {
|
|
47
|
+
return process.env.C64RE_DATA_DIR
|
|
48
|
+
? resolve(process.env.C64RE_DATA_DIR)
|
|
49
|
+
: join(projectRoot(), "recovery");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Directory holding the disk images a registry entry's `disk_image` is
|
|
54
|
+
* relative to. Defaults to the project root itself, because registry entries
|
|
55
|
+
* record project-relative paths like `disks/foo.d64`; override with
|
|
56
|
+
* `C64RE_DISKS_ROOT` when they are relative to something else.
|
|
57
|
+
*/
|
|
58
|
+
export function disksRoot() {
|
|
59
|
+
return process.env.C64RE_DISKS_ROOT
|
|
60
|
+
? resolve(process.env.C64RE_DISKS_ROOT)
|
|
61
|
+
: projectRoot();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The release registry file. Override the whole path with `C64RE_REGISTRY`. */
|
|
65
|
+
export function registryFile() {
|
|
66
|
+
return process.env.C64RE_REGISTRY
|
|
67
|
+
? resolve(process.env.C64RE_REGISTRY)
|
|
68
|
+
: join(dataRoot(), "RELEASES.json");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** One release's own data directory. */
|
|
72
|
+
export function releaseDataDir(id) {
|
|
73
|
+
return join(dataRoot(), id);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
77
|
+
console.log(`project root: ${projectRoot()}`);
|
|
78
|
+
console.log(`data root: ${dataRoot()}`);
|
|
79
|
+
console.log(`disks root: ${disksRoot()}`);
|
|
80
|
+
console.log(`registry: ${registryFile()}`);
|
|
81
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// N-way release registry accessor. This is the only module that reads a release
|
|
3
|
+
// identifier out of the registry -- every other module takes the id as an
|
|
4
|
+
// argument and never touches the registry file directly. `release` is the
|
|
5
|
+
// primary noun rather than "the canonical image": there are N releases, each
|
|
6
|
+
// owning a set of dumps, with `canonical` demoted to a boolean on one entry.
|
|
7
|
+
//
|
|
8
|
+
// Portable: the registry's location comes from `project-paths.mjs`, so a project
|
|
9
|
+
// with a different data layout points the toolkit at its own via
|
|
10
|
+
// `C64RE_DATA_DIR` / `C64RE_REGISTRY` rather than editing this file.
|
|
11
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { resolve } from "node:path";
|
|
14
|
+
|
|
15
|
+
import { registryFile, releaseDataDir } from "./project-paths.mjs";
|
|
16
|
+
|
|
17
|
+
export const registryPath = registryFile();
|
|
18
|
+
|
|
19
|
+
const die = (m) => { console.error(`error: ${m}`); process.exit(1); };
|
|
20
|
+
|
|
21
|
+
export function loadRegistry() {
|
|
22
|
+
if (!existsSync(registryPath)) {
|
|
23
|
+
throw new Error(`no registry at ${registryPath}`);
|
|
24
|
+
}
|
|
25
|
+
return JSON.parse(readFileSync(registryPath, "utf8"));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Persist the registry. JSON.stringify preserves each object's insertion
|
|
30
|
+
* (key) order, so a read-modify-write via upsertRelease keeps a stable key
|
|
31
|
+
* order automatically -- callers should not rebuild release objects from
|
|
32
|
+
* scratch with a different key order.
|
|
33
|
+
*/
|
|
34
|
+
function saveRegistry(reg) {
|
|
35
|
+
writeFileSync(registryPath, JSON.stringify(reg, null, 2) + "\n");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The full entry for `id`, or throws with the known-id list on a miss. */
|
|
39
|
+
export function release(id) {
|
|
40
|
+
const reg = loadRegistry();
|
|
41
|
+
const r = reg.releases.find((r) => r.id === id);
|
|
42
|
+
if (!r) assertKnownRelease(id, reg);
|
|
43
|
+
return r;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The registry's own N-readiness documentation (01-03-PLAN.md's Task 2):
|
|
48
|
+
* a top-level `schema_notes` string, sibling to `schema_version` and
|
|
49
|
+
* `releases`, stating the mechanical claim that adding a release is one
|
|
50
|
+
* `releases[]` entry plus one invocation of `tools/recover.mjs`. Kept as a
|
|
51
|
+
* plain top-level field rather than a JSON comment (JSON has none) or a
|
|
52
|
+
* per-release field (it describes the registry's shape, not any one
|
|
53
|
+
* release). Rehearsed against the real validator in
|
|
54
|
+
* a release's own NOTES.md.
|
|
55
|
+
*/
|
|
56
|
+
export function schemaNotes() {
|
|
57
|
+
return loadRegistry().schema_notes ?? null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function releaseDir(id) {
|
|
61
|
+
const reg = loadRegistry();
|
|
62
|
+
assertKnownRelease(id, reg);
|
|
63
|
+
return releaseDataDir(id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Dies with the list of known ids on a miss -- see plan Layer 2. */
|
|
67
|
+
export function assertKnownRelease(id, reg) {
|
|
68
|
+
const registry = reg || loadRegistry();
|
|
69
|
+
const known = registry.releases.map((r) => r.id);
|
|
70
|
+
if (!known.includes(id)) {
|
|
71
|
+
throw new Error(`unknown release "${id}" -- known releases: ${known.join(", ")}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Read-modify-write: `fn` receives a shallow copy of the release entry and
|
|
77
|
+
* returns the replacement; the whole registry is then re-persisted with
|
|
78
|
+
* stable key order. This is the only sanctioned way to mutate an entry.
|
|
79
|
+
*/
|
|
80
|
+
export function upsertRelease(id, fn) {
|
|
81
|
+
const reg = loadRegistry();
|
|
82
|
+
const idx = reg.releases.findIndex((r) => r.id === id);
|
|
83
|
+
if (idx === -1) {
|
|
84
|
+
throw new Error(`unknown release "${id}" -- known releases: ${reg.releases.map((r) => r.id).join(", ")}`);
|
|
85
|
+
}
|
|
86
|
+
reg.releases[idx] = fn({ ...reg.releases[idx] });
|
|
87
|
+
saveRegistry(reg);
|
|
88
|
+
return reg.releases[idx];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// -------------------------------------------------------------------- CLI
|
|
92
|
+
|
|
93
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
94
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
95
|
+
if (cmd === "list") {
|
|
96
|
+
const reg = loadRegistry();
|
|
97
|
+
for (const r of reg.releases) {
|
|
98
|
+
console.log(`${r.id} canonical=${r.canonical} disk_image=${r.disk_image} dumps=${r.dumps.length}`);
|
|
99
|
+
}
|
|
100
|
+
} else if (cmd === "show") {
|
|
101
|
+
if (!rest[0]) die("usage: show <release-id>");
|
|
102
|
+
console.log(JSON.stringify(release(rest[0]), null, 2));
|
|
103
|
+
} else if (cmd === "schema-notes") {
|
|
104
|
+
console.log(schemaNotes() ?? "(no schema_notes field set)");
|
|
105
|
+
} else {
|
|
106
|
+
console.log(`usage: node ${fileURLToPath(import.meta.url)} <list|show <id>|schema-notes>`);
|
|
107
|
+
process.exit(cmd ? 1 : 0);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Locates committed artifacts through the registry, so tests exercise whatever
|
|
2
|
+
// corpus the host project actually has instead of naming one project's files.
|
|
3
|
+
//
|
|
4
|
+
// The point is portability without losing coverage: a project with captures gets
|
|
5
|
+
// the real-artifact assertions; a project with none gets them skipped, not
|
|
6
|
+
// failed. Hardcoding `recovery/<some-release>/dumps/<some-label>.state.json`
|
|
7
|
+
// meant this toolkit's tests could only ever pass in the repo they were written
|
|
8
|
+
// in, which is the opposite of shippable.
|
|
9
|
+
//
|
|
10
|
+
// Test-support only. Pure filesystem reads; contacts nothing.
|
|
11
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
import { projectRoot } from "./project-paths.mjs";
|
|
15
|
+
import { loadRegistry } from "./releases.mjs";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* First dump in the registry whose `field` names a file that exists, as
|
|
19
|
+
* `{ release, label, path }` -- or null when the registry is absent, empty, or
|
|
20
|
+
* names nothing on disk. Never throws: a missing registry is a skip, not a
|
|
21
|
+
* failure.
|
|
22
|
+
*/
|
|
23
|
+
export function firstDumpArtifact(field) {
|
|
24
|
+
let reg;
|
|
25
|
+
try {
|
|
26
|
+
reg = loadRegistry();
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
for (const r of reg.releases ?? []) {
|
|
31
|
+
for (const d of r.dumps ?? []) {
|
|
32
|
+
const value = d[field];
|
|
33
|
+
if (!value) continue;
|
|
34
|
+
const path = join(projectRoot(), value);
|
|
35
|
+
if (existsSync(path)) return { release: r.id, label: d.label, path };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Every dump in the registry whose `field` names an existing file. */
|
|
42
|
+
export function allDumpArtifacts(field) {
|
|
43
|
+
let reg;
|
|
44
|
+
try {
|
|
45
|
+
reg = loadRegistry();
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const out = [];
|
|
50
|
+
for (const r of reg.releases ?? []) {
|
|
51
|
+
for (const d of r.dumps ?? []) {
|
|
52
|
+
const value = d[field];
|
|
53
|
+
if (!value) continue;
|
|
54
|
+
const path = join(projectRoot(), value);
|
|
55
|
+
if (existsSync(path)) out.push({ release: r.id, label: d.label, path });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Parsed JSON for a `firstDumpArtifact` hit, or null. */
|
|
62
|
+
export function readJsonArtifact(field) {
|
|
63
|
+
const hit = firstDumpArtifact(field);
|
|
64
|
+
if (!hit) return null;
|
|
65
|
+
return { ...hit, json: JSON.parse(readFileSync(hit.path, "utf8")) };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* node:test `skip` value: `false` to run, or a human-readable reason string.
|
|
70
|
+
* Pass the thing you looked for so a skipped run explains itself.
|
|
71
|
+
*/
|
|
72
|
+
export function skipUnless(found, what) {
|
|
73
|
+
if (found && (!Array.isArray(found) || found.length > 0)) return false;
|
|
74
|
+
return `no ${what} found via this project's registry -- corpus-dependent check skipped`;
|
|
75
|
+
}
|