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
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { accessSync, constants } from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
9
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
10
|
+
import {
|
|
11
|
+
LATEST_PROTOCOL_VERSION,
|
|
12
|
+
ListPromptsResultSchema,
|
|
13
|
+
ListResourcesResultSchema,
|
|
14
|
+
ListToolsResultSchema,
|
|
15
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
16
|
+
import { startMockC64Server } from "./mockC64Server.mjs";
|
|
17
|
+
import { getMcpProjectMetadata } from "../src/mcp/metadata.js";
|
|
18
|
+
|
|
19
|
+
type JsonObject = Record<string, unknown>;
|
|
20
|
+
|
|
21
|
+
interface ToolDescriptorLike {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly description?: string;
|
|
24
|
+
readonly inputSchema?: unknown;
|
|
25
|
+
readonly metadata?: {
|
|
26
|
+
readonly examples?: readonly {
|
|
27
|
+
readonly name?: string;
|
|
28
|
+
readonly description?: string;
|
|
29
|
+
readonly arguments?: Record<string, unknown>;
|
|
30
|
+
}[];
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface GenerateOptions {
|
|
35
|
+
readonly outputDir?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
39
|
+
const __dirname = path.dirname(__filename);
|
|
40
|
+
const PROJECT_ROOT = path.resolve(__dirname, "..");
|
|
41
|
+
const DEFAULT_OUTPUT_DIR = path.join(PROJECT_ROOT, "mcp");
|
|
42
|
+
|
|
43
|
+
function fileExists(filePath: string): boolean {
|
|
44
|
+
try {
|
|
45
|
+
accessSync(filePath, constants.F_OK);
|
|
46
|
+
return true;
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveBunExecutable(): string {
|
|
53
|
+
if (typeof Bun !== "undefined") {
|
|
54
|
+
return process.execPath;
|
|
55
|
+
}
|
|
56
|
+
const candidates = [
|
|
57
|
+
process.env.BUN_BIN,
|
|
58
|
+
process.env.C64BRIDGE_TEST_BUN_BIN,
|
|
59
|
+
process.env.C64BRIDGE_BUN_BIN,
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
for (const candidate of candidates) {
|
|
63
|
+
if (candidate && candidate.trim().length > 0) {
|
|
64
|
+
return candidate.trim();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return "bun";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveNodeExecutable(): string {
|
|
72
|
+
const candidates = [
|
|
73
|
+
process.env.C64BRIDGE_TEST_NODE_BIN,
|
|
74
|
+
process.env.C64BRIDGE_NODE_BIN,
|
|
75
|
+
process.env.NODE_BINARY,
|
|
76
|
+
process.env.NODE_EXEC_PATH,
|
|
77
|
+
process.env.npm_node_execpath,
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
for (const candidate of candidates) {
|
|
81
|
+
if (candidate && candidate.trim().length > 0) {
|
|
82
|
+
return candidate.trim();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return typeof Bun !== "undefined" ? "node" : process.execPath;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolveServerProcess(): { command: string; args: string[] } {
|
|
90
|
+
const sourceEntrypoint = path.join(PROJECT_ROOT, "src", "mcp-server.ts");
|
|
91
|
+
const distEntrypoint = path.join(PROJECT_ROOT, "dist", "mcp-server.js");
|
|
92
|
+
|
|
93
|
+
if (typeof Bun !== "undefined" && fileExists(sourceEntrypoint)) {
|
|
94
|
+
return { command: process.execPath, args: [sourceEntrypoint] };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (fileExists(distEntrypoint)) {
|
|
98
|
+
return { command: resolveNodeExecutable(), args: [distEntrypoint] };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (fileExists(sourceEntrypoint)) {
|
|
102
|
+
return { command: resolveBunExecutable(), args: [sourceEntrypoint] };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
throw new Error("Unable to locate MCP server entry point (src/mcp-server.ts or dist/mcp-server.js).");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function json(value: unknown): string {
|
|
109
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function writeJsonFile(filePath: string, value: unknown): Promise<void> {
|
|
113
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
114
|
+
await writeFile(filePath, json(value), "utf8");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sanitizeFilename(input: string): string {
|
|
118
|
+
return input.replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function buildProtocolExamples(tools: readonly ToolDescriptorLike[]) {
|
|
122
|
+
const selectedTool = tools.find((tool) => Array.isArray(tool.metadata?.examples) && tool.metadata.examples.length > 0)
|
|
123
|
+
?? tools[0];
|
|
124
|
+
const selectedExample = selectedTool?.metadata?.examples?.[0];
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
protocolVersion: LATEST_PROTOCOL_VERSION,
|
|
128
|
+
examples: {
|
|
129
|
+
initialize: {
|
|
130
|
+
description: "Client initialization request sent immediately after connecting to the stdio MCP server.",
|
|
131
|
+
request: {
|
|
132
|
+
jsonrpc: "2.0",
|
|
133
|
+
id: 1,
|
|
134
|
+
method: "initialize",
|
|
135
|
+
params: {
|
|
136
|
+
protocolVersion: LATEST_PROTOCOL_VERSION,
|
|
137
|
+
capabilities: {
|
|
138
|
+
resources: {},
|
|
139
|
+
tools: {},
|
|
140
|
+
prompts: {},
|
|
141
|
+
},
|
|
142
|
+
clientInfo: {
|
|
143
|
+
name: "c64bridge-static-interface-generator",
|
|
144
|
+
version: "1.0.0",
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
"tools/list": {
|
|
150
|
+
description: "Enumerates the full tool catalog and input schemas exposed by the server.",
|
|
151
|
+
request: {
|
|
152
|
+
jsonrpc: "2.0",
|
|
153
|
+
id: 2,
|
|
154
|
+
method: "tools/list",
|
|
155
|
+
params: {},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
"tools/call": {
|
|
159
|
+
description: selectedExample?.description
|
|
160
|
+
?? `Invokes the ${selectedTool?.name ?? "tool"} tool with deterministic example arguments.`,
|
|
161
|
+
request: {
|
|
162
|
+
jsonrpc: "2.0",
|
|
163
|
+
id: 3,
|
|
164
|
+
method: "tools/call",
|
|
165
|
+
params: {
|
|
166
|
+
name: selectedTool?.name ?? "c64_program",
|
|
167
|
+
arguments: selectedExample?.arguments ?? {},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function discoverMcpInterface() {
|
|
176
|
+
const mockServer = await startMockC64Server();
|
|
177
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "c64bridge-mcp-interface-"));
|
|
178
|
+
const configPath = path.join(tempDir, "config.json");
|
|
179
|
+
const serverProcess = resolveServerProcess();
|
|
180
|
+
const mockUrl = new URL(mockServer.baseUrl);
|
|
181
|
+
const configPayload = {
|
|
182
|
+
c64u: {
|
|
183
|
+
host: mockUrl.hostname,
|
|
184
|
+
port: mockUrl.port ? Number(mockUrl.port) : 80,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
await writeFile(configPath, json(configPayload), "utf8");
|
|
189
|
+
|
|
190
|
+
const transport = new StdioClientTransport({
|
|
191
|
+
command: serverProcess.command,
|
|
192
|
+
args: serverProcess.args,
|
|
193
|
+
cwd: PROJECT_ROOT,
|
|
194
|
+
env: {
|
|
195
|
+
...process.env,
|
|
196
|
+
NODE_ENV: "test",
|
|
197
|
+
LOG_LEVEL: "error",
|
|
198
|
+
C64_TEST_TARGET: "mock",
|
|
199
|
+
C64BRIDGE_CONFIG: configPath,
|
|
200
|
+
},
|
|
201
|
+
stderr: "pipe",
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const client = new Client(
|
|
205
|
+
{ name: "c64bridge-static-interface-generator", version: "1.0.0" },
|
|
206
|
+
{
|
|
207
|
+
capabilities: {
|
|
208
|
+
resources: {},
|
|
209
|
+
tools: {},
|
|
210
|
+
prompts: {},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
await client.connect(transport);
|
|
217
|
+
|
|
218
|
+
const toolsList = await client.request({ method: "tools/list", params: {} }, ListToolsResultSchema);
|
|
219
|
+
const resourcesList = await client.request({ method: "resources/list", params: {} }, ListResourcesResultSchema);
|
|
220
|
+
const promptsList = await client.request({ method: "prompts/list", params: {} }, ListPromptsResultSchema);
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
serverInfo: client.getServerVersion(),
|
|
224
|
+
capabilities: client.getServerCapabilities(),
|
|
225
|
+
toolsList,
|
|
226
|
+
resourcesList,
|
|
227
|
+
promptsList,
|
|
228
|
+
};
|
|
229
|
+
} finally {
|
|
230
|
+
await client.close().catch(() => undefined);
|
|
231
|
+
await mockServer.close().catch(() => undefined);
|
|
232
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function generateMcpInterface(options: GenerateOptions = {}): Promise<void> {
|
|
237
|
+
const outputDir = options.outputDir ? path.resolve(options.outputDir) : DEFAULT_OUTPUT_DIR;
|
|
238
|
+
const schemasDir = path.join(outputDir, "schemas");
|
|
239
|
+
const snapshot = await discoverMcpInterface();
|
|
240
|
+
const projectMetadata = getMcpProjectMetadata();
|
|
241
|
+
|
|
242
|
+
await rm(outputDir, { recursive: true, force: true });
|
|
243
|
+
await mkdir(schemasDir, { recursive: true });
|
|
244
|
+
|
|
245
|
+
const serverJson: JsonObject = {
|
|
246
|
+
name: snapshot.serverInfo?.name ?? projectMetadata.name,
|
|
247
|
+
version: snapshot.serverInfo?.version ?? projectMetadata.version,
|
|
248
|
+
description: projectMetadata.description,
|
|
249
|
+
transports: [...projectMetadata.transports],
|
|
250
|
+
capabilities: snapshot.capabilities ?? {},
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
if (projectMetadata.repository) {
|
|
254
|
+
serverJson.repository = projectMetadata.repository;
|
|
255
|
+
}
|
|
256
|
+
if (projectMetadata.license) {
|
|
257
|
+
serverJson.license = projectMetadata.license;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
await writeJsonFile(path.join(outputDir, "server.json"), serverJson);
|
|
261
|
+
await writeJsonFile(path.join(outputDir, "tools.json"), snapshot.toolsList);
|
|
262
|
+
await writeJsonFile(path.join(outputDir, "resources.json"), snapshot.resourcesList);
|
|
263
|
+
await writeJsonFile(path.join(outputDir, "prompts.json"), snapshot.promptsList);
|
|
264
|
+
await writeJsonFile(
|
|
265
|
+
path.join(outputDir, "protocol-examples.json"),
|
|
266
|
+
buildProtocolExamples((snapshot.toolsList.tools ?? []) as readonly ToolDescriptorLike[]),
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
for (const tool of snapshot.toolsList.tools ?? []) {
|
|
270
|
+
if (tool.inputSchema === undefined) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
await writeJsonFile(
|
|
274
|
+
path.join(schemasDir, `${sanitizeFilename(tool.name)}.schema.json`),
|
|
275
|
+
tool.inputSchema,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function main(): Promise<void> {
|
|
281
|
+
await generateMcpInterface();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (import.meta.main) {
|
|
285
|
+
await main().catch((error) => {
|
|
286
|
+
console.error("[generate-mcp-interface] Failed:", error);
|
|
287
|
+
process.exitCode = 1;
|
|
288
|
+
});
|
|
289
|
+
}
|
package/scripts/invoke-bun.mjs
CHANGED
|
@@ -3,13 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
-
|
|
7
|
-
if (process.argv.length < 3) {
|
|
8
|
-
console.error("Usage: node scripts/invoke-bun.mjs <command> [args...]");
|
|
9
|
-
process.exit(1);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const bunArgs = process.argv.slice(2);
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
13
7
|
|
|
14
8
|
function resolveBunExecutable() {
|
|
15
9
|
const candidates = [
|
|
@@ -41,18 +35,70 @@ function resolveBunExecutable() {
|
|
|
41
35
|
return "bun";
|
|
42
36
|
}
|
|
43
37
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
38
|
+
const BUN_SUBCOMMANDS = new Set([
|
|
39
|
+
"test",
|
|
40
|
+
"run",
|
|
41
|
+
"x",
|
|
42
|
+
"install",
|
|
43
|
+
"add",
|
|
44
|
+
"remove",
|
|
45
|
+
"update",
|
|
46
|
+
"pm",
|
|
47
|
+
"build",
|
|
48
|
+
"create",
|
|
49
|
+
"exec",
|
|
50
|
+
"repl",
|
|
51
|
+
"help",
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
function looksLikeScriptPath(value) {
|
|
55
|
+
return typeof value === "string"
|
|
56
|
+
&& value.length > 0
|
|
57
|
+
&& !value.startsWith("-")
|
|
58
|
+
&& /\.(?:[cm]?[jt]s|tsx?)$/i.test(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function buildBunArgs(args) {
|
|
62
|
+
const [firstArg, ...rest] = args;
|
|
63
|
+
if (!firstArg) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (BUN_SUBCOMMANDS.has(firstArg) || !looksLikeScriptPath(firstArg)) {
|
|
68
|
+
return args;
|
|
56
69
|
}
|
|
57
|
-
|
|
58
|
-
|
|
70
|
+
|
|
71
|
+
return ["run", firstArg, ...rest];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isMainModule(moduleUrl) {
|
|
75
|
+
const entry = process.argv[1];
|
|
76
|
+
if (!entry) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return fileURLToPath(moduleUrl) === path.resolve(entry);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (isMainModule(import.meta.url)) {
|
|
83
|
+
if (process.argv.length < 3) {
|
|
84
|
+
console.error("Usage: node scripts/invoke-bun.mjs <command> [args...]");
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const rawArgs = process.argv.slice(2);
|
|
89
|
+
const bunExecutable = resolveBunExecutable();
|
|
90
|
+
const child = spawn(bunExecutable, buildBunArgs(rawArgs), {
|
|
91
|
+
stdio: "inherit",
|
|
92
|
+
env: {
|
|
93
|
+
...process.env,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
child.on("exit", (code, signal) => {
|
|
98
|
+
if (signal) {
|
|
99
|
+
process.kill(process.pid, signal);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
process.exit(code ?? 1);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
+
import dgram from "node:dgram";
|
|
2
3
|
import { once } from "node:events";
|
|
3
4
|
import { Buffer } from "node:buffer";
|
|
4
|
-
import {
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const moduleDir = new URL(".", import.meta.url);
|
|
9
|
+
const distUrl = new URL("../dist/chargen.js", moduleDir);
|
|
10
|
+
const srcUrl = new URL("../src/chargen.js", moduleDir);
|
|
11
|
+
const chargenModuleUrl = existsSync(fileURLToPath(distUrl)) ? distUrl : srcUrl;
|
|
12
|
+
const { getChargenGlyphs } = await import(chargenModuleUrl.href);
|
|
5
13
|
|
|
6
14
|
function parseNumeric(value, defaultRadix = 16) {
|
|
7
15
|
if (!value) {
|
|
@@ -82,6 +90,7 @@ function createInitialState() {
|
|
|
82
90
|
poweroffs: 0,
|
|
83
91
|
memory: new Uint8Array(0x10000),
|
|
84
92
|
lastWrite: null,
|
|
93
|
+
writeLog: [],
|
|
85
94
|
lastRequest: null,
|
|
86
95
|
drives: {},
|
|
87
96
|
lastDriveOperation: null,
|
|
@@ -104,11 +113,20 @@ function createInitialState() {
|
|
|
104
113
|
menuToggleCount: 0,
|
|
105
114
|
lastMenuTarget: null,
|
|
106
115
|
streams: {
|
|
107
|
-
video: { active: false, target: null },
|
|
108
|
-
audio: { active: false, target: null },
|
|
109
|
-
debug: { active: false, target: null },
|
|
116
|
+
video: { active: false, target: null, packetsSent: 0 },
|
|
117
|
+
audio: { active: false, target: null, packetsSent: 0 },
|
|
118
|
+
debug: { active: false, target: null, packetsSent: 0 },
|
|
110
119
|
},
|
|
111
120
|
lastStreamAction: null,
|
|
121
|
+
streamActionLog: [],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function createStreamRuntime() {
|
|
126
|
+
return {
|
|
127
|
+
video: { interval: null, socket: null, sequence: 0, frameNumber: 0, sampleIndex: 0 },
|
|
128
|
+
audio: { interval: null, socket: null, sequence: 0, frameNumber: 0, sampleIndex: 0 },
|
|
129
|
+
debug: { interval: null, socket: null, sequence: 0, frameNumber: 0, sampleIndex: 0 },
|
|
112
130
|
};
|
|
113
131
|
}
|
|
114
132
|
|
|
@@ -148,6 +166,7 @@ function seedReadyPrompt(state) {
|
|
|
148
166
|
|
|
149
167
|
export async function startMockC64Server(options = {}) {
|
|
150
168
|
const state = createInitialState();
|
|
169
|
+
const streamRuntime = createStreamRuntime();
|
|
151
170
|
state.networkPassword = typeof options.networkPassword === "string" && options.networkPassword.trim()
|
|
152
171
|
? options.networkPassword
|
|
153
172
|
: null;
|
|
@@ -171,12 +190,72 @@ export async function startMockC64Server(options = {}) {
|
|
|
171
190
|
ensureDriveState("drive8");
|
|
172
191
|
|
|
173
192
|
function resetState() {
|
|
193
|
+
stopAllStreams();
|
|
174
194
|
const fresh = createInitialState();
|
|
175
195
|
Object.assign(state, fresh);
|
|
176
196
|
seedReadyPrompt(state);
|
|
177
197
|
ensureDriveState("drive8");
|
|
178
198
|
}
|
|
179
199
|
|
|
200
|
+
function stopAllStreams() {
|
|
201
|
+
for (const stream of Object.keys(streamRuntime)) {
|
|
202
|
+
stopStreamEmitter(stream);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function stopStreamEmitter(stream) {
|
|
207
|
+
const runtime = streamRuntime[stream];
|
|
208
|
+
if (!runtime) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (runtime.interval) {
|
|
212
|
+
clearInterval(runtime.interval);
|
|
213
|
+
runtime.interval = null;
|
|
214
|
+
}
|
|
215
|
+
if (runtime.socket) {
|
|
216
|
+
runtime.socket.close();
|
|
217
|
+
runtime.socket = null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function startStreamEmitter(stream, target) {
|
|
222
|
+
stopStreamEmitter(stream);
|
|
223
|
+
if (!target || (stream !== "video" && stream !== "audio")) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const runtime = streamRuntime[stream];
|
|
228
|
+
const { host, port } = parseStreamTarget(target, stream === "video" ? 11000 : 11001);
|
|
229
|
+
const socket = dgram.createSocket("udp4");
|
|
230
|
+
runtime.socket = socket;
|
|
231
|
+
|
|
232
|
+
if (stream === "video") {
|
|
233
|
+
const sendFrame = () => {
|
|
234
|
+
const packets = buildMockVideoFramePackets(runtime.frameNumber, runtime.sequence);
|
|
235
|
+
runtime.frameNumber = (runtime.frameNumber + 1) & 0xffff;
|
|
236
|
+
runtime.sequence = (runtime.sequence + packets.length) & 0xffff;
|
|
237
|
+
for (const packet of packets) {
|
|
238
|
+
socket.send(packet, port, host);
|
|
239
|
+
state.streams.video.packetsSent += 1;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
sendFrame();
|
|
243
|
+
runtime.interval = setInterval(sendFrame, 30);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const sendAudioPacket = () => {
|
|
248
|
+
const packet = buildMockAudioPacket(runtime.sequence, runtime.sampleIndex);
|
|
249
|
+
runtime.sequence = (runtime.sequence + 1) & 0xffff;
|
|
250
|
+
runtime.sampleIndex += 192;
|
|
251
|
+
socket.send(packet, port, host);
|
|
252
|
+
state.streams.audio.packetsSent += 1;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
sendAudioPacket();
|
|
256
|
+
runtime.interval = setInterval(sendAudioPacket, 4);
|
|
257
|
+
}
|
|
258
|
+
|
|
180
259
|
const server = createServer(async (req, res) => {
|
|
181
260
|
const { method, url } = req;
|
|
182
261
|
|
|
@@ -386,6 +465,7 @@ export async function startMockC64Server(options = {}) {
|
|
|
386
465
|
|
|
387
466
|
state.memory.set(bytes, address);
|
|
388
467
|
state.lastWrite = { address, bytes };
|
|
468
|
+
state.writeLog.push({ address, bytes: Buffer.from(bytes) });
|
|
389
469
|
|
|
390
470
|
sendJson(res, { result: "wrote", address, length: bytes.length });
|
|
391
471
|
return;
|
|
@@ -404,6 +484,7 @@ export async function startMockC64Server(options = {}) {
|
|
|
404
484
|
|
|
405
485
|
state.memory.set(bytes, address);
|
|
406
486
|
state.lastWrite = { address, bytes };
|
|
487
|
+
state.writeLog.push({ address, bytes: Buffer.from(bytes) });
|
|
407
488
|
|
|
408
489
|
sendJson(res, { result: "wrote", address, length: bytes.length });
|
|
409
490
|
return;
|
|
@@ -512,15 +593,19 @@ export async function startMockC64Server(options = {}) {
|
|
|
512
593
|
if (action === "start" && method === "PUT") {
|
|
513
594
|
const body = await readJson(req);
|
|
514
595
|
const target = routeUrl.searchParams.get("ip") ?? routeUrl.searchParams.get("target") ?? body?.ip ?? body?.target ?? null;
|
|
515
|
-
state.streams[stream] = { active: true, target };
|
|
596
|
+
state.streams[stream] = { active: true, target, packetsSent: 0 };
|
|
516
597
|
state.lastStreamAction = { action: "start", stream, target };
|
|
598
|
+
state.streamActionLog.push({ action: "start", stream, target });
|
|
599
|
+
startStreamEmitter(stream, target);
|
|
517
600
|
sendJson(res, { result: "started", stream, target });
|
|
518
601
|
return;
|
|
519
602
|
}
|
|
520
603
|
|
|
521
604
|
if (action === "stop" && method === "PUT") {
|
|
522
|
-
|
|
605
|
+
stopStreamEmitter(stream);
|
|
606
|
+
state.streams[stream] = { active: false, target: null, packetsSent: state.streams[stream]?.packetsSent ?? 0 };
|
|
523
607
|
state.lastStreamAction = { action: "stop", stream };
|
|
608
|
+
state.streamActionLog.push({ action: "stop", stream });
|
|
524
609
|
sendJson(res, { result: "stopped", stream });
|
|
525
610
|
return;
|
|
526
611
|
}
|
|
@@ -678,9 +763,80 @@ export async function startMockC64Server(options = {}) {
|
|
|
678
763
|
baseUrl: `http://127.0.0.1:${address.port}`,
|
|
679
764
|
state,
|
|
680
765
|
async close() {
|
|
766
|
+
stopAllStreams();
|
|
681
767
|
server.close();
|
|
682
768
|
await once(server, "close");
|
|
683
769
|
},
|
|
684
770
|
reset: resetState,
|
|
685
771
|
};
|
|
686
772
|
}
|
|
773
|
+
|
|
774
|
+
function parseStreamTarget(value, defaultPort) {
|
|
775
|
+
const trimmed = String(value ?? "").trim();
|
|
776
|
+
if (!trimmed) {
|
|
777
|
+
return { host: "127.0.0.1", port: defaultPort };
|
|
778
|
+
}
|
|
779
|
+
const separator = trimmed.lastIndexOf(":");
|
|
780
|
+
if (separator <= 0 || separator === trimmed.length - 1) {
|
|
781
|
+
return { host: trimmed, port: defaultPort };
|
|
782
|
+
}
|
|
783
|
+
const host = trimmed.slice(0, separator);
|
|
784
|
+
const port = Number.parseInt(trimmed.slice(separator + 1), 10);
|
|
785
|
+
return {
|
|
786
|
+
host,
|
|
787
|
+
port: Number.isFinite(port) && port > 0 && port <= 65535 ? port : defaultPort,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function buildMockVideoFramePackets(frameNumber, sequenceStart) {
|
|
792
|
+
const width = 384;
|
|
793
|
+
const height = 272;
|
|
794
|
+
const linesPerPacket = 4;
|
|
795
|
+
const bytesPerLine = width / 2;
|
|
796
|
+
const packets = [];
|
|
797
|
+
let sequence = sequenceStart & 0xffff;
|
|
798
|
+
|
|
799
|
+
for (let line = 0; line < height; line += linesPerPacket) {
|
|
800
|
+
const header = Buffer.alloc(12);
|
|
801
|
+
header.writeUInt16LE(sequence, 0);
|
|
802
|
+
header.writeUInt16LE(frameNumber & 0xffff, 2);
|
|
803
|
+
const isLastPacket = line + linesPerPacket >= height;
|
|
804
|
+
header.writeUInt16LE(isLastPacket ? (line | 0x8000) : line, 4);
|
|
805
|
+
header.writeUInt16LE(width, 6);
|
|
806
|
+
header.writeUInt8(linesPerPacket, 8);
|
|
807
|
+
header.writeUInt8(4, 9);
|
|
808
|
+
header.writeUInt16LE(0, 10);
|
|
809
|
+
|
|
810
|
+
const payload = Buffer.alloc(bytesPerLine * linesPerPacket);
|
|
811
|
+
let offset = 0;
|
|
812
|
+
for (let y = 0; y < linesPerPacket; y += 1) {
|
|
813
|
+
const absoluteLine = line + y;
|
|
814
|
+
for (let x = 0; x < width; x += 2) {
|
|
815
|
+
const low = (frameNumber + absoluteLine + x) & 0x0f;
|
|
816
|
+
const high = (frameNumber + absoluteLine + x + 1) & 0x0f;
|
|
817
|
+
payload[offset++] = low | (high << 4);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
packets.push(Buffer.concat([header, payload]));
|
|
822
|
+
sequence = (sequence + 1) & 0xffff;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return packets;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function buildMockAudioPacket(sequence, sampleIndex) {
|
|
829
|
+
const header = Buffer.alloc(2);
|
|
830
|
+
header.writeUInt16LE(sequence & 0xffff, 0);
|
|
831
|
+
|
|
832
|
+
const payload = Buffer.alloc(768);
|
|
833
|
+
for (let index = 0; index < 192; index += 1) {
|
|
834
|
+
const phase = ((sampleIndex + index) % 256) - 128;
|
|
835
|
+
const left = phase * 128;
|
|
836
|
+
const right = -left;
|
|
837
|
+
payload.writeInt16LE(left, index * 4);
|
|
838
|
+
payload.writeInt16LE(right, index * 4 + 2);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
return Buffer.concat([header, payload]);
|
|
842
|
+
}
|