c64bridge 0.8.1 → 0.9.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/steer.prompt.md +55 -0
- package/AGENTS.md +9 -7
- package/README.md +139 -95
- package/data/embeddings_asm.json +1 -1
- package/data/embeddings_basic.json +1 -1
- package/data/embeddings_hardware.json +1 -1
- package/data/embeddings_mixed.json +1 -1
- package/data/embeddings_other.json +1 -1
- package/data/graphics/petscii-style-guide.md +4 -4
- package/data/graphics/sprite-charset-best-practices.md +9 -9
- package/data/vice/vice-binary-monitor-spec.md +921 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/device.js +86 -10
- package/dist/mcp-server.js +1 -1
- package/dist/prompts/registry.js +27 -25
- package/dist/rag/knowledgeIndex.js +112 -79
- package/dist/rag/resourceUris.js +93 -0
- package/dist/tools/audio.js +11 -11
- package/dist/tools/debug.js +1 -1
- package/dist/tools/developer.js +13 -13
- package/dist/tools/graphics.js +10 -10
- package/dist/tools/machineControl.js +5 -5
- package/dist/tools/memory.js +19 -11
- package/dist/tools/meta/index.js +1 -1
- package/dist/tools/printer.js +11 -12
- package/dist/tools/programRunners.js +16 -16
- package/dist/tools/rag.js +9 -9
- package/dist/tools/registry/config.js +1 -1
- package/dist/tools/registry/disk.js +1 -1
- package/dist/tools/registry/drive.js +1 -1
- package/dist/tools/registry/extract.js +1 -1
- package/dist/tools/registry/graphics.js +1 -1
- package/dist/tools/registry/memory.js +1 -1
- package/dist/tools/registry/platform.js +1 -1
- package/dist/tools/registry/printer.js +1 -1
- package/dist/tools/registry/program.js +1 -1
- package/dist/tools/registry/rag.js +1 -1
- package/dist/tools/registry/sound.js +1 -1
- package/dist/tools/registry/stream.js +1 -1
- package/dist/tools/registry/system.js +1 -1
- package/dist/tools/storage.js +15 -15
- package/dist/tools/streaming.js +3 -3
- package/dist/tools/types.js +41 -4
- package/dist/tools/vice.js +2 -5
- package/dist/vice/process.js +79 -2
- package/dist/vice/readiness.js +35 -0
- package/dist/vice/smokeOptions.js +44 -0
- package/dist/viceRunner.js +2 -0
- package/doc/plans/prompts/README.md +7 -7
- package/mcp/prompts.json +42 -59
- package/mcp/protocol-examples.json +2 -2
- package/mcp/resources.json +179 -166
- package/mcp/schemas/c64_config.schema.json +109 -326
- package/mcp/schemas/c64_debug.schema.json +158 -337
- package/mcp/schemas/c64_disk.schema.json +180 -262
- package/mcp/schemas/c64_drive.schema.json +31 -111
- package/mcp/schemas/c64_extract.schema.json +193 -291
- package/mcp/schemas/c64_graphics.schema.json +208 -308
- package/mcp/schemas/c64_memory.schema.json +94 -144
- package/mcp/schemas/c64_printer.schema.json +151 -205
- package/mcp/schemas/c64_program.schema.json +229 -367
- package/mcp/schemas/c64_rag.schema.json +25 -59
- package/mcp/schemas/c64_select_backend.schema.json +19 -26
- package/mcp/schemas/c64_sound.schema.json +346 -622
- package/mcp/schemas/c64_stream.schema.json +25 -54
- package/mcp/schemas/c64_system.schema.json +79 -212
- package/mcp/schemas/c64_vice.schema.json +24 -55
- package/mcp/server.json +1 -1
- package/mcp/tools.json +1935 -3443
- package/mcp.json +3 -3
- package/package.json +17 -14
- package/scripts/fetch-external-sources.mjs +1 -2
- package/scripts/run-coverage.mjs +26 -1
- package/scripts/run-tests.mjs +2 -2
- package/scripts/run-tests.ts +273 -52
- package/scripts/update-readme.ts +54 -2
- package/server.json +2 -2
package/mcp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c64bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Stdio-based Model Context Protocol (MCP) server that bridges Commodore 64 Ultimate hardware with AI agents.",
|
|
5
5
|
"entry": "dist/index.js",
|
|
6
6
|
"type": "stdio",
|
|
@@ -113,11 +113,11 @@
|
|
|
113
113
|
"default": "120000000"
|
|
114
114
|
},
|
|
115
115
|
"VICE_BINARY": {
|
|
116
|
-
"description": "VICE binary to launch for managed emulator sessions and audio capture",
|
|
116
|
+
"description": "VICE binary to launch for managed emulator sessions and audio capture; automatic search is used only when this override is missing or invalid",
|
|
117
117
|
"default": "x64sc"
|
|
118
118
|
},
|
|
119
119
|
"VICE_DIRECTORY": {
|
|
120
|
-
"description": "Override the VICE resource directory used for ROM and UI asset discovery",
|
|
120
|
+
"description": "Override the VICE resource directory used for ROM and UI asset discovery; automatic search is used only when this override is missing or invalid",
|
|
121
121
|
"default": "auto-detect"
|
|
122
122
|
},
|
|
123
123
|
"VICE_HOST": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c64bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"mcpName": "io.github.chrisgleissner/c64bridge",
|
|
5
5
|
"description": "C64 Bridge MCP server for Commodore 64 Ultimate hardware and VICE emulator control",
|
|
6
6
|
"type": "module",
|
|
@@ -22,9 +22,11 @@
|
|
|
22
22
|
"lint": "npm run check",
|
|
23
23
|
"test": "node scripts/invoke-bun.mjs scripts/run-tests.ts",
|
|
24
24
|
"test:node": "node scripts/run-tests.mjs",
|
|
25
|
+
"test:vice:mock": "npm run test -- --platform=vice --target=mock",
|
|
26
|
+
"test:vice:device": "FORCE_XVFB=1 VICE_VISIBLE=false VICE_WARP=true npm run test -- --platform=vice --target=device",
|
|
25
27
|
"coverage": "node scripts/run-coverage.mjs",
|
|
26
28
|
"coverage:single": "node scripts/invoke-bun.mjs scripts/run-tests.ts --coverage",
|
|
27
|
-
"test:matrix": "npm run test -- --platform=c64u --target=mock && npm run test
|
|
29
|
+
"test:matrix": "npm run test -- --platform=c64u --target=mock && npm run test:vice:mock && npm run test:vice:device",
|
|
28
30
|
"mcp:generate": "node scripts/invoke-bun.mjs scripts/generate-mcp-interface.ts",
|
|
29
31
|
"check:package": "node scripts/check-package.mjs",
|
|
30
32
|
"check:run-local": "bash scripts/run-mcp-check.sh local 5",
|
|
@@ -40,7 +42,8 @@
|
|
|
40
42
|
"changelog:generate": "node scripts/generate-changelog.mjs",
|
|
41
43
|
"release:prepare": "node scripts/prepare-release.mjs",
|
|
42
44
|
"compare:rest": "node scripts/compare-rest-apis.mjs",
|
|
43
|
-
"vice:smoke": "node scripts/invoke-bun.mjs test/vice/viceSmokeTest.ts"
|
|
45
|
+
"vice:smoke": "node scripts/invoke-bun.mjs test/vice/viceSmokeTest.ts",
|
|
46
|
+
"vice:smoke:visible": "node scripts/invoke-bun.mjs test/vice/viceSmokeTest.ts --visible-demo"
|
|
44
47
|
},
|
|
45
48
|
"keywords": [
|
|
46
49
|
"mcp",
|
|
@@ -59,24 +62,24 @@
|
|
|
59
62
|
"node": ">=24"
|
|
60
63
|
},
|
|
61
64
|
"dependencies": {
|
|
62
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
63
|
-
"axios": "^1.
|
|
65
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
66
|
+
"axios": "^1.16.0",
|
|
64
67
|
"date-fns": "^4.1.0",
|
|
65
|
-
"fastify": "^5.
|
|
66
|
-
"jimp": "^1.6.
|
|
67
|
-
"meyda": "^5.6.
|
|
68
|
-
"octokit": "^5.0.
|
|
69
|
-
"pitchfinder": "^2.
|
|
68
|
+
"fastify": "^5.8.5",
|
|
69
|
+
"jimp": "^1.6.1",
|
|
70
|
+
"meyda": "^5.6.3",
|
|
71
|
+
"octokit": "^5.0.5",
|
|
72
|
+
"pitchfinder": "^2.3.4",
|
|
70
73
|
"reflect-metadata": "^0.2.2",
|
|
71
|
-
"yaml": "^2.
|
|
74
|
+
"yaml": "^2.8.4"
|
|
72
75
|
},
|
|
73
76
|
"optionalDependencies": {
|
|
74
77
|
"naudiodon": "^2.3.6"
|
|
75
78
|
},
|
|
76
79
|
"devDependencies": {
|
|
77
|
-
"@types/node": "^
|
|
78
|
-
"
|
|
79
|
-
"typescript": "^
|
|
80
|
+
"@types/node": "^25.6.0",
|
|
81
|
+
"tsx": "^4.21.0",
|
|
82
|
+
"typescript": "^6.0.3"
|
|
80
83
|
},
|
|
81
84
|
"files": [
|
|
82
85
|
"mcp.json",
|
|
@@ -7,8 +7,7 @@ import { register } from 'node:module';
|
|
|
7
7
|
async function main() {
|
|
8
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const projectRoot = path.resolve(__dirname, '..');
|
|
10
|
-
|
|
11
|
-
register(tsNodeLoader, pathToFileURL(projectRoot));
|
|
10
|
+
register("tsx/esm", pathToFileURL(projectRoot));
|
|
12
11
|
const { fetchFromCsv } = await import('../src/rag/externalFetcher.ts');
|
|
13
12
|
// Resolve sources.csv from env, dist/rag, or src/rag (in that order)
|
|
14
13
|
let csvPath = process.env.RAG_SOURCES_CSV;
|
package/scripts/run-coverage.mjs
CHANGED
|
@@ -13,6 +13,22 @@ const runner = path.join(repoRoot, "scripts", "invoke-bun.mjs");
|
|
|
13
13
|
const configPath = path.join(repoRoot, ".c8rc.json");
|
|
14
14
|
const testRoot = path.join(repoRoot, "test");
|
|
15
15
|
const DEFAULT_COVERAGE_SHARD_SIZE = 12;
|
|
16
|
+
const DEFAULT_VICE_MOCK_TEST_FILES = [
|
|
17
|
+
"test/device.test.mjs",
|
|
18
|
+
"test/viceIntegration.test.mjs",
|
|
19
|
+
"test/viceModule.test.mjs",
|
|
20
|
+
"test/groupedToolsShims.test.mjs",
|
|
21
|
+
"test/toolsTypes.test.mjs",
|
|
22
|
+
"test/platformRegistry.test.mjs",
|
|
23
|
+
"test/meta/program.test.mjs",
|
|
24
|
+
"test/mcpServerIntegration.test.mjs",
|
|
25
|
+
"test/c64Client.test.mjs",
|
|
26
|
+
"test/vice/viceSmokeTest.ts",
|
|
27
|
+
];
|
|
28
|
+
const DEFAULT_VICE_DEVICE_TEST_FILES = [
|
|
29
|
+
"test/device.test.mjs",
|
|
30
|
+
"test/vice/viceSmokeTest.ts",
|
|
31
|
+
];
|
|
16
32
|
|
|
17
33
|
const legs = [
|
|
18
34
|
{ name: "c64u-mock", args: ["--platform=c64u", "--target=mock"] },
|
|
@@ -50,11 +66,20 @@ export async function main() {
|
|
|
50
66
|
const allTestFiles = await listRepoTestFiles(testRoot);
|
|
51
67
|
const isolatedTestSet = new Set(isolatedTests);
|
|
52
68
|
const defaultTestFiles = allTestFiles.filter((f) => !isolatedTestSet.has(f));
|
|
53
|
-
const coverageBatches = buildCoverageBatches(defaultTestFiles, [...supplementalTests, ...isolatedTests], process.env);
|
|
54
69
|
for (const leg of legs) {
|
|
55
70
|
const legDir = path.join(coverageDir, "matrix", leg.name);
|
|
56
71
|
await fs.mkdir(legDir, { recursive: true });
|
|
57
72
|
|
|
73
|
+
const legDefaultFiles = leg.name === "vice-mock"
|
|
74
|
+
? DEFAULT_VICE_MOCK_TEST_FILES
|
|
75
|
+
: leg.name === "vice-device"
|
|
76
|
+
? DEFAULT_VICE_DEVICE_TEST_FILES
|
|
77
|
+
: defaultTestFiles;
|
|
78
|
+
const legSupplementalTests = leg.name.startsWith("vice")
|
|
79
|
+
? []
|
|
80
|
+
: [...supplementalTests, ...isolatedTests];
|
|
81
|
+
const coverageBatches = buildCoverageBatches(legDefaultFiles, legSupplementalTests, process.env);
|
|
82
|
+
|
|
58
83
|
const reports = [];
|
|
59
84
|
for (const batch of coverageBatches) {
|
|
60
85
|
reports.push(await runCoverageLeg(leg, legDir, batch.label, batch.files));
|
package/scripts/run-tests.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
export const DEFAULT_TARGET = "mock";
|
|
9
9
|
|
|
10
10
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
-
const
|
|
11
|
+
const tsLoader = "tsx/esm";
|
|
12
12
|
const { nodeArgs, target, explicitBaseUrl } = buildNodeTestArgs(process.argv.slice(2));
|
|
13
13
|
const defaultEmbeddingsDir = path.join(repoRoot, "artifacts", "test-embeddings");
|
|
14
14
|
if (!fs.existsSync(defaultEmbeddingsDir)) {
|
|
@@ -180,7 +180,7 @@ function isMainModule(moduleUrl) {
|
|
|
180
180
|
|
|
181
181
|
export function buildNodeTestArgs(args, root = repoRoot) {
|
|
182
182
|
const defaultTestFiles = listRepoTestFiles(path.join(root, "test"), root);
|
|
183
|
-
const nodeArgs = ["--import",
|
|
183
|
+
const nodeArgs = ["--import", tsLoader, "--test"];
|
|
184
184
|
let target = DEFAULT_TARGET;
|
|
185
185
|
let explicitBaseUrl = null;
|
|
186
186
|
let hasExplicitTestFiles = false;
|
package/scripts/run-tests.ts
CHANGED
|
@@ -9,14 +9,77 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
const DEFAULT_TARGET = "mock";
|
|
10
10
|
const DEFAULT_PLATFORM = "c64u";
|
|
11
11
|
const DEFAULT_BUN_FILE_LIMIT = 4;
|
|
12
|
-
|
|
12
|
+
// Keep default Bun shards small enough to avoid descriptor exhaustion on broad suites.
|
|
13
|
+
const DEFAULT_BUN_BATCH_SIZE = DEFAULT_BUN_FILE_LIMIT;
|
|
14
|
+
const DEFAULT_VICE_MOCK_TEST_FILES = [
|
|
15
|
+
"test/device.test.mjs",
|
|
16
|
+
"test/viceIntegration.test.mjs",
|
|
17
|
+
"test/viceModule.test.mjs",
|
|
18
|
+
"test/groupedToolsShims.test.mjs",
|
|
19
|
+
"test/toolsTypes.test.mjs",
|
|
20
|
+
"test/platformRegistry.test.mjs",
|
|
21
|
+
"test/meta/program.test.mjs",
|
|
22
|
+
"test/mcpServerIntegration.test.mjs",
|
|
23
|
+
"test/c64Client.test.mjs",
|
|
24
|
+
"test/vice/viceSmokeTest.ts",
|
|
25
|
+
];
|
|
26
|
+
const DEFAULT_VICE_DEVICE_TEST_FILES = [
|
|
27
|
+
"test/device.test.mjs",
|
|
28
|
+
"test/vice/viceSmokeTest.ts",
|
|
29
|
+
];
|
|
30
|
+
const BUN_ONLY_TEST_IMPORT_RE = /from\s+["']bun:test["']/;
|
|
31
|
+
const BUN_RUNTIME_REQUIRED_TEST_FILES = new Set([
|
|
32
|
+
"test/generateMcpInterface.test.mjs",
|
|
33
|
+
"test/scripts/start.test.mjs",
|
|
34
|
+
]);
|
|
35
|
+
const ISOLATED_NODE_TEST_FILES = new Set([
|
|
36
|
+
"test/mcpServerIntegration.test.mjs",
|
|
37
|
+
"test/mcpServerPlatformInit.test.mjs",
|
|
38
|
+
"test/meta/background.test.mjs",
|
|
39
|
+
"test/pollIntegration.test.mjs",
|
|
40
|
+
"test/pollValidator.test.mjs",
|
|
41
|
+
"test/programRunnersModule.test.mjs",
|
|
42
|
+
"test/toolsCoverage.test.mjs",
|
|
43
|
+
]);
|
|
13
44
|
const ISOLATED_BUN_TEST_FILES = new Set([
|
|
14
45
|
"test/audioRuntime.test.mjs",
|
|
46
|
+
"test/mcpServerIntegration.test.mjs",
|
|
47
|
+
"test/mcpServerPlatformInit.test.mjs",
|
|
15
48
|
]);
|
|
16
49
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
50
|
const defaultEmbeddingsDir = path.join(repoRoot, "artifacts", "test-embeddings");
|
|
18
51
|
const defaultTestFiles = listRepoTestFiles(path.join(repoRoot, "test"));
|
|
19
52
|
|
|
53
|
+
function findExecutableOnPath(binary: string, envPath: string | undefined = process.env.PATH): string | null {
|
|
54
|
+
const normalized = binary.trim();
|
|
55
|
+
if (!normalized) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if (path.isAbsolute(normalized) || normalized.includes(path.sep)) {
|
|
59
|
+
try {
|
|
60
|
+
fs.accessSync(normalized, fs.constants.X_OK);
|
|
61
|
+
return normalized;
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const entry of (envPath ?? "").split(path.delimiter)) {
|
|
68
|
+
if (!entry) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const candidate = path.join(entry, normalized);
|
|
72
|
+
try {
|
|
73
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
74
|
+
return candidate;
|
|
75
|
+
} catch {
|
|
76
|
+
// Continue searching PATH.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
20
83
|
function resolveNodeExecutable(): string {
|
|
21
84
|
const candidates = [
|
|
22
85
|
process.env.C64BRIDGE_TEST_NODE_BIN,
|
|
@@ -35,6 +98,31 @@ function resolveNodeExecutable(): string {
|
|
|
35
98
|
return "node";
|
|
36
99
|
}
|
|
37
100
|
|
|
101
|
+
function resolveBunExecutable(env: NodeJS.ProcessEnv = process.env): string | null {
|
|
102
|
+
if (typeof (globalThis as { Bun?: unknown }).Bun !== "undefined") {
|
|
103
|
+
return process.execPath;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const candidates = [
|
|
107
|
+
env.C64BRIDGE_TEST_BUN_BIN,
|
|
108
|
+
env.C64BRIDGE_BUN_BIN,
|
|
109
|
+
process.env.C64BRIDGE_TEST_BUN_BIN,
|
|
110
|
+
process.env.C64BRIDGE_BUN_BIN,
|
|
111
|
+
"bun",
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
for (const candidate of candidates) {
|
|
115
|
+
if (candidate && typeof candidate === "string" && candidate.trim()) {
|
|
116
|
+
const resolved = findExecutableOnPath(candidate.trim(), env.PATH ?? process.env.PATH);
|
|
117
|
+
if (resolved) {
|
|
118
|
+
return resolved;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
38
126
|
export type RunTestsArgs = {
|
|
39
127
|
target: string;
|
|
40
128
|
platform: "c64u" | "vice";
|
|
@@ -43,6 +131,53 @@ export type RunTestsArgs = {
|
|
|
43
131
|
passthrough: string[];
|
|
44
132
|
};
|
|
45
133
|
|
|
134
|
+
export function buildMatrixEnv(
|
|
135
|
+
platform: "c64u" | "vice",
|
|
136
|
+
target: "mock" | "device",
|
|
137
|
+
explicitBaseUrl: string | null,
|
|
138
|
+
envSource: NodeJS.ProcessEnv = process.env,
|
|
139
|
+
): Record<string, string> {
|
|
140
|
+
const env: Record<string, string> = { ...envSource } as Record<string, string>;
|
|
141
|
+
env.C64_MODE = platform;
|
|
142
|
+
env.C64_TEST_TARGET = target === "device" ? "real" : "mock";
|
|
143
|
+
|
|
144
|
+
if (platform === "vice") {
|
|
145
|
+
const useViceMock = target !== "device";
|
|
146
|
+
env.VICE_TEST_TARGET = useViceMock ? "mock" : "vice";
|
|
147
|
+
env.C64_TEST_ENABLE_VICE_MOCK = useViceMock ? "1" : "0";
|
|
148
|
+
if (!useViceMock) {
|
|
149
|
+
env.VICE_AVAILABLE = "1";
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
delete env.VICE_TEST_TARGET;
|
|
153
|
+
delete env.C64_TEST_ENABLE_VICE_MOCK;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!env.RAG_EMBEDDINGS_DIR) {
|
|
157
|
+
env.RAG_EMBEDDINGS_DIR = defaultEmbeddingsDir;
|
|
158
|
+
}
|
|
159
|
+
if (explicitBaseUrl) {
|
|
160
|
+
env.C64_TEST_BASE_URL = explicitBaseUrl;
|
|
161
|
+
}
|
|
162
|
+
if (platform === "c64u" && target === "device" && !env.C64_TEST_BASE_URL) {
|
|
163
|
+
env.C64_TEST_BASE_URL = resolveBaseUrlFromConfig(env) ?? "http://c64u";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return env;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function resolveDefaultMatrixTestFiles(
|
|
170
|
+
platform: "c64u" | "vice",
|
|
171
|
+
target: "mock" | "device",
|
|
172
|
+
): string[] {
|
|
173
|
+
if (platform !== "vice") {
|
|
174
|
+
return [...defaultTestFiles];
|
|
175
|
+
}
|
|
176
|
+
return target === "device"
|
|
177
|
+
? [...DEFAULT_VICE_DEVICE_TEST_FILES]
|
|
178
|
+
: [...DEFAULT_VICE_MOCK_TEST_FILES];
|
|
179
|
+
}
|
|
180
|
+
|
|
46
181
|
export function parseRunTestsArgs(args: string[]): RunTestsArgs {
|
|
47
182
|
let target = DEFAULT_TARGET;
|
|
48
183
|
let platform: "c64u" | "vice" = DEFAULT_PLATFORM;
|
|
@@ -106,6 +241,10 @@ export function shouldUseNodeFallback(runCoverage: boolean, passthrough: string[
|
|
|
106
241
|
if (runCoverage) {
|
|
107
242
|
return false;
|
|
108
243
|
}
|
|
244
|
+
if (String(env.C64_MODE ?? "").trim().toLowerCase() === "vice"
|
|
245
|
+
&& String(env.VICE_TEST_TARGET ?? "").trim().toLowerCase() === "vice") {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
109
248
|
|
|
110
249
|
const explicitFiles = passthrough.filter(looksLikeTestFileArg);
|
|
111
250
|
if (passthrough.length === 0) {
|
|
@@ -139,11 +278,72 @@ export function buildBunTestBatches(passthrough: string[], env: NodeJS.ProcessEn
|
|
|
139
278
|
];
|
|
140
279
|
}
|
|
141
280
|
|
|
281
|
+
export function splitPassthroughByRuntime(
|
|
282
|
+
passthrough: string[],
|
|
283
|
+
root: string = repoRoot,
|
|
284
|
+
): { nodePassthrough: string[]; bunPassthrough: string[] } {
|
|
285
|
+
const explicitFiles = passthrough.filter(looksLikeTestFileArg);
|
|
286
|
+
if (explicitFiles.length === 0) {
|
|
287
|
+
return { nodePassthrough: [...passthrough], bunPassthrough: [] };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const sharedArgs = passthrough.filter((arg) => !looksLikeTestFileArg(arg));
|
|
291
|
+
const bunFiles = explicitFiles.filter((file) => isBunOnlyTestFile(path.join(root, file)));
|
|
292
|
+
const nodeFiles = explicitFiles.filter((file) => !bunFiles.includes(file));
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
nodePassthrough: nodeFiles.length > 0 ? [...nodeFiles, ...sharedArgs] : [],
|
|
296
|
+
bunPassthrough: bunFiles.length > 0 ? [...bunFiles, ...sharedArgs] : [],
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function buildNodeFallbackBatches(
|
|
301
|
+
passthrough: string[],
|
|
302
|
+
root: string = repoRoot,
|
|
303
|
+
): string[][] {
|
|
304
|
+
const explicitFiles = passthrough.filter(looksLikeTestFileArg);
|
|
305
|
+
if (explicitFiles.length === 0) {
|
|
306
|
+
return [passthrough];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const sharedArgs = passthrough.filter((arg) => !looksLikeTestFileArg(arg));
|
|
310
|
+
const isolatedFiles = explicitFiles.filter((file) => isNodeIsolatedTestFile(path.join(root, file)));
|
|
311
|
+
const sharedFiles = explicitFiles.filter((file) => !isolatedFiles.includes(file));
|
|
312
|
+
const batches: string[][] = [];
|
|
313
|
+
|
|
314
|
+
if (sharedFiles.length > 0) {
|
|
315
|
+
batches.push([...sharedFiles, ...sharedArgs]);
|
|
316
|
+
}
|
|
317
|
+
for (const file of isolatedFiles) {
|
|
318
|
+
batches.push([file, ...sharedArgs]);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return batches.length > 0 ? batches : [passthrough];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function isBunOnlyTestFile(filePath: string): boolean {
|
|
325
|
+
const relativePath = path.relative(repoRoot, filePath).split(path.sep).join("/");
|
|
326
|
+
if (BUN_RUNTIME_REQUIRED_TEST_FILES.has(relativePath)) {
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
return BUN_ONLY_TEST_IMPORT_RE.test(fs.readFileSync(filePath, "utf8"));
|
|
331
|
+
} catch {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function isNodeIsolatedTestFile(filePath: string): boolean {
|
|
337
|
+
const relativePath = path.relative(repoRoot, filePath).split(path.sep).join("/");
|
|
338
|
+
return ISOLATED_NODE_TEST_FILES.has(relativePath);
|
|
339
|
+
}
|
|
340
|
+
|
|
142
341
|
async function runNodeFallback(target: string, explicitBaseUrl: string | null, passthrough: string[], env: Record<string, string>, runCoverage: boolean): Promise<number> {
|
|
143
342
|
console.warn("[run-tests] Using Node runner for this test set to avoid Bun memory growth on broad suites");
|
|
144
343
|
if (runCoverage) {
|
|
145
344
|
console.warn("[run-tests] Coverage reporting is unavailable in Node fallback mode");
|
|
146
345
|
}
|
|
346
|
+
const { nodePassthrough, bunPassthrough } = splitPassthroughByRuntime(passthrough);
|
|
147
347
|
const nodeExecutable = resolveNodeExecutable();
|
|
148
348
|
const nodeScript = path.join(repoRoot, "scripts", "run-tests.mjs");
|
|
149
349
|
const fallbackArgs = [] as string[];
|
|
@@ -153,23 +353,53 @@ async function runNodeFallback(target: string, explicitBaseUrl: string | null, p
|
|
|
153
353
|
if (explicitBaseUrl) {
|
|
154
354
|
fallbackArgs.push(`--base-url=${explicitBaseUrl}`);
|
|
155
355
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
356
|
+
if (nodePassthrough.length > 0 || bunPassthrough.length === 0) {
|
|
357
|
+
const nodeBatches = buildNodeFallbackBatches(nodePassthrough);
|
|
358
|
+
for (let index = 0; index < nodeBatches.length; index += 1) {
|
|
359
|
+
const batch = nodeBatches[index] ?? [];
|
|
360
|
+
if (nodeBatches.length > 1) {
|
|
361
|
+
console.warn(`[run-tests] Node fallback batch ${index + 1}/${nodeBatches.length} (${batch.filter(looksLikeTestFileArg).length} files)`);
|
|
362
|
+
}
|
|
363
|
+
const nodeExitCode = await new Promise<number>((resolve) => {
|
|
364
|
+
const childProcess = spawn(nodeExecutable, [nodeScript, ...fallbackArgs, ...batch], {
|
|
365
|
+
cwd: repoRoot,
|
|
366
|
+
env,
|
|
367
|
+
stdio: "inherit",
|
|
368
|
+
}) as unknown as {
|
|
369
|
+
on(event: "error", listener: (error: Error) => void): void;
|
|
370
|
+
on(event: "exit", listener: (code: number | null) => void): void;
|
|
371
|
+
};
|
|
372
|
+
childProcess.on("error", (error) => {
|
|
373
|
+
console.error("[run-tests] Failed to launch Node fallback:", error);
|
|
374
|
+
resolve(1);
|
|
375
|
+
});
|
|
376
|
+
childProcess.on("exit", (code) => {
|
|
377
|
+
resolve(typeof code === "number" ? code : 1);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
if (nodeExitCode !== 0) {
|
|
382
|
+
return nodeExitCode;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (bunPassthrough.length === 0) {
|
|
388
|
+
return 0;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const bunExecutable = resolveBunExecutable(env);
|
|
392
|
+
if (!bunExecutable) {
|
|
393
|
+
console.error("[run-tests] Bun-only tests were selected but no Bun executable is available");
|
|
394
|
+
return 1;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
console.warn("[run-tests] Running Bun-only tests under Bun to preserve runtime coverage");
|
|
398
|
+
const bunBatches = buildBunTestBatches(bunPassthrough, env);
|
|
399
|
+
return await runBunBatches(env, bunBatches, {
|
|
400
|
+
coverage: false,
|
|
401
|
+
bunExecutable,
|
|
402
|
+
labelPrefix: "bun-only-suite",
|
|
173
403
|
});
|
|
174
404
|
}
|
|
175
405
|
|
|
@@ -200,24 +430,11 @@ async function main(): Promise<number> {
|
|
|
200
430
|
fs.mkdirSync(defaultEmbeddingsDir, { recursive: true });
|
|
201
431
|
}
|
|
202
432
|
|
|
203
|
-
const env: Record<string, string> = { ...process.env } as Record<string, string>;
|
|
204
433
|
const normalizedTarget = normalizeTarget(target);
|
|
205
|
-
env
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
}
|
|
434
|
+
const env = buildMatrixEnv(platform, normalizedTarget, explicitBaseUrl);
|
|
435
|
+
const effectivePassthrough = passthrough.length > 0
|
|
436
|
+
? passthrough
|
|
437
|
+
: resolveDefaultMatrixTestFiles(platform, normalizedTarget);
|
|
221
438
|
|
|
222
439
|
printMatrixHeading({
|
|
223
440
|
platform,
|
|
@@ -230,24 +447,20 @@ async function main(): Promise<number> {
|
|
|
230
447
|
const bunRuntime = (globalThis as { Bun?: unknown }).Bun;
|
|
231
448
|
if (!bunRuntime) {
|
|
232
449
|
console.warn("[run-tests] Bun runtime not detected; falling back to Node runner");
|
|
233
|
-
return await runNodeFallback(target, explicitBaseUrl,
|
|
450
|
+
return await runNodeFallback(target, explicitBaseUrl, effectivePassthrough, env, runCoverage);
|
|
234
451
|
}
|
|
235
452
|
|
|
236
|
-
if (
|
|
237
|
-
return await
|
|
238
|
-
coverage: false,
|
|
239
|
-
labelPrefix: "default-suite",
|
|
240
|
-
});
|
|
453
|
+
if (shouldUseNodeFallback(runCoverage, effectivePassthrough, env)) {
|
|
454
|
+
return await runNodeFallback(target, explicitBaseUrl, effectivePassthrough, env, runCoverage);
|
|
241
455
|
}
|
|
242
456
|
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
return await runNodeFallback(target, explicitBaseUrl, passthrough, env, runCoverage);
|
|
457
|
+
if (!runCoverage) {
|
|
458
|
+
const batches = buildBunTestBatches(effectivePassthrough, env);
|
|
459
|
+
return await runBunBatches(env, batches, {
|
|
460
|
+
bunExecutable: process.execPath,
|
|
461
|
+
coverage: false,
|
|
462
|
+
labelPrefix: passthrough.length === 0 ? "default-suite" : "sharded-suite",
|
|
463
|
+
});
|
|
251
464
|
}
|
|
252
465
|
|
|
253
466
|
const bunArgs = [
|
|
@@ -259,7 +472,7 @@ async function main(): Promise<number> {
|
|
|
259
472
|
"--coverage-reporter=text",
|
|
260
473
|
]
|
|
261
474
|
: []),
|
|
262
|
-
...(
|
|
475
|
+
...effectivePassthrough.map((arg) => normalizeBunBatchArg(arg)),
|
|
263
476
|
];
|
|
264
477
|
return await runExternalCommand(process.execPath, bunArgs, env);
|
|
265
478
|
}
|
|
@@ -345,7 +558,7 @@ function chunkFiles(files: string[], chunkSize: number): string[][] {
|
|
|
345
558
|
async function runBunBatches(
|
|
346
559
|
env: Record<string, string>,
|
|
347
560
|
batches: string[][],
|
|
348
|
-
options: { coverage: boolean; labelPrefix: string },
|
|
561
|
+
options: { coverage: boolean; labelPrefix: string; bunExecutable: string },
|
|
349
562
|
): Promise<number> {
|
|
350
563
|
const coverageArgs = options.coverage
|
|
351
564
|
? ["--coverage", "--coverage-reporter=lcov", "--coverage-reporter=text"]
|
|
@@ -354,7 +567,8 @@ async function runBunBatches(
|
|
|
354
567
|
for (let index = 0; index < batches.length; index += 1) {
|
|
355
568
|
const batch = batches[index] ?? [];
|
|
356
569
|
console.log(`[run-tests] ${options.labelPrefix} batch ${index + 1}/${batches.length} (${batch.length} entries)`);
|
|
357
|
-
const
|
|
570
|
+
const bunArgs = batch.map((arg) => normalizeBunBatchArg(arg));
|
|
571
|
+
const exitCode = await runExternalCommand(options.bunExecutable, ["test", ...coverageArgs, ...bunArgs], env);
|
|
358
572
|
if (exitCode !== 0) {
|
|
359
573
|
return exitCode;
|
|
360
574
|
}
|
|
@@ -363,6 +577,13 @@ async function runBunBatches(
|
|
|
363
577
|
return 0;
|
|
364
578
|
}
|
|
365
579
|
|
|
580
|
+
export function normalizeBunBatchArg(arg: string): string {
|
|
581
|
+
if (looksLikeTestFileArg(arg) && !arg.startsWith("./") && !arg.startsWith("../") && !path.isAbsolute(arg)) {
|
|
582
|
+
return `./${arg}`;
|
|
583
|
+
}
|
|
584
|
+
return arg;
|
|
585
|
+
}
|
|
586
|
+
|
|
366
587
|
function listRepoTestFiles(testRoot: string): string[] {
|
|
367
588
|
const files: string[] = [];
|
|
368
589
|
|
package/scripts/update-readme.ts
CHANGED
|
@@ -173,10 +173,14 @@ function collectGroupedOperations(schema?: JsonSchema): readonly GroupedOperatio
|
|
|
173
173
|
const discriminator = (schema as JsonSchema & { discriminator?: { propertyName?: string } }).discriminator;
|
|
174
174
|
const variants = (schema as JsonSchema & { oneOf?: readonly JsonSchema[] }).oneOf;
|
|
175
175
|
|
|
176
|
-
if (
|
|
177
|
-
return
|
|
176
|
+
if (discriminator && discriminator.propertyName === "op" && Array.isArray(variants)) {
|
|
177
|
+
return collectFromVariantList(variants);
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
return collectFromFlattenedSchema(schema);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function collectFromVariantList(variants: readonly JsonSchema[]): readonly GroupedOperation[] {
|
|
180
184
|
const operations: GroupedOperation[] = [];
|
|
181
185
|
|
|
182
186
|
for (const variant of variants) {
|
|
@@ -227,6 +231,54 @@ function collectGroupedOperations(schema?: JsonSchema): readonly GroupedOperatio
|
|
|
227
231
|
return operations;
|
|
228
232
|
}
|
|
229
233
|
|
|
234
|
+
function collectFromFlattenedSchema(schema: JsonSchema): readonly GroupedOperation[] {
|
|
235
|
+
const properties = (schema.properties ?? {}) as Record<string, JsonSchema | undefined>;
|
|
236
|
+
const opSchema = properties.op;
|
|
237
|
+
if (!opSchema || !isObject(opSchema)) {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const enumValues = (opSchema as JsonSchema).enum;
|
|
242
|
+
if (!Array.isArray(enumValues) || enumValues.length === 0) {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const opNames = enumValues.filter((value): value is string => typeof value === "string" && value.length > 0);
|
|
247
|
+
if (opNames.length === 0) {
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const descriptionMap = parseOperationDescriptions(getString(schema.description));
|
|
252
|
+
const hasVerificationProperty = Object.keys(properties).some((name) =>
|
|
253
|
+
name !== "op" && name.toLowerCase().startsWith("verify"),
|
|
254
|
+
);
|
|
255
|
+
const notes = hasVerificationProperty ? ["supports verify"] : [];
|
|
256
|
+
|
|
257
|
+
return opNames.map((opName) => ({
|
|
258
|
+
op: opName,
|
|
259
|
+
description: descriptionMap.get(opName) ?? `Operation ${opName}`,
|
|
260
|
+
required: [] as readonly string[],
|
|
261
|
+
notes,
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function parseOperationDescriptions(description: string): Map<string, string> {
|
|
266
|
+
const result = new Map<string, string>();
|
|
267
|
+
if (!description) {
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const lines = description.split(/\r?\n/);
|
|
272
|
+
for (const line of lines) {
|
|
273
|
+
const match = line.match(/^\s*-\s+([A-Za-z0-9_]+):\s*(.+)$/);
|
|
274
|
+
if (match) {
|
|
275
|
+
result.set(match[1], match[2].trim());
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
|
|
230
282
|
export function renderToolsSection(modules: readonly ToolModuleDescriptor[] = describeToolModules()): string[] {
|
|
231
283
|
const lines: string[] = ["### Tools", ""];
|
|
232
284
|
const groupedTools: GroupedToolInfo[] = [];
|
package/server.json
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
10
|
"websiteUrl": "https://github.com/chrisgleissner/c64bridge",
|
|
11
|
-
"version": "0.
|
|
11
|
+
"version": "0.9.0",
|
|
12
12
|
"packages": [
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"identifier": "c64bridge",
|
|
16
|
-
"version": "0.
|
|
16
|
+
"version": "0.9.0",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
},
|