@ruifung/codemode-bridge 1.0.3-1

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.
Files changed (39) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +378 -0
  3. package/dist/cli/commands.d.ts +70 -0
  4. package/dist/cli/commands.js +436 -0
  5. package/dist/cli/config-manager.d.ts +53 -0
  6. package/dist/cli/config-manager.js +142 -0
  7. package/dist/cli/index.d.ts +19 -0
  8. package/dist/cli/index.js +165 -0
  9. package/dist/executor/container-executor.d.ts +81 -0
  10. package/dist/executor/container-executor.js +351 -0
  11. package/dist/executor/executor-test-suite.d.ts +22 -0
  12. package/dist/executor/executor-test-suite.js +395 -0
  13. package/dist/executor/isolated-vm-executor.d.ts +78 -0
  14. package/dist/executor/isolated-vm-executor.js +368 -0
  15. package/dist/executor/vm2-executor.d.ts +21 -0
  16. package/dist/executor/vm2-executor.js +109 -0
  17. package/dist/executor/wrap-code.d.ts +52 -0
  18. package/dist/executor/wrap-code.js +80 -0
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.js +6 -0
  21. package/dist/mcp/config.d.ts +44 -0
  22. package/dist/mcp/config.js +102 -0
  23. package/dist/mcp/e2e-bridge-test-suite.d.ts +28 -0
  24. package/dist/mcp/e2e-bridge-test-suite.js +429 -0
  25. package/dist/mcp/executor.d.ts +31 -0
  26. package/dist/mcp/executor.js +121 -0
  27. package/dist/mcp/mcp-adapter.d.ts +12 -0
  28. package/dist/mcp/mcp-adapter.js +49 -0
  29. package/dist/mcp/mcp-client.d.ts +85 -0
  30. package/dist/mcp/mcp-client.js +441 -0
  31. package/dist/mcp/oauth-handler.d.ts +33 -0
  32. package/dist/mcp/oauth-handler.js +138 -0
  33. package/dist/mcp/server.d.ts +25 -0
  34. package/dist/mcp/server.js +322 -0
  35. package/dist/mcp/token-persistence.d.ts +57 -0
  36. package/dist/mcp/token-persistence.js +131 -0
  37. package/dist/utils/logger.d.ts +44 -0
  38. package/dist/utils/logger.js +123 -0
  39. package/package.json +56 -0
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Code Mode Bridge CLI
4
+ *
5
+ * Main entry point for the command-line interface
6
+ *
7
+ * Usage:
8
+ * codemode-bridge run [options] - Start the bridge server
9
+ * codemode-bridge config list [options] - List configured servers
10
+ * codemode-bridge config show <name> - Show a server configuration
11
+ * codemode-bridge config add <name> - Add a new server
12
+ * codemode-bridge config remove <name> - Remove a server
13
+ * codemode-bridge config edit <name> - Edit a server
14
+ * codemode-bridge config info - Show config file information
15
+ * codemode-bridge auth list [options] - List OAuth-enabled servers and status
16
+ * codemode-bridge auth login <name> - Prepare to login to an OAuth server
17
+ * codemode-bridge auth logout <name> - Logout from an OAuth server
18
+ */
19
+ import { Command } from "commander";
20
+ import { runServer, listServersCommand, showServerCommand, addServerCommand, removeServerCommand, editServerCommand, configInfoCommand, authLoginCommand, authLogoutCommand, authListCommand, } from "./commands.js";
21
+ import * as fs from "fs";
22
+ const pkg = JSON.parse(fs.readFileSync(new URL("../../package.json", import.meta.url), "utf-8"));
23
+ const program = new Command();
24
+ program.name("codemode-bridge").description("Code Mode Bridge CLI").version(pkg.version);
25
+ // Main 'run' command
26
+ program
27
+ .command("run")
28
+ .description("Start the bridge MCP server (default command)")
29
+ .option("-c, --config <path>", "Path to mcp.json configuration file (default: ~/.config/codemode-bridge/mcp.json)")
30
+ .option("-s, --servers <names>", "Comma-separated list of servers to connect to")
31
+ .option("-d, --debug", "Enable debug logging")
32
+ .action(async (options) => {
33
+ const servers = options.servers ? options.servers.split(",").map((s) => s.trim()) : undefined;
34
+ await runServer(options.config, servers, options.debug);
35
+ });
36
+ // Config command group
37
+ const config = program.command("config").description("Manage bridge configuration");
38
+ config
39
+ .command("list")
40
+ .description("List all configured servers")
41
+ .option("-c, --config <path>", "Path to mcp.json configuration file")
42
+ .action((options) => {
43
+ listServersCommand(options.config);
44
+ });
45
+ config
46
+ .command("show <name>")
47
+ .description("Show a server configuration")
48
+ .option("-c, --config <path>", "Path to mcp.json configuration file")
49
+ .action((name, options) => {
50
+ showServerCommand(name, options.config);
51
+ });
52
+ config
53
+ .command("add <name> [commandAndArgs...]")
54
+ .description("Add a new server configuration")
55
+ .requiredOption("-t, --type <type>", "Server type (stdio or http)")
56
+ .option("--url <url>", "Server URL (required for http servers)")
57
+ .option("--env <env...>", 'Environment variables as KEY=VALUE pairs')
58
+ .option("-c, --config <path>", "Path to mcp.json configuration file")
59
+ .action((name, commandAndArgs, options) => {
60
+ let command;
61
+ let args;
62
+ // Parse command and args from positional arguments
63
+ if (commandAndArgs && commandAndArgs.length > 0) {
64
+ command = commandAndArgs[0];
65
+ if (commandAndArgs.length > 1) {
66
+ args = commandAndArgs.slice(1);
67
+ }
68
+ }
69
+ const env = {};
70
+ if (options.env) {
71
+ for (const pair of options.env) {
72
+ const [key, value] = pair.split("=");
73
+ if (key && value) {
74
+ env[key] = value;
75
+ }
76
+ }
77
+ }
78
+ addServerCommand(name, {
79
+ type: options.type,
80
+ command,
81
+ args,
82
+ url: options.url,
83
+ env: Object.keys(env).length > 0 ? env : undefined,
84
+ }, options.config);
85
+ });
86
+ config
87
+ .command("remove <name>")
88
+ .description("Remove a server configuration")
89
+ .option("-c, --config <path>", "Path to mcp.json configuration file")
90
+ .action((name, options) => {
91
+ removeServerCommand(name, options.config);
92
+ });
93
+ config
94
+ .command("edit <name> [commandAndArgs...]")
95
+ .description("Edit a server configuration")
96
+ .option("-t, --type <type>", "Server type (stdio or http)")
97
+ .option("--url <url>", "Server URL (for http servers)")
98
+ .option("--env <env...>", 'Environment variables as KEY=VALUE pairs')
99
+ .option("-c, --config <path>", "Path to mcp.json configuration file")
100
+ .action((name, commandAndArgs, options) => {
101
+ let command;
102
+ let args;
103
+ // Parse command and args from positional arguments
104
+ if (commandAndArgs && commandAndArgs.length > 0) {
105
+ command = commandAndArgs[0];
106
+ if (commandAndArgs.length > 1) {
107
+ args = commandAndArgs.slice(1);
108
+ }
109
+ }
110
+ const env = {};
111
+ if (options.env) {
112
+ for (const pair of options.env) {
113
+ const [key, value] = pair.split("=");
114
+ if (key && value) {
115
+ env[key] = value;
116
+ }
117
+ }
118
+ }
119
+ editServerCommand(name, {
120
+ type: options.type,
121
+ command,
122
+ args,
123
+ url: options.url,
124
+ env: Object.keys(env).length > 0 ? env : undefined,
125
+ }, options.config);
126
+ });
127
+ config
128
+ .command("info")
129
+ .description("Show configuration file information")
130
+ .option("-c, --config <path>", "Path to mcp.json configuration file")
131
+ .action((options) => {
132
+ configInfoCommand(options.config);
133
+ });
134
+ // Auth command group
135
+ const auth = program.command("auth").description("Manage OAuth authentication");
136
+ auth
137
+ .command("list")
138
+ .description("List all OAuth-enabled servers and their authentication status")
139
+ .option("-c, --config <path>", "Path to mcp.json configuration file")
140
+ .action((options) => {
141
+ authListCommand(options.config);
142
+ });
143
+ auth
144
+ .command("login <server-name>")
145
+ .description("Initiate OAuth login for a server")
146
+ .option("-c, --config <path>", "Path to mcp.json configuration file")
147
+ .action(async (serverName, options) => {
148
+ await authLoginCommand(serverName, options.config);
149
+ });
150
+ auth
151
+ .command("logout <server-name>")
152
+ .description("Logout from an OAuth server (clears all authentication data)")
153
+ .option("-c, --config <path>", "Path to mcp.json configuration file")
154
+ .action((serverName, options) => {
155
+ authLogoutCommand(serverName, options.config);
156
+ });
157
+ // Default command: run if no command specified
158
+ program.action(async () => {
159
+ // If no command is specified, run the bridge
160
+ const args = process.argv.slice(2);
161
+ if (args.length === 0) {
162
+ await runServer(undefined, undefined, false);
163
+ }
164
+ });
165
+ program.parse();
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Container-based Executor — maximum isolation.
3
+ *
4
+ * Runs LLM-generated code inside a Docker/Podman container with:
5
+ * --network=none (no network access)
6
+ * --read-only (immutable root filesystem, /tmp is writable)
7
+ * --cap-drop=ALL (no Linux capabilities)
8
+ *
9
+ * Communication uses a line-delimited JSON protocol over the container's
10
+ * stdin/stdout. Tool calls are proxied: the runner script inside the
11
+ * container sends tool-call requests, the host dispatches them to the
12
+ * real tool functions, and writes results back.
13
+ *
14
+ * The container is session-scoped — created once and reused across
15
+ * execute() calls. State is cleaned up between calls by the runner.
16
+ */
17
+ import type { Executor, ExecuteResult } from '@cloudflare/codemode';
18
+ export interface ContainerExecutorOptions {
19
+ /** Execution timeout per call in ms (default 30000) */
20
+ timeout?: number;
21
+ /** Container image (default 'node:22-slim') */
22
+ image?: string;
23
+ /** Container runtime command — 'docker' | 'podman' | auto-detect */
24
+ runtime?: string;
25
+ /** Memory limit (default '256m') */
26
+ memoryLimit?: string;
27
+ /** CPU quota as fractional CPUs (default 1.0) */
28
+ cpuLimit?: number;
29
+ }
30
+ export declare class ContainerExecutor implements Executor {
31
+ private runtime;
32
+ private image;
33
+ private timeout;
34
+ private memoryLimit;
35
+ private cpuLimit;
36
+ private containerId;
37
+ private process;
38
+ private readline;
39
+ private ready;
40
+ /** Resolved when the container sends { type: 'ready' } */
41
+ private readyResolve;
42
+ /** Pending execution — only one at a time */
43
+ private pendingExecution;
44
+ private initPromise;
45
+ constructor(options?: ContainerExecutorOptions);
46
+ /**
47
+ * Start the container and wait for the runner to signal readiness.
48
+ * Called lazily on first execute().
49
+ */
50
+ private init;
51
+ /**
52
+ * Pull the container image if not already present.
53
+ * Runs synchronously so the image is ready before we start the container.
54
+ */
55
+ private pullImage;
56
+ private _init;
57
+ /**
58
+ * Handle a line of JSON from the container's stdout.
59
+ */
60
+ private handleMessage;
61
+ /**
62
+ * Dispatch a tool call from the container to the host-side tool function.
63
+ */
64
+ private handleToolCall;
65
+ /**
66
+ * Send a message to the container via stdin.
67
+ */
68
+ private send;
69
+ /**
70
+ * Execute code inside the container.
71
+ */
72
+ execute(code: string, fns: Record<string, (...args: unknown[]) => Promise<unknown>>): Promise<ExecuteResult>;
73
+ /**
74
+ * Stop and clean up the container.
75
+ */
76
+ dispose(): void;
77
+ }
78
+ /**
79
+ * Factory function matching the pattern of other executors.
80
+ */
81
+ export declare function createContainerExecutor(options?: ContainerExecutorOptions): ContainerExecutor;
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Container-based Executor — maximum isolation.
3
+ *
4
+ * Runs LLM-generated code inside a Docker/Podman container with:
5
+ * --network=none (no network access)
6
+ * --read-only (immutable root filesystem, /tmp is writable)
7
+ * --cap-drop=ALL (no Linux capabilities)
8
+ *
9
+ * Communication uses a line-delimited JSON protocol over the container's
10
+ * stdin/stdout. Tool calls are proxied: the runner script inside the
11
+ * container sends tool-call requests, the host dispatches them to the
12
+ * real tool functions, and writes results back.
13
+ *
14
+ * The container is session-scoped — created once and reused across
15
+ * execute() calls. State is cleaned up between calls by the runner.
16
+ */
17
+ import { spawn, execFileSync } from 'node:child_process';
18
+ import { randomBytes } from 'node:crypto';
19
+ import { createInterface } from 'node:readline';
20
+ import { fileURLToPath } from 'node:url';
21
+ import { dirname, join } from 'node:path';
22
+ import { wrapCode } from './wrap-code.js';
23
+ // ── Runtime detection ───────────────────────────────────────────────
24
+ function detectRuntime(requested) {
25
+ if (requested)
26
+ return requested;
27
+ // Try docker first, then podman
28
+ for (const cmd of ['docker', 'podman']) {
29
+ try {
30
+ execFileSync(cmd, ['--version'], { stdio: 'ignore', timeout: 5000 });
31
+ return cmd;
32
+ }
33
+ catch {
34
+ // not available
35
+ }
36
+ }
37
+ throw new Error('No container runtime found. Install Docker or Podman, ' +
38
+ 'or set CONTAINER_RUNTIME environment variable.');
39
+ }
40
+ // ── Resolve runner script path ──────────────────────────────────────
41
+ function getScriptPaths() {
42
+ // Works in both ESM and CJS contexts
43
+ let dir;
44
+ try {
45
+ // ESM
46
+ dir = dirname(fileURLToPath(import.meta.url));
47
+ }
48
+ catch {
49
+ // CJS fallback
50
+ dir = __dirname;
51
+ }
52
+ return {
53
+ runner: join(dir, 'container-runner.mjs'),
54
+ worker: join(dir, 'container-worker.mjs'),
55
+ };
56
+ }
57
+ // ── ContainerExecutor ───────────────────────────────────────────────
58
+ export class ContainerExecutor {
59
+ constructor(options = {}) {
60
+ this.containerId = null;
61
+ this.process = null;
62
+ this.readline = null;
63
+ this.ready = false;
64
+ /** Resolved when the container sends { type: 'ready' } */
65
+ this.readyResolve = null;
66
+ /** Pending execution — only one at a time */
67
+ this.pendingExecution = null;
68
+ this.initPromise = null;
69
+ this.timeout = options.timeout ?? 30000;
70
+ this.image = options.image ?? 'node:24-slim';
71
+ this.memoryLimit = options.memoryLimit ?? '256m';
72
+ this.cpuLimit = options.cpuLimit ?? 1.0;
73
+ this.runtime = detectRuntime(options.runtime);
74
+ }
75
+ /**
76
+ * Start the container and wait for the runner to signal readiness.
77
+ * Called lazily on first execute().
78
+ */
79
+ async init() {
80
+ if (this.ready)
81
+ return;
82
+ if (this.initPromise)
83
+ return this.initPromise;
84
+ this.initPromise = this._init();
85
+ return this.initPromise;
86
+ }
87
+ /**
88
+ * Pull the container image if not already present.
89
+ * Runs synchronously so the image is ready before we start the container.
90
+ */
91
+ async pullImage() {
92
+ // Check if image exists locally first
93
+ try {
94
+ execFileSync(this.runtime, ['image', 'inspect', this.image], {
95
+ stdio: 'ignore',
96
+ timeout: 10000,
97
+ });
98
+ return; // image already present
99
+ }
100
+ catch {
101
+ // image not found locally, pull it
102
+ }
103
+ // Pull the image — this can take a while on first run
104
+ return new Promise((resolve, reject) => {
105
+ const pull = spawn(this.runtime, ['pull', this.image], {
106
+ stdio: ['ignore', 'pipe', 'pipe'],
107
+ });
108
+ pull.stderr?.on('data', (data) => {
109
+ process.stderr.write(`[container] ${data.toString()}`);
110
+ });
111
+ pull.stdout?.on('data', (data) => {
112
+ process.stderr.write(`[container] ${data.toString()}`);
113
+ });
114
+ pull.on('close', (code) => {
115
+ if (code === 0) {
116
+ resolve();
117
+ }
118
+ else {
119
+ reject(new Error(`Failed to pull image '${this.image}' (exit code ${code})`));
120
+ }
121
+ });
122
+ pull.on('error', (err) => {
123
+ reject(new Error(`Failed to pull image '${this.image}': ${err.message}`));
124
+ });
125
+ });
126
+ }
127
+ async _init() {
128
+ const scripts = getScriptPaths();
129
+ // Pull the image first (no-op if already present)
130
+ await this.pullImage();
131
+ // Start a long-lived container with the runner + worker scripts
132
+ const suffix = randomBytes(4).toString('hex');
133
+ const containerName = `codemode-executor-${suffix}`;
134
+ const args = [
135
+ 'run',
136
+ '--rm',
137
+ '-i', // keep stdin open
138
+ '--name', containerName, // identifiable container name
139
+ '--network=none', // no network
140
+ '--read-only', // immutable rootfs
141
+ '--tmpfs', '/tmp:rw,noexec,nosuid,size=64m', // writable /tmp
142
+ '--cap-drop=ALL', // drop all capabilities
143
+ '--user', 'node', // run as non-root user
144
+ '--memory', this.memoryLimit,
145
+ `--cpus=${this.cpuLimit}`,
146
+ '--pids-limit=64', // limit process spawning
147
+ '-v', `${scripts.runner}:/app/container-runner.mjs:ro`, // mount runner script
148
+ '-v', `${scripts.worker}:/app/container-worker.mjs:ro`, // mount worker script
149
+ '-w', '/app',
150
+ this.image,
151
+ 'node', '/app/container-runner.mjs',
152
+ ];
153
+ this.process = spawn(this.runtime, args, {
154
+ stdio: ['pipe', 'pipe', 'pipe'],
155
+ });
156
+ // Forward stderr for debugging
157
+ this.process.stderr?.on('data', (data) => {
158
+ process.stderr.write(`[container] ${data.toString()}`);
159
+ });
160
+ this.process.on('exit', (code) => {
161
+ this.ready = false;
162
+ this.containerId = null;
163
+ // Reject any pending execution
164
+ if (this.pendingExecution) {
165
+ clearTimeout(this.pendingExecution.timeoutHandle);
166
+ this.pendingExecution.reject(new Error(`Container exited unexpectedly with code ${code}`));
167
+ this.pendingExecution = null;
168
+ }
169
+ });
170
+ // Set up line reader on stdout
171
+ this.readline = createInterface({
172
+ input: this.process.stdout,
173
+ terminal: false,
174
+ });
175
+ this.readline.on('line', (line) => this.handleMessage(line));
176
+ // Wait for the "ready" message (generous timeout to allow image pull)
177
+ await new Promise((resolve, reject) => {
178
+ const timeout = setTimeout(() => {
179
+ reject(new Error('Container failed to become ready within 120s'));
180
+ }, 120000);
181
+ // Store resolve so handleMessage can call it when ready arrives
182
+ this.readyResolve = () => {
183
+ clearTimeout(timeout);
184
+ this.ready = true;
185
+ this.readyResolve = null;
186
+ resolve();
187
+ };
188
+ // Handle startup failure
189
+ this.process.on('error', (err) => {
190
+ clearTimeout(timeout);
191
+ this.readyResolve = null;
192
+ reject(new Error(`Failed to start container: ${err.message}`));
193
+ });
194
+ });
195
+ }
196
+ /**
197
+ * Handle a line of JSON from the container's stdout.
198
+ */
199
+ handleMessage(line) {
200
+ if (!line.trim())
201
+ return;
202
+ let msg;
203
+ try {
204
+ msg = JSON.parse(line);
205
+ }
206
+ catch {
207
+ process.stderr.write(`[container-executor] bad JSON from container: ${line}\n`);
208
+ return;
209
+ }
210
+ switch (msg.type) {
211
+ case 'tool-call':
212
+ this.handleToolCall(msg);
213
+ break;
214
+ case 'result':
215
+ if (this.pendingExecution && this.pendingExecution.id === msg.id) {
216
+ clearTimeout(this.pendingExecution.timeoutHandle);
217
+ this.pendingExecution.resolve({
218
+ result: msg.result,
219
+ logs: msg.logs,
220
+ });
221
+ this.pendingExecution = null;
222
+ }
223
+ break;
224
+ case 'error':
225
+ if (this.pendingExecution && this.pendingExecution.id === msg.id) {
226
+ clearTimeout(this.pendingExecution.timeoutHandle);
227
+ this.pendingExecution.resolve({
228
+ result: undefined,
229
+ error: msg.error,
230
+ logs: msg.logs,
231
+ });
232
+ this.pendingExecution = null;
233
+ }
234
+ break;
235
+ case 'ready':
236
+ if (this.readyResolve) {
237
+ this.readyResolve();
238
+ }
239
+ break;
240
+ }
241
+ }
242
+ /**
243
+ * Dispatch a tool call from the container to the host-side tool function.
244
+ */
245
+ async handleToolCall(msg) {
246
+ if (!this.pendingExecution) {
247
+ this.send({ type: 'tool-error', id: msg.id, error: 'No active execution context' });
248
+ return;
249
+ }
250
+ const fns = this.pendingExecution.fns;
251
+ const fn = fns[msg.name];
252
+ if (!fn) {
253
+ this.send({
254
+ type: 'tool-error',
255
+ id: msg.id,
256
+ error: `Tool '${msg.name}' not found. Available tools: ${Object.keys(fns).join(', ')}`,
257
+ });
258
+ return;
259
+ }
260
+ try {
261
+ const args = msg.args;
262
+ const result = await fn(...(Array.isArray(args) ? args : [args]));
263
+ this.send({ type: 'tool-result', id: msg.id, result });
264
+ }
265
+ catch (err) {
266
+ this.send({
267
+ type: 'tool-error',
268
+ id: msg.id,
269
+ error: err instanceof Error ? err.message : String(err),
270
+ });
271
+ }
272
+ }
273
+ /**
274
+ * Send a message to the container via stdin.
275
+ */
276
+ send(msg) {
277
+ if (!this.process?.stdin?.writable) {
278
+ throw new Error('Container stdin is not writable');
279
+ }
280
+ this.process.stdin.write(JSON.stringify(msg) + '\n');
281
+ }
282
+ /**
283
+ * Execute code inside the container.
284
+ */
285
+ async execute(code, fns) {
286
+ await this.init();
287
+ if (this.pendingExecution) {
288
+ return {
289
+ result: undefined,
290
+ error: 'Another execution is already in progress',
291
+ };
292
+ }
293
+ const id = `exec-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
294
+ const wrappedCode = wrapCode(code);
295
+ return new Promise((resolve, reject) => {
296
+ const timeoutHandle = setTimeout(() => {
297
+ if (this.pendingExecution?.id === id) {
298
+ this.pendingExecution = null;
299
+ resolve({
300
+ result: undefined,
301
+ error: `Code execution timeout after ${this.timeout}ms`,
302
+ });
303
+ }
304
+ }, this.timeout);
305
+ this.pendingExecution = { id, resolve, reject, fns, timeoutHandle };
306
+ this.send({ type: 'execute', id, code: wrappedCode });
307
+ });
308
+ }
309
+ /**
310
+ * Stop and clean up the container.
311
+ */
312
+ dispose() {
313
+ if (this.pendingExecution) {
314
+ clearTimeout(this.pendingExecution.timeoutHandle);
315
+ this.pendingExecution.reject(new Error('Executor disposed'));
316
+ this.pendingExecution = null;
317
+ }
318
+ if (this.process?.stdin?.writable) {
319
+ try {
320
+ this.send({ type: 'shutdown' });
321
+ }
322
+ catch {
323
+ // ignore
324
+ }
325
+ }
326
+ if (this.readline) {
327
+ this.readline.close();
328
+ this.readline = null;
329
+ }
330
+ if (this.process) {
331
+ this.process.kill('SIGTERM');
332
+ // Force kill after 5 seconds if still alive
333
+ const proc = this.process;
334
+ setTimeout(() => {
335
+ try {
336
+ proc.kill('SIGKILL');
337
+ }
338
+ catch { /* already dead */ }
339
+ }, 5000);
340
+ this.process = null;
341
+ }
342
+ this.ready = false;
343
+ this.initPromise = null;
344
+ }
345
+ }
346
+ /**
347
+ * Factory function matching the pattern of other executors.
348
+ */
349
+ export function createContainerExecutor(options) {
350
+ return new ContainerExecutor(options);
351
+ }
@@ -0,0 +1,22 @@
1
+ import type { Executor } from '@cloudflare/codemode';
2
+ /**
3
+ * Universal executor test suite
4
+ *
5
+ * This suite can be run against any Executor implementation to verify
6
+ * compliance with the standard interface and behavior expectations.
7
+ *
8
+ * Usage:
9
+ * ```typescript
10
+ * import { createVM2Executor } from './vm2-executor';
11
+ * import { createExecutorTestSuite } from './executor.test';
12
+ *
13
+ * createExecutorTestSuite('vm2', () => createVM2Executor());
14
+ * ```
15
+ */
16
+ export interface ExecutorTestSuiteOptions {
17
+ /** Test names to skip (exact match against it() description) */
18
+ skipTests?: string[];
19
+ /** Per-test timeout in ms (default: vitest default) */
20
+ testTimeout?: number;
21
+ }
22
+ export declare function createExecutorTestSuite(name: string, createExecutor: () => Executor, options?: ExecutorTestSuiteOptions): void;