@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,68 @@
1
+ /**
2
+ * Typed wrapper around the native Swift N-API addon (forgevm.node).
3
+ * Loads the prebuilt binary or falls back to a local build directory.
4
+ *
5
+ * The addon exposes low-level VM lifecycle, vsock communication, and VirtioFS
6
+ * mount primitives. This module re-exports them with TypeScript types so the
7
+ * rest of the CLI can use them safely.
8
+ */
9
+ import { createRequire } from "node:module";
10
+ import { join } from "node:path";
11
+ import { existsSync } from "node:fs";
12
+ import { vmLog } from "./log.js";
13
+ // ---------------------------------------------------------------------------
14
+ // Singleton loader
15
+ // ---------------------------------------------------------------------------
16
+ let addon = null;
17
+ /**
18
+ * Load the native addon synchronously.
19
+ * Searches for the prebuilt binary first, then falls back to a local build.
20
+ *
21
+ * Uses `createRequire` to load `.node` binaries in an ESM context
22
+ * (native addons cannot be loaded with dynamic `import()`).
23
+ */
24
+ export function loadNativeAddon() {
25
+ if (addon)
26
+ return addon;
27
+ // import.meta.dirname points to the directory of this file (src/vm/ or dist/vm/)
28
+ // The CLI package root is two levels up: vm/ -> src|dist/ -> cli/
29
+ const cliRoot = join(import.meta.dirname, "..", "..");
30
+ const paths = [
31
+ join(cliRoot, "native", "prebuilds", `${process.platform}-${process.arch}`, "forgevm.node"),
32
+ join(cliRoot, "native", "build", "Release", "forgevm.node"),
33
+ ];
34
+ const require = createRequire(import.meta.url);
35
+ for (const p of paths) {
36
+ if (existsSync(p)) {
37
+ addon = require(p);
38
+ vmLog.info("native-addon", `Loaded native addon from ${p}`);
39
+ return addon;
40
+ }
41
+ }
42
+ throw new Error("Native VM addon not found. Build it with: cd packages/cli/native && swift build && node-gyp rebuild");
43
+ }
44
+ // ---------------------------------------------------------------------------
45
+ // VM state constants (match native addon values)
46
+ // ---------------------------------------------------------------------------
47
+ export const VmState = {
48
+ CREATED: 0,
49
+ BOOTING: 1,
50
+ RUNNING: 2,
51
+ STOPPING: 3,
52
+ STOPPED: 4,
53
+ ERROR: 5,
54
+ };
55
+ /**
56
+ * Check whether the native addon binary is available on this machine.
57
+ * Returns false if the addon cannot be loaded (e.g. wrong platform, not built).
58
+ */
59
+ export function isAddonAvailable() {
60
+ try {
61
+ loadNativeAddon();
62
+ return true;
63
+ }
64
+ catch {
65
+ return false;
66
+ }
67
+ }
68
+ //# sourceMappingURL=native-addon.js.map
@@ -0,0 +1,7 @@
1
+ import type { VmProvider } from "./types.js";
2
+ /**
3
+ * Resolve the appropriate VM provider based on the current platform.
4
+ * Caches the provider instance for reuse across the CLI session.
5
+ */
6
+ export declare function resolveVmProvider(): VmProvider;
7
+ //# sourceMappingURL=provider-resolver.d.ts.map
@@ -0,0 +1,27 @@
1
+ import { VfkitProvider } from "./providers/vfkit.js";
2
+ import { QemuProvider } from "./providers/qemu.js";
3
+ import { Wsl2Provider } from "./providers/wsl2.js";
4
+ let cachedProvider = null;
5
+ /**
6
+ * Resolve the appropriate VM provider based on the current platform.
7
+ * Caches the provider instance for reuse across the CLI session.
8
+ */
9
+ export function resolveVmProvider() {
10
+ if (cachedProvider)
11
+ return cachedProvider;
12
+ switch (process.platform) {
13
+ case "darwin":
14
+ cachedProvider = new VfkitProvider();
15
+ break;
16
+ case "linux":
17
+ cachedProvider = new QemuProvider();
18
+ break;
19
+ case "win32":
20
+ cachedProvider = new Wsl2Provider();
21
+ break;
22
+ default:
23
+ throw new Error(`Unsupported platform for VM execution: ${process.platform}`);
24
+ }
25
+ return cachedProvider;
26
+ }
27
+ //# sourceMappingURL=provider-resolver.js.map
@@ -0,0 +1,17 @@
1
+ import type { VmConfig, VmInfo, VmProvider, ExecResult } from "../types.js";
2
+ /**
3
+ * Lima provider for macOS (M-series and Intel).
4
+ * Manages VM lifecycle using limactl CLI.
5
+ */
6
+ export declare class LimaProvider implements VmProvider {
7
+ readonly name = "lima";
8
+ isAvailable(): Promise<boolean>;
9
+ start(id: string, config: VmConfig): Promise<VmInfo>;
10
+ exec(id: string, cmd: string, args?: string[]): Promise<ExecResult>;
11
+ stop(id: string): Promise<void>;
12
+ destroy(id: string): Promise<void>;
13
+ getInfo(id: string): Promise<VmInfo | null>;
14
+ getVsockCid(id: string): number;
15
+ private generateLimaYaml;
16
+ }
17
+ //# sourceMappingURL=lima.d.ts.map
@@ -0,0 +1,191 @@
1
+ import { execFile } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, 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
+ const VM_BASE_DIR = join(homedir(), ".forgeai", "vm", "instances");
9
+ /**
10
+ * Lima provider for macOS (M-series and Intel).
11
+ * Manages VM lifecycle using limactl CLI.
12
+ */
13
+ export class LimaProvider {
14
+ name = "lima";
15
+ async isAvailable() {
16
+ try {
17
+ await execFileAsync("limactl", ["--version"]);
18
+ return true;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ async start(id, config) {
25
+ const instanceDir = join(VM_BASE_DIR, id);
26
+ mkdirSync(instanceDir, { recursive: true, mode: 0o700 });
27
+ const limaYamlPath = join(instanceDir, "lima.yaml");
28
+ const yaml = this.generateLimaYaml(id, config);
29
+ writeFileSync(limaYamlPath, yaml, "utf-8");
30
+ console.log(chalk.cyan(`[lima] Starting VM "${id}" with config: ${config.cpus} CPUs, ${config.memoryMB} MB RAM`));
31
+ try {
32
+ await execFileAsync("limactl", ["start", "--name", id, limaYamlPath]);
33
+ }
34
+ catch (error) {
35
+ console.error(chalk.red(`[lima] Failed to start VM: ${error instanceof Error ? error.message : String(error)}`));
36
+ throw error;
37
+ }
38
+ const info = await this.getInfo(id);
39
+ if (!info) {
40
+ throw new Error(`Failed to retrieve VM info after start for "${id}"`);
41
+ }
42
+ console.log(chalk.green(`[lima] VM "${id}" started successfully`));
43
+ return info;
44
+ }
45
+ async exec(id, cmd, args) {
46
+ const cmdParts = args ? [cmd, ...args] : [cmd];
47
+ try {
48
+ const { stdout, stderr } = await execFileAsync("limactl", [
49
+ "shell",
50
+ id,
51
+ "--",
52
+ ...cmdParts,
53
+ ]);
54
+ return {
55
+ stdout,
56
+ stderr,
57
+ exitCode: 0,
58
+ };
59
+ }
60
+ catch (error) {
61
+ const execError = error;
62
+ return {
63
+ stdout: execError.stdout ?? "",
64
+ stderr: execError.stderr ?? String(error),
65
+ exitCode: execError.code ?? 1,
66
+ };
67
+ }
68
+ }
69
+ async stop(id) {
70
+ console.log(chalk.cyan(`[lima] Stopping VM "${id}"`));
71
+ try {
72
+ await execFileAsync("limactl", ["stop", id]);
73
+ console.log(chalk.green(`[lima] VM "${id}" stopped`));
74
+ }
75
+ catch (error) {
76
+ console.error(chalk.red(`[lima] Failed to stop VM: ${error instanceof Error ? error.message : String(error)}`));
77
+ throw error;
78
+ }
79
+ }
80
+ async destroy(id) {
81
+ console.log(chalk.cyan(`[lima] Destroying VM "${id}"`));
82
+ try {
83
+ await execFileAsync("limactl", ["delete", id, "--force"]);
84
+ console.log(chalk.green(`[lima] VM "${id}" destroyed`));
85
+ }
86
+ catch (error) {
87
+ console.error(chalk.red(`[lima] Failed to destroy VM: ${error instanceof Error ? error.message : String(error)}`));
88
+ throw error;
89
+ }
90
+ const instanceDir = join(VM_BASE_DIR, id);
91
+ if (existsSync(instanceDir)) {
92
+ rmSync(instanceDir, { recursive: true, force: true });
93
+ }
94
+ }
95
+ async getInfo(id) {
96
+ try {
97
+ const { stdout } = await execFileAsync("limactl", ["list", "--json"]);
98
+ const instances = JSON.parse(stdout);
99
+ const instance = instances.find((i) => i.name === id);
100
+ if (!instance)
101
+ return null;
102
+ const vsockCid = this.getVsockCid(id);
103
+ return {
104
+ id,
105
+ status: instance.status === "Running" ? "RUNNING" : "STOPPED",
106
+ pid: instance.pid,
107
+ vsockCid,
108
+ };
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ }
114
+ getVsockCid(id) {
115
+ const instanceDir = join(VM_BASE_DIR, id);
116
+ const configPath = join(instanceDir, "lima.yaml");
117
+ if (!existsSync(configPath)) {
118
+ throw new Error(`Lima config not found for VM "${id}"`);
119
+ }
120
+ try {
121
+ const content = readFileSync(configPath, "utf-8");
122
+ const match = content.match(/guest-cid:\s*(\d+)/);
123
+ if (match && match[1]) {
124
+ return parseInt(match[1], 10);
125
+ }
126
+ }
127
+ catch {
128
+ // Fall through to default
129
+ }
130
+ return 3;
131
+ }
132
+ generateLimaYaml(id, config) {
133
+ const vsockPort = config.vsockPort;
134
+ const cpus = config.cpus;
135
+ const memoryMB = config.memoryMB;
136
+ // Validate CIDR format and generate iptables rules
137
+ const cidrRegex = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/;
138
+ const rules = config.networkAllowlist
139
+ .filter((rule) => cidrRegex.test(rule.cidr))
140
+ .map((rule) => `iptables -A OUTPUT -p ${rule.protocol} -d ${rule.cidr} --dport ${rule.port} -j ACCEPT`)
141
+ .join("\n ");
142
+ return `images:
143
+ - location: "https://cloud-images.ubuntu.com/releases/24.04/release-20240423/ubuntu-24.04-server-cloudimg-arm64.img"
144
+ arch: "aarch64"
145
+ digest: "sha256:..." # Placeholder; Lima will verify
146
+
147
+ cpus: ${cpus}
148
+ memory: "${memoryMB}M"
149
+ vmType: vz
150
+ rosetta: true
151
+
152
+ mounts:
153
+ - location: ${JSON.stringify(config.sessionDir)}
154
+ mountPoint: "/session"
155
+ writable: false
156
+
157
+ networking:
158
+ - socket: "vsock://:${vsockPort}"
159
+ protocol: "tcp"
160
+
161
+ provision:
162
+ - mode: system
163
+ script: |
164
+ #!/bin/bash
165
+ set -e
166
+ apt-get update -qq
167
+ apt-get install -y -qq iptables openssh-server
168
+
169
+ # Default policy: DROP all outbound
170
+ iptables -P OUTPUT DROP
171
+ iptables -P INPUT DROP
172
+ iptables -P FORWARD DROP
173
+
174
+ # Allow loopback
175
+ iptables -A OUTPUT -o lo -j ACCEPT
176
+ iptables -A INPUT -i lo -j ACCEPT
177
+
178
+ # Allow established/related connections
179
+ iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
180
+ iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
181
+
182
+ # Per-rule allowlist (CIDR + port + protocol)
183
+ ${rules}
184
+
185
+ hostResolver:
186
+ enabled: true
187
+ ipv6: false
188
+ `;
189
+ }
190
+ }
191
+ //# sourceMappingURL=lima.js.map
@@ -0,0 +1,28 @@
1
+ import { type ChildProcess } from "node:child_process";
2
+ import type { VmConfig, VmInfo, VmProvider, ExecResult } from "../types.js";
3
+ /**
4
+ * QEMU+KVM provider for Linux.
5
+ * Manages VM lifecycle by spawning QEMU processes with KVM acceleration.
6
+ */
7
+ export declare class QemuProvider implements VmProvider {
8
+ readonly name = "qemu";
9
+ private vms;
10
+ private nextCid;
11
+ isAvailable(): Promise<boolean>;
12
+ start(id: string, config: VmConfig): Promise<VmInfo>;
13
+ exec(id: string, cmd: string, args?: string[]): Promise<ExecResult>;
14
+ /**
15
+ * Spawn a long-lived process in the VM via SSH with piped stdout/stderr.
16
+ */
17
+ spawnStreaming(id: string, cmd: string, args: string[], cwd: string): {
18
+ pid: number;
19
+ process: ChildProcess;
20
+ };
21
+ private generateSetupScript;
22
+ private getInstanceDir;
23
+ stop(id: string): Promise<void>;
24
+ destroy(id: string): Promise<void>;
25
+ getInfo(id: string): Promise<VmInfo | null>;
26
+ getVsockPath(id: string): string;
27
+ }
28
+ //# sourceMappingURL=qemu.d.ts.map
@@ -0,0 +1,260 @@
1
+ import { spawn, execFile } 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
+ * QEMU+KVM provider for Linux.
10
+ * Manages VM lifecycle by spawning QEMU processes with KVM acceleration.
11
+ */
12
+ export class QemuProvider {
13
+ name = "qemu";
14
+ vms = new Map();
15
+ nextCid = 3;
16
+ async isAvailable() {
17
+ try {
18
+ const { execSync } = await import("node:child_process");
19
+ execSync("which qemu-system-x86_64", { stdio: "ignore" });
20
+ if (!existsSync("/dev/kvm")) {
21
+ console.warn(chalk.yellow("[qemu] /dev/kvm not found; KVM acceleration unavailable"));
22
+ return false;
23
+ }
24
+ return true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ async start(id, config) {
31
+ const cid = this.nextCid++;
32
+ const sshPort = 2222 + cid;
33
+ console.log(chalk.cyan(`[qemu] Starting VM "${id}" with ${config.cpus} CPUs, ${config.memoryMB} MB RAM, vsock CID ${cid}`));
34
+ // Generate setup script for network filtering
35
+ const setupScript = this.generateSetupScript(config);
36
+ const instanceDir = this.getInstanceDir(id);
37
+ const setupPath = join(instanceDir, "setup.sh");
38
+ // Create instance directory and write setup script
39
+ try {
40
+ mkdirSync(instanceDir, { recursive: true, mode: 0o700 });
41
+ }
42
+ catch {
43
+ // Directory may already exist
44
+ }
45
+ writeFileSync(setupPath, setupScript, { mode: 0o755 });
46
+ const qemuArgs = [
47
+ "-machine",
48
+ "pc,accelerator=kvm",
49
+ "-cpu",
50
+ "host",
51
+ "-smp",
52
+ `cores=${config.cpus}`,
53
+ "-m",
54
+ `${config.memoryMB}`,
55
+ "-drive",
56
+ `file=${config.rootfsPath},if=virtio,format=raw`,
57
+ "-device",
58
+ `vhost-vsock-pci,guest-cid=${cid}`,
59
+ "-netdev",
60
+ `user,id=net0,hostfwd=tcp:127.0.0.1:${sshPort}-:22`,
61
+ "-device",
62
+ "virtio-net-pci,netdev=net0",
63
+ "-nographic",
64
+ "-monitor",
65
+ "none",
66
+ ];
67
+ return new Promise((resolve, reject) => {
68
+ const qemuProcess = spawn("qemu-system-x86_64", qemuArgs, {
69
+ stdio: "ignore",
70
+ detached: true,
71
+ });
72
+ const pid = qemuProcess.pid;
73
+ if (!pid) {
74
+ reject(new Error("Failed to spawn QEMU process"));
75
+ return;
76
+ }
77
+ qemuProcess.unref();
78
+ this.vms.set(id, { pid, cid, sshPort });
79
+ console.log(chalk.green(`[qemu] VM "${id}" started with PID ${pid}`));
80
+ resolve({
81
+ id,
82
+ status: "RUNNING",
83
+ pid,
84
+ vsockCid: cid,
85
+ });
86
+ });
87
+ }
88
+ async exec(id, cmd, args) {
89
+ const vm = this.vms.get(id);
90
+ if (!vm) {
91
+ return {
92
+ stdout: "",
93
+ stderr: `VM "${id}" not found in running VMs`,
94
+ exitCode: 1,
95
+ };
96
+ }
97
+ console.log(chalk.dim(`[qemu] Executing in "${id}": ${cmd} ${args?.join(" ") ?? ""}`));
98
+ try {
99
+ // Shell-escape each argument for SSH remote command
100
+ const escapedArgs = [cmd, ...(args ?? [])]
101
+ .map((a) => `'${a.replace(/'/g, "'\\''")}'`)
102
+ .join(" ");
103
+ const { stdout, stderr } = await execFileAsync("ssh", [
104
+ "-p",
105
+ String(vm.sshPort),
106
+ "-o",
107
+ "StrictHostKeyChecking=no",
108
+ "-o",
109
+ "UserKnownHostsFile=/dev/null",
110
+ "-i",
111
+ join(this.getInstanceDir(id), "ssh_key"),
112
+ "root@127.0.0.1",
113
+ "--",
114
+ escapedArgs,
115
+ ]);
116
+ return { stdout, stderr, exitCode: 0 };
117
+ }
118
+ catch (error) {
119
+ const execError = error;
120
+ return {
121
+ stdout: execError.stdout ?? "",
122
+ stderr: execError.stderr ?? String(error),
123
+ exitCode: execError.code ?? 1,
124
+ };
125
+ }
126
+ }
127
+ /**
128
+ * Spawn a long-lived process in the VM via SSH with piped stdout/stderr.
129
+ */
130
+ spawnStreaming(id, cmd, args, cwd) {
131
+ const vm = this.vms.get(id);
132
+ if (!vm)
133
+ throw new Error(`VM "${id}" not running`);
134
+ const remoteCmd = args.length > 0
135
+ ? `cd '${cwd}' && '${cmd}' ${args.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ")}`
136
+ : `cd '${cwd}' && '${cmd}'`;
137
+ const sshProcess = spawn("ssh", [
138
+ "-p",
139
+ String(vm.sshPort),
140
+ "-o",
141
+ "StrictHostKeyChecking=no",
142
+ "-o",
143
+ "UserKnownHostsFile=/dev/null",
144
+ "-o",
145
+ "ServerAliveInterval=30",
146
+ "-i",
147
+ join(this.getInstanceDir(id), "ssh_key"),
148
+ "root@127.0.0.1",
149
+ "--",
150
+ remoteCmd,
151
+ ], {
152
+ stdio: ["ignore", "pipe", "pipe"],
153
+ detached: false,
154
+ });
155
+ if (sshProcess.pid === undefined) {
156
+ throw new Error("Failed to spawn SSH process — no PID assigned");
157
+ }
158
+ return { pid: sshProcess.pid, process: sshProcess };
159
+ }
160
+ generateSetupScript(config) {
161
+ // Validate CIDR format and generate iptables rules
162
+ const cidrRegex = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/;
163
+ const rules = config.networkAllowlist
164
+ .filter((rule) => cidrRegex.test(rule.cidr))
165
+ .map((rule) => `iptables -A OUTPUT -p ${rule.protocol} -d ${rule.cidr} --dport ${rule.port} -j ACCEPT`)
166
+ .join("\n");
167
+ return `#!/bin/bash
168
+ set -e
169
+
170
+ apt-get update -qq
171
+ apt-get install -y -qq iptables openssh-server git
172
+
173
+ # Default policy: DROP all outbound
174
+ iptables -P OUTPUT DROP
175
+ iptables -P INPUT DROP
176
+ iptables -P FORWARD DROP
177
+
178
+ # Allow loopback
179
+ iptables -A OUTPUT -o lo -j ACCEPT
180
+ iptables -A INPUT -i lo -j ACCEPT
181
+
182
+ # Allow established/related connections
183
+ iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
184
+ iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
185
+
186
+ # Per-rule allowlist (CIDR + port + protocol)
187
+ ${rules}
188
+
189
+ echo "[qemu] Setup complete"
190
+ `;
191
+ }
192
+ getInstanceDir(id) {
193
+ return join(homedir(), ".forgeai", "vm", "instances", id);
194
+ }
195
+ async stop(id) {
196
+ const vm = this.vms.get(id);
197
+ if (!vm) {
198
+ console.warn(chalk.yellow(`[qemu] VM "${id}" not found in running VMs`));
199
+ return;
200
+ }
201
+ console.log(chalk.cyan(`[qemu] Stopping VM "${id}" (PID ${vm.pid})`));
202
+ try {
203
+ process.kill(vm.pid, "SIGTERM");
204
+ await new Promise((resolve) => setTimeout(resolve, 2000));
205
+ }
206
+ catch {
207
+ // Process may already be dead
208
+ }
209
+ this.vms.delete(id);
210
+ console.log(chalk.green(`[qemu] VM "${id}" stopped`));
211
+ }
212
+ async destroy(id) {
213
+ const vm = this.vms.get(id);
214
+ if (vm) {
215
+ try {
216
+ process.kill(vm.pid, "SIGKILL");
217
+ }
218
+ catch {
219
+ // Process may already be dead
220
+ }
221
+ this.vms.delete(id);
222
+ }
223
+ const instanceDir = join(homedir(), ".forgeai", "vm", "instances", id);
224
+ if (existsSync(instanceDir)) {
225
+ rmSync(instanceDir, { recursive: true, force: true });
226
+ }
227
+ console.log(chalk.green(`[qemu] VM "${id}" destroyed`));
228
+ }
229
+ async getInfo(id) {
230
+ const vm = this.vms.get(id);
231
+ if (!vm)
232
+ return null;
233
+ try {
234
+ process.kill(vm.pid, 0);
235
+ return {
236
+ id,
237
+ status: "RUNNING",
238
+ pid: vm.pid,
239
+ vsockCid: vm.cid,
240
+ };
241
+ }
242
+ catch {
243
+ this.vms.delete(id);
244
+ return {
245
+ id,
246
+ status: "STOPPED",
247
+ vsockCid: vm.cid,
248
+ };
249
+ }
250
+ }
251
+ getVsockPath(id) {
252
+ const vm = this.vms.get(id);
253
+ if (!vm) {
254
+ throw new Error(`VM "${id}" not found in running VMs`);
255
+ }
256
+ // QEMU uses TCP for vsock bridging
257
+ return `tcp:127.0.0.1:${6789 + vm.cid}`;
258
+ }
259
+ }
260
+ //# sourceMappingURL=qemu.js.map
@@ -0,0 +1,40 @@
1
+ import { type ChildProcess } from "node:child_process";
2
+ import type { VmConfig, VmInfo, VmProvider, ExecResult } from "../types.js";
3
+ /**
4
+ * vfkit provider for macOS.
5
+ * Uses Apple's Virtualization Framework via the vfkit binary (auto-downloaded).
6
+ * No brew install needed — single binary CLI.
7
+ */
8
+ export declare class VfkitProvider implements VmProvider {
9
+ readonly name = "vfkit";
10
+ private vms;
11
+ isAvailable(): Promise<boolean>;
12
+ start(id: string, config: VmConfig): Promise<VmInfo>;
13
+ /**
14
+ * Discover the guest IP from the ARP table (NAT networking gives 192.168.64.x).
15
+ */
16
+ private waitForBoot;
17
+ /**
18
+ * Get all candidate guest IPs from the ARP table (192.168.64.x range).
19
+ */
20
+ private discoverCandidateIps;
21
+ exec(id: string, cmd: string, args?: string[]): Promise<ExecResult>;
22
+ /**
23
+ * Spawn a long-lived process in the VM via SSH with piped stdout/stderr.
24
+ * Returns the SSH ChildProcess which satisfies AgentProcess.
25
+ */
26
+ spawnStreaming(id: string, cmd: string, args: string[], cwd: string, options?: {
27
+ reverseTunnels?: Array<{
28
+ remotePort: number;
29
+ localPort: number;
30
+ }>;
31
+ }): {
32
+ pid: number;
33
+ process: ChildProcess;
34
+ };
35
+ stop(id: string): Promise<void>;
36
+ destroy(id: string): Promise<void>;
37
+ getInfo(id: string): Promise<VmInfo | null>;
38
+ getVsockPath(id: string): string;
39
+ }
40
+ //# sourceMappingURL=vfkit.d.ts.map