@henols/c64-re-tools 2.0.1 → 2.1.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.
- package/README.md +8 -4
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +24 -9
- package/skills/c64-assembler/scripts/assembler.ts +95 -0
- package/skills/c64-basic/SKILL.md +17 -9
- package/skills/c64-basic/scripts/basic.ts +26 -36
- package/skills/c64-disk/SKILL.md +17 -7
- package/skills/c64-disk/scripts/disk.ts +130 -104
- package/skills/c64-disk/scripts/lib/containers.ts +10 -12
- package/skills/c64-docs/SKILL.md +47 -28
- package/skills/c64-docs/references/style.md +6 -6
- package/skills/c64-docs/scripts/docs.ts +323 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +1 -1
- package/skills/c64-emulator/SKILL.md +11 -7
- package/skills/c64-knowledge/SKILL.md +34 -21
- package/skills/c64-knowledge/scripts/knowledge.ts +597 -718
- package/skills/c64-knowledge/scripts/lib/listing.ts +5 -7
- package/skills/c64-memory-map/SKILL.md +18 -10
- package/skills/c64-memory-map/scripts/memory-map.ts +81 -0
- package/skills/c64-provenance/SKILL.md +82 -26
- package/skills/c64-provenance/scripts/lib/compare.ts +8 -12
- package/skills/c64-provenance/scripts/lib/leftovers.ts +442 -0
- package/skills/c64-provenance/scripts/lib/library.ts +15 -25
- package/skills/c64-provenance/scripts/lib/runtime.ts +12 -37
- package/skills/c64-provenance/scripts/lib/separate.ts +9 -10
- package/skills/c64-provenance/scripts/provenance.ts +222 -252
- package/skills/c64-reverse-engineering/SKILL.md +18 -15
- package/skills/c64-source/SKILL.md +28 -16
- package/skills/c64-source/scripts/lib/loader.ts +52 -77
- package/skills/c64-source/scripts/lib/source.ts +33 -42
- package/skills/c64-source/scripts/{generate.ts → source.ts} +68 -109
- package/skills/c64-static-analysis/SKILL.md +24 -13
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +3 -4
- package/skills/c64-static-analysis/scripts/lib/findings.ts +2 -2
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +1 -1
- package/skills/c64-static-analysis/scripts/lib/macros.ts +6 -8
- package/skills/c64-static-analysis/scripts/{analyze.ts → static-analysis.ts} +58 -118
- package/skills/c64-testing/SKILL.md +94 -19
- package/skills/c64-testing/references/recording-format.md +125 -0
- package/skills/c64-testing/references/scenario-format.md +24 -19
- package/skills/c64-testing/scripts/{diff.ts → lib/diff-files.ts} +18 -69
- package/skills/c64-testing/scripts/lib/diff.ts +5 -3
- package/skills/c64-testing/scripts/lib/play.ts +334 -0
- package/skills/c64-testing/scripts/lib/recording.ts +662 -0
- package/skills/c64-testing/scripts/lib/replay.ts +105 -0
- package/skills/c64-testing/scripts/{run.ts → lib/runs.ts} +36 -59
- package/skills/c64-testing/scripts/lib/scenario.ts +73 -81
- package/skills/c64-testing/scripts/testing.ts +189 -0
- package/skills/c64-unpacker/SKILL.md +42 -25
- package/skills/c64-unpacker/scripts/lib/evidence.ts +3 -4
- package/skills/c64-unpacker/scripts/lib/loads.ts +12 -10
- package/skills/c64-unpacker/scripts/lib/machine.ts +0 -2
- package/skills/c64-unpacker/scripts/lib/stages.ts +14 -19
- package/skills/c64-unpacker/scripts/{unpack.ts → unpacker.ts} +109 -211
- package/src/c64.ts +35 -20
- package/src/cli/main.ts +2 -2
- package/src/errors.ts +38 -0
- package/src/graphics.ts +3 -1
- package/src/host/server.ts +3 -2
- package/src/host/vice/session.ts +4 -4
- package/src/host-client/entry-state.ts +106 -0
- package/src/host-client/image.ts +21 -10
- package/src/host-client/steps.ts +285 -108
- package/src/host-client/transfer.ts +21 -15
- package/src/host-client/vice-session.ts +1 -1
- package/src/knowledge/database.ts +3 -23
- package/src/knowledge/expression.ts +8 -11
- package/src/knowledge/history.ts +2 -2
- package/src/knowledge/import.ts +13 -14
- package/src/knowledge/names.ts +50 -0
- package/src/knowledge/program.ts +11 -10
- package/src/knowledge/read.ts +16 -21
- package/src/knowledge/schema.ts +9 -3
- package/src/knowledge/text.ts +5 -5
- package/src/knowledge/write.ts +93 -137
- package/src/mcp/inputs.ts +105 -0
- package/src/mcp/output.ts +2 -13
- package/src/mcp/server.ts +12 -60
- package/src/mcp/tools/debug.ts +17 -12
- package/src/mcp/tools/execution.ts +5 -32
- package/src/mcp/tools/input.ts +7 -32
- package/src/mcp/tools/media.ts +6 -5
- package/src/mcp/tools/memory.ts +11 -38
- package/src/mcp/tools/video.ts +6 -5
- package/src/native/c64run.ts +162 -0
- package/src/native/discover.ts +8 -0
- package/src/native/dxa.ts +7 -6
- package/src/native/ghidra/analyze.ts +2 -1
- package/src/native/ghidra/index.ts +2 -1
- package/src/native/processes.ts +4 -3
- package/src/native/status.ts +25 -4
- package/src/native/types.ts +3 -7
- package/src/opcodes.ts +13 -13
- package/src/platform.ts +9 -9
- package/src/project.ts +16 -26
- package/src/protocol/messages.ts +3 -21
- package/src/protocol/vice.ts +7 -7
- package/src/script.ts +491 -49
- package/skills/c64-assembler/scripts/assemble.ts +0 -138
- package/skills/c64-docs/scripts/generate.ts +0 -109
- package/skills/c64-docs/scripts/link.ts +0 -239
- package/skills/c64-memory-map/scripts/memmap.ts +0 -92
- package/skills/c64-testing/scripts/checklist.ts +0 -51
- package/skills/c64-testing/scripts/test.ts +0 -65
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
// The c64-assembler script: assembles project source with ACME, which it runs
|
|
2
|
-
// itself, and writes the program into the project.
|
|
3
|
-
|
|
4
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { dirname } from "node:path";
|
|
6
|
-
import { parseArgs } from "node:util";
|
|
7
|
-
|
|
8
|
-
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
9
|
-
import { readProjectTree } from "#src/host-client/transfer.ts";
|
|
10
|
-
import { assemble } from "#src/native/acme.ts";
|
|
11
|
-
import { localToolContext } from "#src/native/local.ts";
|
|
12
|
-
import type { AcmeParams } from "#src/native/types.ts";
|
|
13
|
-
import { resolveProjectPath } from "#src/project.ts";
|
|
14
|
-
import { runScript, UsageError } from "#src/script.ts";
|
|
15
|
-
import { compareBuild } from "./lib/compare.ts";
|
|
16
|
-
import { KnowledgeError, openForRead } from "#src/knowledge/database.ts";
|
|
17
|
-
import { requireProgram, type ProgramImage } from "#src/knowledge/program.ts";
|
|
18
|
-
import { DEFAULT_SPACE, SPACE } from "#src/knowledge/schema.ts";
|
|
19
|
-
|
|
20
|
-
const USAGE = `assemble.ts --source-root <dir> --entry <file> --out <file.prg> [options]
|
|
21
|
-
|
|
22
|
-
--source-root <dir> folder in the project to assemble from; all of it is sent
|
|
23
|
-
--entry <file> the main source file, relative to the source root
|
|
24
|
-
--out <file.prg> where to write the program, relative to the project
|
|
25
|
-
--include <dir> include directory relative to the source root (repeatable)
|
|
26
|
-
--define NAME=VALUE assembler symbol: an integer ($hex allowed) or true/false (repeatable)
|
|
27
|
-
--set-pc <address> start address when the source sets none, for example '$0801'
|
|
28
|
-
--compare check the program byte by byte against the program in the
|
|
29
|
-
knowledge, each segment; put it in with knowledge.ts
|
|
30
|
-
image of the c64-knowledge skill
|
|
31
|
-
--space <space> with --compare: the knowledge space of the program, c64 (the
|
|
32
|
-
default), drive8, or bank0 to bank255
|
|
33
|
-
--plain write the bytes only, without the 2-byte load address of a PRG
|
|
34
|
-
--all-symbols list also the constants that the source does not use
|
|
35
|
-
|
|
36
|
-
The result is one JSON object. Addresses and symbol values are $ and at least four hex digits.`;
|
|
37
|
-
|
|
38
|
-
function parseDefine(text: string): [string, number | boolean] {
|
|
39
|
-
const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.+)$/.exec(text);
|
|
40
|
-
if (match === null) throw new UsageError(`--define needs NAME=VALUE, not ${text}`);
|
|
41
|
-
const value = match[2]!;
|
|
42
|
-
if (value === "true" || value === "false") return [match[1]!, value === "true"];
|
|
43
|
-
if (/^\$[0-9a-fA-F]+$/.test(value)) return [match[1]!, Number.parseInt(value.slice(1), 16)];
|
|
44
|
-
if (/^-?\d+$/.test(value)) return [match[1]!, Number(value)];
|
|
45
|
-
throw new UsageError(`--define ${match[1]} needs an integer or true/false, not ${value}`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }> {
|
|
49
|
-
const { values } = parseArgs({
|
|
50
|
-
args: argv,
|
|
51
|
-
strict: true,
|
|
52
|
-
options: {
|
|
53
|
-
"source-root": { type: "string" },
|
|
54
|
-
entry: { type: "string" },
|
|
55
|
-
out: { type: "string" },
|
|
56
|
-
include: { type: "string", multiple: true },
|
|
57
|
-
define: { type: "string", multiple: true },
|
|
58
|
-
"set-pc": { type: "string" },
|
|
59
|
-
compare: { type: "boolean" },
|
|
60
|
-
space: { type: "string" },
|
|
61
|
-
plain: { type: "boolean" },
|
|
62
|
-
"all-symbols": { type: "boolean" },
|
|
63
|
-
help: { type: "boolean" },
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
if (values.help) throw new UsageError("");
|
|
67
|
-
const sourceRoot = values["source-root"];
|
|
68
|
-
const entry = values.entry;
|
|
69
|
-
const out = values.out;
|
|
70
|
-
if (sourceRoot === undefined || entry === undefined || out === undefined) throw new UsageError("--source-root, --entry and --out are necessary");
|
|
71
|
-
let outPath: string;
|
|
72
|
-
try {
|
|
73
|
-
outPath = resolveProjectPath(out);
|
|
74
|
-
} catch {
|
|
75
|
-
throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${out}`);
|
|
76
|
-
}
|
|
77
|
-
if (values.space !== undefined && !values.compare) throw new UsageError("--space works only with --compare");
|
|
78
|
-
const space = values.space ?? DEFAULT_SPACE;
|
|
79
|
-
if (!SPACE.test(space)) throw new UsageError("--space must be c64, drive8, or bank0 to bank255");
|
|
80
|
-
// Read the program before ACME runs, so a project without it stops at once.
|
|
81
|
-
let program: ProgramImage | undefined;
|
|
82
|
-
if (values.compare) {
|
|
83
|
-
const db = openForRead();
|
|
84
|
-
try {
|
|
85
|
-
program = requireProgram(db, space);
|
|
86
|
-
} finally {
|
|
87
|
-
db?.close();
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
const defines = Object.fromEntries((values.define ?? []).map(parseDefine));
|
|
91
|
-
let setPc: number | undefined;
|
|
92
|
-
if (values["set-pc"] !== undefined) {
|
|
93
|
-
try {
|
|
94
|
-
setPc = parseC64Address(values["set-pc"]);
|
|
95
|
-
} catch {
|
|
96
|
-
throw new UsageError(`--set-pc must be $ followed by four hex digits, not ${values["set-pc"]}`);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
const tree = readProjectTree(sourceRoot);
|
|
100
|
-
if (!tree.files.some((file) => file.path === entry)) throw new UsageError(`--entry ${entry} is not a file in ${sourceRoot}`);
|
|
101
|
-
const params: AcmeParams = { files: tree.files, entrySource: entry, includeDirs: values.include ?? [], defines, ...(setPc === undefined ? {} : { setPc }) };
|
|
102
|
-
|
|
103
|
-
const { result, attachments } = await assemble(params, tree.contents, localToolContext());
|
|
104
|
-
if (!result.assembled) return { output: { assembled: false, diagnostics: result.diagnostics }, failed: true };
|
|
105
|
-
mkdirSync(dirname(outPath), { recursive: true });
|
|
106
|
-
writeFileSync(outPath, values.plain ? attachments![0]!.subarray(2) : attachments![0]!);
|
|
107
|
-
const range = result.loadRange!;
|
|
108
|
-
const compared = program === undefined ? undefined : compareBuild(attachments![0]!, program);
|
|
109
|
-
// An include file of platform names gives many constants that the program does not use.
|
|
110
|
-
const listed = result.symbols!.filter((symbol) => values["all-symbols"] || symbol.kind === "address" || symbol.used);
|
|
111
|
-
return {
|
|
112
|
-
output: {
|
|
113
|
-
assembled: true,
|
|
114
|
-
output: out,
|
|
115
|
-
...(values.plain ? { plain: true } : {}),
|
|
116
|
-
loadRange: { start: formatC64Address(range.start), end: formatC64Address(range.end), bytes: range.bytes },
|
|
117
|
-
symbols: listed.map((symbol) =>
|
|
118
|
-
symbol.kind === "address"
|
|
119
|
-
? { name: symbol.name, kind: "address", address: formatC64Address(symbol.address), used: symbol.used }
|
|
120
|
-
: { name: symbol.name, kind: "constant", value: `$${symbol.value.toString(16).padStart(4, "0")}`, used: symbol.used },
|
|
121
|
-
),
|
|
122
|
-
...(values["all-symbols"] ? {} : { unusedConstants: result.symbols!.length - listed.length }),
|
|
123
|
-
diagnostics: result.diagnostics,
|
|
124
|
-
...(compared === undefined ? {} : { compare: compared }),
|
|
125
|
-
},
|
|
126
|
-
// A build that differs from its reference is a failed check.
|
|
127
|
-
failed: compared !== undefined && !compared.identical,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
await runScript(
|
|
132
|
-
async (argv) => {
|
|
133
|
-
const { output, failed } = await run(argv);
|
|
134
|
-
if (failed) process.exitCode = 1;
|
|
135
|
-
return output;
|
|
136
|
-
},
|
|
137
|
-
{ usage: USAGE, refusals: [KnowledgeError] },
|
|
138
|
-
);
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
// c64-docs generate.ts: the reference pages of a program from the
|
|
2
|
-
// knowledge, the source map of c64-source and the program in the knowledge. The pages are
|
|
3
|
-
// generated and never edited; the pages of a person link to them.
|
|
4
|
-
|
|
5
|
-
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
6
|
-
import { join, posix } from "node:path";
|
|
7
|
-
import { parseArgs } from "node:util";
|
|
8
|
-
|
|
9
|
-
import { WireFailure } from "#src/host-client/tools.ts";
|
|
10
|
-
import { writeProjectFile } from "#src/host-client/transfer.ts";
|
|
11
|
-
import { conflicts } from "#src/knowledge/conflicts.ts";
|
|
12
|
-
import { KnowledgeError, openForRead } from "#src/knowledge/database.ts";
|
|
13
|
-
import { currentRevision, listComments, listModules, listReferences, listRegions, listSymbols } from "#src/knowledge/read.ts";
|
|
14
|
-
import { resolveProjectFile, resolveProjectPath } from "#src/project.ts";
|
|
15
|
-
import { runScript, UsageError } from "#src/script.ts";
|
|
16
|
-
import { buildReference, GENERATED_MARK } from "./lib/reference.ts";
|
|
17
|
-
import { readSourceMap } from "./lib/source-map.ts";
|
|
18
|
-
import { programImage, type ProgramImage } from "#src/knowledge/program.ts";
|
|
19
|
-
|
|
20
|
-
const USAGE = `generate.ts --source <dir> --out <dir>
|
|
21
|
-
|
|
22
|
-
--source <dir> the source directory of c64-source, with its source-map.json,
|
|
23
|
-
relative to the project folder
|
|
24
|
-
--out <dir> the directory for the reference pages, relative to the project
|
|
25
|
-
folder, for example docs/reference; generate.ts writes only
|
|
26
|
-
there and refuses a page in it that it did not write
|
|
27
|
-
When the knowledge has the program of the source (knowledge.ts image of the
|
|
28
|
-
c64-knowledge skill), the reference decodes the tables and shows the
|
|
29
|
-
graphics. The result is one JSON object.`;
|
|
30
|
-
|
|
31
|
-
/** The files of an earlier run in `directory`, the real folder of --out, or a refusal that names a file of a person. */
|
|
32
|
-
function oldFiles(out: string, directory: string): string[] {
|
|
33
|
-
if (!existsSync(directory)) return [];
|
|
34
|
-
const found: string[] = [];
|
|
35
|
-
const walk = (relative: string) => {
|
|
36
|
-
for (const entry of readdirSync(join(directory, relative), { withFileTypes: true })) {
|
|
37
|
-
const path = posix.join(relative, entry.name);
|
|
38
|
-
if (entry.isDirectory()) {
|
|
39
|
-
walk(path);
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
const generated = path.endsWith(".md") ? readFileSync(join(directory, path), "utf8").startsWith(GENERATED_MARK) : path.startsWith("images/") && path.endsWith(".png");
|
|
43
|
-
if (!generated) throw new WireFailure("invalid-input", `${posix.join(out, path)} is not a page of generate.ts. Nothing was written. Give --out a directory that holds only the reference, and keep the pages of a person outside it.`);
|
|
44
|
-
found.push(path);
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
walk("");
|
|
48
|
-
return found;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function run(argv: string[]): Record<string, unknown> {
|
|
52
|
-
const { values, positionals } = parseArgs({ args: argv, strict: true, allowPositionals: true, options: { source: { type: "string" }, out: { type: "string" }, help: { type: "boolean" } } });
|
|
53
|
-
if (values.help) throw new UsageError("");
|
|
54
|
-
if (positionals.length > 0) throw new UsageError(`generate.ts takes no positional arguments, not ${positionals[0]}`);
|
|
55
|
-
if (values.source === undefined) throw new UsageError("give --source <dir>: the source directory of c64-source");
|
|
56
|
-
if (values.out === undefined) throw new UsageError("give --out <dir>: the directory for the reference pages");
|
|
57
|
-
for (const [option, path] of [["--source", values.source], ["--out", values.out]] as const) {
|
|
58
|
-
try {
|
|
59
|
-
resolveProjectPath(path);
|
|
60
|
-
} catch {
|
|
61
|
-
throw new UsageError(`${option} must be a path relative to the project folder that stays inside it, not ${path}`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
// The files of an earlier run are deleted: only in a folder that is inside the project, also after its symbolic links.
|
|
65
|
-
let directory = resolveProjectPath(values.out);
|
|
66
|
-
if (existsSync(directory)) {
|
|
67
|
-
try {
|
|
68
|
-
directory = resolveProjectFile(values.out);
|
|
69
|
-
} catch {
|
|
70
|
-
throw new UsageError(`--out must be a path relative to the project folder that stays inside it, also through symbolic links, not ${values.out}`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
const map = readSourceMap(values.source);
|
|
74
|
-
const db = openForRead();
|
|
75
|
-
let knowledge;
|
|
76
|
-
let image: ProgramImage | undefined;
|
|
77
|
-
try {
|
|
78
|
-
image = programImage(db, map.space);
|
|
79
|
-
const found = conflicts(db).length;
|
|
80
|
-
if (found > 0) throw new KnowledgeError("conflict", `The knowledge has ${found} conflict${found === 1 ? "" : "s"} from a git merge. Nothing was written. Run c64-knowledge's knowledge.ts merge, then generate the source and the reference again.`);
|
|
81
|
-
const revision = currentRevision(db);
|
|
82
|
-
// A source of another revision has other lines: the links would point to the wrong code.
|
|
83
|
-
if (map.revision !== revision) {
|
|
84
|
-
throw new WireFailure("invalid-input", `${map.pathOf("source-map.json")} is from knowledge revision ${map.revision}, and the knowledge is at revision ${revision}. Nothing was written. Generate the source again with c64-source, then run generate.ts.`);
|
|
85
|
-
}
|
|
86
|
-
knowledge = {
|
|
87
|
-
revision,
|
|
88
|
-
symbols: listSymbols(db, undefined, undefined, undefined, map.space),
|
|
89
|
-
regions: listRegions(db, undefined, undefined, undefined, map.space),
|
|
90
|
-
comments: listComments(db, undefined, undefined, map.space),
|
|
91
|
-
references: listReferences(db, undefined, undefined, undefined, map.space),
|
|
92
|
-
modules: listModules(db, undefined, undefined, map.space),
|
|
93
|
-
};
|
|
94
|
-
} finally {
|
|
95
|
-
db?.close();
|
|
96
|
-
}
|
|
97
|
-
const old = oldFiles(values.out, directory);
|
|
98
|
-
const built = buildReference({
|
|
99
|
-
out: posix.normalize(values.out),
|
|
100
|
-
map,
|
|
101
|
-
...knowledge,
|
|
102
|
-
...(image === undefined ? {} : { image: { byteAt: image.byteAt, inImage: (address: number) => image.segments.some((segment) => address >= segment.start && address <= segment.end) } }),
|
|
103
|
-
});
|
|
104
|
-
for (const path of old) rmSync(join(directory, path), { force: true });
|
|
105
|
-
for (const file of built.files) writeProjectFile(file.path, file.bytes, { what: "--out" });
|
|
106
|
-
return { out: values.out, revision: knowledge.revision, index: posix.join(posix.normalize(values.out), "index.md"), ...built.counts, program: image === undefined ? "none: put the program into the knowledge with knowledge.ts image to decode the tables and show the graphics" : "the knowledge" };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
await runScript(run, { usage: USAGE, refusals: [KnowledgeError] });
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
// c64-docs link.ts: links the name mentions of the documentation to the
|
|
2
|
-
// source and the reference, and checks every link and image of the pages.
|
|
3
|
-
// With --check it writes nothing and fails when a page is not up to date.
|
|
4
|
-
|
|
5
|
-
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
6
|
-
import { join, posix } from "node:path";
|
|
7
|
-
import { parseArgs } from "node:util";
|
|
8
|
-
|
|
9
|
-
import { WireFailure } from "#src/host-client/tools.ts";
|
|
10
|
-
import { writeProjectFile } from "#src/host-client/transfer.ts";
|
|
11
|
-
import { KnowledgeError, openForRead } from "#src/knowledge/database.ts";
|
|
12
|
-
import { currentRevision, listSymbols } from "#src/knowledge/read.ts";
|
|
13
|
-
import { resolveProjectFile, resolveProjectPath } from "#src/project.ts";
|
|
14
|
-
import { runScript, UsageError } from "#src/script.ts";
|
|
15
|
-
import { checkLinks, linkPage, type Files, type Problem } from "./lib/links.ts";
|
|
16
|
-
import { GENERATED_MARK } from "./lib/reference.ts";
|
|
17
|
-
import { readSourceMap } from "./lib/source-map.ts";
|
|
18
|
-
|
|
19
|
-
const USAGE = `link.ts <pages>... --source <dir> [--reference <dir>] [--check]
|
|
20
|
-
|
|
21
|
-
<pages>... .md files and directories of the documentation, relative to
|
|
22
|
-
the project folder, for example README.md docs; link.ts reads
|
|
23
|
-
each file and every .md file below each directory
|
|
24
|
-
--source <dir> the source directory of c64-source, with its source-map.json
|
|
25
|
-
--reference <dir> the reference pages of generate.ts; the default is the
|
|
26
|
-
reference/ folder of the first directory that has one
|
|
27
|
-
--check write nothing; fail when a link is not up to date or a page
|
|
28
|
-
has a problem
|
|
29
|
-
|
|
30
|
-
A name in backticks gets a link to its line in the source. An address in
|
|
31
|
-
backticks after the name gets a link to the entry of the name in the reference.
|
|
32
|
-
The result is one JSON object.`;
|
|
33
|
-
|
|
34
|
-
const MAX_LISTED = 50;
|
|
35
|
-
/** A name outside the image covers the addresses up to the next name, at most this many. */
|
|
36
|
-
const MAX_BODY = 256;
|
|
37
|
-
|
|
38
|
-
const isDirectory = (path: string) => {
|
|
39
|
-
const real = resolveProjectPath(path);
|
|
40
|
-
return existsSync(real) && statSync(real).isDirectory();
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
function markdownFiles(directory: string): string[] {
|
|
44
|
-
if (!isDirectory(directory)) throw new WireFailure("not-found", `There is no directory ${directory} in the project folder.`);
|
|
45
|
-
let root: string;
|
|
46
|
-
try {
|
|
47
|
-
root = resolveProjectFile(directory);
|
|
48
|
-
} catch {
|
|
49
|
-
throw new WireFailure("invalid-input", `${directory} leads out of the project folder through a symbolic link.`);
|
|
50
|
-
}
|
|
51
|
-
const found: string[] = [];
|
|
52
|
-
const walk = (relative: string) => {
|
|
53
|
-
for (const entry of readdirSync(join(root, relative), { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
54
|
-
const path = posix.join(relative, entry.name);
|
|
55
|
-
if (entry.isDirectory()) walk(path);
|
|
56
|
-
else if (entry.name.endsWith(".md")) found.push(posix.join(directory, path));
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
walk("");
|
|
60
|
-
return found;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** The text of each file that the run read: a page, a source file or a reference page is read once. */
|
|
64
|
-
const texts = new Map<string, string | undefined>();
|
|
65
|
-
|
|
66
|
-
const files: Files = {
|
|
67
|
-
inside: (path) => {
|
|
68
|
-
try {
|
|
69
|
-
resolveProjectPath(path);
|
|
70
|
-
return true;
|
|
71
|
-
} catch {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
exists: (path) => {
|
|
76
|
-
try {
|
|
77
|
-
resolveProjectFile(path);
|
|
78
|
-
return true;
|
|
79
|
-
} catch {
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
text: (path) => {
|
|
84
|
-
if (texts.has(path)) return texts.get(path);
|
|
85
|
-
let text: string | undefined;
|
|
86
|
-
try {
|
|
87
|
-
const real = resolveProjectFile(path);
|
|
88
|
-
text = statSync(real).isFile() ? readFileSync(real, "utf8") : undefined;
|
|
89
|
-
} catch {
|
|
90
|
-
text = undefined;
|
|
91
|
-
}
|
|
92
|
-
texts.set(path, text);
|
|
93
|
-
return text;
|
|
94
|
-
},
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
/** A .md entry of a directory that is a symbolic link to no file, or to a file outside the project. */
|
|
98
|
-
const unreadable = (page: string): Problem => ({ page, line: 1, kind: "missing-file", message: `${page} is a symbolic link to no file or to a file outside the project folder.` });
|
|
99
|
-
|
|
100
|
-
function run(argv: string[]): { output: Record<string, unknown>; failed: boolean } {
|
|
101
|
-
const { values, positionals } = parseArgs({ args: argv, strict: true, allowPositionals: true, options: { source: { type: "string" }, reference: { type: "string" }, check: { type: "boolean" }, help: { type: "boolean" } } });
|
|
102
|
-
if (values.help) throw new UsageError("");
|
|
103
|
-
if (positionals.length === 0) throw new UsageError("give the pages: .md files and directories of the documentation, for example README.md docs");
|
|
104
|
-
if (values.source === undefined) throw new UsageError("give --source <dir>: the source directory of c64-source");
|
|
105
|
-
const given = [...new Set(positionals.map((path) => posix.normalize(path)))];
|
|
106
|
-
for (const [option, path] of [...given.map((path) => ["<pages>", path] as const), ["--source", values.source], ["--reference", values.reference]] as const) {
|
|
107
|
-
if (path === undefined) continue;
|
|
108
|
-
if (!files.inside(path)) throw new UsageError(`${option} must be a path relative to the project folder that stays inside it, not ${path}`);
|
|
109
|
-
}
|
|
110
|
-
for (const path of given) {
|
|
111
|
-
if (!path.endsWith(".md") && files.exists(path) && !isDirectory(path)) throw new UsageError(`<pages> takes .md files and directories, not ${path}`);
|
|
112
|
-
}
|
|
113
|
-
const directories = given.filter((path) => !path.endsWith(".md") || isDirectory(path));
|
|
114
|
-
const map = readSourceMap(values.source);
|
|
115
|
-
const reference = values.reference === undefined ? directories.map((path) => posix.join(path, "reference")).find((path) => isDirectory(path)) : posix.normalize(values.reference);
|
|
116
|
-
const problems: Problem[] = [];
|
|
117
|
-
const db = openForRead();
|
|
118
|
-
let symbols;
|
|
119
|
-
let revision;
|
|
120
|
-
try {
|
|
121
|
-
revision = currentRevision(db);
|
|
122
|
-
symbols = listSymbols(db, undefined, undefined, undefined, map.space);
|
|
123
|
-
} finally {
|
|
124
|
-
db?.close();
|
|
125
|
-
}
|
|
126
|
-
if (map.revision !== revision) {
|
|
127
|
-
problems.push({ page: map.pathOf("source-map.json"), line: 1, kind: "stale-reference", message: `The source is from knowledge revision ${map.revision}, and the knowledge is at ${revision}. Generate the source and the reference again, then run link.ts.` });
|
|
128
|
-
}
|
|
129
|
-
const addresses = new Map<string, number>();
|
|
130
|
-
for (const label of map.labels) addresses.set(label.name, label.address);
|
|
131
|
-
for (const symbol of symbols) addresses.set(symbol.name, symbol.address);
|
|
132
|
-
// A routine runs to the next routine; any other name to the next name of the knowledge, in its file, and outside the image at most MAX_BODY bytes.
|
|
133
|
-
const routines = symbols.filter((symbol) => symbol.kind === "routine").map((symbol) => symbol.address).sort((a, b) => a - b);
|
|
134
|
-
const named = [...new Set(symbols.map((symbol) => symbol.address))].sort((a, b) => a - b);
|
|
135
|
-
const kinds = new Map(symbols.map((symbol) => [symbol.name, symbol.kind]));
|
|
136
|
-
const bodyEnds = new Map<string, number | undefined>();
|
|
137
|
-
const bodyEnd = (name: string) => {
|
|
138
|
-
if (bodyEnds.has(name)) return bodyEnds.get(name);
|
|
139
|
-
const address = addresses.get(name);
|
|
140
|
-
let end: number | undefined;
|
|
141
|
-
if (address !== undefined) {
|
|
142
|
-
const next = ((kinds.get(name) === "routine" ? routines : named).find((other) => other > address) ?? 0x10000) - 1;
|
|
143
|
-
const file = map.lineOf(address)?.file;
|
|
144
|
-
if (file === undefined) end = Math.min(next, address + MAX_BODY - 1);
|
|
145
|
-
else {
|
|
146
|
-
end = address;
|
|
147
|
-
while (end < next && map.lineOf(end + 1)?.file === file) end++;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
bodyEnds.set(name, end);
|
|
151
|
-
return end;
|
|
152
|
-
};
|
|
153
|
-
const nameAt = new Map(symbols.map((symbol) => [symbol.address, symbol.name]));
|
|
154
|
-
/** The routine, else the name, whose body holds an address. */
|
|
155
|
-
const placeOf = (address: number) => {
|
|
156
|
-
for (const list of [routines, named]) {
|
|
157
|
-
let start: number | undefined;
|
|
158
|
-
for (const other of list) if (other <= address) start = other;
|
|
159
|
-
const name = start === undefined ? undefined : nameAt.get(start);
|
|
160
|
-
if (name !== undefined && (bodyEnd(name) ?? start!) >= address) return name;
|
|
161
|
-
}
|
|
162
|
-
return undefined;
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
// The entries of the reference: each <a id> that is a name.
|
|
166
|
-
const entries = new Map<string, string>();
|
|
167
|
-
const referencePages = reference === undefined ? [] : markdownFiles(reference);
|
|
168
|
-
for (const page of referencePages) {
|
|
169
|
-
const text = files.text(page);
|
|
170
|
-
if (text === undefined) {
|
|
171
|
-
problems.push(unreadable(page));
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
const stamp = /^<!-- Generated by c64-docs from knowledge revision (\S+) /.exec(text);
|
|
175
|
-
if (text.startsWith(GENERATED_MARK) && stamp !== null && stamp[1] !== map.revision) {
|
|
176
|
-
problems.push({ page, line: 1, kind: "stale-reference", message: `${page} is from knowledge revision ${stamp[1]}, and the source from ${map.revision}. Run generate.ts again.` });
|
|
177
|
-
}
|
|
178
|
-
for (const match of text.matchAll(/<a id="([^"]+)"><\/a>/g)) if (addresses.has(match[1]!) && !entries.has(match[1]!)) entries.set(match[1]!, `${page}#${match[1]}`);
|
|
179
|
-
}
|
|
180
|
-
const inside = (path: string, directory: string | undefined) => directory !== undefined && (path === directory || path.startsWith(`${directory}/`));
|
|
181
|
-
const context = {
|
|
182
|
-
addressOf: (name: string) => addresses.get(name),
|
|
183
|
-
codeOf: (name: string) => {
|
|
184
|
-
const label = map.labelOf(name);
|
|
185
|
-
return label === undefined ? undefined : `${map.pathOf(label.file)}#L${label.line}`;
|
|
186
|
-
},
|
|
187
|
-
entryOf: (name: string) => entries.get(name),
|
|
188
|
-
bodyEnd,
|
|
189
|
-
placeOf,
|
|
190
|
-
lineOf: (address: number) => {
|
|
191
|
-
const line = map.lineOf(address);
|
|
192
|
-
return line === undefined ? undefined : `${map.pathOf(line.file)}#L${line.line}`;
|
|
193
|
-
},
|
|
194
|
-
managed: (path: string) => inside(path, posix.normalize(map.directory)) || inside(path, reference),
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const pages = [...new Set(given.flatMap((path) => {
|
|
198
|
-
if (directories.includes(path)) return markdownFiles(path);
|
|
199
|
-
if (files.text(path) === undefined) throw new WireFailure("not-found", `There is no file ${path} in the project folder.`);
|
|
200
|
-
return [path];
|
|
201
|
-
}))];
|
|
202
|
-
const changedPages: string[] = [];
|
|
203
|
-
let links = 0;
|
|
204
|
-
for (const page of pages) {
|
|
205
|
-
const text = files.text(page);
|
|
206
|
-
if (text === undefined) {
|
|
207
|
-
problems.push(unreadable(page));
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
// A generated page is right by its making: it is only checked.
|
|
211
|
-
const generated = text.startsWith(GENERATED_MARK);
|
|
212
|
-
const linked = generated ? { text, changed: 0, problems: [] } : linkPage(text, { page, ...context });
|
|
213
|
-
problems.push(...linked.problems, ...checkLinks(page, linked.text, files));
|
|
214
|
-
if (linked.changed === 0) continue;
|
|
215
|
-
links += linked.changed;
|
|
216
|
-
changedPages.push(page);
|
|
217
|
-
if (values.check) continue;
|
|
218
|
-
writeProjectFile(page, Buffer.from(linked.text, "utf8"));
|
|
219
|
-
texts.set(page, linked.text);
|
|
220
|
-
}
|
|
221
|
-
const byKind: Record<string, number> = {};
|
|
222
|
-
for (const problem of problems) byKind[problem.kind] = (byKind[problem.kind] ?? 0) + 1;
|
|
223
|
-
const output: Record<string, unknown> = {
|
|
224
|
-
pages: pages.length,
|
|
225
|
-
...(reference === undefined ? { reference: "none: run generate.ts for links to the entries" } : { reference }),
|
|
226
|
-
...(values.check ? { notUpToDate: { links, pages: changedPages } } : { linked: { links, pages: changedPages } }),
|
|
227
|
-
problems: { count: problems.length, byKind, listed: problems.slice(0, MAX_LISTED).map((problem) => ({ at: `${problem.page}:${problem.line}`, kind: problem.kind, message: problem.message })) },
|
|
228
|
-
};
|
|
229
|
-
return { output, failed: problems.length > 0 || (values.check === true && links > 0) };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
await runScript(
|
|
233
|
-
(argv) => {
|
|
234
|
-
const { output, failed } = run(argv);
|
|
235
|
-
if (failed) process.exitCode = 1;
|
|
236
|
-
return output;
|
|
237
|
-
},
|
|
238
|
-
{ usage: USAGE, refusals: [KnowledgeError] },
|
|
239
|
-
);
|
|
@@ -1,92 +0,0 @@
|
|
|
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.
|
|
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((argv) => run(argv), { usage: USAGE });
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
// The c64-testing playtest checklist: a Markdown list for a human tester,
|
|
2
|
-
// made from the scenarios' checkpoints and the behavior that automated
|
|
3
|
-
// scenarios cannot check. Nothing is stored.
|
|
4
|
-
|
|
5
|
-
import { parseArgs } from "node:util";
|
|
6
|
-
|
|
7
|
-
import { runScript, UsageError } from "#src/script.ts";
|
|
8
|
-
import { readScenario, ScenarioError, type Scenario } from "./lib/scenario.ts";
|
|
9
|
-
|
|
10
|
-
const USAGE = `checklist.ts <scenario.json> [<scenario.json> ...] [--item <text> ...]
|
|
11
|
-
|
|
12
|
-
Prints a Markdown checklist. --item adds an application-specific line.`;
|
|
13
|
-
|
|
14
|
-
/** Behavior that only a person can judge. */
|
|
15
|
-
const ALWAYS = [
|
|
16
|
-
"The controls respond as fast as in the original.",
|
|
17
|
-
"The animation is smooth and has no glitches.",
|
|
18
|
-
"The music and the sound effects sound the same as in the original.",
|
|
19
|
-
"The game flow and the level changes work from start to end.",
|
|
20
|
-
"Nothing unexpected happens outside the tested scenarios.",
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
function run(argv: string[]): string {
|
|
24
|
-
const { values, positionals } = parseArgs({ args: argv, strict: true, allowPositionals: true, options: { item: { type: "string", multiple: true }, help: { type: "boolean" } } });
|
|
25
|
-
if (values.help) throw new UsageError("");
|
|
26
|
-
if (positionals.length === 0) throw new UsageError("give at least one scenario file");
|
|
27
|
-
const lines = ["# Playtest checklist", ""];
|
|
28
|
-
for (const path of positionals) {
|
|
29
|
-
let scenario: Scenario;
|
|
30
|
-
try {
|
|
31
|
-
scenario = readScenario(path);
|
|
32
|
-
} catch (error) {
|
|
33
|
-
if (error instanceof ScenarioError) throw new UsageError(error.message);
|
|
34
|
-
throw error;
|
|
35
|
-
}
|
|
36
|
-
lines.push(`## ${scenario.name}`, "");
|
|
37
|
-
for (const step of scenario.steps) if ("observe" in step) lines.push(`- [ ] At "${step.observe}", the rebuild looks and behaves as the original.`);
|
|
38
|
-
lines.push("");
|
|
39
|
-
}
|
|
40
|
-
lines.push("## Application", "");
|
|
41
|
-
for (const item of values.item ?? []) lines.push(`- [ ] ${item}`);
|
|
42
|
-
for (const item of ALWAYS) lines.push(`- [ ] ${item}`);
|
|
43
|
-
return `${lines.join("\n")}\n`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
await runScript(
|
|
47
|
-
(argv) => {
|
|
48
|
-
process.stdout.write(run(argv));
|
|
49
|
-
},
|
|
50
|
-
{ usage: USAGE },
|
|
51
|
-
);
|