@henols/c64-re-tools 1.1.0 → 2.0.0-rc.2

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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +190 -37
  3. package/distribution/package.ts +36 -0
  4. package/distribution/plugin.ts +120 -0
  5. package/package.json +34 -40
  6. package/skills/c64-assembler/SKILL.md +73 -0
  7. package/skills/c64-assembler/scripts/assemble.ts +103 -0
  8. package/skills/c64-basic/SKILL.md +67 -0
  9. package/skills/c64-basic/scripts/basic.ts +46 -0
  10. package/skills/c64-disk/SKILL.md +79 -0
  11. package/skills/c64-disk/scripts/disk.ts +65 -0
  12. package/skills/c64-emulator/SKILL.md +124 -0
  13. package/skills/c64-knowledge/SKILL.md +102 -0
  14. package/skills/c64-knowledge/scripts/knowledge.ts +257 -0
  15. package/skills/c64-memory-map/SKILL.md +76 -0
  16. package/skills/c64-memory-map/scripts/memmap.ts +81 -0
  17. package/skills/c64-memory-map/scripts/platform.ts +424 -0
  18. package/skills/c64-provenance/SKILL.md +76 -0
  19. package/skills/c64-provenance/scripts/compare.ts +92 -0
  20. package/skills/c64-provenance/scripts/provenance.ts +83 -0
  21. package/skills/c64-reverse-engineering/SKILL.md +90 -0
  22. package/skills/c64-static-analysis/SKILL.md +96 -0
  23. package/skills/c64-static-analysis/scripts/analyze.ts +175 -0
  24. package/skills/c64-static-analysis/scripts/findings.ts +73 -0
  25. package/skills/c64-testing/SKILL.md +79 -0
  26. package/skills/c64-testing/references/scenario-format.md +76 -0
  27. package/skills/c64-testing/scripts/checklist.ts +51 -0
  28. package/skills/c64-testing/scripts/scenario.ts +473 -0
  29. package/skills/c64-testing/scripts/test.ts +55 -0
  30. package/skills/c64-unpacker/SKILL.md +82 -0
  31. package/skills/c64-unpacker/scripts/evidence.ts +135 -0
  32. package/skills/c64-unpacker/scripts/unpack.ts +216 -0
  33. package/src/c64.ts +281 -0
  34. package/src/cli/ap-sdk.ts +44 -0
  35. package/src/cli/cleanup.ts +194 -0
  36. package/src/cli/main.ts +122 -0
  37. package/src/cli/status.ts +34 -0
  38. package/src/host/main.ts +121 -0
  39. package/src/host/server.ts +409 -0
  40. package/src/host/tools/c1541.ts +255 -0
  41. package/src/host/tools/index.ts +31 -0
  42. package/src/host/tools/petcat.ts +70 -0
  43. package/src/host/tools/status.ts +30 -0
  44. package/src/host/tools/version.ts +41 -0
  45. package/src/host/vice/adapter.ts +761 -0
  46. package/src/host/vice/binary-monitor.ts +389 -0
  47. package/src/host/vice/chips.ts +108 -0
  48. package/src/host/vice/connect.ts +34 -0
  49. package/src/host/vice/process.ts +402 -0
  50. package/src/host/vice/screen.ts +183 -0
  51. package/src/host/vice/session.ts +1316 -0
  52. package/src/host/vice/text-monitor.ts +223 -0
  53. package/src/host-client/connect.ts +241 -0
  54. package/src/host-client/tools.ts +89 -0
  55. package/src/host-client/transfer.ts +100 -0
  56. package/src/host-client/vice-session.ts +254 -0
  57. package/src/knowledge/database.ts +132 -0
  58. package/src/knowledge/history.ts +128 -0
  59. package/src/knowledge/import.ts +344 -0
  60. package/src/knowledge/read.ts +191 -0
  61. package/src/knowledge/schema.ts +140 -0
  62. package/src/knowledge/write.ts +348 -0
  63. package/src/mcp/main.ts +109 -0
  64. package/src/mcp/server.ts +307 -0
  65. package/src/mcp/tools/debug.ts +237 -0
  66. package/src/mcp/tools/execution.ts +116 -0
  67. package/src/mcp/tools/input.ts +69 -0
  68. package/src/mcp/tools/machine.ts +97 -0
  69. package/src/mcp/tools/media.ts +106 -0
  70. package/src/mcp/tools/memory.ts +203 -0
  71. package/src/mcp/tools/video.ts +260 -0
  72. package/src/native/acme.ts +116 -0
  73. package/src/native/discover.ts +141 -0
  74. package/src/native/dxa.ts +166 -0
  75. package/src/native/ghidra/analyze.ts +86 -0
  76. package/src/native/ghidra/index.ts +157 -0
  77. package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
  78. package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
  79. package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
  80. package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
  81. package/src/native/ghidra/scripts/C64Export.java +242 -0
  82. package/src/native/ghidra/scripts/C64Prepare.java +60 -0
  83. package/src/native/local.ts +34 -0
  84. package/src/native/processes.ts +350 -0
  85. package/src/native/run.ts +172 -0
  86. package/src/native/staging.ts +83 -0
  87. package/src/native/status.ts +101 -0
  88. package/src/native/types.ts +228 -0
  89. package/src/native/watchdog.ts +78 -0
  90. package/src/project.ts +39 -0
  91. package/src/protocol/framing.ts +132 -0
  92. package/src/protocol/messages.ts +252 -0
  93. package/src/protocol/tools.ts +247 -0
  94. package/src/protocol/vice.ts +1235 -0
  95. package/src/script.ts +59 -0
  96. package/src/time.ts +15 -0
  97. package/src/trace.ts +162 -0
  98. package/THIRD-PARTY-NOTICES.md +0 -26
  99. package/bin/cli.mjs +0 -283
  100. package/skills/acme-build/SKILL.md +0 -260
  101. package/skills/acme-build/scripts/acme.mjs +0 -341
  102. package/skills/acme-build/template.a +0 -53
  103. package/skills/c64-disk-access/SKILL.md +0 -159
  104. package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
  105. package/skills/c64-memory-mapping/SKILL.md +0 -631
  106. package/skills/c64-memory-mapping/memmap.json +0 -8800
  107. package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
  108. package/skills/c64-petcat/SKILL.md +0 -87
  109. package/skills/c64-petcat/scripts/petcat.mjs +0 -221
  110. package/skills/c64-program-recon/SKILL.md +0 -706
  111. package/skills/c64-program-recon/references/control-flow.md +0 -180
  112. package/skills/c64-program-recon/references/graphics.md +0 -79
  113. package/skills/c64-program-recon/references/observation-hazards.md +0 -143
  114. package/skills/c64-program-recon/references/reconstruction.md +0 -133
  115. package/skills/c64-program-recon/references/sound-and-input.md +0 -74
  116. package/skills/c64-program-recon/references/tool-selection.md +0 -78
  117. package/skills/c64-program-recon/scripts/derive.mjs +0 -364
  118. package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
  119. package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
  120. package/skills/c64-provenance-diff/SKILL.md +0 -292
  121. package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
  122. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
  123. package/skills/c64-ram-capture/RELEASES.json.example +0 -17
  124. package/skills/c64-ram-capture/SKILL.md +0 -474
  125. package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
  126. package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
  127. package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
  128. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
  129. package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
  130. package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
  131. package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
  132. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
  133. package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
  134. package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
  135. package/skills/c64-ram-capture/transients/README.md +0 -137
  136. package/skills/routine-queue-walker/SKILL.md +0 -394
  137. package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
  138. package/skills/vice-wedge-triage/SKILL.md +0 -269
