c64bridge 0.7.4 → 0.8.0
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/.github/prompts/README.md +14 -4
- package/.github/prompts/assembly-routine-session.prompt.md +9 -9
- package/.github/prompts/basic-program-session.prompt.md +11 -9
- package/.github/prompts/drive-management-session.prompt.md +9 -9
- package/.github/prompts/hello-world-session.prompt.md +17 -0
- package/.github/prompts/petscii-art-session.prompt.md +9 -9
- package/.github/prompts/play-classic-game.prompt.md +9 -9
- package/.github/prompts/pr-converge.prompt.md +13 -0
- package/.github/prompts/sid-music-session.prompt.md +9 -9
- package/AGENTS.md +59 -60
- package/README.md +550 -191
- package/data/assembly/assembly-spec.md +290 -1
- package/data/context/bootstrap.md +26 -2
- package/data/context/fast-paths.md +44 -0
- package/data/graphics/petscii-style-guide.md +6 -2
- package/data/graphics/sprite-charset-best-practices.md +5 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +990 -26
- package/dist/config.js +5 -1
- package/dist/device.js +680 -57
- package/dist/diagnostics.js +371 -0
- package/dist/mcp/metadata.js +39 -0
- package/dist/mcp-server.js +170 -20
- package/dist/platform.js +10 -2
- package/dist/prompts/registry.js +166 -309
- package/dist/rag/init.js +37 -1
- package/dist/rag/knowledgeIndex.js +18 -6
- package/dist/rag/rateLimiter.js +2 -2
- package/dist/streamCapture.js +135 -0
- package/dist/tools/audio.js +3 -1
- package/dist/tools/debug.js +470 -0
- package/dist/tools/developer.js +10 -4
- package/dist/tools/graphics.js +183 -17
- package/dist/tools/machineControl.js +11 -5
- package/dist/tools/memory.js +19 -10
- package/dist/tools/meta/audio.js +240 -1
- package/dist/tools/meta/config.js +20 -28
- package/dist/tools/meta/configInventory.js +104 -0
- package/dist/tools/meta/diagnostics.js +45 -2
- package/dist/tools/meta/graphics.js +14 -6
- package/dist/tools/meta/index.js +1 -0
- package/dist/tools/meta/memory.js +20 -6
- package/dist/tools/meta/program.js +473 -0
- package/dist/tools/pollValidator.js +4 -2
- package/dist/tools/programRunners.js +8 -4
- package/dist/tools/registry/config.js +17 -0
- package/dist/tools/registry/disk.js +7 -0
- package/dist/tools/registry/drive.js +3 -0
- package/dist/tools/registry/graphics.js +111 -27
- package/dist/tools/registry/index.js +6 -0
- package/dist/tools/registry/memory.js +1 -0
- package/dist/tools/registry/platform.js +121 -0
- package/dist/tools/registry/program.js +26 -8
- package/dist/tools/registry/sound.js +96 -0
- package/dist/tools/registry/system.js +15 -2
- package/dist/tools/storage.js +1 -0
- package/dist/tools/streaming.js +1 -0
- package/dist/tools/types.js +38 -4
- package/dist/tools/vice.js +160 -0
- package/dist/vicBitmap.js +317 -0
- package/dist/vice/mockServer.js +439 -0
- package/dist/vice/process.js +310 -0
- package/dist/vice/readiness.js +18 -2
- package/dist/vice/viceClient.js +382 -22
- package/doc/developer.md +62 -24
- package/doc/img/backend-switch/hello-c64u.png +0 -0
- package/doc/img/backend-switch/hello-vice-and-c64u.png +0 -0
- package/doc/img/backend-switch/hello-vice.png +0 -0
- package/doc/img/backend-switch/metadata.json +50 -0
- package/doc/plans/vice-backend-switch/plan.md +486 -0
- package/doc/plans/vice-backend-switch-2/plan.md +48 -0
- package/doc/plans/vice-support/notes.md +1 -0
- package/doc/plans/vice-support/plan.md +5 -0
- package/doc/plans/vice-support/rollout.md +23 -17
- package/doc/reviews/vice-support-review.md +185 -0
- package/doc/troubleshooting-mcp.md +35 -6
- package/doc/vice/support-matrix.md +54 -0
- package/mcp/prompts.json +292 -0
- package/mcp/protocol-examples.json +46 -0
- package/mcp/resources.json +940 -0
- package/mcp/schemas/c64_config.schema.json +350 -0
- package/mcp/schemas/c64_debug.schema.json +380 -0
- package/mcp/schemas/c64_disk.schema.json +278 -0
- package/mcp/schemas/c64_drive.schema.json +120 -0
- package/mcp/schemas/c64_extract.schema.json +302 -0
- package/mcp/schemas/c64_graphics.schema.json +333 -0
- package/mcp/schemas/c64_memory.schema.json +155 -0
- package/mcp/schemas/c64_printer.schema.json +218 -0
- package/mcp/schemas/c64_program.schema.json +391 -0
- package/mcp/schemas/c64_rag.schema.json +67 -0
- package/mcp/schemas/c64_select_backend.schema.json +32 -0
- package/mcp/schemas/c64_sound.schema.json +644 -0
- package/mcp/schemas/c64_stream.schema.json +61 -0
- package/mcp/schemas/c64_system.schema.json +227 -0
- package/mcp/schemas/c64_vice.schema.json +63 -0
- package/mcp/server.json +15 -0
- package/mcp/tools.json +4517 -0
- package/mcp.json +83 -9
- package/package.json +8 -3
- package/scripts/capture-backend-switch-screenshots.mjs +276 -0
- package/scripts/generate-mcp-interface.ts +289 -0
- package/scripts/invoke-bun.mjs +67 -21
- package/scripts/mockC64Server.mjs +162 -6
- package/scripts/run-coverage.mjs +284 -0
- package/scripts/run-tests.mjs +95 -39
- package/scripts/run-tests.ts +378 -73
- package/scripts/update-readme.ts +147 -13
package/scripts/run-tests.ts
CHANGED
|
@@ -3,84 +3,389 @@ import path from "node:path";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import process from "node:process";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
7
8
|
|
|
8
9
|
const DEFAULT_TARGET = "mock";
|
|
10
|
+
const DEFAULT_PLATFORM = "c64u";
|
|
11
|
+
const DEFAULT_BUN_FILE_LIMIT = 4;
|
|
12
|
+
const DEFAULT_BUN_BATCH_SIZE = 12;
|
|
13
|
+
const ISOLATED_BUN_TEST_FILES = new Set([
|
|
14
|
+
"test/audioRuntime.test.mjs",
|
|
15
|
+
]);
|
|
9
16
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
17
|
const defaultEmbeddingsDir = path.join(repoRoot, "artifacts", "test-embeddings");
|
|
18
|
+
const defaultTestFiles = listRepoTestFiles(path.join(repoRoot, "test"));
|
|
11
19
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (!
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
]
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
20
|
+
function resolveNodeExecutable(): string {
|
|
21
|
+
const candidates = [
|
|
22
|
+
process.env.C64BRIDGE_TEST_NODE_BIN,
|
|
23
|
+
process.env.C64BRIDGE_NODE_BIN,
|
|
24
|
+
process.env.NODE_BINARY,
|
|
25
|
+
process.env.NODE_EXEC_PATH,
|
|
26
|
+
process.env.npm_node_execpath,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
for (const candidate of candidates) {
|
|
30
|
+
if (candidate && typeof candidate === "string" && candidate.trim()) {
|
|
31
|
+
return candidate.trim();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return "node";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type RunTestsArgs = {
|
|
39
|
+
target: string;
|
|
40
|
+
platform: "c64u" | "vice";
|
|
41
|
+
explicitBaseUrl: string | null;
|
|
42
|
+
runCoverage: boolean;
|
|
43
|
+
passthrough: string[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function parseRunTestsArgs(args: string[]): RunTestsArgs {
|
|
47
|
+
let target = DEFAULT_TARGET;
|
|
48
|
+
let platform: "c64u" | "vice" = DEFAULT_PLATFORM;
|
|
49
|
+
let explicitBaseUrl: string | null = null;
|
|
50
|
+
let runCoverage = false;
|
|
51
|
+
const passthrough: string[] = [];
|
|
52
|
+
|
|
53
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
54
|
+
const arg = args[index];
|
|
55
|
+
if (!arg || !arg.trim()) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (arg === "--mock") {
|
|
59
|
+
target = "mock";
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (arg === "--real") {
|
|
63
|
+
target = "device";
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (arg === "--platform" && index + 1 < args.length) {
|
|
67
|
+
platform = normalizePlatform(args[index + 1]);
|
|
68
|
+
index += 1;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (arg.startsWith("--platform=")) {
|
|
72
|
+
platform = normalizePlatform(arg.split("=", 2)[1] ?? DEFAULT_PLATFORM);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (arg.startsWith("--target=")) {
|
|
76
|
+
target = arg.split("=", 2)[1] ?? DEFAULT_TARGET;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (arg === "--target" && index + 1 < args.length) {
|
|
80
|
+
target = args[index + 1] ?? DEFAULT_TARGET;
|
|
81
|
+
index += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (arg.startsWith("--base-url=")) {
|
|
85
|
+
explicitBaseUrl = arg.split("=", 2)[1] ?? null;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (arg === "--coverage") {
|
|
89
|
+
runCoverage = true;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
passthrough.push(arg);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { target, platform, explicitBaseUrl, runCoverage, passthrough };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function shouldUseNodeFallback(runCoverage: boolean, passthrough: string[], env: NodeJS.ProcessEnv = process.env): boolean {
|
|
99
|
+
const requestedRunner = String(env.C64BRIDGE_TEST_RUNNER ?? "").trim().toLowerCase();
|
|
100
|
+
if (requestedRunner === "bun") {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
if (requestedRunner === "node") {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
if (runCoverage) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const explicitFiles = passthrough.filter(looksLikeTestFileArg);
|
|
111
|
+
if (passthrough.length === 0) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const maxBunFiles = resolveBunFileLimit(env.C64BRIDGE_BUN_FILE_LIMIT);
|
|
116
|
+
return explicitFiles.length > maxBunFiles;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function buildBunTestBatches(passthrough: string[], env: NodeJS.ProcessEnv = process.env): string[][] {
|
|
120
|
+
const explicitFiles = passthrough.filter(looksLikeTestFileArg);
|
|
121
|
+
if (passthrough.length === 0) {
|
|
122
|
+
const sharedFiles = defaultTestFiles.filter((file) => !ISOLATED_BUN_TEST_FILES.has(file));
|
|
123
|
+
return [
|
|
124
|
+
...chunkFiles(sharedFiles, resolveBunBatchSize(env.C64BRIDGE_BUN_BATCH_SIZE)),
|
|
125
|
+
...[...ISOLATED_BUN_TEST_FILES].filter((file) => defaultTestFiles.includes(file)).map((file) => [file]),
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
if (explicitFiles.length === 0) {
|
|
129
|
+
return [passthrough];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const batchSize = resolveBunBatchSize(env.C64BRIDGE_BUN_BATCH_SIZE);
|
|
133
|
+
const sharedArgs = passthrough.filter((arg) => !looksLikeTestFileArg(arg));
|
|
134
|
+
const sharedFiles = explicitFiles.filter((file) => !ISOLATED_BUN_TEST_FILES.has(file));
|
|
135
|
+
const isolatedFiles = explicitFiles.filter((file) => ISOLATED_BUN_TEST_FILES.has(file));
|
|
136
|
+
return [
|
|
137
|
+
...chunkFiles(sharedFiles, batchSize).map((files) => [...files, ...sharedArgs]),
|
|
138
|
+
...isolatedFiles.map((file) => [file, ...sharedArgs]),
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function runNodeFallback(target: string, explicitBaseUrl: string | null, passthrough: string[], env: Record<string, string>, runCoverage: boolean): Promise<number> {
|
|
143
|
+
console.warn("[run-tests] Using Node runner for this test set to avoid Bun memory growth on broad suites");
|
|
144
|
+
if (runCoverage) {
|
|
145
|
+
console.warn("[run-tests] Coverage reporting is unavailable in Node fallback mode");
|
|
146
|
+
}
|
|
147
|
+
const nodeExecutable = resolveNodeExecutable();
|
|
148
|
+
const nodeScript = path.join(repoRoot, "scripts", "run-tests.mjs");
|
|
149
|
+
const fallbackArgs = [] as string[];
|
|
150
|
+
if (target !== DEFAULT_TARGET) {
|
|
151
|
+
fallbackArgs.push(`--target=${target}`);
|
|
152
|
+
}
|
|
153
|
+
if (explicitBaseUrl) {
|
|
154
|
+
fallbackArgs.push(`--base-url=${explicitBaseUrl}`);
|
|
155
|
+
}
|
|
156
|
+
fallbackArgs.push(...passthrough);
|
|
157
|
+
return await new Promise<number>((resolve) => {
|
|
158
|
+
const childProcess = spawn(nodeExecutable, [nodeScript, ...fallbackArgs], {
|
|
159
|
+
cwd: repoRoot,
|
|
160
|
+
env,
|
|
161
|
+
stdio: "inherit",
|
|
162
|
+
}) as unknown as {
|
|
163
|
+
on(event: "error", listener: (error: Error) => void): void;
|
|
164
|
+
on(event: "exit", listener: (code: number | null) => void): void;
|
|
165
|
+
};
|
|
166
|
+
childProcess.on("error", (error) => {
|
|
167
|
+
console.error("[run-tests] Failed to launch Node fallback:", error);
|
|
168
|
+
resolve(1);
|
|
169
|
+
});
|
|
170
|
+
childProcess.on("exit", (code) => {
|
|
171
|
+
resolve(typeof code === "number" ? code : 1);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function runExternalCommand(command: string, args: string[], env: Record<string, string>): Promise<number> {
|
|
177
|
+
return await new Promise<number>((resolve) => {
|
|
178
|
+
const childProcess = spawn(command, args, {
|
|
179
|
+
cwd: repoRoot,
|
|
180
|
+
env,
|
|
181
|
+
stdio: "inherit",
|
|
182
|
+
}) as unknown as {
|
|
183
|
+
on(event: "error", listener: (error: Error) => void): void;
|
|
184
|
+
on(event: "exit", listener: (code: number | null) => void): void;
|
|
185
|
+
};
|
|
186
|
+
childProcess.on("error", (error) => {
|
|
187
|
+
console.error("[run-tests] Failed to launch test runner:", error);
|
|
188
|
+
resolve(1);
|
|
189
|
+
});
|
|
190
|
+
childProcess.on("exit", (code) => {
|
|
191
|
+
resolve(typeof code === "number" ? code : 1);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function main(): Promise<number> {
|
|
197
|
+
const { target, platform, explicitBaseUrl, runCoverage, passthrough } = parseRunTestsArgs(process.argv.slice(2));
|
|
198
|
+
|
|
199
|
+
if (!fs.existsSync(defaultEmbeddingsDir)) {
|
|
200
|
+
fs.mkdirSync(defaultEmbeddingsDir, { recursive: true });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const env: Record<string, string> = { ...process.env } as Record<string, string>;
|
|
204
|
+
const normalizedTarget = normalizeTarget(target);
|
|
205
|
+
env.C64_MODE = platform;
|
|
206
|
+
env.C64_TEST_TARGET = normalizedTarget === "device" ? "real" : "mock";
|
|
207
|
+
if (platform === "vice") {
|
|
208
|
+
env.VICE_TEST_TARGET = normalizedTarget === "device" ? "vice" : "mock";
|
|
209
|
+
} else {
|
|
210
|
+
delete env.VICE_TEST_TARGET;
|
|
211
|
+
}
|
|
212
|
+
if (!env.RAG_EMBEDDINGS_DIR) {
|
|
213
|
+
env.RAG_EMBEDDINGS_DIR = defaultEmbeddingsDir;
|
|
214
|
+
}
|
|
215
|
+
if (explicitBaseUrl) {
|
|
216
|
+
env.C64_TEST_BASE_URL = explicitBaseUrl;
|
|
217
|
+
}
|
|
218
|
+
if (platform === "c64u" && normalizedTarget === "device" && !env.C64_TEST_BASE_URL) {
|
|
219
|
+
env.C64_TEST_BASE_URL = resolveBaseUrlFromConfig(env) ?? "http://c64u";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
printMatrixHeading({
|
|
223
|
+
platform,
|
|
224
|
+
target: normalizedTarget,
|
|
225
|
+
coverage: runCoverage,
|
|
226
|
+
baseUrl: env.C64_TEST_BASE_URL ?? null,
|
|
227
|
+
passthrough,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const bunRuntime = (globalThis as { Bun?: unknown }).Bun;
|
|
231
|
+
if (!bunRuntime) {
|
|
232
|
+
console.warn("[run-tests] Bun runtime not detected; falling back to Node runner");
|
|
233
|
+
return await runNodeFallback(target, explicitBaseUrl, passthrough, env, runCoverage);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!runCoverage && passthrough.length === 0) {
|
|
237
|
+
return await runBunBatches(env, buildBunTestBatches([], env), {
|
|
238
|
+
coverage: false,
|
|
239
|
+
labelPrefix: "default-suite",
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (shouldUseNodeFallback(runCoverage, passthrough, env)) {
|
|
244
|
+
if (!runCoverage) {
|
|
245
|
+
return await runBunBatches(env, buildBunTestBatches(passthrough, env), {
|
|
246
|
+
coverage: false,
|
|
247
|
+
labelPrefix: "sharded-suite",
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return await runNodeFallback(target, explicitBaseUrl, passthrough, env, runCoverage);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const bunArgs = [
|
|
254
|
+
"test",
|
|
255
|
+
...(runCoverage
|
|
256
|
+
? [
|
|
257
|
+
"--coverage",
|
|
258
|
+
"--coverage-reporter=lcov",
|
|
259
|
+
"--coverage-reporter=text",
|
|
260
|
+
]
|
|
261
|
+
: []),
|
|
262
|
+
...(passthrough.length > 0 ? passthrough : defaultTestFiles),
|
|
263
|
+
];
|
|
264
|
+
return await runExternalCommand(process.execPath, bunArgs, env);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (import.meta.main) {
|
|
268
|
+
process.exit(await main());
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
type MatrixHeadingOptions = {
|
|
272
|
+
platform: "c64u" | "vice";
|
|
273
|
+
target: "mock" | "device";
|
|
274
|
+
coverage: boolean;
|
|
275
|
+
baseUrl: string | null;
|
|
276
|
+
passthrough: string[];
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
function printMatrixHeading(options: MatrixHeadingOptions): void {
|
|
280
|
+
const { platform, target, coverage, baseUrl, passthrough } = options;
|
|
281
|
+
const useColor = Boolean(process.stdout.isTTY) && process.env.NO_COLOR !== "1";
|
|
282
|
+
const color = (code: string): string => (useColor ? `\x1b[${code}m` : "");
|
|
283
|
+
const reset = useColor ? "\x1b[0m" : "";
|
|
284
|
+
const bold = useColor ? "\x1b[1m" : "";
|
|
285
|
+
|
|
286
|
+
const platformColor = platform === "vice" ? color("34") : color("36");
|
|
287
|
+
const targetColor = target === "device" ? color("33") : color("32");
|
|
288
|
+
const coverageColor = coverage ? color("31") : color("90");
|
|
289
|
+
const labelColor = color("90");
|
|
290
|
+
|
|
291
|
+
const header = `${bold}${color("97")}=== test-matrix run ===${reset}`;
|
|
292
|
+
const platformLine = `${labelColor}platform:${reset} ${platformColor}${platform}${reset}`;
|
|
293
|
+
const targetLine = `${labelColor}target:${reset} ${targetColor}${target}${reset}`;
|
|
294
|
+
const coverageLine = `${labelColor}coverage:${reset} ${coverageColor}${coverage ? "enabled" : "disabled"}${reset}`;
|
|
295
|
+
|
|
296
|
+
console.log("\n" + header);
|
|
297
|
+
console.log(`${platformLine} ${targetLine} ${coverageLine}`);
|
|
298
|
+
if (baseUrl) {
|
|
299
|
+
console.log(`${labelColor}base-url:${reset} ${baseUrl}`);
|
|
300
|
+
}
|
|
301
|
+
if (passthrough.length > 0) {
|
|
302
|
+
console.log(`${labelColor}extra args:${reset} ${passthrough.join(" ")}`);
|
|
303
|
+
}
|
|
304
|
+
console.log("");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function normalizePlatform(value: string): "c64u" | "vice" {
|
|
308
|
+
const lower = (value ?? "").toLowerCase();
|
|
309
|
+
return lower === "vice" ? "vice" : "c64u";
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function normalizeTarget(value: string): "mock" | "device" {
|
|
313
|
+
const lower = (value ?? "").toLowerCase();
|
|
314
|
+
if (lower === "real" || lower === "device" || lower === "hardware" || lower === "vice") {
|
|
315
|
+
return "device";
|
|
316
|
+
}
|
|
317
|
+
return "mock";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function resolveBunFileLimit(raw: string | undefined): number {
|
|
321
|
+
const parsed = Number(raw ?? "");
|
|
322
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_BUN_FILE_LIMIT;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function resolveBunBatchSize(raw: string | undefined): number {
|
|
326
|
+
const parsed = Number(raw ?? "");
|
|
327
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_BUN_BATCH_SIZE;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function looksLikeTestFileArg(arg: string): boolean {
|
|
331
|
+
return /^test\/.*\.test\.(mjs|ts)$/i.test(arg) || /\.test\.(mjs|ts)$/i.test(arg);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function chunkFiles(files: string[], chunkSize: number): string[][] {
|
|
335
|
+
if (files.length === 0) {
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
const chunks: string[][] = [];
|
|
339
|
+
for (let index = 0; index < files.length; index += chunkSize) {
|
|
340
|
+
chunks.push(files.slice(index, index + chunkSize));
|
|
341
|
+
}
|
|
342
|
+
return chunks;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function runBunBatches(
|
|
346
|
+
env: Record<string, string>,
|
|
347
|
+
batches: string[][],
|
|
348
|
+
options: { coverage: boolean; labelPrefix: string },
|
|
349
|
+
): Promise<number> {
|
|
350
|
+
const coverageArgs = options.coverage
|
|
351
|
+
? ["--coverage", "--coverage-reporter=lcov", "--coverage-reporter=text"]
|
|
352
|
+
: [];
|
|
353
|
+
|
|
354
|
+
for (let index = 0; index < batches.length; index += 1) {
|
|
355
|
+
const batch = batches[index] ?? [];
|
|
356
|
+
console.log(`[run-tests] ${options.labelPrefix} batch ${index + 1}/${batches.length} (${batch.length} entries)`);
|
|
357
|
+
const exitCode = await runExternalCommand(process.execPath, ["test", ...coverageArgs, ...batch], env);
|
|
358
|
+
if (exitCode !== 0) {
|
|
359
|
+
return exitCode;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return 0;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function listRepoTestFiles(testRoot: string): string[] {
|
|
367
|
+
const files: string[] = [];
|
|
368
|
+
|
|
369
|
+
function walk(dir: string): void {
|
|
370
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
371
|
+
for (const entry of entries) {
|
|
372
|
+
const fullPath = path.join(dir, entry.name);
|
|
373
|
+
if (entry.isDirectory()) {
|
|
374
|
+
walk(fullPath);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (entry.isFile() && /\.test\.(mjs|ts)$/i.test(entry.name)) {
|
|
378
|
+
files.push(path.relative(repoRoot, fullPath));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
walk(testRoot);
|
|
384
|
+
files.sort((a, b) => a.localeCompare(b));
|
|
385
|
+
return files;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function resolveBaseUrlFromConfig(env: Record<string, string>): string | null {
|
|
84
389
|
const configPathEnv = env.C64BRIDGE_CONFIG;
|
|
85
390
|
const homeConfig = os.homedir() ? path.join(os.homedir(), ".c64bridge.json") : null;
|
|
86
391
|
const repoConfig = path.join(repoRoot, ".c64bridge.json");
|
|
@@ -150,7 +455,7 @@ function normalizeBase(input: string): string {
|
|
|
150
455
|
return /^[a-z][a-z0-9+.-]*:\/\//i.test(input) ? input.replace(/\/+$/, "") : `http://${input}`;
|
|
151
456
|
}
|
|
152
457
|
|
|
153
|
-
function parseEndpoint(value: string | null): { hostname?: string; port?: number } {
|
|
458
|
+
function parseEndpoint(value: string | null | undefined): { hostname?: string; port?: number } {
|
|
154
459
|
const input = value && value.trim() ? value.trim() : null;
|
|
155
460
|
if (!input) return {};
|
|
156
461
|
try {
|
package/scripts/update-readme.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
4
|
import { dirname, join, resolve as resolvePath } from "node:path";
|
|
4
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -9,23 +10,134 @@ import { createPromptRegistry } from "../src/prompts/registry.js";
|
|
|
9
10
|
|
|
10
11
|
const START_MARKER = "<!-- AUTO-GENERATED:MCP-DOCS-START -->";
|
|
11
12
|
const END_MARKER = "<!-- AUTO-GENERATED:MCP-DOCS-END -->";
|
|
13
|
+
const ENV_START_MARKER = "<!-- AUTO-GENERATED:ENV-VARS-START -->";
|
|
14
|
+
const ENV_END_MARKER = "<!-- AUTO-GENERATED:ENV-VARS-END -->";
|
|
12
15
|
|
|
13
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
17
|
const __dirname = dirname(__filename);
|
|
15
18
|
const PROJECT_ROOT = join(__dirname, "..");
|
|
16
19
|
const README_PATH = join(PROJECT_ROOT, "README.md");
|
|
20
|
+
const MCP_MANIFEST_PATH = join(PROJECT_ROOT, "mcp.json");
|
|
21
|
+
|
|
22
|
+
type ManifestEnvEntry = {
|
|
23
|
+
readonly description: string;
|
|
24
|
+
readonly default?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type EnvCategory =
|
|
28
|
+
| "Server Runtime"
|
|
29
|
+
| "C64 Ultimate"
|
|
30
|
+
| "VICE Runtime"
|
|
31
|
+
| "VICE Audio Capture"
|
|
32
|
+
| "SID Playback"
|
|
33
|
+
| "RAG"
|
|
34
|
+
| "Testing";
|
|
17
35
|
|
|
18
36
|
function escapeCell(value: string): string {
|
|
19
37
|
return value.replace(/\|/g, "\\|").replace(/\r?\n|\r/g, " ").trim();
|
|
20
38
|
}
|
|
21
39
|
|
|
22
|
-
function renderTable(headers: readonly string[], rows: readonly (readonly string[])[]): string {
|
|
40
|
+
export function renderTable(headers: readonly string[], rows: readonly (readonly string[])[]): string {
|
|
23
41
|
const headerLine = `| ${headers.join(" | ")} |`;
|
|
24
42
|
const separator = `| ${headers.map(() => "---").join(" | ")} |`;
|
|
25
43
|
const body = rows.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
26
44
|
return [headerLine, separator, body].filter(Boolean).join("\n");
|
|
27
45
|
}
|
|
28
46
|
|
|
47
|
+
function loadManifestEnv(): Readonly<Record<string, ManifestEnvEntry>> {
|
|
48
|
+
const manifest = JSON.parse(readFileSync(MCP_MANIFEST_PATH, "utf8")) as {
|
|
49
|
+
env?: Record<string, ManifestEnvEntry>;
|
|
50
|
+
};
|
|
51
|
+
return manifest.env ?? {};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function classifyEnvVariable(name: string): EnvCategory {
|
|
55
|
+
if (name.startsWith("C64U_")) {
|
|
56
|
+
return "C64 Ultimate";
|
|
57
|
+
}
|
|
58
|
+
if (name.startsWith("VICE_") || name === "FORCE_XVFB" || name === "DISABLE_XVFB") {
|
|
59
|
+
if (name === "VICE_MODE" || name === "VICE_LIMIT_CYCLES" || name === "VICE_RUN_TIMEOUT_MS") {
|
|
60
|
+
return "VICE Audio Capture";
|
|
61
|
+
}
|
|
62
|
+
return "VICE Runtime";
|
|
63
|
+
}
|
|
64
|
+
if (name.startsWith("SIDPLAY")) {
|
|
65
|
+
return "SID Playback";
|
|
66
|
+
}
|
|
67
|
+
if (name.startsWith("RAG_") || name === "GITHUB_TOKEN") {
|
|
68
|
+
return "RAG";
|
|
69
|
+
}
|
|
70
|
+
if (name === "C64_TEST_TARGET") {
|
|
71
|
+
return "Testing";
|
|
72
|
+
}
|
|
73
|
+
return "Server Runtime";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveJsonConfigKey(name: string): string {
|
|
77
|
+
const map: Record<string, string> = {
|
|
78
|
+
C64BRIDGE_CONFIG: "config path",
|
|
79
|
+
C64U_HOST: "c64u.host",
|
|
80
|
+
C64U_PORT: "c64u.port",
|
|
81
|
+
C64U_PASSWORD: "c64u.networkPassword",
|
|
82
|
+
VICE_BINARY: "vice.exe",
|
|
83
|
+
VICE_DIRECTORY: "vice.directory",
|
|
84
|
+
VICE_HOST: "vice.host",
|
|
85
|
+
VICE_PORT: "vice.port",
|
|
86
|
+
VICE_VISIBLE: "vice.visible",
|
|
87
|
+
VICE_WARP: "vice.warp",
|
|
88
|
+
VICE_ARGS: "vice.args",
|
|
89
|
+
};
|
|
90
|
+
return map[name] ?? "—";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function renderEnvironmentSection(envEntries: Readonly<Record<string, ManifestEnvEntry>> = loadManifestEnv()): string[] {
|
|
94
|
+
const grouped = new Map<EnvCategory, Array<[string, ManifestEnvEntry]>>();
|
|
95
|
+
|
|
96
|
+
for (const entry of Object.entries(envEntries).sort(([left], [right]) => left.localeCompare(right))) {
|
|
97
|
+
const category = classifyEnvVariable(entry[0]);
|
|
98
|
+
const rows = grouped.get(category) ?? [];
|
|
99
|
+
rows.push(entry);
|
|
100
|
+
grouped.set(category, rows);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const orderedCategories: readonly EnvCategory[] = [
|
|
104
|
+
"Server Runtime",
|
|
105
|
+
"C64 Ultimate",
|
|
106
|
+
"VICE Runtime",
|
|
107
|
+
"VICE Audio Capture",
|
|
108
|
+
"SID Playback",
|
|
109
|
+
"RAG",
|
|
110
|
+
"Testing",
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const lines: string[] = [
|
|
114
|
+
"Every runtime environment variable documented in `mcp.json` can be set in your MCP client configuration, including `.vscode/mcp.json` under `servers.c64bridge.env`.",
|
|
115
|
+
"",
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
for (const category of orderedCategories) {
|
|
119
|
+
const entries = grouped.get(category);
|
|
120
|
+
if (!entries || entries.length === 0) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
lines.push(`#### ${category}`);
|
|
125
|
+
lines.push("");
|
|
126
|
+
lines.push(renderTable(
|
|
127
|
+
["Variable", "Default", "JSON Config Key", "Description"],
|
|
128
|
+
entries.map(([name, spec]) => [
|
|
129
|
+
`\`${name}\``,
|
|
130
|
+
escapeCell(spec.default ?? "—"),
|
|
131
|
+
escapeCell(resolveJsonConfigKey(name)),
|
|
132
|
+
escapeCell(spec.description),
|
|
133
|
+
]),
|
|
134
|
+
));
|
|
135
|
+
lines.push("");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return lines;
|
|
139
|
+
}
|
|
140
|
+
|
|
29
141
|
type GroupedOperation = {
|
|
30
142
|
readonly op: string;
|
|
31
143
|
readonly description: string;
|
|
@@ -138,17 +250,24 @@ export function renderToolsSection(modules: readonly ToolModuleDescriptor[] = de
|
|
|
138
250
|
if (operations.length === 0) {
|
|
139
251
|
lines.push("_No operations defined._");
|
|
140
252
|
} else {
|
|
253
|
+
const toolPlatforms: readonly string[] = tool.metadata.platforms ?? ["c64u"];
|
|
254
|
+
const opPlatformOverrides: Record<string, readonly string[]> = tool.metadata.operationPlatforms ?? {};
|
|
141
255
|
const operationRows = operations
|
|
142
256
|
.slice()
|
|
143
257
|
.sort((a, b) => a.op.localeCompare(b.op))
|
|
144
|
-
.map((operation) =>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
258
|
+
.map((operation) => {
|
|
259
|
+
const effectivePlatforms = opPlatformOverrides[operation.op] ?? toolPlatforms;
|
|
260
|
+
return [
|
|
261
|
+
`\`${operation.op}\``,
|
|
262
|
+
escapeCell(operation.description),
|
|
263
|
+
escapeCell(toTableValue(operation.required.map((name) => `\`${name}\``))),
|
|
264
|
+
operation.notes.length ? escapeCell(operation.notes.join(", ")) : "—",
|
|
265
|
+
effectivePlatforms.includes("c64u") ? "✅" : "",
|
|
266
|
+
effectivePlatforms.includes("vice") ? "✅" : "",
|
|
267
|
+
];
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
lines.push(renderTable(["Operation", "Description", "Required Inputs", "Notes", "C64U", "VICE"], operationRows));
|
|
152
271
|
}
|
|
153
272
|
|
|
154
273
|
lines.push("");
|
|
@@ -222,19 +341,34 @@ export function buildDocumentation(): string {
|
|
|
222
341
|
return sections.flat().join("\n").trim();
|
|
223
342
|
}
|
|
224
343
|
|
|
344
|
+
export function buildEnvironmentDocumentation(): string {
|
|
345
|
+
return renderEnvironmentSection().join("\n").trim();
|
|
346
|
+
}
|
|
347
|
+
|
|
225
348
|
export async function updateReadme(): Promise<boolean> {
|
|
226
349
|
const readme = await readFile(README_PATH, "utf8");
|
|
227
|
-
const
|
|
350
|
+
const docsPattern = new RegExp(
|
|
228
351
|
`${START_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([\\s\\S]*?)${END_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`,
|
|
229
352
|
);
|
|
230
|
-
|
|
353
|
+
const envPattern = new RegExp(
|
|
354
|
+
`${ENV_START_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([\\s\\S]*?)${ENV_END_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`,
|
|
355
|
+
);
|
|
356
|
+
if (!docsPattern.test(readme)) {
|
|
231
357
|
throw new Error(
|
|
232
358
|
`Could not find auto-generated section markers (${START_MARKER} / ${END_MARKER}) in README.md`,
|
|
233
359
|
);
|
|
234
360
|
}
|
|
361
|
+
if (!envPattern.test(readme)) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`Could not find auto-generated section markers (${ENV_START_MARKER} / ${ENV_END_MARKER}) in README.md`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
235
366
|
|
|
236
|
-
const
|
|
237
|
-
const
|
|
367
|
+
const generatedDocs = `\n\n${buildDocumentation()}\n\n`;
|
|
368
|
+
const generatedEnv = `\n\n${buildEnvironmentDocumentation()}\n\n`;
|
|
369
|
+
const nextReadme = readme
|
|
370
|
+
.replace(docsPattern, `${START_MARKER}${generatedDocs}${END_MARKER}`)
|
|
371
|
+
.replace(envPattern, `${ENV_START_MARKER}${generatedEnv}${ENV_END_MARKER}`);
|
|
238
372
|
|
|
239
373
|
if (nextReadme === readme) {
|
|
240
374
|
return false;
|