@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.21
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 +28 -62
- package/distribution/package.ts +1 -16
- package/distribution/plugin.ts +5 -3
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +25 -6
- package/skills/c64-assembler/scripts/assemble.ts +45 -10
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +15 -7
- package/skills/c64-basic/scripts/basic.ts +4 -4
- package/skills/c64-disk/SKILL.md +34 -11
- package/skills/c64-disk/scripts/disk.ts +65 -10
- package/skills/c64-disk/scripts/lib/containers.ts +124 -0
- package/skills/c64-docs/SKILL.md +138 -0
- package/skills/c64-docs/references/page-types.md +141 -0
- package/skills/c64-docs/references/style.md +43 -0
- package/skills/c64-docs/scripts/generate.ts +102 -0
- package/skills/c64-docs/scripts/lib/links.ts +155 -0
- package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
- package/skills/c64-docs/scripts/lib/reference.ts +406 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
- package/skills/c64-docs/scripts/link.ts +207 -0
- package/skills/c64-emulator/SKILL.md +29 -8
- package/skills/c64-knowledge/SKILL.md +201 -15
- package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +14 -2
- package/skills/c64-memory-map/scripts/memmap.ts +15 -4
- package/skills/c64-provenance/SKILL.md +100 -11
- package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
- package/skills/c64-provenance/scripts/provenance.ts +477 -24
- package/skills/c64-reverse-engineering/SKILL.md +90 -11
- package/skills/c64-source/SKILL.md +174 -0
- package/skills/c64-source/scripts/generate.ts +788 -0
- package/skills/c64-source/scripts/lib/loader.ts +630 -0
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +53 -13
- package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
- package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
- package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +57 -4
- package/skills/c64-testing/references/scenario-format.md +50 -6
- package/skills/c64-testing/scripts/checklist.ts +3 -3
- package/skills/c64-testing/scripts/diff.ts +167 -0
- package/skills/c64-testing/scripts/lib/diff.ts +70 -0
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
- package/skills/c64-testing/scripts/run.ts +241 -0
- package/skills/c64-testing/scripts/test.ts +20 -10
- package/skills/c64-unpacker/SKILL.md +88 -21
- package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
- package/skills/c64-unpacker/scripts/unpack.ts +545 -61
- package/src/c64.ts +240 -12
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +14 -8
- package/src/cli/status.ts +105 -11
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +57 -31
- package/src/host/server.ts +30 -47
- package/src/host/tools/c1541.ts +34 -5
- package/src/host/tools/index.ts +6 -2
- package/src/host/tools/petcat.ts +10 -3
- package/src/host/tools/status.ts +23 -6
- package/src/host/vice/adapter.ts +64 -20
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/process.ts +19 -8
- package/src/host/vice/screen.ts +4 -44
- package/src/host/vice/session.ts +343 -60
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +14 -14
- package/src/host-client/coverage.ts +36 -0
- package/src/host-client/image.ts +63 -0
- package/src/host-client/steps.ts +175 -0
- package/src/host-client/tools.ts +7 -6
- package/src/host-client/transfer.ts +60 -10
- package/src/host-client/vice-session.ts +107 -28
- package/src/knowledge/conflicts.ts +161 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/graph.ts +104 -0
- package/src/knowledge/history.ts +105 -27
- package/src/knowledge/import.ts +141 -85
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +258 -0
- package/src/knowledge/read.ts +445 -63
- package/src/knowledge/schema.ts +277 -65
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +1145 -150
- package/src/mcp/main.ts +44 -19
- package/src/mcp/output.ts +39 -0
- package/src/mcp/server.ts +15 -4
- package/src/mcp/tools/debug.ts +202 -31
- package/src/mcp/tools/execution.ts +39 -5
- package/src/mcp/tools/input.ts +7 -4
- package/src/mcp/tools/machine.ts +14 -2
- package/src/mcp/tools/media.ts +44 -16
- package/src/mcp/tools/memory.ts +68 -12
- package/src/mcp/tools/video.ts +20 -9
- package/src/native/discover.ts +44 -6
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +19 -3
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +33 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/status.ts +15 -3
- package/src/native/types.ts +43 -8
- package/src/opcodes.ts +133 -0
- package/src/package.ts +21 -0
- package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
- package/src/project.ts +166 -3
- package/src/protocol/messages.ts +17 -10
- package/src/protocol/tools.ts +27 -22
- package/src/protocol/vice.ts +255 -33
- package/src/script.ts +49 -9
- package/src/trace.ts +55 -23
- package/skills/c64-provenance/scripts/compare.ts +0 -92
- package/skills/c64-unpacker/scripts/evidence.ts +0 -135
package/src/project.ts
CHANGED
|
@@ -1,8 +1,171 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// The project (D67): the nearest folder at or above the working directory that
|
|
2
|
+
// holds .c64-re-tools/, below the home folder, the way git finds .git. A skill
|
|
3
|
+
// script names it with --project <folder> and an MCP tool with `project`. Every
|
|
4
|
+
// project path is relative to that folder (D68). .c64-re-tools/local/ holds what
|
|
5
|
+
// the agent saves for itself and ignores itself in git (D70).
|
|
3
6
|
|
|
7
|
+
import { existsSync, mkdirSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
10
|
+
|
|
11
|
+
/** The folder that marks a project and holds its knowledge. */
|
|
12
|
+
export const PROJECT_DIRECTORY = ".c64-re-tools";
|
|
13
|
+
|
|
14
|
+
/** The folder for machine-local scratch: saved results, batches and step files (D70). */
|
|
15
|
+
export const LOCAL_DIRECTORY = "local";
|
|
16
|
+
|
|
17
|
+
/** The .gitignore of LOCAL_DIRECTORY: git ignores everything in it, the .gitignore too. */
|
|
18
|
+
const LOCAL_GITIGNORE = "# Machine-local scratch of c64-re-tools (saved results, batches, step files). Never committed.\n*\n";
|
|
19
|
+
|
|
20
|
+
/** How deep a working directory without a project is searched for projects below it. */
|
|
21
|
+
const SEARCH_DEPTH = 3;
|
|
22
|
+
|
|
23
|
+
/** At most this many projects are named in a refusal. */
|
|
24
|
+
const LISTED_PROJECTS = 20;
|
|
25
|
+
|
|
26
|
+
/** A refusal of the project rules, with a stable code, shown to the LLM unchanged. */
|
|
27
|
+
export class ProjectError extends Error {
|
|
28
|
+
override name = "ProjectError";
|
|
29
|
+
readonly code: "invalid-input" | "not-found";
|
|
30
|
+
|
|
31
|
+
constructor(code: "invalid-input" | "not-found", message: string) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.code = code;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** How a refusal tells the caller to name a project: "--project <folder>" for a script, "project" for an MCP tool. */
|
|
38
|
+
export interface ProjectRemedy {
|
|
39
|
+
remedy?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const SCRIPT_REMEDY = "Give --project <folder>";
|
|
43
|
+
|
|
44
|
+
/** The folder that --project chose for this process, or undefined. */
|
|
45
|
+
let chosen: string | undefined;
|
|
46
|
+
|
|
47
|
+
function isProject(folder: string): boolean {
|
|
48
|
+
return statSync(join(folder, PROJECT_DIRECTORY), { throwIfNoEntry: false })?.isDirectory() === true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** `start` and each folder above it, nearest first, below the home folder when `start` is under it. */
|
|
52
|
+
function foldersUpFrom(start: string): string[] {
|
|
53
|
+
const home = homedir();
|
|
54
|
+
const underHome = start !== home && isInside(home, start);
|
|
55
|
+
if (start === home) return [];
|
|
56
|
+
const folders: string[] = [];
|
|
57
|
+
for (let folder = start; ; folder = dirname(folder)) {
|
|
58
|
+
if (underHome && folder === home) break;
|
|
59
|
+
folders.push(folder);
|
|
60
|
+
if (dirname(folder) === folder) break;
|
|
61
|
+
}
|
|
62
|
+
return folders;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The projects below `start` to SEARCH_DEPTH, relative to it and sorted. Dot folders and node_modules are skipped. */
|
|
66
|
+
function projectsBelow(start: string): string[] {
|
|
67
|
+
const found: string[] = [];
|
|
68
|
+
const visit = (folder: string, depth: number) => {
|
|
69
|
+
let entries;
|
|
70
|
+
try {
|
|
71
|
+
entries = readdirSync(folder, { withFileTypes: true });
|
|
72
|
+
} catch {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
77
|
+
const path = join(folder, entry.name);
|
|
78
|
+
if (isProject(path)) found.push(relative(start, path));
|
|
79
|
+
else if (depth < SEARCH_DEPTH) visit(path, depth + 1);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
visit(start, 1);
|
|
83
|
+
return found.sort();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function listed(projects: readonly string[]): string {
|
|
87
|
+
const shown = projects.slice(0, LISTED_PROJECTS).join(", ");
|
|
88
|
+
return projects.length > LISTED_PROJECTS ? `${shown} and ${projects.length - LISTED_PROJECTS} more` : shown;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The project of the working directory `cwd` by the D67 rules: the nearest
|
|
93
|
+
* folder at or above it with .c64-re-tools/, below the home folder. Without
|
|
94
|
+
* one, `cwd` itself, unless projects are below it or it is the home folder:
|
|
95
|
+
* then a ProjectError names them and the remedy, so no write lands outside a
|
|
96
|
+
* project.
|
|
97
|
+
*/
|
|
98
|
+
export function findProject(cwd: string = process.cwd(), options: ProjectRemedy = {}): string {
|
|
99
|
+
const start = resolve(cwd);
|
|
100
|
+
const remedy = options.remedy ?? SCRIPT_REMEDY;
|
|
101
|
+
for (const folder of foldersUpFrom(start)) {
|
|
102
|
+
if (isProject(folder)) return folder;
|
|
103
|
+
}
|
|
104
|
+
if (start === homedir()) {
|
|
105
|
+
throw new ProjectError("invalid-input", `The home folder is not a project folder. Start in a project folder, or ${remedy.charAt(0).toLowerCase()}${remedy.slice(1)}.`);
|
|
106
|
+
}
|
|
107
|
+
const below = projectsBelow(start);
|
|
108
|
+
if (below.length > 0) {
|
|
109
|
+
throw new ProjectError("invalid-input", `${start} is not a project folder. These folders below it are projects: ${listed(below)}. ${remedy} with one of them.`);
|
|
110
|
+
}
|
|
111
|
+
return start;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The project folder that `folder` names, relative to `cwd` or absolute. It
|
|
116
|
+
* must be a folder. It is the project as given, also without .c64-re-tools/
|
|
117
|
+
* (the first write makes it), but not a folder inside another project or the
|
|
118
|
+
* home folder.
|
|
119
|
+
*/
|
|
120
|
+
export function namedProject(folder: string, cwd: string = process.cwd()): string {
|
|
121
|
+
if (folder.length === 0) throw new ProjectError("invalid-input", "The project folder must not be empty.");
|
|
122
|
+
const path = resolve(cwd, folder);
|
|
123
|
+
const stat = statSync(path, { throwIfNoEntry: false });
|
|
124
|
+
if (stat === undefined) throw new ProjectError("not-found", `There is no folder ${folder}.`);
|
|
125
|
+
if (!stat.isDirectory()) throw new ProjectError("invalid-input", `${folder} is not a folder.`);
|
|
126
|
+
if (path === homedir()) throw new ProjectError("invalid-input", "The home folder is not a project folder.");
|
|
127
|
+
const outer = foldersUpFrom(path).slice(1).find(isProject);
|
|
128
|
+
if (outer !== undefined) {
|
|
129
|
+
throw new ProjectError("invalid-input", `${folder} is inside the project ${relative(cwd, outer) || "."}. Give that project folder.`);
|
|
130
|
+
}
|
|
131
|
+
return path;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Makes .c64-re-tools/local/ with its .gitignore when the project has
|
|
136
|
+
* .c64-re-tools/ and local/ has no .gitignore yet (D70). A folder that is not
|
|
137
|
+
* yet a project gets nothing. A failure (for example a read-only project) is
|
|
138
|
+
* left to the write into local/ that needs the folder.
|
|
139
|
+
*/
|
|
140
|
+
export function ensureLocal(root: string): void {
|
|
141
|
+
const local = join(root, PROJECT_DIRECTORY, LOCAL_DIRECTORY);
|
|
142
|
+
const gitignore = join(local, ".gitignore");
|
|
143
|
+
if (existsSync(gitignore) || !isProject(root)) return;
|
|
144
|
+
try {
|
|
145
|
+
mkdirSync(local, { recursive: true });
|
|
146
|
+
writeFileSync(gitignore, LOCAL_GITIGNORE, { flag: "wx" });
|
|
147
|
+
} catch {
|
|
148
|
+
// Another process made it, or the project is read-only.
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The project of an MCP call or another caller without process state: `folder` when given, else the D67 lookup from `cwd`. */
|
|
153
|
+
export function resolveProject(folder: string | undefined, cwd: string = process.cwd(), options: ProjectRemedy = {}): string {
|
|
154
|
+
const root = folder === undefined ? findProject(cwd, options) : namedProject(folder, cwd);
|
|
155
|
+
ensureLocal(root);
|
|
156
|
+
return root;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Sets the project of this process from --project. Throws ProjectError for a folder that the rules refuse. */
|
|
160
|
+
export function useProject(folder: string): void {
|
|
161
|
+
chosen = namedProject(folder);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The project folder of this process: the --project choice, else the D67 lookup from the working directory. */
|
|
4
165
|
export function projectRoot(): string {
|
|
5
|
-
|
|
166
|
+
const root = chosen ?? findProject();
|
|
167
|
+
ensureLocal(root);
|
|
168
|
+
return root;
|
|
6
169
|
}
|
|
7
170
|
|
|
8
171
|
export function resolveProjectPath(path: string, root = projectRoot()): string {
|
package/src/protocol/messages.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { ViceOperation, ViceOperations } from "./vice.ts";
|
|
|
7
7
|
/** Private Host Runtime protocol identifier. */
|
|
8
8
|
export const HOST_PROTOCOL_ID = "c64-re-tools-host" as const;
|
|
9
9
|
/** Bumped on any incompatible change; a mismatch fails the handshake. */
|
|
10
|
-
export const HOST_PROTOCOL_VERSION =
|
|
10
|
+
export const HOST_PROTOCOL_VERSION = 3;
|
|
11
11
|
/** After ready, the client pings this often, on every connection. */
|
|
12
12
|
export const HEARTBEAT_INTERVAL_MS = 10_000;
|
|
13
13
|
/** Either side closes a connection that has sent nothing for this long. */
|
|
@@ -54,13 +54,8 @@ export interface Hello {
|
|
|
54
54
|
role: Role;
|
|
55
55
|
/** vice-session only; fixed for the session. */
|
|
56
56
|
videoStandard?: VideoStandard;
|
|
57
|
-
/** The shared secret (C64RT_HOST_TOKEN) for a host that listens beyond loopback. */
|
|
58
|
-
token?: string;
|
|
59
57
|
}
|
|
60
58
|
|
|
61
|
-
/** Longest accepted shared token. */
|
|
62
|
-
const MAX_TOKEN_LENGTH = 256;
|
|
63
|
-
|
|
64
59
|
export interface Ready {
|
|
65
60
|
type: "ready";
|
|
66
61
|
}
|
|
@@ -111,6 +106,22 @@ export class ProtocolError extends Error {
|
|
|
111
106
|
}
|
|
112
107
|
|
|
113
108
|
/** Thrown by parameter/result validators; carries the wire code to answer with. */
|
|
109
|
+
/**
|
|
110
|
+
* The instruction at the end of a refusal that only the user can fix: a
|
|
111
|
+
* missing or broken tool, or a Host Runtime that does not run (D37).
|
|
112
|
+
*/
|
|
113
|
+
export const STOP_AND_ASK = "Stop the task. Give this message to the user and wait for the user. Do not make a replacement for the tool or the host runtime.";
|
|
114
|
+
|
|
115
|
+
/** The message with STOP_AND_ASK at its end, once. */
|
|
116
|
+
export function withStopAndAsk(message: string): string {
|
|
117
|
+
return message.endsWith(STOP_AND_ASK) ? message : `${message} ${STOP_AND_ASK}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The message that the agent sees for a refusal: an installation-incomplete one ends with STOP_AND_ASK. */
|
|
121
|
+
export function shownMessage(error: { code: string; message: string }): string {
|
|
122
|
+
return error.code === "installation-incomplete" ? withStopAndAsk(error.message) : error.message;
|
|
123
|
+
}
|
|
124
|
+
|
|
114
125
|
export class WireFailure extends Error implements WireError {
|
|
115
126
|
override name = "WireFailure";
|
|
116
127
|
readonly code: ErrorCode;
|
|
@@ -166,12 +177,8 @@ export function parseClientMessage(value: unknown): ReceivedClientMessage {
|
|
|
166
177
|
if (value.videoStandard !== undefined && !isOneOf(VIDEO_STANDARDS, value.videoStandard)) {
|
|
167
178
|
throw new ProtocolError("hello has an unknown video standard");
|
|
168
179
|
}
|
|
169
|
-
if (value.token !== undefined && (typeof value.token !== "string" || value.token.length > MAX_TOKEN_LENGTH)) {
|
|
170
|
-
throw new ProtocolError("hello has a malformed token");
|
|
171
|
-
}
|
|
172
180
|
const hello: Hello = { type: "hello", protocol: value.protocol, version: value.version, role: value.role };
|
|
173
181
|
if (value.videoStandard !== undefined) hello.videoStandard = value.videoStandard;
|
|
174
|
-
if (value.token !== undefined) hello.token = value.token;
|
|
175
182
|
return hello;
|
|
176
183
|
}
|
|
177
184
|
if (value.type === "request") {
|
package/src/protocol/tools.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { type Fields, invalid, isInteger, isObject, isOneOf, onlyFields, Protoco
|
|
|
5
5
|
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
// Native-tool operations. Short-lived requests on "tool" connections; they
|
|
8
|
-
// never touch an emulator or knowledge.
|
|
8
|
+
// never touch an emulator or the knowledge.
|
|
9
9
|
|
|
10
10
|
/** A transferred source tree: relative POSIX paths, one attachment per file in this order. */
|
|
11
11
|
export type SourceTree = Array<{ path: string; size: number }>;
|
|
@@ -41,7 +41,8 @@ export interface DiskSector {
|
|
|
41
41
|
|
|
42
42
|
export type C1541Result =
|
|
43
43
|
| { action: "directory"; diskName: string; diskId: string; dosType: string; freeBlocks: number; entries: DiskFile[] }
|
|
44
|
-
|
|
44
|
+
/** unreferenced: the used sectors that no file, the header, the directory or the BAM holds. */
|
|
45
|
+
| { action: "bam"; tracks: Array<{ track: number; freeSectors: number[]; usedSectors: number[] }>; unreferenced: DiskSector[] }
|
|
45
46
|
| { action: "entry"; found: false }
|
|
46
47
|
| { action: "entry"; found: true; entry: DiskFile & { startTrack: number; startSector: number } }
|
|
47
48
|
| { action: "chain"; found: false }
|
|
@@ -61,7 +62,7 @@ export type PetcatResult =
|
|
|
61
62
|
| {
|
|
62
63
|
decoded: true;
|
|
63
64
|
loadAddress: number;
|
|
64
|
-
/** The address
|
|
65
|
+
/** The first address that holds no BASIC: after a 00 00 end marker, or at a link whose high byte only is zero. */
|
|
65
66
|
basicEnd: number;
|
|
66
67
|
listing: string;
|
|
67
68
|
lines: Array<{ number: number; text: string }>;
|
|
@@ -79,10 +80,22 @@ export interface ToolStatus {
|
|
|
79
80
|
runs: boolean;
|
|
80
81
|
/** What is wrong and what to do, when it is missing or does not run. */
|
|
81
82
|
problem?: string;
|
|
83
|
+
/** Known flaws of this version, each with the version that fixes it (D56). The tool still runs. */
|
|
84
|
+
flaws?: string[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The Host Runtime itself: its package version and the system it runs on. */
|
|
88
|
+
export interface HostInfo {
|
|
89
|
+
version: string;
|
|
90
|
+
/** Node's name of the operating system: linux, darwin or win32. */
|
|
91
|
+
platform: string;
|
|
92
|
+
/** The release of the operating system. */
|
|
93
|
+
release: string;
|
|
94
|
+
arch: string;
|
|
82
95
|
}
|
|
83
96
|
|
|
84
97
|
export interface ToolOperations {
|
|
85
|
-
"host.status": { params: Record<string, never>; result: { tools: ToolStatus[] } };
|
|
98
|
+
"host.status": { params: Record<string, never>; result: { host: HostInfo; tools: ToolStatus[] } };
|
|
86
99
|
"c1541.inspect": { params: C1541Params; result: C1541Result };
|
|
87
100
|
"petcat.decode": { params: PetcatParams; result: PetcatResult };
|
|
88
101
|
}
|
|
@@ -96,20 +109,6 @@ export function isRelativePath(value: unknown): value is string {
|
|
|
96
109
|
return typeof value === "string" && value.length <= 1024 && RELATIVE_PATH.test(value) && !value.includes("//") && !value.endsWith("/");
|
|
97
110
|
}
|
|
98
111
|
|
|
99
|
-
/** Validates a transferred source tree against its attachments. Throws WireFailure(invalid-input). */
|
|
100
|
-
export function validateSourceTree(value: unknown, attachments: readonly Uint8Array[]): SourceTree {
|
|
101
|
-
if (!Array.isArray(value) || value.length > MAX_TREE_FILES) invalid(`files must list at most ${MAX_TREE_FILES} files`);
|
|
102
|
-
if (value.length !== attachments.length) invalid("each listed file needs exactly one attachment");
|
|
103
|
-
const seen = new Set<string>();
|
|
104
|
-
return value.map((entry, index) => {
|
|
105
|
-
if (!isObject(entry) || !isRelativePath(entry.path)) invalid("each file needs a relative path inside the source root");
|
|
106
|
-
if (seen.has(entry.path)) invalid(`the file ${entry.path} is listed twice`);
|
|
107
|
-
seen.add(entry.path);
|
|
108
|
-
if (entry.size !== attachments[index]!.length) invalid(`the size of ${entry.path} does not match its bytes`);
|
|
109
|
-
return { path: entry.path, size: entry.size as number };
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
112
|
/** Refuses anything but exactly one attachment with at least one byte, named by what it must hold. */
|
|
114
113
|
function oneFile(attachments: readonly Uint8Array[], what: string): void {
|
|
115
114
|
if (attachments.length !== 1 || attachments[0]!.length === 0) invalid(`${what} must be the one attachment, and it must not be empty`);
|
|
@@ -173,8 +172,11 @@ export function validateToolResult<O extends ToolOperation>(op: O, value: unknow
|
|
|
173
172
|
typeof tool.name === "string" &&
|
|
174
173
|
typeof tool.found === "boolean" &&
|
|
175
174
|
typeof tool.runs === "boolean" &&
|
|
176
|
-
[tool.path, tool.version, tool.problem].every((field) => field === undefined || typeof field === "string")
|
|
177
|
-
|
|
175
|
+
[tool.path, tool.version, tool.problem].every((field) => field === undefined || typeof field === "string") &&
|
|
176
|
+
(tool.flaws === undefined || (Array.isArray(tool.flaws) && tool.flaws.every((flaw: unknown) => typeof flaw === "string")));
|
|
177
|
+
const host = value.host;
|
|
178
|
+
const isHost = isObject(host) && [host.version, host.platform, host.release, host.arch].every((field) => typeof field === "string");
|
|
179
|
+
if (!isHost || !Array.isArray(tools) || !tools.every(isStatus) || attachments.length !== 0) throw new ProtocolError("host.status result is malformed");
|
|
178
180
|
return value as unknown as ToolOperations[O]["result"];
|
|
179
181
|
}
|
|
180
182
|
}
|
|
@@ -211,7 +213,9 @@ function isC1541Result(value: Fields, attachments: readonly Uint8Array[]): boole
|
|
|
211
213
|
case "bam":
|
|
212
214
|
return (
|
|
213
215
|
Array.isArray(value.tracks) &&
|
|
214
|
-
value.tracks.every((track) => isObject(track) && isInteger(track.track, 1, 255) && isSectorList(track.freeSectors) && isSectorList(track.usedSectors))
|
|
216
|
+
value.tracks.every((track) => isObject(track) && isInteger(track.track, 1, 255) && isSectorList(track.freeSectors) && isSectorList(track.usedSectors)) &&
|
|
217
|
+
Array.isArray(value.unreferenced) &&
|
|
218
|
+
value.unreferenced.every(isTrackSector)
|
|
215
219
|
);
|
|
216
220
|
case "entry":
|
|
217
221
|
return (
|
|
@@ -233,7 +237,8 @@ function isPetcatResult(value: Fields): boolean {
|
|
|
233
237
|
isObject(handoff) &&
|
|
234
238
|
isInteger(handoff.line, 0, 0xffff) &&
|
|
235
239
|
((handoff.kind === "sys" && isInteger(handoff.address, 0, 0xffff) && handoff.computed === undefined) ||
|
|
236
|
-
(
|
|
240
|
+
(handoff.kind === "sys" && handoff.computed === true && handoff.address === undefined && typeof handoff.expression === "string") ||
|
|
241
|
+
(handoff.kind === "usr" && handoff.computed === true && handoff.address === undefined));
|
|
237
242
|
return (
|
|
238
243
|
value.decoded === true &&
|
|
239
244
|
isInteger(value.loadAddress, 0, 0xffff) &&
|