@getforgeai/cli 0.1.0-beta.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 (221) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +80 -0
  3. package/dist/bin/forgeai.d.ts +3 -0
  4. package/dist/bin/forgeai.js +8 -0
  5. package/dist/cli.d.ts +4 -0
  6. package/dist/cli.js +180 -0
  7. package/dist/commands/auth-login.d.ts +4 -0
  8. package/dist/commands/auth-login.js +202 -0
  9. package/dist/commands/auth-logout.d.ts +2 -0
  10. package/dist/commands/auth-logout.js +41 -0
  11. package/dist/commands/auth-status.d.ts +4 -0
  12. package/dist/commands/auth-status.js +81 -0
  13. package/dist/commands/cli-list.d.ts +4 -0
  14. package/dist/commands/cli-list.js +40 -0
  15. package/dist/commands/cli-unlink.d.ts +4 -0
  16. package/dist/commands/cli-unlink.js +45 -0
  17. package/dist/commands/config-set.d.ts +5 -0
  18. package/dist/commands/config-set.js +63 -0
  19. package/dist/commands/connect.d.ts +9 -0
  20. package/dist/commands/connect.js +165 -0
  21. package/dist/commands/disconnect.d.ts +5 -0
  22. package/dist/commands/disconnect.js +126 -0
  23. package/dist/commands/journal-clear.d.ts +2 -0
  24. package/dist/commands/journal-clear.js +16 -0
  25. package/dist/commands/journal-list.d.ts +2 -0
  26. package/dist/commands/journal-list.js +20 -0
  27. package/dist/commands/journal-status.d.ts +2 -0
  28. package/dist/commands/journal-status.js +18 -0
  29. package/dist/commands/mcp-serve.d.ts +13 -0
  30. package/dist/commands/mcp-serve.js +74 -0
  31. package/dist/commands/org-list.d.ts +4 -0
  32. package/dist/commands/org-list.js +56 -0
  33. package/dist/commands/org-unlink.d.ts +4 -0
  34. package/dist/commands/org-unlink.js +63 -0
  35. package/dist/commands/reconnect.d.ts +4 -0
  36. package/dist/commands/reconnect.js +46 -0
  37. package/dist/commands/release-open.d.ts +2 -0
  38. package/dist/commands/release-open.js +43 -0
  39. package/dist/commands/session-respond.d.ts +2 -0
  40. package/dist/commands/session-respond.js +29 -0
  41. package/dist/commands/task-open.d.ts +2 -0
  42. package/dist/commands/task-open.js +42 -0
  43. package/dist/commands/vm.d.ts +21 -0
  44. package/dist/commands/vm.js +122 -0
  45. package/dist/commands/workflow-open.d.ts +2 -0
  46. package/dist/commands/workflow-open.js +43 -0
  47. package/dist/config/config-manager.d.ts +31 -0
  48. package/dist/config/config-manager.js +70 -0
  49. package/dist/config/token-manager.d.ts +11 -0
  50. package/dist/config/token-manager.js +87 -0
  51. package/dist/index.d.ts +11 -0
  52. package/dist/index.js +22 -0
  53. package/dist/lib/auth-client.d.ts +1670 -0
  54. package/dist/lib/auth-client.js +10 -0
  55. package/dist/lib/branch-manager.d.ts +79 -0
  56. package/dist/lib/branch-manager.js +620 -0
  57. package/dist/lib/clone-and-checkout.d.ts +6 -0
  58. package/dist/lib/clone-and-checkout.js +41 -0
  59. package/dist/lib/connection-manager.d.ts +58 -0
  60. package/dist/lib/connection-manager.js +436 -0
  61. package/dist/lib/daemon-entry.d.ts +2 -0
  62. package/dist/lib/daemon-entry.js +210 -0
  63. package/dist/lib/daemon.d.ts +38 -0
  64. package/dist/lib/daemon.js +148 -0
  65. package/dist/lib/event-journal.d.ts +24 -0
  66. package/dist/lib/event-journal.js +76 -0
  67. package/dist/lib/ipc-client.d.ts +10 -0
  68. package/dist/lib/ipc-client.js +70 -0
  69. package/dist/lib/ipc-server.d.ts +36 -0
  70. package/dist/lib/ipc-server.js +70 -0
  71. package/dist/lib/node-version-guard.d.ts +8 -0
  72. package/dist/lib/node-version-guard.js +60 -0
  73. package/dist/lib/open-directory.d.ts +7 -0
  74. package/dist/lib/open-directory.js +41 -0
  75. package/dist/lib/repo-manager.d.ts +29 -0
  76. package/dist/lib/repo-manager.js +103 -0
  77. package/dist/lib/resilient-emitter.d.ts +53 -0
  78. package/dist/lib/resilient-emitter.js +160 -0
  79. package/dist/lib/rest-client.d.ts +164 -0
  80. package/dist/lib/rest-client.js +139 -0
  81. package/dist/lib/skill-manager.d.ts +23 -0
  82. package/dist/lib/skill-manager.js +167 -0
  83. package/dist/lib/socket-client.d.ts +68 -0
  84. package/dist/lib/socket-client.js +160 -0
  85. package/dist/mcp/mcp-probe.d.ts +11 -0
  86. package/dist/mcp/mcp-probe.js +106 -0
  87. package/dist/mcp/mcp-response.d.ts +29 -0
  88. package/dist/mcp/mcp-response.js +67 -0
  89. package/dist/mcp/mcp-server.d.ts +19 -0
  90. package/dist/mcp/mcp-server.js +76 -0
  91. package/dist/mcp/resources/project-conventions.d.ts +4 -0
  92. package/dist/mcp/resources/project-conventions.js +50 -0
  93. package/dist/mcp/resources/task-current.d.ts +4 -0
  94. package/dist/mcp/resources/task-current.js +51 -0
  95. package/dist/mcp/tools/forge-add-group-dependency.d.ts +4 -0
  96. package/dist/mcp/tools/forge-add-group-dependency.js +20 -0
  97. package/dist/mcp/tools/forge-add-task-dependency.d.ts +4 -0
  98. package/dist/mcp/tools/forge-add-task-dependency.js +20 -0
  99. package/dist/mcp/tools/forge-ask-human.d.ts +4 -0
  100. package/dist/mcp/tools/forge-ask-human.js +80 -0
  101. package/dist/mcp/tools/forge-column-done.d.ts +4 -0
  102. package/dist/mcp/tools/forge-column-done.js +46 -0
  103. package/dist/mcp/tools/forge-complete-step.d.ts +4 -0
  104. package/dist/mcp/tools/forge-complete-step.js +139 -0
  105. package/dist/mcp/tools/forge-create-task-group.d.ts +4 -0
  106. package/dist/mcp/tools/forge-create-task-group.js +22 -0
  107. package/dist/mcp/tools/forge-create-task.d.ts +4 -0
  108. package/dist/mcp/tools/forge-create-task.js +28 -0
  109. package/dist/mcp/tools/forge-delete-task-group.d.ts +4 -0
  110. package/dist/mcp/tools/forge-delete-task-group.js +17 -0
  111. package/dist/mcp/tools/forge-delete-task.d.ts +4 -0
  112. package/dist/mcp/tools/forge-delete-task.js +17 -0
  113. package/dist/mcp/tools/forge-get-task.d.ts +4 -0
  114. package/dist/mcp/tools/forge-get-task.js +17 -0
  115. package/dist/mcp/tools/forge-merge-back.d.ts +4 -0
  116. package/dist/mcp/tools/forge-merge-back.js +179 -0
  117. package/dist/mcp/tools/forge-release-merge.d.ts +4 -0
  118. package/dist/mcp/tools/forge-release-merge.js +159 -0
  119. package/dist/mcp/tools/forge-release-notes.d.ts +4 -0
  120. package/dist/mcp/tools/forge-release-notes.js +51 -0
  121. package/dist/mcp/tools/forge-release-tag.d.ts +4 -0
  122. package/dist/mcp/tools/forge-release-tag.js +125 -0
  123. package/dist/mcp/tools/forge-remove-group-dependency.d.ts +4 -0
  124. package/dist/mcp/tools/forge-remove-group-dependency.js +19 -0
  125. package/dist/mcp/tools/forge-remove-task-dependency.d.ts +4 -0
  126. package/dist/mcp/tools/forge-remove-task-dependency.js +19 -0
  127. package/dist/mcp/tools/forge-requeue.d.ts +4 -0
  128. package/dist/mcp/tools/forge-requeue.js +39 -0
  129. package/dist/mcp/tools/forge-submit-pr.d.ts +4 -0
  130. package/dist/mcp/tools/forge-submit-pr.js +68 -0
  131. package/dist/mcp/tools/forge-update-status.d.ts +4 -0
  132. package/dist/mcp/tools/forge-update-status.js +77 -0
  133. package/dist/mcp/tools/forge-update-task-group.d.ts +4 -0
  134. package/dist/mcp/tools/forge-update-task-group.js +21 -0
  135. package/dist/mcp/tools/forge-update-task.d.ts +4 -0
  136. package/dist/mcp/tools/forge-update-task.js +25 -0
  137. package/dist/mcp/tools/forge-validate-merge.d.ts +4 -0
  138. package/dist/mcp/tools/forge-validate-merge.js +194 -0
  139. package/dist/mcp/types.d.ts +39 -0
  140. package/dist/mcp/types.js +8 -0
  141. package/dist/methodology/artifact-manager.d.ts +14 -0
  142. package/dist/methodology/artifact-manager.js +55 -0
  143. package/dist/orchestrator/agent-registry.d.ts +53 -0
  144. package/dist/orchestrator/agent-registry.js +96 -0
  145. package/dist/orchestrator/agent-spawner.d.ts +112 -0
  146. package/dist/orchestrator/agent-spawner.js +1518 -0
  147. package/dist/orchestrator/connectors/claude-code-connector.d.ts +3 -0
  148. package/dist/orchestrator/connectors/claude-code-connector.js +60 -0
  149. package/dist/orchestrator/connectors/connector.d.ts +95 -0
  150. package/dist/orchestrator/connectors/connector.js +25 -0
  151. package/dist/orchestrator/connectors/docker-connector.d.ts +14 -0
  152. package/dist/orchestrator/connectors/docker-connector.js +337 -0
  153. package/dist/orchestrator/connectors/native-vm-connector.d.ts +11 -0
  154. package/dist/orchestrator/connectors/native-vm-connector.js +162 -0
  155. package/dist/orchestrator/connectors/opencode-connector.d.ts +3 -0
  156. package/dist/orchestrator/connectors/opencode-connector.js +39 -0
  157. package/dist/orchestrator/connectors/vm-connector.d.ts +3 -0
  158. package/dist/orchestrator/connectors/vm-connector.js +139 -0
  159. package/dist/orchestrator/dispatch-queue.d.ts +43 -0
  160. package/dist/orchestrator/dispatch-queue.js +93 -0
  161. package/dist/orchestrator/resume-handler.d.ts +34 -0
  162. package/dist/orchestrator/resume-handler.js +159 -0
  163. package/dist/orchestrator/stream-batcher.d.ts +16 -0
  164. package/dist/orchestrator/stream-batcher.js +50 -0
  165. package/dist/orchestrator/stream-json-extractor.d.ts +97 -0
  166. package/dist/orchestrator/stream-json-extractor.js +579 -0
  167. package/dist/vm/claude-token-manager.d.ts +36 -0
  168. package/dist/vm/claude-token-manager.js +223 -0
  169. package/dist/vm/docker-executor.d.ts +131 -0
  170. package/dist/vm/docker-executor.js +360 -0
  171. package/dist/vm/docker-process-proxy.d.ts +31 -0
  172. package/dist/vm/docker-process-proxy.js +105 -0
  173. package/dist/vm/forge-vm-agent.d.ts +11 -0
  174. package/dist/vm/forge-vm-agent.js +159 -0
  175. package/dist/vm/git-credentials.d.ts +43 -0
  176. package/dist/vm/git-credentials.js +53 -0
  177. package/dist/vm/image-manager.d.ts +7 -0
  178. package/dist/vm/image-manager.js +61 -0
  179. package/dist/vm/log.d.ts +9 -0
  180. package/dist/vm/log.js +18 -0
  181. package/dist/vm/mcp-bridge.d.ts +63 -0
  182. package/dist/vm/mcp-bridge.js +243 -0
  183. package/dist/vm/mcp-stub.d.ts +15 -0
  184. package/dist/vm/mcp-stub.js +67 -0
  185. package/dist/vm/native-addon.d.ts +69 -0
  186. package/dist/vm/native-addon.js +68 -0
  187. package/dist/vm/provider-resolver.d.ts +7 -0
  188. package/dist/vm/provider-resolver.js +27 -0
  189. package/dist/vm/providers/lima.d.ts +17 -0
  190. package/dist/vm/providers/lima.js +191 -0
  191. package/dist/vm/providers/qemu.d.ts +28 -0
  192. package/dist/vm/providers/qemu.js +260 -0
  193. package/dist/vm/providers/vfkit.d.ts +40 -0
  194. package/dist/vm/providers/vfkit.js +371 -0
  195. package/dist/vm/providers/wsl2.d.ts +27 -0
  196. package/dist/vm/providers/wsl2.js +226 -0
  197. package/dist/vm/session-manager.d.ts +130 -0
  198. package/dist/vm/session-manager.js +494 -0
  199. package/dist/vm/session-snapshot.d.ts +31 -0
  200. package/dist/vm/session-snapshot.js +180 -0
  201. package/dist/vm/types.d.ts +73 -0
  202. package/dist/vm/types.js +2 -0
  203. package/dist/vm/vm-helper-client.d.ts +81 -0
  204. package/dist/vm/vm-helper-client.js +335 -0
  205. package/dist/vm/vm-helper.d.ts +11 -0
  206. package/dist/vm/vm-helper.js +156 -0
  207. package/dist/vm/vm-ipc-protocol.d.ts +84 -0
  208. package/dist/vm/vm-ipc-protocol.js +9 -0
  209. package/dist/vm/vm-manager-facade.d.ts +23 -0
  210. package/dist/vm/vm-manager-facade.js +43 -0
  211. package/dist/vm/vm-manager.d.ts +74 -0
  212. package/dist/vm/vm-manager.js +515 -0
  213. package/dist/vm/vm-process-proxy.d.ts +34 -0
  214. package/dist/vm/vm-process-proxy.js +64 -0
  215. package/dist/vm/vsock-bridge.d.ts +44 -0
  216. package/dist/vm/vsock-bridge.js +140 -0
  217. package/dist/vm/vsock-protocol.d.ts +98 -0
  218. package/dist/vm/vsock-protocol.js +57 -0
  219. package/package.json +65 -0
  220. package/rootfs/Dockerfile +50 -0
  221. package/rootfs/forge-mcp-relay.mjs +158 -0