package/src/script.ts ADDED
@@ -0,0 +1,59 @@
1
+ // The one error envelope for the skill scripts and the CLI: a script prints one JSON line,
2
+ // a refusal prints {"error":{code,message}} with exit status 1, and a usage error prints the
3
+ // usage text as an invalid-input error with exit status 2.
4
+
5
+ import { WireFailure } from "./protocol/messages.ts";
6
+ import { elapsedMs, startTrace, trace, type TraceFields } from "./trace.ts";
7
+
8
+ /** A wrong command line. The message names the problem; an empty message asks for the usage text alone. */
9
+ export class UsageError extends Error {}
10
+
11
+ /** A refusal class: an Error with a stable `code` (WireFailure, KnowledgeError, ...). */
12
+ export type RefusalClass = abstract new (...args: never[]) => Error & { code: string };
13
+
14
+ /** True for an error that node:util parseArgs throws for a wrong command line. */
15
+ function isParseArgsError(error: unknown): error is Error {
16
+ const code = (error as NodeJS.ErrnoException | undefined)?.code;
17
+ return error instanceof Error && typeof code === "string" && code.startsWith("ERR_PARSE_ARGS_");
18
+ }
19
+
20
+ function printLine(value: unknown): void {
21
+ process.stdout.write(`${JSON.stringify(value)}\n`);
22
+ }
23
+
24
+ /**
25
+ * Runs a script body. Prints its result as one JSON line when it returns a value (undefined prints nothing).
26
+ * A refusal (WireFailure always, plus `refusals`) prints {"error":{code,message}} with exit status 1.
27
+ * A UsageError or a node:util parseArgs error (code ERR_PARSE_ARGS_*) prints {"error":{"code":"invalid-input",
28
+ * "message": message === "" ? usage : message + "\n\n" + usage}} with exit status 2. Anything else is rethrown.
29
+ * The body may set process.exitCode itself for an extra exit status; a value it returns is printed as usual.
30
+ */
31
+ export async function runScript(body: () => unknown, options: { usage: string; refusals?: readonly RefusalClass[] }): Promise<void> {
32
+ // The trace of the script, when C64RT_TRACE is set; its start event carries the script and its arguments.
33
+ startTrace("script", { warn: (line) => process.stderr.write(`${line}\n`) });
34
+ const started = performance.now();
35
+ const end = (fields: TraceFields) => trace().event("script.end", { ms: elapsedMs(started), status: process.exitCode ?? 0, ...fields });
36
+ let result: unknown;
37
+ try {
38
+ result = await body();
39
+ } catch (error) {
40
+ if ([WireFailure, ...(options.refusals ?? [])].some((refusal) => error instanceof refusal)) {
41
+ const { code, message } = error as Error & { code: string };
42
+ printLine({ error: { code, message } });
43
+ process.exitCode = 1;
44
+ end({ error: { code, message } });
45
+ return;
46
+ }
47
+ if (error instanceof UsageError || isParseArgsError(error)) {
48
+ const { message } = error;
49
+ printLine({ error: { code: "invalid-input", message: message === "" ? options.usage : `${message}\n\n${options.usage}` } });
50
+ process.exitCode = 2;
51
+ end({ error: { code: "invalid-input", message } });
52
+ return;
53
+ }
54
+ end({ status: 1, error });
55
+ throw error;
56
+ }
57
+ if (result !== undefined) printLine(result);
58
+ end({ result });
59
+ }
package/src/time.ts ADDED
@@ -0,0 +1,15 @@
1
+ // Every date and time the code writes is ISO 8601 at the fixed CET offset
2
+ // +01:00, also in summer: no timestamp repeats when the clocks go back, and
3
+ // the text of two timestamps sorts in time order.
4
+
5
+ const CET_OFFSET_MS = 60 * 60 * 1000;
6
+
7
+ /** `time` as `2026-10-09T14:03:21.123+01:00`. */
8
+ export function isoCet(time: Date = new Date()): string {
9
+ return `${new Date(time.getTime() + CET_OFFSET_MS).toISOString().slice(0, -1)}+01:00`;
10
+ }
11
+
12
+ /** `time` to the second, for a file name: `2026-10-09T14-03-21`. A colon is not allowed in a name on Windows. */
13
+ export function fileStampCet(time: Date = new Date()): string {
14
+ return isoCet(time).slice(0, 19).replace(/:/g, "-");
15
+ }
package/src/trace.ts ADDED
@@ -0,0 +1,162 @@
1
+ // An opt-in trace for the person who debugs a run. With C64RT_TRACE=<directory>
2
+ // in the environment, each process appends its events as JSON lines to a file
3
+ // of its own in that directory; unset, every call does nothing. The trace is
4
+ // for people: nothing of it reaches the agent, and no result depends on it.
5
+
6
+ import { appendFileSync, copyFileSync, mkdirSync, readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+
9
+ import { fileStampCet, isoCet } from "./time.ts";
10
+
11
+ export const TRACE_VARIABLE = "C64RT_TRACE";
12
+
13
+ /** The programs that write a trace, each to a file named after it. */
14
+ export type TraceKind = "host" | "mcp" | "script" | "cli";
15
+
16
+ /** The fields of one event. `t`, `kind` and `event` are set by the trace; a field that is undefined is left out. */
17
+ export type TraceFields = Record<string, unknown>;
18
+
19
+ const TEXT_LIMIT = 2_000;
20
+ const ITEM_LIMIT = 32;
21
+ const DEPTH_LIMIT = 4;
22
+ const RESERVED = new Set(["t", "kind", "event"]);
23
+
24
+ /**
25
+ * A value as the trace records it: bytes become their count, text and arrays
26
+ * beyond a limit are cut and say so, an Error keeps its name, message and
27
+ * code, and an object deeper than four levels ends in "…".
28
+ */
29
+ export function summarize(value: unknown, depth = 0): unknown {
30
+ if (value instanceof Uint8Array) return { bytes: value.length };
31
+ if (typeof value === "string") return value.length > TEXT_LIMIT ? `${value.slice(0, TEXT_LIMIT)}… (${value.length} characters)` : value;
32
+ if (typeof value === "bigint") return value.toString();
33
+ if (value instanceof Error) {
34
+ const code = (value as { code?: unknown }).code;
35
+ return { name: value.name, message: value.message, ...(typeof code === "string" ? { code } : {}) };
36
+ }
37
+ if (typeof value !== "object" || value === null) return value;
38
+ if (depth >= DEPTH_LIMIT) return "…";
39
+ if (Array.isArray(value)) {
40
+ const items = value.slice(0, ITEM_LIMIT).map((item) => summarize(item, depth + 1));
41
+ return value.length > ITEM_LIMIT ? [...items, `… (${value.length} items)`] : items;
42
+ }
43
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, summarize(item, depth + 1)]));
44
+ }
45
+
46
+ /**
47
+ * The version of c64-re-tools that runs, from the package.json next to src/:
48
+ * the package's own, or the one that install writes into a skill. Undefined
49
+ * when there is none.
50
+ */
51
+ function runningVersion(): string | undefined {
52
+ try {
53
+ const { version } = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf8")) as { version?: unknown };
54
+ return typeof version === "string" ? version : undefined;
55
+ } catch {
56
+ return undefined;
57
+ }
58
+ }
59
+
60
+ /** Milliseconds since `since`, a performance.now() reading, to a tenth. */
61
+ export function elapsedMs(since: number): number {
62
+ return Math.round((performance.now() - since) * 10) / 10;
63
+ }
64
+
65
+ export class Trace {
66
+ readonly kind: TraceKind;
67
+ /** The directory of the trace, or undefined when the trace is off. */
68
+ readonly dir: string | undefined;
69
+ /** The file this trace appends to, or undefined when the trace is off. */
70
+ readonly file: string | undefined;
71
+ #broken = false;
72
+
73
+ private constructor(kind: TraceKind, dir: string | undefined, file: string | undefined) {
74
+ this.kind = kind;
75
+ this.dir = dir;
76
+ this.file = file;
77
+ }
78
+
79
+ /** A trace that records nothing. */
80
+ static off(kind: TraceKind): Trace {
81
+ return new Trace(kind, undefined, undefined);
82
+ }
83
+
84
+ /**
85
+ * A trace into `dir`, created when it is missing. The file is
86
+ * `<kind>-<CET time>-<pid>.jsonl`, and its first event is `trace.start`.
87
+ * Throws when the directory cannot be used.
88
+ */
89
+ static open(kind: TraceKind, dir: string, now = new Date()): Trace {
90
+ mkdirSync(dir, { recursive: true });
91
+ const trace = new Trace(kind, dir, join(dir, `${kind}-${fileStampCet(now)}-${process.pid}.jsonl`));
92
+ trace.event("trace.start", { version: runningVersion(), pid: process.pid, argv: process.argv.slice(1), cwd: process.cwd(), node: process.version, platform: process.platform });
93
+ return trace;
94
+ }
95
+
96
+ get on(): boolean {
97
+ return this.file !== undefined && !this.#broken;
98
+ }
99
+
100
+ /** Appends one event. A write that fails turns the trace off; nothing throws. */
101
+ event(name: string, fields: TraceFields = {}): void {
102
+ if (!this.on) return;
103
+ const line: Record<string, unknown> = { t: isoCet(), kind: this.kind, event: name };
104
+ for (const [key, value] of Object.entries(fields)) {
105
+ if (!RESERVED.has(key) && value !== undefined) line[key] = summarize(value);
106
+ }
107
+ try {
108
+ appendFileSync(this.file!, `${JSON.stringify(line)}\n`);
109
+ } catch {
110
+ this.#broken = true;
111
+ }
112
+ }
113
+
114
+ /** A free-text line, as the programs print to stderr, recorded as the event `log`. */
115
+ log(line: string): void {
116
+ this.event("log", { line });
117
+ }
118
+
119
+ /**
120
+ * Copies a file that would otherwise go away (VICE's own log) next to the
121
+ * trace, as `<kind>-<pid>-<name>`. A copy that fails is recorded, not thrown.
122
+ */
123
+ keep(source: string, name: string): void {
124
+ if (!this.on) return;
125
+ const target = join(this.dir!, `${this.kind}-${process.pid}-${name}`);
126
+ try {
127
+ copyFileSync(source, target);
128
+ this.event("trace.kept", { source, file: target });
129
+ } catch (error) {
130
+ this.event("trace.keep-failed", { source, error });
131
+ }
132
+ }
133
+ }
134
+
135
+ // Off until a program calls startTrace; the kind is a placeholder until then.
136
+ let current = Trace.off("script");
137
+
138
+ /**
139
+ * Switches the trace of this process on when `env` (the environment by
140
+ * default) has C64RT_TRACE, and off otherwise. A directory that cannot be used
141
+ * leaves the trace off and reports that through `warn`. Called once, at the
142
+ * start of a program.
143
+ */
144
+ export function startTrace(kind: TraceKind, options: { env?: NodeJS.ProcessEnv; warn?: (line: string) => void } = {}): Trace {
145
+ const dir = (options.env ?? process.env)[TRACE_VARIABLE];
146
+ if (dir === undefined || dir === "") {
147
+ current = Trace.off(kind);
148
+ return current;
149
+ }
150
+ try {
151
+ current = Trace.open(kind, dir);
152
+ } catch (error) {
153
+ current = Trace.off(kind);
154
+ options.warn?.(`${TRACE_VARIABLE}=${dir} cannot be used (${error instanceof Error ? error.message : String(error)}); the trace is off.`);
155
+ }
156
+ return current;
157
+ }
158
+
159
+ /** The trace of this process: off until startTrace switches it on. */
160
+ export function trace(): Trace {
161
+ return current;
162
+ }
@@ -1,26 +0,0 @@
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 DELETED
@@ -1,283 +0,0 @@
1
- #!/usr/bin/env node
2
- // @henols/c64-re-tools installer.
3
- //
4
- // Installs the C64 reverse-engineering skills and wires the VICE MCP server into
5
- // a target project:
6
- // npx @henols/c64-re-tools [targetDir] [--force] [--dry-run] [--vendor]
7
- //
8
- // * copies the bundled skills into <target>/.claude/skills/
9
- // * merges a `vice` server entry into <target>/.mcp.json (never clobbering
10
- // other servers), launching it via `npx -y @henols/vice-mcp`
11
- // * with --vendor, also `npm install`s @henols/vice-mcp into the project and
12
- // wires .mcp.json to the local copy (pinned/offline use)
13
- import { spawnSync } from "node:child_process";
14
- import { fileURLToPath } from "node:url";
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.
19
- import {
20
- existsSync,
21
- readFileSync,
22
- writeFileSync,
23
- mkdirSync,
24
- readdirSync,
25
- cpSync,
26
- } from "node:fs";
27
-
28
- const HERE = dirname(fileURLToPath(import.meta.url)); // installer/bin (packed) or repo installer/bin (dev)
29
- const PKG_ROOT = dirname(HERE);
30
- const SKILLS_SRC = join(PKG_ROOT, "skills");
31
-
32
- // The single version-resolution seam this repo maintains is
33
- // `src/mcp/vice/version.ts` -- one algorithm for resolving a VERSION
34
- // template against a published version, kept in exactly one place after
35
- // this repo once carried several independent copies of it. This package
36
- // deliberately does NOT import it: it ships without the seam file (its
37
- // `files[]` is `bin/`, `skills/`, `README.md`) and targets node >= 18, which
38
- // cannot type-strip the seam's `.ts` the way the vice-mcp package's own
39
- // node >= 24 runtime can. What follows is exactly the seam's own
40
- // precedence step 1 -- "read my own package.json's `.version`, trust it
41
- // when it is a real published number" -- not a second, independent
42
- // implementation of the resolution algorithm; there is no template/`-`
43
- // handling here because this package is never resolved from `VERSION`,
44
- // only ever published with a concrete version already stamped in. That
45
- // number is PRODUCED elsewhere: `npm version` writes the
46
- // working-tree placeholder, and CI's `npm version` writes the real one at
47
- // publish time. Do not reimplement the seam's own VERSION-template
48
- // parsing and prefix-resolution rules here.
49
- const SELF = readJson(join(PKG_ROOT, "package.json")) ?? {};
50
- const SELF_VERSION = typeof SELF.version === "string" ? SELF.version : "0.0.0";
51
- const MCP_PKG = "@henols/vice-mcp";
52
- // The dev placeholder every derived, publishable version string carries in
53
- // the working tree outside a stamped release. Defined authoritatively as
54
- // `DEV_PLACEHOLDER` in `src/mcp/vice/version.ts` -- repeated here as a
55
- // literal, NOT imported, because this package deliberately ships without
56
- // that seam file (see the comment above) and targets node >= 18, which
57
- // cannot type-strip a `.ts` import the way the vice-mcp package's own
58
- // node >= 24 runtime can. This is the same disclosed divergence as
59
- // `SELF_VERSION` above: one literal, kept in sync by hand, documented here
60
- // so a future edit to the seam's placeholder is not missed.
61
- const MCP_DEV_PLACEHOLDER = "0.0.0-dev";
62
- // Wire the project to the exact vice-mcp version this installer was built
63
- // against -- EXCEPT when run from an unstamped dev checkout:
64
- // `installer/package.json`'s dependency pin is the permanent working-tree
65
- // placeholder outside a CI-stamped publish job, and `@henols/vice-mcp` at
66
- // that literal version will never exist on the npm registry. Silently
67
- // writing it into a consumer's .mcp.json would 404 every time Claude Code
68
- // tries to launch the server, with no error until the user actually tries
69
- // to use it -- strictly worse than falling back to "latest". Fall back AND
70
- // warn loudly, so a developer testing the installer locally knows their
71
- // .mcp.json is unpinned rather than discovering it via a silent 404 later.
72
- const MCP_VERSION_RAW =
73
- SELF.dependencies && typeof SELF.dependencies[MCP_PKG] === "string"
74
- ? SELF.dependencies[MCP_PKG].replace(/^[\^~]/, "")
75
- : SELF_VERSION;
76
- if (MCP_VERSION_RAW === MCP_DEV_PLACEHOLDER) {
77
- console.error(
78
- `c64-re-tools: WARNING -- running from an unstamped dev checkout (installer/package.json pins ` +
79
- `${MCP_PKG}@${MCP_DEV_PLACEHOLDER}, the dev placeholder, not a real published version). ` +
80
- `Falling back to "${MCP_PKG}@latest" in the generated .mcp.json instead of writing an ` +
81
- `unresolvable pin. If you intended to test against a specific version, pass --vendor or fix ` +
82
- `installer/package.json's dependency pin before packaging a release.`
83
- );
84
- }
85
- const MCP_VERSION = MCP_VERSION_RAW === MCP_DEV_PLACEHOLDER ? "latest" : MCP_VERSION_RAW;
86
-
87
- function readJson(path) {
88
- try {
89
- return JSON.parse(readFileSync(path, "utf8"));
90
- } catch {
91
- return undefined;
92
- }
93
- }
94
-
95
- function parseArgs(argv) {
96
- const opts = { force: false, dryRun: false, vendor: false, help: false, target: undefined };
97
- for (const arg of argv) {
98
- if (arg === "--force") opts.force = true;
99
- else if (arg === "--dry-run" || arg === "-n") opts.dryRun = true;
100
- else if (arg === "--vendor") opts.vendor = true;
101
- else if (arg === "--help" || arg === "-h") opts.help = true;
102
- else if (arg.startsWith("-")) {
103
- console.error(`c64-re-tools: unknown flag ${JSON.stringify(arg)} (try --help)`);
104
- process.exit(2);
105
- } else if (opts.target === undefined) opts.target = arg;
106
- else {
107
- console.error(`c64-re-tools: unexpected extra argument ${JSON.stringify(arg)} (try --help)`);
108
- process.exit(2);
109
- }
110
- }
111
- return opts;
112
- }
113
-
114
- const HELP = `c64-re-tools -- install the C64 reverse-engineering skills + VICE MCP server into a project
115
-
116
- Usage:
117
- npx @henols/c64-re-tools [targetDir] [options]
118
-
119
- Arguments:
120
- targetDir Project to install into (default: current directory)
121
-
122
- Options:
123
- --force Overwrite existing skills and an existing 'vice' MCP entry
124
- --vendor Also 'npm install -D ${MCP_PKG}' into the project and wire
125
- .mcp.json to the local copy (pinned/offline), instead of npx
126
- --dry-run, -n Show what would change without writing anything
127
- --help, -h Show this help
128
-
129
- What it does:
130
- 1. Copies bundled skills into <target>/.claude/skills/
131
- 2. Adds a 'vice' server to <target>/.mcp.json (other servers are preserved)
132
-
133
- Requires Node >= 24 to RUN the vice MCP server (this installer runs on Node >= 18).`;
134
-
135
- function viceServerEntry(vendor) {
136
- return {
137
- command: "npx",
138
- args: vendor ? [MCP_PKG] : ["-y", `${MCP_PKG}@${MCP_VERSION}`],
139
- timeout: 150000,
140
- env: { MASTRA_TELEMETRY_DISABLED: "1" },
141
- };
142
- }
143
-
144
- function installSkills(target, { force, dryRun }) {
145
- if (!existsSync(SKILLS_SRC)) {
146
- console.error(
147
- `c64-re-tools: FAIL -- bundled skills not found at ${SKILLS_SRC}. ` +
148
- `(In a dev checkout, run 'node scripts/sync-skills.mjs' first.)`
149
- );
150
- process.exit(1);
151
- }
152
- const names = readdirSync(SKILLS_SRC, { withFileTypes: true })
153
- .filter((d) => d.isDirectory())
154
- .map((d) => d.name)
155
- .sort();
156
- const destRoot = join(target, ".claude", "skills");
157
- const installed = [];
158
- const skipped = [];
159
- for (const name of names) {
160
- const dest = join(destRoot, name);
161
- if (existsSync(dest) && !force) {
162
- skipped.push(name);
163
- continue;
164
- }
165
- if (!dryRun) {
166
- mkdirSync(destRoot, { recursive: true });
167
- cpSync(join(SKILLS_SRC, name), dest, { recursive: true, force: true });
168
- }
169
- installed.push(name);
170
- }
171
- return { destRoot, installed, skipped, total: names.length };
172
- }
173
-
174
- function wireMcp(target, { force, dryRun, vendor }) {
175
- const mcpPath = join(target, ".mcp.json");
176
- let config = { mcpServers: {} };
177
- if (existsSync(mcpPath)) {
178
- const parsed = readJson(mcpPath);
179
- if (parsed === undefined) {
180
- console.error(
181
- `c64-re-tools: FAIL -- ${mcpPath} exists but is not valid JSON. ` +
182
- `Refusing to overwrite it; fix or remove it and re-run.`
183
- );
184
- process.exit(1);
185
- }
186
- config = parsed;
187
- if (typeof config !== "object" || config === null || Array.isArray(config)) {
188
- console.error(`c64-re-tools: FAIL -- ${mcpPath} is not a JSON object.`);
189
- process.exit(1);
190
- }
191
- if (typeof config.mcpServers !== "object" || config.mcpServers === null) {
192
- config.mcpServers = {};
193
- }
194
- }
195
- const existed = Object.prototype.hasOwnProperty.call(config.mcpServers, "vice");
196
- let action;
197
- if (existed && !force) {
198
- action = "kept"; // leave the user's existing entry alone
199
- } else {
200
- action = existed ? "updated" : "added";
201
- if (!dryRun) {
202
- config.mcpServers.vice = viceServerEntry(vendor);
203
- mkdirSync(dirname(mcpPath), { recursive: true });
204
- writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n");
205
- }
206
- }
207
- return { mcpPath, action };
208
- }
209
-
210
- function vendorInstall(target, { dryRun }) {
211
- const spec = `${MCP_PKG}@${MCP_VERSION}`;
212
- if (dryRun) return { ran: false, spec };
213
- const res = spawnSync("npm", ["install", "--save-dev", spec], {
214
- cwd: target,
215
- stdio: "inherit",
216
- });
217
- if (res.status !== 0) {
218
- console.error(
219
- `c64-re-tools: WARN -- 'npm install --save-dev ${spec}' exited ${res.status}. ` +
220
- `Skills and .mcp.json were still written; install the package manually if needed.`
221
- );
222
- return { ran: true, ok: false, spec };
223
- }
224
- return { ran: true, ok: true, spec };
225
- }
226
-
227
- function main() {
228
- const opts = parseArgs(process.argv.slice(2));
229
- if (opts.help) {
230
- console.log(HELP);
231
- return;
232
- }
233
- const target = resolve(opts.target ?? process.cwd());
234
- if (!existsSync(target)) {
235
- console.error(`c64-re-tools: FAIL -- target directory does not exist: ${target}`);
236
- process.exit(1);
237
- }
238
-
239
- console.error(`c64-re-tools ${SELF_VERSION} -> ${target}${opts.dryRun ? " (dry run)" : ""}`);
240
-
241
- const skills = installSkills(target, opts);
242
- if (opts.vendor) vendorInstall(target, opts);
243
- const mcp = wireMcp(target, opts);
244
-
245
- // Summary
246
- console.error("");
247
- console.error(` skills -> ${skills.destRoot}`);
248
- console.error(
249
- ` ${skills.installed.length} installed${
250
- opts.force ? "" : `, ${skills.skipped.length} already present (use --force to overwrite)`
251
- } of ${skills.total}`
252
- );
253
- if (skills.installed.length) console.error(` + ${skills.installed.join(", ")}`);
254
- if (skills.skipped.length && !opts.force)
255
- console.error(` = ${skills.skipped.join(", ")} (kept)`);
256
- console.error(` mcp -> ${mcp.mcpPath}`);
257
- if (mcp.action === "kept") {
258
- console.error(` 'vice' already configured -- kept (use --force to overwrite)`);
259
- } else {
260
- console.error(
261
- ` 'vice' ${mcp.action}${
262
- opts.vendor ? ` (local ${MCP_PKG})` : ` (npx -y ${MCP_PKG}@${MCP_VERSION})`
263
- }`
264
- );
265
- }
266
- console.error("");
267
- if (opts.dryRun) {
268
- console.error("Dry run -- nothing was written.");
269
- } else {
270
- console.error("Done. Restart Claude Code in this project so it picks up the skills and MCP server.");
271
- console.error("Note: running the vice MCP server requires Node >= 24.");
272
- }
273
- }
274
-
275
- // Only dispatch when this module is the process entry point -- i.e. run as
276
- // `node bin/cli.mjs ...` or via the `c64-re-tools` bin shim -- not when
277
- // imported by a test (installer/wire-mcp.test.mjs imports `wireMcp`/`readJson`
278
- // below and must not trigger a real install as a side effect).
279
- if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
280
- main();
281
- }
282
-
283
- export { wireMcp, readJson };