@henols/c64-re-tools 1.1.0 → 2.0.0-rc.10
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/LICENSE +21 -0
- package/README.md +154 -37
- package/distribution/package.ts +21 -0
- package/distribution/plugin.ts +122 -0
- package/package.json +34 -40
- package/skills/c64-assembler/SKILL.md +76 -0
- package/skills/c64-assembler/scripts/assemble.ts +110 -0
- package/skills/c64-assembler/scripts/compare.ts +84 -0
- package/skills/c64-basic/SKILL.md +66 -0
- package/skills/c64-basic/scripts/basic.ts +46 -0
- package/skills/c64-disk/SKILL.md +93 -0
- package/skills/c64-disk/scripts/containers.ts +124 -0
- package/skills/c64-disk/scripts/disk.ts +120 -0
- package/skills/c64-emulator/SKILL.md +134 -0
- package/skills/c64-knowledge/SKILL.md +166 -0
- package/skills/c64-knowledge/scripts/knowledge.ts +492 -0
- package/skills/c64-memory-map/SKILL.md +79 -0
- package/skills/c64-memory-map/scripts/memmap.ts +92 -0
- package/skills/c64-provenance/SKILL.md +110 -0
- package/skills/c64-provenance/scripts/compare.ts +152 -0
- package/skills/c64-provenance/scripts/provenance.ts +334 -0
- package/skills/c64-provenance/scripts/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/separate.ts +140 -0
- package/skills/c64-reverse-engineering/SKILL.md +144 -0
- package/skills/c64-source/SKILL.md +126 -0
- package/skills/c64-source/scripts/generate.ts +490 -0
- package/skills/c64-source/scripts/loader.ts +571 -0
- package/skills/c64-source/scripts/opcodes.ts +61 -0
- package/skills/c64-source/scripts/source.ts +595 -0
- package/skills/c64-static-analysis/SKILL.md +119 -0
- package/skills/c64-static-analysis/scripts/analyze.ts +275 -0
- package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
- package/skills/c64-static-analysis/scripts/findings.ts +75 -0
- package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
- package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
- package/skills/c64-testing/SKILL.md +103 -0
- package/skills/c64-testing/references/scenario-format.md +116 -0
- package/skills/c64-testing/scripts/checklist.ts +51 -0
- package/skills/c64-testing/scripts/run.ts +189 -0
- package/skills/c64-testing/scripts/scenario.ts +493 -0
- package/skills/c64-testing/scripts/test.ts +65 -0
- package/skills/c64-unpacker/SKILL.md +135 -0
- package/skills/c64-unpacker/scripts/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/stages.ts +130 -0
- package/skills/c64-unpacker/scripts/unpack.ts +683 -0
- package/src/c64.ts +411 -0
- package/src/cli/ap-sdk.ts +44 -0
- package/src/cli/cleanup.ts +194 -0
- package/src/cli/main.ts +125 -0
- package/src/cli/status.ts +61 -0
- package/src/host/main.ts +134 -0
- package/src/host/server.ts +392 -0
- package/src/host/tools/c1541.ts +284 -0
- package/src/host/tools/index.ts +35 -0
- package/src/host/tools/petcat.ts +77 -0
- package/src/host/tools/status.ts +47 -0
- package/src/host/tools/version.ts +41 -0
- package/src/host/vice/adapter.ts +804 -0
- package/src/host/vice/binary-monitor.ts +389 -0
- package/src/host/vice/chips.ts +118 -0
- package/src/host/vice/connect.ts +34 -0
- package/src/host/vice/process.ts +413 -0
- package/src/host/vice/screen.ts +184 -0
- package/src/host/vice/session.ts +1426 -0
- package/src/host/vice/text-monitor.ts +223 -0
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +241 -0
- package/src/host-client/coverage.ts +63 -0
- package/src/host-client/steps.ts +170 -0
- package/src/host-client/tools.ts +89 -0
- package/src/host-client/transfer.ts +126 -0
- package/src/host-client/vice-session.ts +316 -0
- package/src/knowledge/database.ts +132 -0
- package/src/knowledge/history.ts +163 -0
- package/src/knowledge/import.ts +391 -0
- package/src/knowledge/read.ts +342 -0
- package/src/knowledge/schema.ts +229 -0
- package/src/knowledge/write.ts +663 -0
- package/src/mcp/main.ts +134 -0
- package/src/mcp/output.ts +32 -0
- package/src/mcp/server.ts +316 -0
- package/src/mcp/tools/debug.ts +327 -0
- package/src/mcp/tools/execution.ts +117 -0
- package/src/mcp/tools/input.ts +72 -0
- package/src/mcp/tools/machine.ts +100 -0
- package/src/mcp/tools/media.ts +129 -0
- package/src/mcp/tools/memory.ts +250 -0
- package/src/mcp/tools/video.ts +271 -0
- package/src/native/acme.ts +116 -0
- package/src/native/discover.ts +179 -0
- package/src/native/dxa.ts +166 -0
- package/src/native/ghidra/analyze.ts +86 -0
- package/src/native/ghidra/index.ts +173 -0
- package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
- package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
- package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
- package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
- package/src/native/ghidra/scripts/C64Export.java +274 -0
- package/src/native/ghidra/scripts/C64Prepare.java +60 -0
- package/src/native/local.ts +34 -0
- package/src/native/processes.ts +350 -0
- package/src/native/run.ts +172 -0
- package/src/native/staging.ts +83 -0
- package/src/native/status.ts +113 -0
- package/src/native/types.ts +257 -0
- package/src/native/watchdog.ts +78 -0
- package/src/package.ts +21 -0
- package/src/platform.ts +441 -0
- package/src/project.ts +39 -0
- package/src/protocol/framing.ts +132 -0
- package/src/protocol/messages.ts +259 -0
- package/src/protocol/tools.ts +266 -0
- package/src/protocol/vice.ts +1348 -0
- package/src/script.ts +60 -0
- package/src/time.ts +15 -0
- package/src/trace.ts +194 -0
- package/THIRD-PARTY-NOTICES.md +0 -26
- package/bin/cli.mjs +0 -283
- package/skills/acme-build/SKILL.md +0 -260
- package/skills/acme-build/scripts/acme.mjs +0 -341
- package/skills/acme-build/template.a +0 -53
- package/skills/c64-disk-access/SKILL.md +0 -159
- package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
- package/skills/c64-memory-mapping/SKILL.md +0 -631
- package/skills/c64-memory-mapping/memmap.json +0 -8800
- package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
- package/skills/c64-petcat/SKILL.md +0 -87
- package/skills/c64-petcat/scripts/petcat.mjs +0 -221
- package/skills/c64-program-recon/SKILL.md +0 -706
- package/skills/c64-program-recon/references/control-flow.md +0 -180
- package/skills/c64-program-recon/references/graphics.md +0 -79
- package/skills/c64-program-recon/references/observation-hazards.md +0 -143
- package/skills/c64-program-recon/references/reconstruction.md +0 -133
- package/skills/c64-program-recon/references/sound-and-input.md +0 -74
- package/skills/c64-program-recon/references/tool-selection.md +0 -78
- package/skills/c64-program-recon/scripts/derive.mjs +0 -364
- package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
- package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
- package/skills/c64-provenance-diff/SKILL.md +0 -292
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
- package/skills/c64-ram-capture/RELEASES.json.example +0 -17
- package/skills/c64-ram-capture/SKILL.md +0 -474
- package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
- package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
- package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
- package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
- package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
- package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
- package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
- package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
- package/skills/c64-ram-capture/transients/README.md +0 -137
- package/skills/routine-queue-walker/SKILL.md +0 -394
- package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
- package/skills/vice-wedge-triage/SKILL.md +0 -269
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
// The c64-knowledge script: reads and writes .c64-re-tools/knowledge.db in the
|
|
2
|
+
// project directory (the working directory) and prints one compact JSON result.
|
|
3
|
+
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
7
|
+
import { parseArgs } from "node:util";
|
|
8
|
+
|
|
9
|
+
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
10
|
+
import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
|
|
11
|
+
import { KnowledgeError, openForRead, withWrite } from "#src/knowledge/database.ts";
|
|
12
|
+
import { historyAt, historyNamed, revision, revisionNote, revisions, type HistoryEntry } from "#src/knowledge/history.ts";
|
|
13
|
+
import {
|
|
14
|
+
at,
|
|
15
|
+
currentRevision,
|
|
16
|
+
listComments,
|
|
17
|
+
listModules,
|
|
18
|
+
listSpaces,
|
|
19
|
+
listReferences,
|
|
20
|
+
listRegions,
|
|
21
|
+
listSuppressions,
|
|
22
|
+
listSymbols,
|
|
23
|
+
search,
|
|
24
|
+
type CommentRow,
|
|
25
|
+
type ModuleRow,
|
|
26
|
+
type ReferenceRow,
|
|
27
|
+
type RegionRow,
|
|
28
|
+
type SuppressionRow,
|
|
29
|
+
type SymbolRow,
|
|
30
|
+
} from "#src/knowledge/read.ts";
|
|
31
|
+
import {
|
|
32
|
+
COMMENT_PLACEMENTS,
|
|
33
|
+
DEFAULT_SPACE,
|
|
34
|
+
EVIDENCE_KINDS,
|
|
35
|
+
ORIGINS,
|
|
36
|
+
REFERENCE_KINDS,
|
|
37
|
+
REGION_TYPES,
|
|
38
|
+
SPACE,
|
|
39
|
+
SYMBOL_KINDS,
|
|
40
|
+
type CommentPlacement,
|
|
41
|
+
type Origin,
|
|
42
|
+
type ReferenceKind,
|
|
43
|
+
type RegionType,
|
|
44
|
+
type SymbolKind,
|
|
45
|
+
} from "#src/knowledge/schema.ts";
|
|
46
|
+
import {
|
|
47
|
+
addReference,
|
|
48
|
+
applyBatch,
|
|
49
|
+
classifyRegion,
|
|
50
|
+
removeComment,
|
|
51
|
+
removeModule,
|
|
52
|
+
removeReference,
|
|
53
|
+
removeSymbol,
|
|
54
|
+
renameSymbol,
|
|
55
|
+
revert,
|
|
56
|
+
setComment,
|
|
57
|
+
setModule,
|
|
58
|
+
unclassifyRegion,
|
|
59
|
+
type BatchOperation,
|
|
60
|
+
type Evidence,
|
|
61
|
+
type WriteContext,
|
|
62
|
+
} from "#src/knowledge/write.ts";
|
|
63
|
+
import { runScript, UsageError } from "#src/script.ts";
|
|
64
|
+
|
|
65
|
+
const USAGE = `knowledge.ts <command> [arguments] [options]
|
|
66
|
+
|
|
67
|
+
Read (no database needed):
|
|
68
|
+
at <address> everything known at one address
|
|
69
|
+
search <text> symbol names and comment texts
|
|
70
|
+
symbols [--start A --end A --kind K --origin O]
|
|
71
|
+
regions [--start A --end A --type T --origin O]
|
|
72
|
+
comments [--start A --end A --origin O]
|
|
73
|
+
references [--start A --end A --kind K --origin O]
|
|
74
|
+
suppressions [--start A --end A] places that removals keep free of analyzer facts
|
|
75
|
+
modules [--start A --end A --origin O] where the files of generated source start
|
|
76
|
+
spaces the address spaces that hold knowledge
|
|
77
|
+
history <address> | history --name <name>
|
|
78
|
+
revisions [--limit N --before N]
|
|
79
|
+
revision <id>
|
|
80
|
+
export --out <file> [--start A --end A --origin O] all current knowledge to a project file
|
|
81
|
+
|
|
82
|
+
Every command takes --space S: c64 (the default), drive8 for code in the 1541
|
|
83
|
+
drive, or bank0 to bank255 for cartridge banks. A write in a batch can name its
|
|
84
|
+
own space.
|
|
85
|
+
|
|
86
|
+
Read option --origin O: user, llm, dxa, ghidra, heuristic, semantic (user and
|
|
87
|
+
llm) or analyzer (dxa, ghidra and heuristic), or a comma list. search takes it too.
|
|
88
|
+
|
|
89
|
+
Write (one revision each; creates the database when needed):
|
|
90
|
+
rename <address> <name> [--kind K]
|
|
91
|
+
remove-symbol <address>
|
|
92
|
+
classify <start> <end> <type> [--element N]
|
|
93
|
+
unclassify <start> <end>
|
|
94
|
+
comment <address> <line|side> <text>
|
|
95
|
+
uncomment <address> <line|side>
|
|
96
|
+
reference <from> <to> <kind>
|
|
97
|
+
unreference <from> <to> <kind>
|
|
98
|
+
module <start> <file.asm> [--title T] a file of generated source starts here
|
|
99
|
+
unmodule <start>
|
|
100
|
+
revert <revision>
|
|
101
|
+
apply <file> many writes from a JSON file ('-' reads standard input),
|
|
102
|
+
all as one revision, or none when one is refused
|
|
103
|
+
|
|
104
|
+
Write options:
|
|
105
|
+
--reason <text> why, kept with the revision
|
|
106
|
+
--evidence <kind:ref> what the change rests on: coverage:<file>, test:<file>,
|
|
107
|
+
file:<file> or note:<text> (repeatable)
|
|
108
|
+
--expect-revision <N> refuse the write if knowledge changed after revision N
|
|
109
|
+
--origin user|llm who decided (default llm)
|
|
110
|
+
|
|
111
|
+
Addresses are $ and four hex digits; quote them in a shell: '$2100'.`;
|
|
112
|
+
|
|
113
|
+
function address(value: string | undefined, what = "address"): number {
|
|
114
|
+
if (value === undefined) throw new UsageError(`${what} is missing`);
|
|
115
|
+
try {
|
|
116
|
+
return parseC64Address(value);
|
|
117
|
+
} catch {
|
|
118
|
+
throw new UsageError(`${what} must be $ followed by four hex digits, for example '$2100', not ${value}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function oneOf<T extends string>(values: readonly T[], value: string | undefined, what: string): T {
|
|
123
|
+
if (value === undefined || !(values as readonly string[]).includes(value)) throw new UsageError(`${what} must be one of: ${values.join(", ")}`);
|
|
124
|
+
return value as T;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function integer(value: string | undefined, what: string): number {
|
|
128
|
+
if (value === undefined || !/^\d+$/.test(value)) throw new UsageError(`${what} must be a whole number`);
|
|
129
|
+
return Number(value);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Output shapes: addresses as $xxxx, nothing internal.
|
|
133
|
+
/** The space of a row, shown only when it is not the C64 (D53). */
|
|
134
|
+
const spaceOf = (row: { space: string }) => (row.space === "c64" ? {} : { space: row.space });
|
|
135
|
+
const symbol = (row: SymbolRow) => ({ ...spaceOf(row), address: formatC64Address(row.address), name: row.name, kind: row.kind, origin: row.origin, revision: row.revision });
|
|
136
|
+
const region = (row: RegionRow) => ({
|
|
137
|
+
...spaceOf(row),
|
|
138
|
+
start: formatC64Address(row.start),
|
|
139
|
+
end: formatC64Address(row.end),
|
|
140
|
+
type: row.type,
|
|
141
|
+
...(row.element === undefined ? {} : { element: row.element, count: (row.end - row.start + 1) / row.element }),
|
|
142
|
+
origin: row.origin,
|
|
143
|
+
revision: row.revision,
|
|
144
|
+
});
|
|
145
|
+
const comment = (row: CommentRow) => ({ ...spaceOf(row), address: formatC64Address(row.address), placement: row.placement, text: row.text, origin: row.origin, revision: row.revision });
|
|
146
|
+
const module = (row: ModuleRow) => ({ ...spaceOf(row), start: formatC64Address(row.start), file: row.file, ...(row.title === undefined ? {} : { title: row.title }), origin: row.origin, revision: row.revision });
|
|
147
|
+
const reference = (row: ReferenceRow) => ({ ...spaceOf(row), from: formatC64Address(row.from), to: formatC64Address(row.to), kind: row.kind, origin: row.origin, revision: row.revision });
|
|
148
|
+
function suppression(row: SuppressionRow) {
|
|
149
|
+
const place =
|
|
150
|
+
row.entity === "symbol"
|
|
151
|
+
? { address: formatC64Address(row.start) }
|
|
152
|
+
: row.entity === "region"
|
|
153
|
+
? { start: formatC64Address(row.start), end: formatC64Address(row.end) }
|
|
154
|
+
: { from: formatC64Address(row.start), to: formatC64Address(row.to!), kind: row.kind! };
|
|
155
|
+
return { ...spaceOf(row), suppresses: row.entity, ...place, origin: row.origin, revision: row.revision };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function historyEntry(entry: HistoryEntry) {
|
|
159
|
+
const shown =
|
|
160
|
+
entry.entity === "symbol"
|
|
161
|
+
? symbol(entry.row)
|
|
162
|
+
: entry.entity === "region"
|
|
163
|
+
? region(entry.row)
|
|
164
|
+
: entry.entity === "comment"
|
|
165
|
+
? comment(entry.row)
|
|
166
|
+
: entry.entity === "reference"
|
|
167
|
+
? reference(entry.row)
|
|
168
|
+
: entry.entity === "module"
|
|
169
|
+
? module(entry.row)
|
|
170
|
+
: suppression(entry.row);
|
|
171
|
+
const { revision: _revision, ...row } = shown;
|
|
172
|
+
return { entity: entry.entity, ...row, fromRevision: entry.from, ...(entry.to === undefined ? {} : { toRevision: entry.to }) };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The reason of each revision that wrote one of the rows. */
|
|
176
|
+
function reasonsOf(db: DatabaseSync | undefined, rows: SuppressionRow[]): Map<number, string> {
|
|
177
|
+
const reasons = new Map<number, string>();
|
|
178
|
+
const query = db?.prepare("SELECT description FROM revisions WHERE id = ?");
|
|
179
|
+
for (const { revision } of rows) {
|
|
180
|
+
if (query === undefined || reasons.has(revision)) continue;
|
|
181
|
+
const found = query.get(revision) as { description: string | null } | undefined;
|
|
182
|
+
if (found?.description != null) reasons.set(revision, found.description);
|
|
183
|
+
}
|
|
184
|
+
return reasons;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function run(argv: string[]): unknown {
|
|
188
|
+
const { values, positionals } = parseArgs({
|
|
189
|
+
args: argv,
|
|
190
|
+
allowPositionals: true,
|
|
191
|
+
strict: true,
|
|
192
|
+
options: {
|
|
193
|
+
start: { type: "string" },
|
|
194
|
+
end: { type: "string" },
|
|
195
|
+
kind: { type: "string" },
|
|
196
|
+
type: { type: "string" },
|
|
197
|
+
name: { type: "string" },
|
|
198
|
+
limit: { type: "string" },
|
|
199
|
+
before: { type: "string" },
|
|
200
|
+
reason: { type: "string" },
|
|
201
|
+
"expect-revision": { type: "string" },
|
|
202
|
+
origin: { type: "string" },
|
|
203
|
+
element: { type: "string" },
|
|
204
|
+
evidence: { type: "string", multiple: true },
|
|
205
|
+
out: { type: "string" },
|
|
206
|
+
title: { type: "string" },
|
|
207
|
+
space: { type: "string" },
|
|
208
|
+
help: { type: "boolean" },
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
const [command, ...args] = positionals;
|
|
212
|
+
if (values.help || command === undefined) throw new UsageError("");
|
|
213
|
+
const range = { start: values.start === undefined ? 0x0000 : address(values.start, "--start"), end: values.end === undefined ? 0xffff : address(values.end, "--end") };
|
|
214
|
+
if (values.space !== undefined && !SPACE.test(values.space)) throw new UsageError("--space must be c64, drive8, or bank0 to bank255");
|
|
215
|
+
const space = values.space ?? DEFAULT_SPACE;
|
|
216
|
+
|
|
217
|
+
const read = new Set(["at", "search", "symbols", "regions", "comments", "references", "suppressions", "modules", "spaces", "history", "revisions", "revision", "export"]);
|
|
218
|
+
const write = new Set(["rename", "remove-symbol", "classify", "unclassify", "comment", "uncomment", "reference", "unreference", "module", "unmodule", "revert", "apply"]);
|
|
219
|
+
// Refuse a mistyped command before anything opens, so no database appears by accident.
|
|
220
|
+
if (!read.has(command) && !write.has(command)) throw new UsageError(`unknown command: ${command}`);
|
|
221
|
+
if (read.has(command)) {
|
|
222
|
+
const origins = values.origin === undefined ? undefined : originFilter(values.origin);
|
|
223
|
+
if (command === "export" && values.out === undefined) throw new UsageError("export needs --out <file>");
|
|
224
|
+
const db = openForRead();
|
|
225
|
+
try {
|
|
226
|
+
switch (command) {
|
|
227
|
+
case "at": {
|
|
228
|
+
const known = at(db, address(args[0]), space);
|
|
229
|
+
return {
|
|
230
|
+
...(space === DEFAULT_SPACE ? {} : { space }),
|
|
231
|
+
address: formatC64Address(known.address),
|
|
232
|
+
...(known.symbol === undefined ? {} : { symbol: symbol(known.symbol) }),
|
|
233
|
+
...(known.region === undefined ? {} : { region: region(known.region) }),
|
|
234
|
+
comments: known.comments.map(comment),
|
|
235
|
+
referencesFrom: known.referencesFrom.map(reference),
|
|
236
|
+
referencesTo: known.referencesTo.map(reference),
|
|
237
|
+
...(known.suppressions.length === 0 ? {} : { suppressions: known.suppressions.map(suppression) }),
|
|
238
|
+
revision: known.revision,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
case "search": {
|
|
242
|
+
if (args[0] === undefined || args[0] === "") throw new UsageError("search needs text");
|
|
243
|
+
const found = search(db, args[0], values.limit === undefined ? 50 : integer(values.limit, "--limit"), origins, space);
|
|
244
|
+
return { symbols: found.symbols.map(symbol), comments: found.comments.map(comment) };
|
|
245
|
+
}
|
|
246
|
+
case "symbols":
|
|
247
|
+
return { symbols: listSymbols(db, range, values.kind === undefined ? undefined : oneOf(SYMBOL_KINDS, values.kind, "--kind"), origins, space).map(symbol) };
|
|
248
|
+
case "regions":
|
|
249
|
+
return { regions: listRegions(db, range, values.type === undefined ? undefined : oneOf(REGION_TYPES, values.type, "--type"), origins, space).map(region) };
|
|
250
|
+
case "comments":
|
|
251
|
+
return { comments: listComments(db, range, origins, space).map(comment) };
|
|
252
|
+
case "references":
|
|
253
|
+
return { references: listReferences(db, range, values.kind === undefined ? undefined : oneOf(REFERENCE_KINDS, values.kind, "--kind"), origins, space).map(reference) };
|
|
254
|
+
case "export": {
|
|
255
|
+
const exported = {
|
|
256
|
+
revision: currentRevision(db),
|
|
257
|
+
...(space === DEFAULT_SPACE ? {} : { space }),
|
|
258
|
+
symbols: listSymbols(db, range, undefined, origins, space).map(symbol),
|
|
259
|
+
regions: listRegions(db, range, undefined, origins, space).map(region),
|
|
260
|
+
comments: listComments(db, range, origins, space).map(comment),
|
|
261
|
+
references: listReferences(db, range, undefined, origins, space).map(reference),
|
|
262
|
+
suppressions: listSuppressions(db, range, space).map(suppression),
|
|
263
|
+
modules: listModules(db, range, origins, space).map(module),
|
|
264
|
+
};
|
|
265
|
+
const bytes = Buffer.from(`${JSON.stringify(exported, null, 2)}\n`, "utf8");
|
|
266
|
+
writeProjectFile(values.out!, bytes, { what: "--out" });
|
|
267
|
+
return {
|
|
268
|
+
out: values.out,
|
|
269
|
+
bytes: bytes.length,
|
|
270
|
+
counts: { symbols: exported.symbols.length, regions: exported.regions.length, comments: exported.comments.length, references: exported.references.length, suppressions: exported.suppressions.length, modules: exported.modules.length },
|
|
271
|
+
revision: exported.revision,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
case "suppressions": {
|
|
275
|
+
const rows = listSuppressions(db, range, space);
|
|
276
|
+
const reasons = reasonsOf(db, rows);
|
|
277
|
+
return { suppressions: rows.map((row) => ({ ...suppression(row), ...(reasons.has(row.revision) ? { reason: reasons.get(row.revision) } : {}) })) };
|
|
278
|
+
}
|
|
279
|
+
case "modules":
|
|
280
|
+
return { modules: listModules(db, range, origins, space).map(module) };
|
|
281
|
+
case "spaces":
|
|
282
|
+
return { spaces: listSpaces(db) };
|
|
283
|
+
case "history":
|
|
284
|
+
// Each row with the reason and the evidence of the revision that wrote it (F-26).
|
|
285
|
+
return {
|
|
286
|
+
history: (values.name === undefined ? historyAt(db, address(args[0]), space) : historyNamed(db, values.name, space)).map((entry) => ({ ...historyEntry(entry), ...revisionNote(db, entry.from) })),
|
|
287
|
+
};
|
|
288
|
+
case "revisions": {
|
|
289
|
+
const options: { limit?: number; before?: number } = {};
|
|
290
|
+
if (values.limit !== undefined) options.limit = integer(values.limit, "--limit");
|
|
291
|
+
if (values.before !== undefined) options.before = integer(values.before, "--before");
|
|
292
|
+
return { revisions: revisions(db, options) };
|
|
293
|
+
}
|
|
294
|
+
default: {
|
|
295
|
+
const changes = revision(db, integer(args[0], "revision"));
|
|
296
|
+
return { revision: changes.revision, added: changes.added.map(historyEntry), closed: changes.closed.map(historyEntry) };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
} finally {
|
|
300
|
+
db?.close();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const context: WriteContext = { origin: values.origin === undefined ? "llm" : oneOf(["user", "llm"] as const, values.origin, "--origin") };
|
|
305
|
+
if (values.reason !== undefined) context.description = values.reason;
|
|
306
|
+
if (values["expect-revision"] !== undefined) context.expectedRevision = integer(values["expect-revision"], "--expect-revision");
|
|
307
|
+
if (values.evidence !== undefined) context.evidence = values.evidence.map(evidenceOf);
|
|
308
|
+
// Every argument is checked before the database opens: a refused write creates no database.
|
|
309
|
+
if (values.title !== undefined && command !== "module") throw new UsageError("--title works only with module");
|
|
310
|
+
const operation = command === "apply" ? applyOperation(args, context, space) : writeOperation(command, args, values.kind, values.element, context, values.title, space);
|
|
311
|
+
return withWrite(operation);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** --origin for reads: one origin, semantic (user and llm), analyzer (dxa and ghidra), or a comma list. */
|
|
315
|
+
function originFilter(value: string): Origin[] {
|
|
316
|
+
const origins = new Set<Origin>();
|
|
317
|
+
for (const part of value.split(",")) {
|
|
318
|
+
if (part === "semantic") origins.add("user").add("llm");
|
|
319
|
+
else if (part === "analyzer") origins.add("dxa").add("ghidra").add("heuristic");
|
|
320
|
+
else origins.add(oneOf(ORIGINS, part, "--origin"));
|
|
321
|
+
}
|
|
322
|
+
return [...origins];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** --evidence kind:ref. A file kind names a project file and keeps its SHA-256, so a later change of the file shows. */
|
|
326
|
+
function evidenceOf(value: string): Evidence {
|
|
327
|
+
const colon = value.indexOf(":");
|
|
328
|
+
const kind = oneOf(EVIDENCE_KINDS, colon < 0 ? value : value.slice(0, colon), "--evidence kind");
|
|
329
|
+
const ref = colon < 0 ? "" : value.slice(colon + 1);
|
|
330
|
+
if (ref === "") throw new UsageError(`--evidence needs ${kind}:<${kind === "note" ? "text" : "file"}>`);
|
|
331
|
+
if (kind === "note") return { kind, ref };
|
|
332
|
+
return { kind, ref, sha256: createHash("sha256").update(readProjectFile(ref).bytes).digest("hex") };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** apply <file>: the batch as BatchOperation values, checked before the database opens. */
|
|
336
|
+
function applyOperation(args: string[], context: WriteContext, space: string): WriteOperation {
|
|
337
|
+
if (args.length !== 1) throw new UsageError("apply needs one file, or - for standard input");
|
|
338
|
+
const text = args[0] === "-" ? readFileSync(0, "utf8") : readProjectFile(args[0]!).bytes.toString("utf8");
|
|
339
|
+
let parsed: unknown;
|
|
340
|
+
try {
|
|
341
|
+
parsed = JSON.parse(text);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
throw new UsageError(`the batch is not JSON: ${(error as Error).message}`);
|
|
344
|
+
}
|
|
345
|
+
if (!Array.isArray(parsed)) throw new UsageError("the batch is a JSON list of writes, for example [{\"op\": \"rename\", \"address\": \"$2100\", \"name\": \"update_player\"}]");
|
|
346
|
+
const operations = parsed.map((item, index) => batchOperation(item, `write ${index + 1}`, space));
|
|
347
|
+
return (db) => {
|
|
348
|
+
const applied = applyBatch(db, context, operations);
|
|
349
|
+
// replaced: each write that replaced a row of another origin, with that row as previous gives it for one write (b2/07).
|
|
350
|
+
const replaced = applied.steps.flatMap((step, index) =>
|
|
351
|
+
step.previous === undefined ? [] : [{ write: index + 1, op: operations[index]!.op, previous: Array.isArray(step.previous) ? step.previous.map(region) : symbol(step.previous) }],
|
|
352
|
+
);
|
|
353
|
+
return { revision: applied.revision, writes: applied.steps.length, replaced };
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const BATCH_FIELDS: Record<BatchOperation["op"], string[]> = {
|
|
358
|
+
rename: ["address", "name", "kind"],
|
|
359
|
+
"remove-symbol": ["address"],
|
|
360
|
+
classify: ["start", "end", "type", "element"],
|
|
361
|
+
unclassify: ["start", "end"],
|
|
362
|
+
comment: ["address", "placement", "text"],
|
|
363
|
+
uncomment: ["address", "placement"],
|
|
364
|
+
reference: ["from", "to", "kind"],
|
|
365
|
+
unreference: ["from", "to", "kind"],
|
|
366
|
+
module: ["start", "file", "title"],
|
|
367
|
+
unmodule: ["start"],
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
/** One write of a batch. A write names its own space, or takes the one of --space. */
|
|
371
|
+
function batchOperation(item: unknown, at: string, space: string): BatchOperation {
|
|
372
|
+
const operation = batchFields(item, at);
|
|
373
|
+
const own = (item as Record<string, unknown>).space;
|
|
374
|
+
if (own !== undefined && typeof own !== "string") throw new UsageError(`${at}: space must be a string`);
|
|
375
|
+
return { ...operation, space: own ?? space };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** The fields of one write of a batch, with its addresses parsed. */
|
|
379
|
+
function batchFields(item: unknown, at: string): BatchOperation {
|
|
380
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) throw new UsageError(`${at}: a write is an object with op`);
|
|
381
|
+
const fields = item as Record<string, unknown>;
|
|
382
|
+
const op = oneOf(Object.keys(BATCH_FIELDS) as Array<BatchOperation["op"]>, String(fields.op), `${at}: op`);
|
|
383
|
+
const unknown = Object.keys(fields).filter((key) => key !== "op" && key !== "space" && !BATCH_FIELDS[op].includes(key));
|
|
384
|
+
if (unknown.length > 0) throw new UsageError(`${at}: ${op} takes ${BATCH_FIELDS[op].join(", ")}, not ${unknown.join(", ")}`);
|
|
385
|
+
const text = (key: string) => {
|
|
386
|
+
if (typeof fields[key] !== "string") throw new UsageError(`${at}: ${key} must be a string`);
|
|
387
|
+
return fields[key] as string;
|
|
388
|
+
};
|
|
389
|
+
const place = (key: string) => address(text(key), `${at}: ${key}`);
|
|
390
|
+
switch (op) {
|
|
391
|
+
case "rename":
|
|
392
|
+
return { op, address: place("address"), name: text("name"), ...(fields.kind === undefined ? {} : { kind: oneOf(SYMBOL_KINDS, text("kind"), `${at}: kind`) }) };
|
|
393
|
+
case "remove-symbol":
|
|
394
|
+
return { op, address: place("address") };
|
|
395
|
+
case "classify": {
|
|
396
|
+
if (fields.element !== undefined && typeof fields.element !== "number") throw new UsageError(`${at}: element must be a number`);
|
|
397
|
+
return { op, start: place("start"), end: place("end"), type: oneOf(REGION_TYPES, text("type"), `${at}: type`), ...(fields.element === undefined ? {} : { element: fields.element as number }) };
|
|
398
|
+
}
|
|
399
|
+
case "unclassify":
|
|
400
|
+
return { op, start: place("start"), end: place("end") };
|
|
401
|
+
case "comment":
|
|
402
|
+
return { op, address: place("address"), placement: oneOf(COMMENT_PLACEMENTS, text("placement"), `${at}: placement`), text: text("text") };
|
|
403
|
+
case "uncomment":
|
|
404
|
+
return { op, address: place("address"), placement: oneOf(COMMENT_PLACEMENTS, text("placement"), `${at}: placement`) };
|
|
405
|
+
case "module":
|
|
406
|
+
return { op, start: place("start"), file: text("file"), ...(fields.title === undefined ? {} : { title: text("title") }) };
|
|
407
|
+
case "unmodule":
|
|
408
|
+
return { op, start: place("start") };
|
|
409
|
+
default:
|
|
410
|
+
return { op, from: place("from"), to: place("to"), kind: oneOf(REFERENCE_KINDS, text("kind"), `${at}: kind`) };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
type WriteOperation = (db: DatabaseSync) => unknown;
|
|
415
|
+
|
|
416
|
+
function writeOperation(command: string, args: string[], kind: string | undefined, element: string | undefined, context: WriteContext, title: string | undefined, space: string): WriteOperation {
|
|
417
|
+
switch (command) {
|
|
418
|
+
case "rename": {
|
|
419
|
+
if (args[1] === undefined) throw new UsageError("name is missing");
|
|
420
|
+
const request: { space: string; address: number; name: string; kind?: SymbolKind } = { space, address: address(args[0]), name: args[1] };
|
|
421
|
+
if (kind !== undefined) request.kind = oneOf(SYMBOL_KINDS, kind, "--kind");
|
|
422
|
+
return (db) => {
|
|
423
|
+
const result = renameSymbol(db, context, request);
|
|
424
|
+
return { revision: result.revision, symbol: symbol(result.current), ...(result.previous === undefined ? {} : { previous: symbol(result.previous) }) };
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
case "remove-symbol": {
|
|
428
|
+
const request = { space, address: address(args[0]) };
|
|
429
|
+
return (db) => ({ revision: removeSymbol(db, context, request).revision });
|
|
430
|
+
}
|
|
431
|
+
case "classify": {
|
|
432
|
+
const request = {
|
|
433
|
+
space,
|
|
434
|
+
start: address(args[0], "start"),
|
|
435
|
+
end: address(args[1], "end"),
|
|
436
|
+
type: oneOf(REGION_TYPES, args[2], "type") as RegionType,
|
|
437
|
+
...(element === undefined ? {} : { element: integer(element, "--element") }),
|
|
438
|
+
};
|
|
439
|
+
return (db) => {
|
|
440
|
+
const result = classifyRegion(db, context, request);
|
|
441
|
+
return { revision: result.revision, regions: result.current.map(region), ...(result.previous === undefined ? {} : { previous: result.previous.map(region) }) };
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
case "unclassify": {
|
|
445
|
+
const request = { space, start: address(args[0], "start"), end: address(args[1], "end") };
|
|
446
|
+
return (db) => ({ revision: unclassifyRegion(db, context, request).revision });
|
|
447
|
+
}
|
|
448
|
+
case "comment": {
|
|
449
|
+
if (args.length < 3) throw new UsageError("text is missing");
|
|
450
|
+
const request = { space, address: address(args[0]), placement: oneOf(COMMENT_PLACEMENTS, args[1], "placement") as CommentPlacement, text: args.slice(2).join(" ") };
|
|
451
|
+
return (db) => {
|
|
452
|
+
const result = setComment(db, context, request);
|
|
453
|
+
return { revision: result.revision, comment: comment(result.current) };
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
case "uncomment": {
|
|
457
|
+
const request = { space, address: address(args[0]), placement: oneOf(COMMENT_PLACEMENTS, args[1], "placement") };
|
|
458
|
+
return (db) => ({ revision: removeComment(db, context, request).revision });
|
|
459
|
+
}
|
|
460
|
+
case "reference": {
|
|
461
|
+
const request = { space, from: address(args[0], "from"), to: address(args[1], "to"), kind: oneOf(REFERENCE_KINDS, args[2], "kind") as ReferenceKind };
|
|
462
|
+
return (db) => {
|
|
463
|
+
const result = addReference(db, context, request);
|
|
464
|
+
return { revision: result.revision, reference: reference(result.current) };
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
case "unreference": {
|
|
468
|
+
const request = { space, from: address(args[0], "from"), to: address(args[1], "to"), kind: oneOf(REFERENCE_KINDS, args[2], "kind") };
|
|
469
|
+
return (db) => ({ revision: removeReference(db, context, request).revision });
|
|
470
|
+
}
|
|
471
|
+
case "module": {
|
|
472
|
+
if (args[1] === undefined) throw new UsageError("file is missing");
|
|
473
|
+
const request = { space, start: address(args[0], "start"), file: args[1], ...(title === undefined ? {} : { title }) };
|
|
474
|
+
return (db) => {
|
|
475
|
+
const result = setModule(db, context, request);
|
|
476
|
+
return { revision: result.revision, module: module(result.current) };
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
case "unmodule": {
|
|
480
|
+
const request = { space, start: address(args[0], "start") };
|
|
481
|
+
return (db) => ({ revision: removeModule(db, context, request).revision });
|
|
482
|
+
}
|
|
483
|
+
case "revert": {
|
|
484
|
+
const request = { revision: integer(args[0], "revision") };
|
|
485
|
+
return (db) => ({ revision: revert(db, context, request).revision });
|
|
486
|
+
}
|
|
487
|
+
default:
|
|
488
|
+
throw new UsageError(`unknown command: ${command}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
await runScript(() => run(process.argv.slice(2)), { usage: USAGE, refusals: [KnowledgeError] });
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: c64-memory-map
|
|
3
|
+
description: Use this skill to find what a C64 address means on the platform. Examples are a VIC-II, SID or CIA register, a KERNAL routine, a vector or a system variable. It also decodes the value of a hardware register, for example the memory configuration in $01 or the screen address in $D018. It tells you when an address has no platform meaning and belongs to the application.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# c64-memory-map
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
Give the C64 platform meaning of an address or of a register value.
|
|
11
|
+
|
|
12
|
+
This skill does not read the emulator and does not change project knowledge.
|
|
13
|
+
|
|
14
|
+
## Inputs
|
|
15
|
+
|
|
16
|
+
- One or more addresses.
|
|
17
|
+
- Or a register and a value to decode.
|
|
18
|
+
|
|
19
|
+
## Tools / execution path
|
|
20
|
+
|
|
21
|
+
The script looks up local reference data. It does not use the host runtime.
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
node <skill>/scripts/memmap.ts at '$d020' '$0314' '$ffd2'
|
|
25
|
+
node <skill>/scripts/memmap.ts decode '$d018' '$15' --bank '$97'
|
|
26
|
+
node <skill>/scripts/memmap.ts list vic
|
|
27
|
+
node <skill>/scripts/memmap.ts export --out src/hw.asm
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Command | Result |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `at <address> ...` | For each address: `kind` (`platform` or `application`), the area, the name and the meaning. |
|
|
33
|
+
| `decode <register> <value>` | The meaning of the value. Use `--bank` with the value of `$DD00` to get the addresses for `$D018`. |
|
|
34
|
+
| `list <vic\|sid\|cia1\|cia2\|kernal\|system>` | All entries of that group. |
|
|
35
|
+
| `export --out <file>` | Writes all platform names to a project file as an ACME include: one `NAME = $xxxx` line for each name, with its meaning as a comment. |
|
|
36
|
+
|
|
37
|
+
Write an address or a value as `$` and hex digits, as a decimal number, or as `%` and binary digits. Put a `$` value in single quotes in a shell.
|
|
38
|
+
|
|
39
|
+
The script prints one JSON object.
|
|
40
|
+
|
|
41
|
+
## Workflow
|
|
42
|
+
|
|
43
|
+
1. Run `at` for the addresses in the code or the question.
|
|
44
|
+
2. Read `kind`:
|
|
45
|
+
- `platform`: the C64 gives the address a meaning. Read `name` and `meaning`.
|
|
46
|
+
- `application`: the address has no platform meaning. The program decides its use. Find its use with the c64-static-analysis or c64-emulator skill.
|
|
47
|
+
3. For an I/O register, `mirrorOf` shows the register that a mirror address repeats. `visible` tells that an address in `$D000`-`$DFFF` is visible only when `$01` shows I/O at `$D000`. This area holds the chip registers, the color RAM and the cartridge I/O areas.
|
|
48
|
+
4. For a byte in a multi-byte entry, for example the high byte of a vector, `entryStart` gives the first address of the entry.
|
|
49
|
+
5. To understand a value in a register, run `decode`. For the memory configuration, decode the value of `$01`.
|
|
50
|
+
6. For source that you write by hand, run `export` and include the file with `!source`. The c64-source skill uses the same names without the file.
|
|
51
|
+
|
|
52
|
+
The decoders are for `$01`, `$D010`, `$D011`, `$D015` to `$D01F` (the sprite and interrupt bits), `$D016`, `$D018`, `$DC0D`, `$DD00` and `$DD0D`.
|
|
53
|
+
|
|
54
|
+
## Knowledge
|
|
55
|
+
|
|
56
|
+
Automatic write:
|
|
57
|
+
- none.
|
|
58
|
+
|
|
59
|
+
Platform facts are reference data. Do not store them as project knowledge.
|
|
60
|
+
|
|
61
|
+
A conclusion about the application can be project knowledge. For example: "the program writes its own IRQ handler address to `$0314`". Record it with the c64-knowledge skill.
|
|
62
|
+
|
|
63
|
+
## Result
|
|
64
|
+
|
|
65
|
+
The platform meaning of each address, or the decoded meaning of a register value. For `export`: the file, the count of names and the bytes.
|
|
66
|
+
|
|
67
|
+
The exit status is 0 for a result. The exit status is 2 for a wrong argument.
|
|
68
|
+
|
|
69
|
+
## Failure and conflicts
|
|
70
|
+
|
|
71
|
+
- `kind: application`: this is not a failure. The address has no platform meaning.
|
|
72
|
+
- `invalid-input`: the address, the register or the value is not correct, or no decoder exists for the register. Read the message.
|
|
73
|
+
|
|
74
|
+
Some registers have a different meaning for a read and for a write, for example `$DC0D`. The decoded result shows both.
|
|
75
|
+
|
|
76
|
+
## Handoffs
|
|
77
|
+
|
|
78
|
+
- The value of an address at run time → c64-emulator
|
|
79
|
+
- A conclusion about the application → c64-knowledge
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// The c64-memory-map script: the platform meaning of C64 addresses and of
|
|
2
|
+
// values in platform registers. Local lookup only; nothing is stored. export
|
|
3
|
+
// writes the platform names to a project file as an ACME include (D52).
|
|
4
|
+
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
|
|
7
|
+
import { writeProjectFile } from "#src/host-client/transfer.ts";
|
|
8
|
+
import { decode, IO, lookup, platformNames, ROM, SYSTEM, type Entry } from "#src/platform.ts";
|
|
9
|
+
import { runScript, UsageError } from "#src/script.ts";
|
|
10
|
+
|
|
11
|
+
const USAGE = `memmap.ts at <address> [<address> ...]
|
|
12
|
+
memmap.ts decode <register> <value> [--bank <$dd00 value>]
|
|
13
|
+
memmap.ts list <vic|sid|cia1|cia2|kernal|system>
|
|
14
|
+
memmap.ts export --out <file> the platform names as an ACME include (NAME = $xxxx)
|
|
15
|
+
|
|
16
|
+
Addresses and values are $ and hex digits, decimal, or % and binary digits.
|
|
17
|
+
The result is one JSON object.`;
|
|
18
|
+
|
|
19
|
+
const hex = (value: number, digits = 4) => `$${value.toString(16).padStart(digits, "0")}`;
|
|
20
|
+
|
|
21
|
+
function number(text: string, max: number, what: string): number {
|
|
22
|
+
let value = Number.NaN;
|
|
23
|
+
if (/^\$[0-9a-f]{1,4}$/i.test(text)) value = Number.parseInt(text.slice(1), 16);
|
|
24
|
+
else if (/^%[01]{1,16}$/.test(text)) value = Number.parseInt(text.slice(1), 2);
|
|
25
|
+
else if (/^\d{1,5}$/.test(text)) value = Number(text);
|
|
26
|
+
if (!(value >= 0 && value <= max)) throw new UsageError(`${what} must be from 0 to ${hex(max, max > 0xff ? 4 : 2)}, not ${text}`);
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const show = (entry: Entry) => ({ start: hex(entry.start), ...(entry.end !== entry.start ? { end: hex(entry.end) } : {}), name: entry.name, meaning: entry.meaning });
|
|
31
|
+
|
|
32
|
+
function run(argv: string[]): unknown {
|
|
33
|
+
const { values, positionals } = parseArgs({ args: argv, strict: true, allowPositionals: true, options: { bank: { type: "string" }, out: { type: "string" }, help: { type: "boolean" } } });
|
|
34
|
+
if (values.help || positionals.length === 0) throw new UsageError("");
|
|
35
|
+
const [command, ...rest] = positionals;
|
|
36
|
+
switch (command) {
|
|
37
|
+
case "at": {
|
|
38
|
+
if (rest.length === 0) throw new UsageError("give at least one address");
|
|
39
|
+
return {
|
|
40
|
+
addresses: rest.map((text) => {
|
|
41
|
+
const found = lookup(number(text, 0xffff, "an address"));
|
|
42
|
+
// The chip registers, color RAM and the cartridge I/O areas show only while $01 maps I/O in.
|
|
43
|
+
const io = found.address >= 0xd000 && found.address <= 0xdfff;
|
|
44
|
+
return {
|
|
45
|
+
...found,
|
|
46
|
+
address: hex(found.address),
|
|
47
|
+
...(found.mirrorOf === undefined ? {} : { mirrorOf: hex(found.mirrorOf) }),
|
|
48
|
+
...(found.entryStart === undefined ? {} : { entryStart: hex(found.entryStart) }),
|
|
49
|
+
...(io ? { visible: "when $01 shows I/O at $D000 (the default)" } : {}),
|
|
50
|
+
};
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
case "decode": {
|
|
55
|
+
if (rest.length !== 2) throw new UsageError("decode takes a register and a value");
|
|
56
|
+
const register = number(rest[0]!, 0xffff, "the register");
|
|
57
|
+
const value = number(rest[1]!, 0xff, "the value");
|
|
58
|
+
const bank = values.bank === undefined ? undefined : number(values.bank, 0xff, "--bank");
|
|
59
|
+
try {
|
|
60
|
+
return { register: hex(register), value: hex(value, 2), name: lookup(register).name, ...decode(register, value, bank) };
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error instanceof RangeError) throw new UsageError(error.message);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
case "list": {
|
|
67
|
+
const lists: Record<string, Entry[]> = {
|
|
68
|
+
vic: IO[0]!.registers,
|
|
69
|
+
sid: IO[1]!.registers,
|
|
70
|
+
cia1: IO[2]!.registers,
|
|
71
|
+
cia2: IO[3]!.registers,
|
|
72
|
+
kernal: ROM,
|
|
73
|
+
system: SYSTEM,
|
|
74
|
+
};
|
|
75
|
+
const list = lists[rest[0] ?? ""];
|
|
76
|
+
if (list === undefined || rest.length !== 1) throw new UsageError(`list takes one of ${Object.keys(lists).join(", ")}`);
|
|
77
|
+
return { entries: list.map(show) };
|
|
78
|
+
}
|
|
79
|
+
case "export": {
|
|
80
|
+
if (rest.length !== 0 || values.out === undefined) throw new UsageError("export takes --out <file> and nothing else");
|
|
81
|
+
const names = platformNames();
|
|
82
|
+
const lines = ["; C64 platform names from the c64-memory-map skill, for ACME.", ...names.map((item) => `${item.name} = ${hex(item.address, item.address < 0x100 ? 2 : 4)}`.padEnd(24) + `; ${item.meaning}`)];
|
|
83
|
+
const bytes = Buffer.from(`${lines.join("\n")}\n`, "utf8");
|
|
84
|
+
writeProjectFile(values.out, bytes, { what: "--out" });
|
|
85
|
+
return { out: values.out, names: names.length, bytes: bytes.length };
|
|
86
|
+
}
|
|
87
|
+
default:
|
|
88
|
+
throw new UsageError(`unknown command ${command}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await runScript(() => run(process.argv.slice(2)), { usage: USAGE });
|