@@ -0,0 +1,371 @@
1
+ import { spawn, execFile } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
3
+ import { homedir, platform } from "node:os";
4
+ import { join } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import chalk from "chalk";
7
+ import { vmLog } from "../log.js";
8
+ const execFileAsync = promisify(execFile);
9
+ import { createHash } from "node:crypto";
10
+ const VM_BASE_DIR = join(homedir(), ".forgeai", "vm", "instances");
11
+ /**
12
+ * Generate a unique MAC address from a session ID.
13
+ * Uses 52:54:00 prefix (QEMU/KVM convention for locally administered MACs)
14
+ * + 3 bytes derived from hash of the ID.
15
+ */
16
+ function generateMac(id) {
17
+ const hash = createHash("md5").update(id).digest("hex");
18
+ return `52:54:00:${hash.slice(0, 2)}:${hash.slice(2, 4)}:${hash.slice(4, 6)}`;
19
+ }
20
+ /**
21
+ * Resolve the bundled vfkit binary path.
22
+ * The binary is vendored at packages/cli/vendor/darwin/vfkit and included in the npm package.
23
+ * No download needed — it ships with the CLI.
24
+ */
25
+ function resolveVfkitPath() {
26
+ // Walk up from dist/vm/providers/ to find vendor/darwin/vfkit
27
+ // In dev: packages/cli/src/vm/providers/vfkit.ts → ../../vendor/darwin/vfkit
28
+ // In dist: packages/cli/dist/vm/providers/vfkit.js → ../../vendor/darwin/vfkit
29
+ const cliRoot = join(import.meta.dirname, "..", "..", "..");
30
+ return join(cliRoot, "vendor", "darwin", "vfkit");
31
+ }
32
+ const VFKIT_PATH = resolveVfkitPath();
33
+ const SSH_USER = "ubuntu";
34
+ /**
35
+ * vfkit provider for macOS.
36
+ * Uses Apple's Virtualization Framework via the vfkit binary (auto-downloaded).
37
+ * No brew install needed — single binary CLI.
38
+ */
39
+ export class VfkitProvider {
40
+ name = "vfkit";
41
+ vms = new Map();
42
+ async isAvailable() {
43
+ if (platform() !== "darwin")
44
+ return false;
45
+ // Check macOS version >= 13 (Ventura) for Virtualization.framework
46
+ try {
47
+ const { stdout } = await execFileAsync("sw_vers", ["-productVersion"]);
48
+ const major = parseInt(stdout.trim().split(".")[0], 10);
49
+ if (major < 13)
50
+ return false;
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ // Check bundled vfkit binary exists
56
+ if (!existsSync(VFKIT_PATH)) {
57
+ vmLog.warn("vfkit", `Bundled vfkit binary not found at ${VFKIT_PATH}`);
58
+ return false;
59
+ }
60
+ return true;
61
+ }
62
+ async start(id, config) {
63
+ // Clean up any stale instance from a previous failed run
64
+ const existingVm = this.vms.get(id);
65
+ if (existingVm) {
66
+ console.log(chalk.dim(`[vfkit] Cleaning up stale VM "${id}" before start`));
67
+ await this.destroy(id);
68
+ }
69
+ const instanceDir = join(VM_BASE_DIR, id);
70
+ // Remove stale instance dir (previous crash may have left files without a tracked process)
71
+ if (existsSync(instanceDir)) {
72
+ rmSync(instanceDir, { recursive: true, force: true });
73
+ }
74
+ mkdirSync(instanceDir, { recursive: true, mode: 0o700 });
75
+ const vsockSocket = join(instanceDir, "vsock.sock");
76
+ const consoleLog = join(instanceDir, "console.log");
77
+ const efiVars = join(instanceDir, "efi-vars");
78
+ // Clone the base disk image per session using APFS copy-on-write (instant on macOS)
79
+ const diskImage = join(instanceDir, "disk.raw");
80
+ vmLog.dim("vfkit", `Cloning base image (APFS CoW)...`);
81
+ try {
82
+ await execFileAsync("cp", ["-c", config.rootfsPath, diskImage], {
83
+ timeout: 30_000,
84
+ });
85
+ }
86
+ catch {
87
+ // Fallback to regular copy if APFS clone not supported (non-APFS volume)
88
+ vmLog.dim("vfkit", `APFS clone failed, falling back to regular copy...`);
89
+ await execFileAsync("cp", [config.rootfsPath, diskImage], {
90
+ timeout: 300_000,
91
+ });
92
+ }
93
+ // Generate SSH key pair for this VM session
94
+ const sshKeyPath = join(instanceDir, "ssh_key");
95
+ await execFileAsync("ssh-keygen", [
96
+ "-t",
97
+ "ed25519",
98
+ "-f",
99
+ sshKeyPath,
100
+ "-N",
101
+ "",
102
+ "-q",
103
+ ]);
104
+ const pubKey = readFileSync(`${sshKeyPath}.pub`, "utf-8").trim();
105
+ // Write per-session cloud-init with the SSH key
106
+ const userData = `#cloud-config
107
+ ssh_authorized_keys:
108
+ - ${pubKey}
109
+ hostname: forgeai-agent
110
+ package_update: false
111
+ runcmd:
112
+ - mkdir -p /workspace
113
+ `;
114
+ const metaData = `instance-id: ${id}\nlocal-hostname: forgeai-agent\n`;
115
+ writeFileSync(join(instanceDir, "user-data"), userData);
116
+ writeFileSync(join(instanceDir, "meta-data"), metaData);
117
+ vmLog.cyan("vfkit", `Starting VM "${id}" — ${config.cpus} CPUs, ${config.memoryMB}MB RAM`);
118
+ const args = [
119
+ "--cpus",
120
+ String(config.cpus),
121
+ "--memory",
122
+ String(config.memoryMB),
123
+ "--bootloader",
124
+ `efi,variable-store=${efiVars},create`,
125
+ "--device",
126
+ `virtio-blk,path=${diskImage}`,
127
+ "--device",
128
+ `virtio-fs,mountTag=session,sharedDir=${config.sessionDir}`,
129
+ "--device",
130
+ `virtio-vsock,port=${config.vsockPort},socketURL=${vsockSocket}`,
131
+ "--device",
132
+ `virtio-net,nat,mac=${generateMac(id)}`,
133
+ "--device",
134
+ `virtio-serial,logFilePath=${consoleLog}`,
135
+ "--cloud-init",
136
+ `${join(instanceDir, "user-data")},${join(instanceDir, "meta-data")}`,
137
+ ];
138
+ const child = spawn(VFKIT_PATH, args, {
139
+ stdio: ["ignore", "pipe", "pipe"],
140
+ detached: true,
141
+ });
142
+ if (!child.pid) {
143
+ throw new Error("Failed to start vfkit — no PID");
144
+ }
145
+ // Log stderr for debugging
146
+ child.stderr?.on("data", (data) => {
147
+ const text = data.toString().trim();
148
+ if (text)
149
+ console.log(chalk.dim(`[vfkit:${id}] ${text}`));
150
+ });
151
+ child.on("exit", (code) => {
152
+ vmLog.dim("vfkit", `VM "${id}" exited (code: ${code})`);
153
+ this.vms.delete(id);
154
+ });
155
+ const vm = {
156
+ process: child,
157
+ pid: child.pid,
158
+ guestIp: "",
159
+ vsockSocket,
160
+ instanceDir,
161
+ };
162
+ this.vms.set(id, vm);
163
+ // Discover guest IP and wait for SSH readiness
164
+ await this.waitForBoot(id);
165
+ vmLog.info("vfkit", `VM "${id}" running (PID: ${child.pid})`);
166
+ return {
167
+ id,
168
+ status: "RUNNING",
169
+ pid: child.pid,
170
+ vsockCid: 3, // vfkit uses vsock Unix socket, CID is implicit
171
+ };
172
+ }
173
+ /**
174
+ * Discover the guest IP from the ARP table (NAT networking gives 192.168.64.x).
175
+ */
176
+ async waitForBoot(id) {
177
+ const vm = this.vms.get(id);
178
+ if (!vm)
179
+ throw new Error(`VM "${id}" not tracked — cannot wait for boot`);
180
+ const maxWait = 120_000;
181
+ const interval = 3_000;
182
+ const start = Date.now();
183
+ let attempts = 0;
184
+ while (Date.now() - start < maxWait) {
185
+ attempts++;
186
+ const elapsed = Math.round((Date.now() - start) / 1000);
187
+ vmLog.dim("vfkit", `Waiting for VM "${id}" (attempt ${attempts}, ${elapsed}s elapsed)`);
188
+ // Get all candidate IPs from ARP table
189
+ const candidateIps = await this.discoverCandidateIps();
190
+ // Try SSH on each candidate with OUR key (filters out other VMs)
191
+ for (const ip of candidateIps) {
192
+ try {
193
+ await execFileAsync("ssh", [
194
+ "-o",
195
+ "StrictHostKeyChecking=no",
196
+ "-o",
197
+ "UserKnownHostsFile=/dev/null",
198
+ "-o",
199
+ "ConnectTimeout=2",
200
+ "-o",
201
+ "BatchMode=yes",
202
+ "-i",
203
+ join(vm.instanceDir, "ssh_key"),
204
+ `${SSH_USER}@${ip}`,
205
+ "echo ready",
206
+ ], { timeout: 5_000 });
207
+ // SSH works with our key — this is our VM
208
+ vm.guestIp = ip;
209
+ vmLog.info("vfkit", `VM "${id}" ready (IP: ${ip})`);
210
+ return;
211
+ }
212
+ catch {
213
+ // This IP doesn't accept our key or isn't ready — try next
214
+ }
215
+ }
216
+ await new Promise((resolve) => setTimeout(resolve, interval));
217
+ }
218
+ throw new Error(`VM "${id}" boot timeout — SSH not ready after ${maxWait / 1000}s`);
219
+ }
220
+ /**
221
+ * Get all candidate guest IPs from the ARP table (192.168.64.x range).
222
+ */
223
+ async discoverCandidateIps() {
224
+ try {
225
+ const { stdout } = await execFileAsync("arp", ["-a"], { timeout: 5_000 });
226
+ const ips = [];
227
+ for (const match of stdout.matchAll(/\(192\.168\.64\.(\d+)\)/g)) {
228
+ const ip = `192.168.64.${match[1]}`;
229
+ if (ip !== "192.168.64.1" && ip !== "192.168.64.255") {
230
+ ips.push(ip);
231
+ }
232
+ }
233
+ return ips;
234
+ }
235
+ catch {
236
+ return [];
237
+ }
238
+ }
239
+ async exec(id, cmd, args) {
240
+ const vm = this.vms.get(id);
241
+ if (!vm)
242
+ throw new Error(`VM "${id}" not running`);
243
+ if (!vm.guestIp)
244
+ throw new Error(`VM "${id}" has no guest IP — boot may have failed`);
245
+ const cmdParts = args ? [cmd, ...args] : [cmd];
246
+ // Shell-escape each argument for SSH remote command
247
+ const escapedArgs = cmdParts
248
+ .map((a) => `'${a.replace(/'/g, "'\\''")}'`)
249
+ .join(" ");
250
+ try {
251
+ const { stdout, stderr } = await execFileAsync("ssh", [
252
+ "-A",
253
+ "-o",
254
+ "StrictHostKeyChecking=no",
255
+ "-o",
256
+ "UserKnownHostsFile=/dev/null",
257
+ "-i",
258
+ join(vm.instanceDir, "ssh_key"),
259
+ `${SSH_USER}@${vm.guestIp}`,
260
+ "--",
261
+ escapedArgs,
262
+ ], { timeout: 600_000 });
263
+ return { stdout, stderr, exitCode: 0 };
264
+ }
265
+ catch (error) {
266
+ const execError = error;
267
+ return {
268
+ stdout: execError.stdout ?? "",
269
+ stderr: execError.stderr ?? String(error),
270
+ exitCode: execError.code ?? 1,
271
+ };
272
+ }
273
+ }
274
+ /**
275
+ * Spawn a long-lived process in the VM via SSH with piped stdout/stderr.
276
+ * Returns the SSH ChildProcess which satisfies AgentProcess.
277
+ */
278
+ spawnStreaming(id, cmd, args, cwd, options) {
279
+ const vm = this.vms.get(id);
280
+ if (!vm)
281
+ throw new Error(`VM "${id}" not running`);
282
+ // Build remote command with cd to cwd
283
+ const escapedArgs = args
284
+ .map((a) => `'${a.replace(/'/g, "'\\''")}'`)
285
+ .join(" ");
286
+ const cmdStr = args.length > 0 ? `'${cmd}' ${escapedArgs}` : `'${cmd}'`;
287
+ // Pass ANTHROPIC_API_KEY if explicitly set (overrides everything else)
288
+ const apiKey = process.env.ANTHROPIC_API_KEY ?? "";
289
+ const envPrefix = apiKey ? `ANTHROPIC_API_KEY='${apiKey}' ` : "";
290
+ const remoteCmd = `${envPrefix}cd '${cwd}' && ${cmdStr}`;
291
+ if (!vm.guestIp)
292
+ throw new Error(`VM "${id}" has no guest IP`);
293
+ // Build SSH args with optional reverse tunnels for MCP bridge
294
+ const tunnelArgs = [];
295
+ for (const t of options?.reverseTunnels ?? []) {
296
+ tunnelArgs.push("-R", `${t.remotePort}:localhost:${t.localPort}`);
297
+ }
298
+ const sshProcess = spawn("ssh", [
299
+ "-A",
300
+ ...tunnelArgs,
301
+ "-o",
302
+ "StrictHostKeyChecking=no",
303
+ "-o",
304
+ "UserKnownHostsFile=/dev/null",
305
+ "-o",
306
+ "ServerAliveInterval=30",
307
+ "-i",
308
+ join(vm.instanceDir, "ssh_key"),
309
+ `${SSH_USER}@${vm.guestIp}`,
310
+ "--",
311
+ remoteCmd,
312
+ ], {
313
+ stdio: ["ignore", "pipe", "pipe"],
314
+ detached: false,
315
+ });
316
+ if (sshProcess.pid === undefined) {
317
+ throw new Error("Failed to spawn SSH process — no PID assigned");
318
+ }
319
+ return { pid: sshProcess.pid, process: sshProcess };
320
+ }
321
+ async stop(id) {
322
+ const vm = this.vms.get(id);
323
+ if (!vm)
324
+ return;
325
+ vmLog.cyan("vfkit", `Stopping VM "${id}"`);
326
+ try {
327
+ vm.process.kill("SIGTERM");
328
+ await new Promise((resolve) => {
329
+ const timeout = setTimeout(() => {
330
+ if (vm.process.exitCode === null)
331
+ vm.process.kill("SIGKILL");
332
+ resolve();
333
+ }, 10_000);
334
+ vm.process.once("exit", () => {
335
+ clearTimeout(timeout);
336
+ resolve();
337
+ });
338
+ });
339
+ }
340
+ catch {
341
+ // Best-effort
342
+ }
343
+ this.vms.delete(id);
344
+ vmLog.info("vfkit", `VM "${id}" stopped`);
345
+ }
346
+ async destroy(id) {
347
+ await this.stop(id);
348
+ const instanceDir = join(VM_BASE_DIR, id);
349
+ if (existsSync(instanceDir)) {
350
+ rmSync(instanceDir, { recursive: true, force: true });
351
+ }
352
+ vmLog.info("vfkit", `VM "${id}" destroyed`);
353
+ }
354
+ async getInfo(id) {
355
+ const vm = this.vms.get(id);
356
+ if (!vm)
357
+ return null;
358
+ return {
359
+ id,
360
+ status: vm.process.exitCode === null ? "RUNNING" : "STOPPED",
361
+ pid: vm.pid,
362
+ };
363
+ }
364
+ getVsockPath(id) {
365
+ const vm = this.vms.get(id);
366
+ if (!vm)
367
+ throw new Error(`VM "${id}" not running — cannot get vsock path`);
368
+ return vm.vsockSocket;
369
+ }
370
+ }
371
+ //# sourceMappingURL=vfkit.js.map
@@ -0,0 +1,27 @@
1
+ import { type ChildProcess } from "node:child_process";
2
+ import type { VmConfig, VmInfo, VmProvider, ExecResult } from "../types.js";
3
+ /**
4
+ * WSL2 provider for Windows.
5
+ * Manages VM lifecycle using wsl CLI and Windows Hyper-V.
6
+ */
7
+ export declare class Wsl2Provider implements VmProvider {
8
+ readonly name = "wsl2";
9
+ private vms;
10
+ private vmidMap;
11
+ isAvailable(): Promise<boolean>;
12
+ start(id: string, config: VmConfig): Promise<VmInfo>;
13
+ exec(id: string, cmd: string, args?: string[]): Promise<ExecResult>;
14
+ stop(id: string): Promise<void>;
15
+ destroy(id: string): Promise<void>;
16
+ getInfo(id: string): Promise<VmInfo | null>;
17
+ getVsockPath(id: string): string;
18
+ /**
19
+ * Spawn a long-lived process in the WSL2 distro with piped stdout/stderr.
20
+ */
21
+ spawnStreaming(id: string, cmd: string, args: string[], cwd: string): {
22
+ pid: number;
23
+ process: ChildProcess;
24
+ };
25
+ private generateSetupScript;
26
+ }
27
+ //# sourceMappingURL=wsl2.d.ts.map
@@ -0,0 +1,226 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import chalk from "chalk";
7
+ const execFileAsync = promisify(execFile);
8
+ /**
9
+ * WSL2 provider for Windows.
10
+ * Manages VM lifecycle using wsl CLI and Windows Hyper-V.
11
+ */
12
+ export class Wsl2Provider {
13
+ name = "wsl2";
14
+ vms = new Map();
15
+ vmidMap = new Map();
16
+ async isAvailable() {
17
+ try {
18
+ await execFileAsync("wsl", ["--status"]);
19
+ return true;
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
25
+ async start(id, config) {
26
+ const distroName = `forgeai-${id}`;
27
+ const sessionDir = config.sessionDir;
28
+ const rootfsDir = join(sessionDir, "rootfs");
29
+ mkdirSync(rootfsDir, { recursive: true, mode: 0o700 });
30
+ console.log(chalk.cyan(`[wsl2] Importing distro "${distroName}" from rootfs at ${config.rootfsPath}`));
31
+ try {
32
+ await execFileAsync("wsl", [
33
+ "--import",
34
+ distroName,
35
+ rootfsDir,
36
+ config.rootfsPath,
37
+ ]);
38
+ }
39
+ catch (error) {
40
+ console.error(chalk.red(`[wsl2] Failed to import distro: ${error instanceof Error ? error.message : String(error)}`));
41
+ throw error;
42
+ }
43
+ const setupScript = this.generateSetupScript(config);
44
+ const setupPath = join(rootfsDir, ".setup.sh");
45
+ writeFileSync(setupPath, setupScript, { mode: 0o755 });
46
+ console.log(chalk.cyan(`[wsl2] Running setup script in "${distroName}"`));
47
+ try {
48
+ await execFileAsync("wsl", [
49
+ "-d",
50
+ distroName,
51
+ "--exec",
52
+ "bash",
53
+ "/.setup.sh",
54
+ ]);
55
+ }
56
+ catch (error) {
57
+ console.error(chalk.red(`[wsl2] Setup script failed: ${error instanceof Error ? error.message : String(error)}`));
58
+ throw error;
59
+ }
60
+ const vmid = `vm-${id}`;
61
+ this.vms.set(id, {
62
+ distroName,
63
+ cid: 0,
64
+ vmid,
65
+ });
66
+ this.vmidMap.set(distroName, vmid);
67
+ console.log(chalk.green(`[wsl2] Distro "${distroName}" started successfully`));
68
+ return {
69
+ id,
70
+ status: "RUNNING",
71
+ vsockCid: 0,
72
+ };
73
+ }
74
+ async exec(id, cmd, args) {
75
+ const vm = this.vms.get(id);
76
+ if (!vm) {
77
+ return {
78
+ stdout: "",
79
+ stderr: `Distro "${id}" not found in running WSL2 distros`,
80
+ exitCode: 1,
81
+ };
82
+ }
83
+ const cmdParts = args ? [cmd, ...args] : [cmd];
84
+ try {
85
+ const { stdout, stderr } = await execFileAsync("wsl", [
86
+ "-d",
87
+ vm.distroName,
88
+ "--",
89
+ ...cmdParts,
90
+ ]);
91
+ return {
92
+ stdout,
93
+ stderr,
94
+ exitCode: 0,
95
+ };
96
+ }
97
+ catch (error) {
98
+ const execError = error;
99
+ return {
100
+ stdout: execError.stdout ?? "",
101
+ stderr: execError.stderr ?? String(error),
102
+ exitCode: execError.code ?? 1,
103
+ };
104
+ }
105
+ }
106
+ async stop(id) {
107
+ const vm = this.vms.get(id);
108
+ if (!vm) {
109
+ console.warn(chalk.yellow(`[wsl2] Distro "${id}" not found`));
110
+ return;
111
+ }
112
+ console.log(chalk.cyan(`[wsl2] Terminating distro "${vm.distroName}"`));
113
+ try {
114
+ await execFileAsync("wsl", ["--terminate", vm.distroName]);
115
+ console.log(chalk.green(`[wsl2] Distro "${vm.distroName}" terminated`));
116
+ }
117
+ catch (error) {
118
+ console.error(chalk.red(`[wsl2] Failed to terminate distro: ${error instanceof Error ? error.message : String(error)}`));
119
+ throw error;
120
+ }
121
+ }
122
+ async destroy(id) {
123
+ const vm = this.vms.get(id);
124
+ if (!vm) {
125
+ console.warn(chalk.yellow(`[wsl2] Distro "${id}" not found`));
126
+ return;
127
+ }
128
+ console.log(chalk.cyan(`[wsl2] Unregistering distro "${vm.distroName}"`));
129
+ try {
130
+ await execFileAsync("wsl", ["--unregister", vm.distroName]);
131
+ console.log(chalk.green(`[wsl2] Distro "${vm.distroName}" unregistered`));
132
+ }
133
+ catch (error) {
134
+ console.error(chalk.red(`[wsl2] Failed to unregister distro: ${error instanceof Error ? error.message : String(error)}`));
135
+ }
136
+ this.vms.delete(id);
137
+ this.vmidMap.delete(vm.distroName);
138
+ const instanceDir = join(homedir(), ".forgeai", "vm", "instances", id);
139
+ if (existsSync(instanceDir)) {
140
+ rmSync(instanceDir, { recursive: true, force: true });
141
+ }
142
+ }
143
+ async getInfo(id) {
144
+ const vm = this.vms.get(id);
145
+ if (!vm)
146
+ return null;
147
+ try {
148
+ const { stdout } = await execFileAsync("wsl", ["--list", "--verbose"]);
149
+ const lines = stdout.split("\n");
150
+ const found = lines.some((line) => line.includes(vm.distroName));
151
+ return {
152
+ id,
153
+ status: found ? "RUNNING" : "STOPPED",
154
+ vsockCid: 0,
155
+ };
156
+ }
157
+ catch {
158
+ return {
159
+ id,
160
+ status: "ERROR",
161
+ vsockCid: 0,
162
+ };
163
+ }
164
+ }
165
+ getVsockPath(id) {
166
+ const vm = this.vms.get(id);
167
+ if (!vm) {
168
+ throw new Error(`Distro "${id}" not found in running WSL2 distros`);
169
+ }
170
+ // WSL2 uses AF_HYPERV — exposed as TCP for Node.js compatibility
171
+ return `tcp:127.0.0.1:${6789 + vm.cid}`;
172
+ }
173
+ /**
174
+ * Spawn a long-lived process in the WSL2 distro with piped stdout/stderr.
175
+ */
176
+ spawnStreaming(id, cmd, args, cwd) {
177
+ const vm = this.vms.get(id);
178
+ if (!vm)
179
+ throw new Error(`Distro "${id}" not running`);
180
+ // Build the command to run inside WSL with cd to cwd
181
+ const remoteCmd = args.length > 0
182
+ ? `cd '${cwd}' && '${cmd}' ${args.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ")}`
183
+ : `cd '${cwd}' && '${cmd}'`;
184
+ const wslProcess = spawn("wsl", ["-d", vm.distroName, "--", "bash", "-c", remoteCmd], {
185
+ stdio: ["ignore", "pipe", "pipe"],
186
+ detached: false,
187
+ });
188
+ if (wslProcess.pid === undefined) {
189
+ throw new Error("Failed to spawn WSL process — no PID assigned");
190
+ }
191
+ return { pid: wslProcess.pid, process: wslProcess };
192
+ }
193
+ generateSetupScript(config) {
194
+ // Validate CIDR format and generate iptables rules
195
+ const cidrRegex = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/;
196
+ const rules = config.networkAllowlist
197
+ .filter((rule) => cidrRegex.test(rule.cidr))
198
+ .map((rule) => `iptables -A OUTPUT -p ${rule.protocol} -d ${rule.cidr} --dport ${rule.port} -j ACCEPT`)
199
+ .join("\n");
200
+ return `#!/bin/bash
201
+ set -e
202
+
203
+ apt-get update -qq
204
+ apt-get install -y -qq iptables openssh-server git
205
+
206
+ # Default policy: DROP all outbound
207
+ iptables -P OUTPUT DROP
208
+ iptables -P INPUT DROP
209
+ iptables -P FORWARD DROP
210
+
211
+ # Allow loopback
212
+ iptables -A OUTPUT -o lo -j ACCEPT
213
+ iptables -A INPUT -i lo -j ACCEPT
214
+
215
+ # Allow established/related connections
216
+ iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
217
+ iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
218
+
219
+ # Per-rule allowlist (CIDR + port + protocol)
220
+ ${rules}
221
+
222
+ echo "[wsl2] Setup complete"
223
+ `;
224
+ }
225
+ }
226
+ //# sourceMappingURL=wsl2.js.map