@henols/c64-re-tools 2.0.0-rc.11 → 2.0.0-rc.12
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 +7 -5
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +9 -2
- package/skills/c64-assembler/scripts/assemble.ts +4 -4
- package/skills/c64-basic/SKILL.md +8 -1
- package/skills/c64-basic/scripts/basic.ts +3 -3
- package/skills/c64-disk/SKILL.md +10 -3
- package/skills/c64-disk/scripts/disk.ts +4 -4
- package/skills/c64-emulator/SKILL.md +4 -0
- package/skills/c64-knowledge/SKILL.md +21 -9
- package/skills/c64-knowledge/scripts/knowledge.ts +129 -90
- package/skills/c64-memory-map/SKILL.md +7 -0
- package/skills/c64-memory-map/scripts/memmap.ts +1 -1
- package/skills/c64-provenance/SKILL.md +56 -3
- package/skills/c64-provenance/scripts/library.ts +584 -0
- package/skills/c64-provenance/scripts/provenance.ts +201 -9
- package/skills/c64-reverse-engineering/SKILL.md +15 -2
- package/skills/c64-source/SKILL.md +10 -3
- package/skills/c64-source/scripts/generate.ts +10 -6
- package/skills/c64-source/scripts/opcodes.ts +19 -0
- package/skills/c64-source/scripts/source.ts +87 -13
- package/skills/c64-static-analysis/SKILL.md +9 -2
- package/skills/c64-static-analysis/scripts/analyze.ts +6 -6
- package/skills/c64-testing/SKILL.md +8 -1
- package/skills/c64-testing/references/scenario-format.md +1 -1
- package/skills/c64-testing/scripts/checklist.ts +2 -2
- package/skills/c64-testing/scripts/run.ts +4 -4
- package/skills/c64-testing/scripts/scenario.ts +1 -1
- package/skills/c64-testing/scripts/test.ts +4 -4
- package/skills/c64-unpacker/SKILL.md +11 -4
- package/skills/c64-unpacker/scripts/unpack.ts +4 -4
- package/src/cli/main.ts +5 -3
- package/src/host-client/image.ts +1 -1
- package/src/host-client/transfer.ts +34 -10
- package/src/host-client/vice-session.ts +9 -9
- package/src/knowledge/conflicts.ts +145 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/history.ts +25 -20
- package/src/knowledge/import.ts +3 -2
- package/src/knowledge/read.ts +41 -19
- package/src/knowledge/schema.ts +68 -71
- package/src/knowledge/text.ts +364 -0
- package/src/knowledge/write.ts +140 -45
- package/src/mcp/output.ts +14 -7
- package/src/mcp/tools/debug.ts +8 -6
- package/src/mcp/tools/media.ts +17 -13
- package/src/mcp/tools/memory.ts +9 -5
- package/src/mcp/tools/video.ts +6 -5
- package/src/platform.ts +28 -5
- package/src/project.ts +166 -3
- package/src/protocol/tools.ts +1 -1
- package/src/script.ts +46 -7
package/README.md
CHANGED
|
@@ -12,8 +12,8 @@ functional-equivalence tests.
|
|
|
12
12
|
Architecture in one line: one MCP process owns one VICE instance and is
|
|
13
13
|
VICE-only; a Host Runtime on the graphical host owns VICE and the tools that
|
|
14
14
|
come with it (c1541, petcat); skill scripts call the Host Runtime for those,
|
|
15
|
-
run ACME, DXA and Ghidra themselves, and keep project knowledge local
|
|
16
|
-
`.c64-re-tools/knowledge
|
|
15
|
+
run ACME, DXA and Ghidra themselves, and keep project knowledge local as
|
|
16
|
+
text in `.c64-re-tools/knowledge/`.
|
|
17
17
|
The design is in [`docs/redesign/`](docs/redesign/README.md), and
|
|
18
18
|
[`docs/plan.md`](docs/plan.md) tracks the implementation.
|
|
19
19
|
|
|
@@ -37,6 +37,8 @@ npx -y @henols/c64-re-tools@latest install --target claude
|
|
|
37
37
|
claude
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
Several programs in one repository: install once at the root, give each program its own folder, and start the agent at the root. A folder with `.c64-re-tools/` is a project; the agent names one with `--project <folder>`.
|
|
41
|
+
|
|
40
42
|
`npx -y @henols/c64-re-tools@latest status` shows what works.
|
|
41
43
|
|
|
42
44
|
Until 2.0.0 is released, `@latest` is the old 1.1.0. Use the prerelease instead. For the prerelease, a dev container and debugging, see [docs/debug-install.md](docs/debug-install.md).
|
|
@@ -76,7 +78,7 @@ and settings stay unchanged.
|
|
|
76
78
|
|
|
77
79
|
1. Install for your harness: `--target claude`, `codex`, `pi`, `opencode`, `gemini`, `copilot`, `cursor` or `windsurf`. Without `--target`, `install` installs for all of them.
|
|
78
80
|
2. Start the Host Runtime and leave it running.
|
|
79
|
-
3. Start the harness in the project, for example `claude` or `codex`, or open the project in Cursor or Windsurf. The harness starts the MCP server and finds the skills by itself.
|
|
81
|
+
3. Start the harness in the project (or at the root of a repository with several projects), for example `claude` or `codex`, or open the project in Cursor or Windsurf. The harness starts the MCP server and finds the skills by itself.
|
|
80
82
|
4. Ask for the result you want, in plain words. The agent picks the skills and the `c64_*` tools.
|
|
81
83
|
|
|
82
84
|
Examples:
|
|
@@ -93,7 +95,7 @@ Build it, run it in the emulator, and write a test for it.
|
|
|
93
95
|
Show me the emulator window.
|
|
94
96
|
```
|
|
95
97
|
|
|
96
|
-
The agent keeps what it learns about the program in `.c64-re-tools/knowledge
|
|
98
|
+
The agent keeps what it learns about the program as text in `.c64-re-tools/knowledge/`. Commit that folder with the project. After a `git pull` that joined the work of two computers, run `knowledge.ts merge` when a command reports conflicts.
|
|
97
99
|
|
|
98
100
|
## Skills
|
|
99
101
|
|
|
@@ -107,7 +109,7 @@ The agent keeps what it learns about the program in `.c64-re-tools/knowledge.db`
|
|
|
107
109
|
| c64-basic | BASIC V2 listings and the SYS/USR handoff to machine code. |
|
|
108
110
|
| c64-unpacker | Packing evidence, and capture of the unpacked program from the emulator. |
|
|
109
111
|
| c64-memory-map | Platform meaning of addresses and register values. |
|
|
110
|
-
| c64-provenance | Byte comparison of releases. |
|
|
112
|
+
| c64-provenance | Byte comparison of releases, and a library of routines that several programs share. |
|
|
111
113
|
| c64-source | ACME source from the image and the knowledge, checked byte by byte, and a loader for the entry state. |
|
|
112
114
|
| c64-assembler | ACME builds with diagnostics and symbols. |
|
|
113
115
|
| c64-testing | Original-against-rebuild scenarios with PASS, FAIL or INCONCLUSIVE. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henols/c64-re-tools",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.12",
|
|
4
4
|
"description": "Agent-friendly Commodore 64 engineering toolkit for reverse engineering, debugging, development and testing.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Henrik Olsson",
|
|
@@ -13,9 +13,9 @@ This skill does not run the program and does not change project knowledge.
|
|
|
13
13
|
|
|
14
14
|
## Inputs
|
|
15
15
|
|
|
16
|
-
- The source root: the project
|
|
16
|
+
- The source root: the folder in the project that holds all source files of the program.
|
|
17
17
|
- The entry source: the main file, relative to the source root.
|
|
18
|
-
- The output path: where to write the PRG, relative to the project
|
|
18
|
+
- The output path: where to write the PRG, relative to the project folder.
|
|
19
19
|
- Optional include directories, symbol definitions and a start address.
|
|
20
20
|
|
|
21
21
|
## Tools / execution path
|
|
@@ -37,6 +37,13 @@ The script copies the full source root. Put all files that the program includes
|
|
|
37
37
|
|
|
38
38
|
The script prints one JSON object. The target CPU is the 6510 and the output is always a C64 PRG.
|
|
39
39
|
|
|
40
|
+
### Project folder
|
|
41
|
+
|
|
42
|
+
- Paths are relative to the project folder. The project folder is the nearest folder at or above the working directory that holds `.c64-re-tools/`.
|
|
43
|
+
- If the working directory is not in a project and has projects below it, the script refuses and lists them. Then give `--project <folder>` to each script.
|
|
44
|
+
- Keep the results, batches and step files that you save for yourself in `.c64-re-tools/local/` of the project. Git ignores that folder.
|
|
45
|
+
- A shell redirect (`>`) is relative to the working directory, not to the project folder. From the folder above the project, write `<folder>/.c64-re-tools/local/result.json`.
|
|
46
|
+
|
|
40
47
|
## Workflow
|
|
41
48
|
|
|
42
49
|
1. Find the source root and the entry source. The entry source sets its own start address with `* = $0801` or similar.
|
|
@@ -16,7 +16,7 @@ import { compareBuild } from "./compare.ts";
|
|
|
16
16
|
|
|
17
17
|
const USAGE = `assemble.ts --source-root <dir> --entry <file> --out <file.prg> [options]
|
|
18
18
|
|
|
19
|
-
--source-root <dir> project
|
|
19
|
+
--source-root <dir> folder in the project to assemble from; all of it is sent
|
|
20
20
|
--entry <file> the main source file, relative to the source root
|
|
21
21
|
--out <file.prg> where to write the program, relative to the project
|
|
22
22
|
--include <dir> include directory relative to the source root (repeatable)
|
|
@@ -61,7 +61,7 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
|
|
|
61
61
|
try {
|
|
62
62
|
outPath = resolveProjectPath(out);
|
|
63
63
|
} catch {
|
|
64
|
-
throw new UsageError(`--out must be a path relative to the project
|
|
64
|
+
throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${out}`);
|
|
65
65
|
}
|
|
66
66
|
const defines = Object.fromEntries((values.define ?? []).map(parseDefine));
|
|
67
67
|
let setPc: number | undefined;
|
|
@@ -101,8 +101,8 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
await runScript(
|
|
104
|
-
async () => {
|
|
105
|
-
const { output, failed } = await run(
|
|
104
|
+
async (argv) => {
|
|
105
|
+
const { output, failed } = await run(argv);
|
|
106
106
|
if (failed) process.exitCode = 1;
|
|
107
107
|
return output;
|
|
108
108
|
},
|
|
@@ -13,7 +13,7 @@ This skill does not run the program and does not change project knowledge.
|
|
|
13
13
|
|
|
14
14
|
## Inputs
|
|
15
15
|
|
|
16
|
-
- The program: a PRG file, relative to the project
|
|
16
|
+
- The program: a PRG file, relative to the project folder.
|
|
17
17
|
|
|
18
18
|
## Tools / execution path
|
|
19
19
|
|
|
@@ -25,6 +25,13 @@ node <skill>/scripts/basic.ts extracted/game.prg
|
|
|
25
25
|
|
|
26
26
|
The script prints one JSON object. Addresses are `$` and four hex digits.
|
|
27
27
|
|
|
28
|
+
### Project folder
|
|
29
|
+
|
|
30
|
+
- Paths are relative to the project folder. The project folder is the nearest folder at or above the working directory that holds `.c64-re-tools/`.
|
|
31
|
+
- If the working directory is not in a project and has projects below it, the script refuses and lists them. Then give `--project <folder>` to each script.
|
|
32
|
+
- Keep the results, batches and step files that you save for yourself in `.c64-re-tools/local/` of the project. Git ignores that folder.
|
|
33
|
+
- A shell redirect (`>`) is relative to the working directory, not to the project folder. From the folder above the project, write `<folder>/.c64-re-tools/local/result.json`.
|
|
34
|
+
|
|
28
35
|
## Workflow
|
|
29
36
|
|
|
30
37
|
1. Run the script on the program.
|
|
@@ -11,7 +11,7 @@ import { runScript, UsageError } from "#src/script.ts";
|
|
|
11
11
|
|
|
12
12
|
const USAGE = `basic.ts <program>
|
|
13
13
|
|
|
14
|
-
<program> is a PRG file relative to the project
|
|
14
|
+
<program> is a PRG file relative to the project folder. The result is one
|
|
15
15
|
JSON object. Addresses are $ and four hex digits.`;
|
|
16
16
|
|
|
17
17
|
async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }> {
|
|
@@ -37,8 +37,8 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
await runScript(
|
|
40
|
-
async () => {
|
|
41
|
-
const { output, failed } = await run(
|
|
40
|
+
async (argv) => {
|
|
41
|
+
const { output, failed } = await run(argv);
|
|
42
42
|
if (failed) process.exitCode = 1;
|
|
43
43
|
return output;
|
|
44
44
|
},
|
package/skills/c64-disk/SKILL.md
CHANGED
|
@@ -13,10 +13,10 @@ This skill does not change the disk image and does not change project knowledge.
|
|
|
13
13
|
|
|
14
14
|
## Inputs
|
|
15
15
|
|
|
16
|
-
- The disk image: a `.d64`, `.d71`, `.d81` or `.g64` file, relative to the project
|
|
16
|
+
- The disk image: a `.d64`, `.d71`, `.d81` or `.g64` file, relative to the project folder.
|
|
17
17
|
- For one file: the file name as the directory shows it.
|
|
18
|
-
- To extract a file: the output path, relative to the project
|
|
19
|
-
- A T64 tape file or a CRT cartridge file, relative to the project
|
|
18
|
+
- To extract a file: the output path, relative to the project folder.
|
|
19
|
+
- A T64 tape file or a CRT cartridge file, relative to the project folder.
|
|
20
20
|
|
|
21
21
|
## Tools / execution path
|
|
22
22
|
|
|
@@ -42,6 +42,13 @@ The script prints one JSON object.
|
|
|
42
42
|
|
|
43
43
|
File names show the characters that the C64 shows in a directory list. A byte that has no character shows as `{$xx}`, for example `{$c1}`. Write the name in the same form. The letter case of a name is not important. Put the name in single quotes in a shell, for example `'GAME {$c1}'`.
|
|
44
44
|
|
|
45
|
+
### Project folder
|
|
46
|
+
|
|
47
|
+
- Paths are relative to the project folder. The project folder is the nearest folder at or above the working directory that holds `.c64-re-tools/`.
|
|
48
|
+
- If the working directory is not in a project and has projects below it, the script refuses and lists them. Then give `--project <folder>` to each script.
|
|
49
|
+
- Keep the results, batches and step files that you save for yourself in `.c64-re-tools/local/` of the project. Git ignores that folder.
|
|
50
|
+
- A shell redirect (`>`) is relative to the working directory, not to the project folder. From the folder above the project, write `<folder>/.c64-re-tools/local/result.json`.
|
|
51
|
+
|
|
45
52
|
## Workflow
|
|
46
53
|
|
|
47
54
|
1. Run `directory`. Find the file in `entries`. Each entry has a `name`, a `type`, a size in `blocks` and the `closed` and `locked` flags.
|
|
@@ -23,7 +23,7 @@ const USAGE = `disk.ts <action> <image> [name] [--out <file>]
|
|
|
23
23
|
read <image> <name> --out <file> write the bytes of one file into the project
|
|
24
24
|
crt <file.crt> --out <dir> write each CHIP packet of a cartridge as a PRG
|
|
25
25
|
|
|
26
|
-
<image> is a .d64, .d71, .d81 or .g64 file, relative to the project
|
|
26
|
+
<image> is a .d64, .d71, .d81 or .g64 file, relative to the project folder.
|
|
27
27
|
directory and read also take a .t64 tape file, and directory a .crt cartridge
|
|
28
28
|
file; the script reads those itself. A CRT can have hardware type 0 (normal
|
|
29
29
|
8K, 16K or Ultimax), 5 (Ocean), 19 (Magic Desk) or 32 (EasyFlash).
|
|
@@ -49,7 +49,7 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
|
|
|
49
49
|
try {
|
|
50
50
|
outPath = resolveProjectPath(values.out);
|
|
51
51
|
} catch {
|
|
52
|
-
throw new UsageError(`--out must be a path relative to the project
|
|
52
|
+
throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -111,8 +111,8 @@ function local(kind: "t64" | "crt", action: Action, image: string, name: string
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
await runScript(
|
|
114
|
-
async () => {
|
|
115
|
-
const { output, failed } = await run(
|
|
114
|
+
async (argv) => {
|
|
115
|
+
const { output, failed } = await run(argv);
|
|
116
116
|
if (failed) process.exitCode = 1;
|
|
117
117
|
return output;
|
|
118
118
|
},
|
|
@@ -52,6 +52,10 @@ For a large result, give `out` with a path in the project. `c64_memory_read`, `c
|
|
|
52
52
|
|
|
53
53
|
`c64_memmap` keeps RAM, ROM and I/O apart. Use action `summary` to see which code ran. Use action `read` with `out` to keep a coverage run as a file, for example `coverage/attract.bin`. Clear the map before each run that you want to keep apart.
|
|
54
54
|
|
|
55
|
+
### Project folder
|
|
56
|
+
|
|
57
|
+
Paths (`path`, `file` and `out`) are relative to the project folder. A project folder holds `.c64-re-tools/`. The tools use the nearest project folder at or above the folder where the agent started. If that folder is not in a project and has projects below it, a tool with a path refuses and lists them. Then give `project` with the project folder, for example `"project": "game"`. Give the same folder as the `--project` of the skill scripts. A tool without a path does not need `project`.
|
|
58
|
+
|
|
55
59
|
## Workflow
|
|
56
60
|
|
|
57
61
|
1. Read the project knowledge for the addresses in the question. Use the c64-knowledge skill.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: c64-knowledge
|
|
3
|
-
description: Use this skill to read, write, correct and review the durable knowledge of a C64 project in .c64-re-tools/knowledge
|
|
3
|
+
description: Use this skill to read, write, correct and review the durable knowledge of a C64 project in .c64-re-tools/knowledge/. It gives the knowledge at an address, and it names symbols, classifies memory regions, sets comments and records references. It also shows the history of each change, and it can revert to an earlier revision. Do not use it for live emulator state or for static analysis.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# c64-knowledge
|
|
7
7
|
|
|
8
8
|
## Purpose
|
|
9
9
|
|
|
10
|
-
Read, write, correct and review the durable understanding of a C64 project. The knowledge is in `.c64-re-tools/knowledge.db`
|
|
10
|
+
Read, write, correct and review the durable understanding of a C64 project. The knowledge is text in `.c64-re-tools/knowledge/` in the project folder: one JSON line for each row. Commit that folder with git. The script keeps an index of the text in `.c64-re-tools/local/knowledge.db` and makes the index again when the text changes. Do not commit the index.
|
|
11
11
|
|
|
12
12
|
This skill is the only write path for conclusions of the user and the LLM. It does not run an emulator or an analyzer.
|
|
13
13
|
|
|
@@ -22,8 +22,6 @@ One of these:
|
|
|
22
22
|
|
|
23
23
|
## Tools / execution path
|
|
24
24
|
|
|
25
|
-
Run the script in the project directory. The project directory is the directory that the harness started in.
|
|
26
|
-
|
|
27
25
|
```
|
|
28
26
|
node <skill>/scripts/knowledge.ts <command> [arguments] [options]
|
|
29
27
|
```
|
|
@@ -41,7 +39,7 @@ The script prints one JSON object. An error prints `{"error": {"code": ..., "mes
|
|
|
41
39
|
| Remove a symbol | `remove-symbol '$2100'` |
|
|
42
40
|
| Classify memory | `classify '$3000' '$303f' sprite` |
|
|
43
41
|
| Classify a table of records | `classify '$3000' '$303b' bytes --element 6` |
|
|
44
|
-
| Many writes as one revision | `apply
|
|
42
|
+
| Many writes as one revision | `apply -` with the JSON on standard input, or `apply <file>` |
|
|
45
43
|
| Make memory unknown, and keep the analyzers out of it | `unclassify '$3000' '$303f'` |
|
|
46
44
|
| Set or remove a comment | `comment '$2100' line Reads joystick 2.` / `uncomment '$2100' line` |
|
|
47
45
|
| Add or remove a reference | `reference '$2000' '$2100' call` / `unreference ...` |
|
|
@@ -51,8 +49,11 @@ The script prints one JSON object. An error prints `{"error": {"code": ..., "mes
|
|
|
51
49
|
| Operand expressions | `operands` with `--start` and `--end` |
|
|
52
50
|
| Files of generated source | `modules` |
|
|
53
51
|
| The address spaces that hold knowledge | `spaces` |
|
|
54
|
-
| History | `history '$2100'`, `history --name update_player`, `revisions`, `revision
|
|
55
|
-
| Go back to an earlier state | `revert
|
|
52
|
+
| History | `history '$2100'`, `history --name update_player`, `revisions`, `revision 20261015T2145` |
|
|
53
|
+
| Go back to an earlier state | `revert 20261015T2145` |
|
|
54
|
+
| Decide the conflicts after a git merge | `merge --reason 'merge of two computers'` |
|
|
55
|
+
|
|
56
|
+
A revision has an id such as `20261015T214501.123-k3f9qz`. The id starts with the date and time. A command takes the full id, or a start of the id that only one revision has. The revision `0` is the knowledge before the first revision.
|
|
56
57
|
|
|
57
58
|
Every command takes `--space`: `c64` (the default), `drive8` for code in the 1541 drive, or `bank0` to `bank255` for cartridge banks. Drive code and banks use the same addresses as the C64, so each space has its own knowledge. A name is unique inside one space. In a batch, a write can name its own `space`.
|
|
58
59
|
|
|
@@ -73,6 +74,13 @@ Region types:
|
|
|
73
74
|
|
|
74
75
|
`--element` gives the bytes of one element of a table. The length of the region must be a multiple of it. The result shows `element` and `count`.
|
|
75
76
|
|
|
77
|
+
### Project folder
|
|
78
|
+
|
|
79
|
+
- Paths are relative to the project folder. The project folder is the nearest folder at or above the working directory that holds `.c64-re-tools/`.
|
|
80
|
+
- If the working directory is not in a project and has projects below it, the script refuses and lists them. Then give `--project <folder>` to each script.
|
|
81
|
+
- Keep the results, batches and step files that you save for yourself in `.c64-re-tools/local/` of the project. Git ignores that folder.
|
|
82
|
+
- A shell redirect (`>`) is relative to the working directory, not to the project folder. From the folder above the project, write `<folder>/.c64-re-tools/local/result.json`.
|
|
83
|
+
|
|
76
84
|
## Workflow
|
|
77
85
|
|
|
78
86
|
Read:
|
|
@@ -89,7 +97,7 @@ Write:
|
|
|
89
97
|
|
|
90
98
|
Each write is one revision. A classification inside a region splits that region, so regions never overlap.
|
|
91
99
|
|
|
92
|
-
For more than a few writes,
|
|
100
|
+
For more than a few writes, use `apply -` and give the writes on standard input as one JSON list. Each write has `op` and the arguments of the command with the same name, for example:
|
|
93
101
|
|
|
94
102
|
```json
|
|
95
103
|
[
|
|
@@ -117,6 +125,8 @@ The ops and their fields:
|
|
|
117
125
|
|
|
118
126
|
A `line` comment is a line of its own above the address. A `side` comment is at the end of the line of the address. All writes become one revision. If the knowledge refuses one write, the script writes nothing, and the error names the write.
|
|
119
127
|
|
|
128
|
+
The c64-provenance skill writes a batch with `match --out`. The batch holds the names of library routines at their addresses in this program. Read the `batch.leftOut` of its result first. Then apply the batch with the command in its `apply`.
|
|
129
|
+
|
|
120
130
|
## Modules
|
|
121
131
|
|
|
122
132
|
A module tells the c64-source skill where a file of the generated source starts. The file runs to the next module or to the end of its segment. Each address has one module, and each file name has one module. A file name has letters, digits, underscores or hyphens, and ends in `.asm`. The c64-source skill writes `main.asm`, `loader.asm` and `constants.asm` itself. Thus a module cannot have one of these names, in upper or lower case. The title becomes the first line of the file. A `module` write without a title keeps the title that the module has. Modules have a history, and `revert` restores them.
|
|
@@ -181,7 +191,8 @@ The exit status is 0 for a result. The exit status is 1 for an error from the kn
|
|
|
181
191
|
- `conflict`: the change contradicts current knowledge. For example, the name belongs to another address. Read that address and its history, then decide.
|
|
182
192
|
- `stale-revision`: the knowledge changed after you read it. Read it again, then decide again.
|
|
183
193
|
- `not-found`: there is nothing to remove or no such revision.
|
|
184
|
-
- `
|
|
194
|
+
- `conflict` with "from a git merge": git joined the knowledge of two computers, and two rows are current where the knowledge permits one row. For example, two names are at one address. Every read shows these rows in `conflicts`. Writes stop until you run `merge`. The `merge` command keeps the newest row of each conflict and closes the other rows, in one revision. Its result lists each choice. Tell the user the choices.
|
|
195
|
+
- `invalid-database` or `unsupported-schema`: tell the user the message. Do not edit the knowledge files.
|
|
185
196
|
|
|
186
197
|
A revert makes a new revision. The history stays complete.
|
|
187
198
|
|
|
@@ -190,3 +201,4 @@ A revert makes a new revision. The history stays complete.
|
|
|
190
201
|
- Runtime evidence → c64-emulator
|
|
191
202
|
- Static evidence → c64-static-analysis
|
|
192
203
|
- Source from the knowledge → c64-source
|
|
204
|
+
- Names of routines that other programs share → c64-provenance
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// The c64-knowledge script: reads and writes .c64-re-tools/knowledge
|
|
2
|
-
// project
|
|
1
|
+
// The c64-knowledge script: reads and writes .c64-re-tools/knowledge/ in the
|
|
2
|
+
// project folder (--project, or the one found from the working directory) and
|
|
3
|
+
// prints one compact JSON result.
|
|
3
4
|
|
|
4
5
|
import { createHash } from "node:crypto";
|
|
5
6
|
import { readFileSync } from "node:fs";
|
|
@@ -9,6 +10,7 @@ import { parseArgs } from "node:util";
|
|
|
9
10
|
import { formatC64Address, parseC64Address } from "#src/c64.ts";
|
|
10
11
|
import { ImageError, readImage } from "#src/host-client/image.ts";
|
|
11
12
|
import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
|
|
13
|
+
import { CONFLICT_RULES, conflicts, type Conflict } from "#src/knowledge/conflicts.ts";
|
|
12
14
|
import { KnowledgeError, openForRead, withWrite } from "#src/knowledge/database.ts";
|
|
13
15
|
import { operandText } from "#src/knowledge/expression.ts";
|
|
14
16
|
import { historyAt, historyNamed, revision, revisionNote, revisions, type HistoryEntry } from "#src/knowledge/history.ts";
|
|
@@ -53,6 +55,7 @@ import {
|
|
|
53
55
|
addReference,
|
|
54
56
|
applyBatch,
|
|
55
57
|
classifyRegion,
|
|
58
|
+
mergeConflicts,
|
|
56
59
|
removeComment,
|
|
57
60
|
removeModule,
|
|
58
61
|
removeOperand,
|
|
@@ -66,6 +69,7 @@ import {
|
|
|
66
69
|
unclassifyRegion,
|
|
67
70
|
type BatchOperation,
|
|
68
71
|
type Evidence,
|
|
72
|
+
type MergeChoice,
|
|
69
73
|
type OperandImage,
|
|
70
74
|
type WriteContext,
|
|
71
75
|
} from "#src/knowledge/write.ts";
|
|
@@ -86,8 +90,8 @@ Read (no database needed):
|
|
|
86
90
|
operands [--start A --end A --origin O] operand expressions of instructions
|
|
87
91
|
spaces the address spaces that hold knowledge
|
|
88
92
|
history <address> | history --name <name>
|
|
89
|
-
revisions [--limit N --before
|
|
90
|
-
revision <
|
|
93
|
+
revisions [--limit N --before R]
|
|
94
|
+
revision <revision>
|
|
91
95
|
export --out <file> [--start A --end A --origin O] all current knowledge to a project file
|
|
92
96
|
|
|
93
97
|
Every command takes --space S: c64 (the default), drive8 for code in the 1541
|
|
@@ -97,7 +101,7 @@ own space.
|
|
|
97
101
|
Read option --origin O: user, llm, dxa, ghidra, heuristic, semantic (user and
|
|
98
102
|
llm) or analyzer (dxa, ghidra and heuristic), or a comma list. search takes it too.
|
|
99
103
|
|
|
100
|
-
Write (one revision each; creates
|
|
104
|
+
Write (one revision each; creates .c64-re-tools/knowledge/ when needed):
|
|
101
105
|
rename <address> <name> [--kind K]
|
|
102
106
|
remove-symbol <address>
|
|
103
107
|
classify <start> <end> <type> [--element N]
|
|
@@ -117,6 +121,9 @@ Write (one revision each; creates the database when needed):
|
|
|
117
121
|
or PRG); give a negative offset as --offset=-1
|
|
118
122
|
unoperand <address>
|
|
119
123
|
revert <revision>
|
|
124
|
+
merge decide the conflicts that a git merge of two
|
|
125
|
+
computers' work left: of the rows of one conflict,
|
|
126
|
+
the newest stays; one revision for all
|
|
120
127
|
apply <file> many writes from a JSON file ('-' reads standard input),
|
|
121
128
|
all as one revision, or none when one is refused;
|
|
122
129
|
operand writes need --image <file>
|
|
@@ -125,10 +132,12 @@ Write options:
|
|
|
125
132
|
--reason <text> why, kept with the revision
|
|
126
133
|
--evidence <kind:ref> what the change rests on: coverage:<file>, test:<file>,
|
|
127
134
|
file:<file> or note:<text> (repeatable)
|
|
128
|
-
--expect-revision <
|
|
135
|
+
--expect-revision <R> refuse the write if knowledge changed after revision R
|
|
129
136
|
--origin user|llm who decided (default llm)
|
|
130
137
|
|
|
131
|
-
Addresses are $ and four hex digits; quote them in a shell: '$2100'
|
|
138
|
+
Addresses are $ and four hex digits; quote them in a shell: '$2100'.
|
|
139
|
+
A revision is its id, for example 20261015T214501.123-k3f9qz, or a start of
|
|
140
|
+
the id that only one revision has; 0 is the knowledge before every revision.`;
|
|
132
141
|
|
|
133
142
|
function address(value: string | undefined, what = "address"): number {
|
|
134
143
|
if (value === undefined) throw new UsageError(`${what} is missing`);
|
|
@@ -205,9 +214,19 @@ function historyEntry(entry: HistoryEntry) {
|
|
|
205
214
|
return { entity: entry.entity, ...row, fromRevision: entry.from, ...(entry.to === undefined ? {} : { toRevision: entry.to }) };
|
|
206
215
|
}
|
|
207
216
|
|
|
217
|
+
/** A conflict of a git merge (D69): what is wrong, and the rows, oldest first. */
|
|
218
|
+
function showConflict(conflict: Conflict) {
|
|
219
|
+
return { conflict: CONFLICT_RULES[conflict.rule], ...spaceOf(conflict), rows: conflict.rows.map(historyEntry) };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** What merge decided for one conflict: the row that stays and the rows it closed. */
|
|
223
|
+
function showChoice(choice: MergeChoice) {
|
|
224
|
+
return { conflict: CONFLICT_RULES[choice.rule], ...spaceOf(choice), kept: historyEntry(choice.kept), closed: choice.closed.map(historyEntry) };
|
|
225
|
+
}
|
|
226
|
+
|
|
208
227
|
/** The reason of each revision that wrote one of the rows. */
|
|
209
|
-
function reasonsOf(db: DatabaseSync | undefined, rows: SuppressionRow[]): Map<
|
|
210
|
-
const reasons = new Map<
|
|
228
|
+
function reasonsOf(db: DatabaseSync | undefined, rows: SuppressionRow[]): Map<string, string> {
|
|
229
|
+
const reasons = new Map<string, string>();
|
|
211
230
|
const query = db?.prepare("SELECT description FROM revisions WHERE id = ?");
|
|
212
231
|
for (const { revision } of rows) {
|
|
213
232
|
if (query === undefined || reasons.has(revision)) continue;
|
|
@@ -254,7 +273,7 @@ function run(argv: string[]): unknown {
|
|
|
254
273
|
const space = values.space ?? DEFAULT_SPACE;
|
|
255
274
|
|
|
256
275
|
const read = new Set(["at", "search", "symbols", "regions", "comments", "references", "suppressions", "modules", "operands", "spaces", "history", "revisions", "revision", "export"]);
|
|
257
|
-
const write = new Set(["rename", "remove-symbol", "classify", "unclassify", "comment", "uncomment", "reference", "unreference", "module", "unmodule", "operand", "unoperand", "revert", "apply"]);
|
|
276
|
+
const write = new Set(["rename", "remove-symbol", "classify", "unclassify", "comment", "uncomment", "reference", "unreference", "module", "unmodule", "operand", "unoperand", "revert", "apply", "merge"]);
|
|
258
277
|
// Refuse a mistyped command before anything opens, so no database appears by accident.
|
|
259
278
|
if (!read.has(command) && !write.has(command)) throw new UsageError(`unknown command: ${command}`);
|
|
260
279
|
if (read.has(command)) {
|
|
@@ -262,83 +281,10 @@ function run(argv: string[]): unknown {
|
|
|
262
281
|
if (command === "export" && values.out === undefined) throw new UsageError("export needs --out <file>");
|
|
263
282
|
const db = openForRead();
|
|
264
283
|
try {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
...(space === DEFAULT_SPACE ? {} : { space }),
|
|
270
|
-
address: formatC64Address(known.address),
|
|
271
|
-
...(known.symbol === undefined ? {} : { symbol: symbol(known.symbol) }),
|
|
272
|
-
...(known.region === undefined ? {} : { region: region(known.region) }),
|
|
273
|
-
comments: known.comments.map(comment),
|
|
274
|
-
referencesFrom: known.referencesFrom.map(reference),
|
|
275
|
-
referencesTo: known.referencesTo.map(reference),
|
|
276
|
-
...(known.operand === undefined ? {} : { operand: operand(known.operand) }),
|
|
277
|
-
...(known.suppressions.length === 0 ? {} : { suppressions: known.suppressions.map(suppression) }),
|
|
278
|
-
revision: known.revision,
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
case "search": {
|
|
282
|
-
if (args[0] === undefined || args[0] === "") throw new UsageError("search needs text");
|
|
283
|
-
const found = search(db, args[0], values.limit === undefined ? 50 : integer(values.limit, "--limit"), origins, space);
|
|
284
|
-
return { symbols: found.symbols.map(symbol), comments: found.comments.map(comment) };
|
|
285
|
-
}
|
|
286
|
-
case "symbols":
|
|
287
|
-
return { symbols: listSymbols(db, range, values.kind === undefined ? undefined : oneOf(SYMBOL_KINDS, values.kind, "--kind"), origins, space).map(symbol) };
|
|
288
|
-
case "regions":
|
|
289
|
-
return { regions: listRegions(db, range, values.type === undefined ? undefined : oneOf(REGION_TYPES, values.type, "--type"), origins, space).map(region) };
|
|
290
|
-
case "comments":
|
|
291
|
-
return { comments: listComments(db, range, origins, space).map(comment) };
|
|
292
|
-
case "references":
|
|
293
|
-
return { references: listReferences(db, range, values.kind === undefined ? undefined : oneOf(REFERENCE_KINDS, values.kind, "--kind"), origins, space).map(reference) };
|
|
294
|
-
case "export": {
|
|
295
|
-
const exported = {
|
|
296
|
-
revision: currentRevision(db),
|
|
297
|
-
...(space === DEFAULT_SPACE ? {} : { space }),
|
|
298
|
-
symbols: listSymbols(db, range, undefined, origins, space).map(symbol),
|
|
299
|
-
regions: listRegions(db, range, undefined, origins, space).map(region),
|
|
300
|
-
comments: listComments(db, range, origins, space).map(comment),
|
|
301
|
-
references: listReferences(db, range, undefined, origins, space).map(reference),
|
|
302
|
-
suppressions: listSuppressions(db, range, space).map(suppression),
|
|
303
|
-
modules: listModules(db, range, origins, space).map(module),
|
|
304
|
-
operands: listOperands(db, range, origins, space).map(operand),
|
|
305
|
-
};
|
|
306
|
-
const bytes = Buffer.from(`${JSON.stringify(exported, null, 2)}\n`, "utf8");
|
|
307
|
-
writeProjectFile(values.out!, bytes, { what: "--out" });
|
|
308
|
-
return {
|
|
309
|
-
out: values.out,
|
|
310
|
-
bytes: bytes.length,
|
|
311
|
-
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, operands: exported.operands.length },
|
|
312
|
-
revision: exported.revision,
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
case "suppressions": {
|
|
316
|
-
const rows = listSuppressions(db, range, space);
|
|
317
|
-
const reasons = reasonsOf(db, rows);
|
|
318
|
-
return { suppressions: rows.map((row) => ({ ...suppression(row), ...(reasons.has(row.revision) ? { reason: reasons.get(row.revision) } : {}) })) };
|
|
319
|
-
}
|
|
320
|
-
case "modules":
|
|
321
|
-
return { modules: listModules(db, range, origins, space).map(module) };
|
|
322
|
-
case "operands":
|
|
323
|
-
return { operands: listOperands(db, range, origins, space).map(operand) };
|
|
324
|
-
case "spaces":
|
|
325
|
-
return { spaces: listSpaces(db) };
|
|
326
|
-
case "history":
|
|
327
|
-
// Each row with the reason and the evidence of the revision that wrote it (F-26).
|
|
328
|
-
return {
|
|
329
|
-
history: (values.name === undefined ? historyAt(db, address(args[0]), space) : historyNamed(db, values.name, space)).map((entry) => ({ ...historyEntry(entry), ...revisionNote(db, entry.from) })),
|
|
330
|
-
};
|
|
331
|
-
case "revisions": {
|
|
332
|
-
const options: { limit?: number; before?: number } = {};
|
|
333
|
-
if (values.limit !== undefined) options.limit = integer(values.limit, "--limit");
|
|
334
|
-
if (values.before !== undefined) options.before = integer(values.before, "--before");
|
|
335
|
-
return { revisions: revisions(db, options) };
|
|
336
|
-
}
|
|
337
|
-
default: {
|
|
338
|
-
const changes = revision(db, integer(args[0], "revision"));
|
|
339
|
-
return { revision: changes.revision, added: changes.added.map(historyEntry), closed: changes.closed.map(historyEntry) };
|
|
340
|
-
}
|
|
341
|
-
}
|
|
284
|
+
const answer = readAnswer(db, command, args, values, range, origins, space);
|
|
285
|
+
const found = conflicts(db);
|
|
286
|
+
// A git merge can leave conflicts: every read shows them until merge decides them (D69).
|
|
287
|
+
return found.length === 0 ? answer : { ...answer, conflicts: found.map(showConflict) };
|
|
342
288
|
} finally {
|
|
343
289
|
db?.close();
|
|
344
290
|
}
|
|
@@ -346,7 +292,7 @@ function run(argv: string[]): unknown {
|
|
|
346
292
|
|
|
347
293
|
const context: WriteContext = { origin: values.origin === undefined ? "llm" : oneOf(["user", "llm"] as const, values.origin, "--origin") };
|
|
348
294
|
if (values.reason !== undefined) context.description = values.reason;
|
|
349
|
-
if (values["expect-revision"] !== undefined) context.expectedRevision =
|
|
295
|
+
if (values["expect-revision"] !== undefined) context.expectedRevision = values["expect-revision"];
|
|
350
296
|
if (values.evidence !== undefined) context.evidence = values.evidence.map(evidenceOf);
|
|
351
297
|
// Every argument is checked before the database opens: a refused write creates no database.
|
|
352
298
|
if (values.title !== undefined && command !== "module") throw new UsageError("--title works only with module");
|
|
@@ -363,6 +309,90 @@ function run(argv: string[]): unknown {
|
|
|
363
309
|
return withWrite(operation);
|
|
364
310
|
}
|
|
365
311
|
|
|
312
|
+
type ReadValues = { kind?: string | undefined; type?: string | undefined; name?: string | undefined; limit?: string | undefined; before?: string | undefined; out?: string | undefined };
|
|
313
|
+
|
|
314
|
+
/** The answer of one read command. */
|
|
315
|
+
function readAnswer(db: DatabaseSync | undefined, command: string, args: string[], values: ReadValues, range: { start: number; end: number }, origins: Origin[] | undefined, space: string): Record<string, unknown> {
|
|
316
|
+
switch (command) {
|
|
317
|
+
case "at": {
|
|
318
|
+
const known = at(db, address(args[0]), space);
|
|
319
|
+
return {
|
|
320
|
+
...(space === DEFAULT_SPACE ? {} : { space }),
|
|
321
|
+
address: formatC64Address(known.address),
|
|
322
|
+
...(known.symbol === undefined ? {} : { symbol: symbol(known.symbol) }),
|
|
323
|
+
...(known.region === undefined ? {} : { region: region(known.region) }),
|
|
324
|
+
comments: known.comments.map(comment),
|
|
325
|
+
referencesFrom: known.referencesFrom.map(reference),
|
|
326
|
+
referencesTo: known.referencesTo.map(reference),
|
|
327
|
+
...(known.operand === undefined ? {} : { operand: operand(known.operand) }),
|
|
328
|
+
...(known.suppressions.length === 0 ? {} : { suppressions: known.suppressions.map(suppression) }),
|
|
329
|
+
revision: known.revision,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
case "search": {
|
|
333
|
+
if (args[0] === undefined || args[0] === "") throw new UsageError("search needs text");
|
|
334
|
+
const found = search(db, args[0], values.limit === undefined ? 50 : integer(values.limit, "--limit"), origins, space);
|
|
335
|
+
return { symbols: found.symbols.map(symbol), comments: found.comments.map(comment) };
|
|
336
|
+
}
|
|
337
|
+
case "symbols":
|
|
338
|
+
return { symbols: listSymbols(db, range, values.kind === undefined ? undefined : oneOf(SYMBOL_KINDS, values.kind, "--kind"), origins, space).map(symbol) };
|
|
339
|
+
case "regions":
|
|
340
|
+
return { regions: listRegions(db, range, values.type === undefined ? undefined : oneOf(REGION_TYPES, values.type, "--type"), origins, space).map(region) };
|
|
341
|
+
case "comments":
|
|
342
|
+
return { comments: listComments(db, range, origins, space).map(comment) };
|
|
343
|
+
case "references":
|
|
344
|
+
return { references: listReferences(db, range, values.kind === undefined ? undefined : oneOf(REFERENCE_KINDS, values.kind, "--kind"), origins, space).map(reference) };
|
|
345
|
+
case "export": {
|
|
346
|
+
const exported = {
|
|
347
|
+
revision: currentRevision(db),
|
|
348
|
+
...(space === DEFAULT_SPACE ? {} : { space }),
|
|
349
|
+
symbols: listSymbols(db, range, undefined, origins, space).map(symbol),
|
|
350
|
+
regions: listRegions(db, range, undefined, origins, space).map(region),
|
|
351
|
+
comments: listComments(db, range, origins, space).map(comment),
|
|
352
|
+
references: listReferences(db, range, undefined, origins, space).map(reference),
|
|
353
|
+
suppressions: listSuppressions(db, range, space).map(suppression),
|
|
354
|
+
modules: listModules(db, range, origins, space).map(module),
|
|
355
|
+
operands: listOperands(db, range, origins, space).map(operand),
|
|
356
|
+
};
|
|
357
|
+
const bytes = Buffer.from(`${JSON.stringify(exported, null, 2)}\n`, "utf8");
|
|
358
|
+
writeProjectFile(values.out!, bytes, { what: "--out" });
|
|
359
|
+
return {
|
|
360
|
+
out: values.out,
|
|
361
|
+
bytes: bytes.length,
|
|
362
|
+
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, operands: exported.operands.length },
|
|
363
|
+
revision: exported.revision,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
case "suppressions": {
|
|
367
|
+
const rows = listSuppressions(db, range, space);
|
|
368
|
+
const reasons = reasonsOf(db, rows);
|
|
369
|
+
return { suppressions: rows.map((row) => ({ ...suppression(row), ...(reasons.has(row.revision) ? { reason: reasons.get(row.revision) } : {}) })) };
|
|
370
|
+
}
|
|
371
|
+
case "modules":
|
|
372
|
+
return { modules: listModules(db, range, origins, space).map(module) };
|
|
373
|
+
case "operands":
|
|
374
|
+
return { operands: listOperands(db, range, origins, space).map(operand) };
|
|
375
|
+
case "spaces":
|
|
376
|
+
return { spaces: listSpaces(db) };
|
|
377
|
+
case "history":
|
|
378
|
+
// Each row with the reason and the evidence of the revision that wrote it (F-26).
|
|
379
|
+
return {
|
|
380
|
+
history: (values.name === undefined ? historyAt(db, address(args[0]), space) : historyNamed(db, values.name, space)).map((entry) => ({ ...historyEntry(entry), ...revisionNote(db, entry.from) })),
|
|
381
|
+
};
|
|
382
|
+
case "revisions": {
|
|
383
|
+
const options: { limit?: number; before?: string } = {};
|
|
384
|
+
if (values.limit !== undefined) options.limit = integer(values.limit, "--limit");
|
|
385
|
+
if (values.before !== undefined) options.before = values.before;
|
|
386
|
+
return { revisions: revisions(db, options) };
|
|
387
|
+
}
|
|
388
|
+
default: {
|
|
389
|
+
if (args[0] === undefined) throw new UsageError("revision is missing");
|
|
390
|
+
const changes = revision(db, args[0]);
|
|
391
|
+
return { revision: changes.revision, added: changes.added.map(historyEntry), closed: changes.closed.map(historyEntry) };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
366
396
|
/** --origin for reads: one origin, semantic (user and llm), analyzer (dxa and ghidra), or a comma list. */
|
|
367
397
|
function originFilter(value: string): Origin[] {
|
|
368
398
|
const origins = new Set<Origin>();
|
|
@@ -601,8 +631,17 @@ function writeOperation(command: string, args: string[], kind: string | undefine
|
|
|
601
631
|
const request = { space, address: address(args[0]) };
|
|
602
632
|
return (db) => ({ revision: removeOperand(db, context, request).revision });
|
|
603
633
|
}
|
|
634
|
+
case "merge": {
|
|
635
|
+
if (args.length > 0) throw new UsageError("merge takes no arguments");
|
|
636
|
+
const platform = new Map(platformNames().map((entry) => [entry.name, entry.address]));
|
|
637
|
+
return (db) => {
|
|
638
|
+
const merged = mergeConflicts(db, context, platform);
|
|
639
|
+
return { revision: merged.revision, choices: merged.current.map(showChoice) };
|
|
640
|
+
};
|
|
641
|
+
}
|
|
604
642
|
case "revert": {
|
|
605
|
-
|
|
643
|
+
if (args[0] === undefined) throw new UsageError("revision is missing");
|
|
644
|
+
const request = { revision: args[0] };
|
|
606
645
|
return (db) => ({ revision: revert(db, context, request).revision });
|
|
607
646
|
}
|
|
608
647
|
default:
|
|
@@ -610,4 +649,4 @@ function writeOperation(command: string, args: string[], kind: string | undefine
|
|
|
610
649
|
}
|
|
611
650
|
}
|
|
612
651
|
|
|
613
|
-
await runScript(() => run(
|
|
652
|
+
await runScript((argv) => run(argv), { usage: USAGE, refusals: [KnowledgeError] });
|