@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.
- package/LICENSE +201 -0
- package/README.md +80 -0
- package/dist/bin/forgeai.d.ts +3 -0
- package/dist/bin/forgeai.js +8 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +180 -0
- package/dist/commands/auth-login.d.ts +4 -0
- package/dist/commands/auth-login.js +202 -0
- package/dist/commands/auth-logout.d.ts +2 -0
- package/dist/commands/auth-logout.js +41 -0
- package/dist/commands/auth-status.d.ts +4 -0
- package/dist/commands/auth-status.js +81 -0
- package/dist/commands/cli-list.d.ts +4 -0
- package/dist/commands/cli-list.js +40 -0
- package/dist/commands/cli-unlink.d.ts +4 -0
- package/dist/commands/cli-unlink.js +45 -0
- package/dist/commands/config-set.d.ts +5 -0
- package/dist/commands/config-set.js +63 -0
- package/dist/commands/connect.d.ts +9 -0
- package/dist/commands/connect.js +165 -0
- package/dist/commands/disconnect.d.ts +5 -0
- package/dist/commands/disconnect.js +126 -0
- package/dist/commands/journal-clear.d.ts +2 -0
- package/dist/commands/journal-clear.js +16 -0
- package/dist/commands/journal-list.d.ts +2 -0
- package/dist/commands/journal-list.js +20 -0
- package/dist/commands/journal-status.d.ts +2 -0
- package/dist/commands/journal-status.js +18 -0
- package/dist/commands/mcp-serve.d.ts +13 -0
- package/dist/commands/mcp-serve.js +74 -0
- package/dist/commands/org-list.d.ts +4 -0
- package/dist/commands/org-list.js +56 -0
- package/dist/commands/org-unlink.d.ts +4 -0
- package/dist/commands/org-unlink.js +63 -0
- package/dist/commands/reconnect.d.ts +4 -0
- package/dist/commands/reconnect.js +46 -0
- package/dist/commands/release-open.d.ts +2 -0
- package/dist/commands/release-open.js +43 -0
- package/dist/commands/session-respond.d.ts +2 -0
- package/dist/commands/session-respond.js +29 -0
- package/dist/commands/task-open.d.ts +2 -0
- package/dist/commands/task-open.js +42 -0
- package/dist/commands/vm.d.ts +21 -0
- package/dist/commands/vm.js +122 -0
- package/dist/commands/workflow-open.d.ts +2 -0
- package/dist/commands/workflow-open.js +43 -0
- package/dist/config/config-manager.d.ts +31 -0
- package/dist/config/config-manager.js +70 -0
- package/dist/config/token-manager.d.ts +11 -0
- package/dist/config/token-manager.js +87 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +22 -0
- package/dist/lib/auth-client.d.ts +1670 -0
- package/dist/lib/auth-client.js +10 -0
- package/dist/lib/branch-manager.d.ts +79 -0
- package/dist/lib/branch-manager.js +620 -0
- package/dist/lib/clone-and-checkout.d.ts +6 -0
- package/dist/lib/clone-and-checkout.js +41 -0
- package/dist/lib/connection-manager.d.ts +58 -0
- package/dist/lib/connection-manager.js +436 -0
- package/dist/lib/daemon-entry.d.ts +2 -0
- package/dist/lib/daemon-entry.js +210 -0
- package/dist/lib/daemon.d.ts +38 -0
- package/dist/lib/daemon.js +148 -0
- package/dist/lib/event-journal.d.ts +24 -0
- package/dist/lib/event-journal.js +76 -0
- package/dist/lib/ipc-client.d.ts +10 -0
- package/dist/lib/ipc-client.js +70 -0
- package/dist/lib/ipc-server.d.ts +36 -0
- package/dist/lib/ipc-server.js +70 -0
- package/dist/lib/node-version-guard.d.ts +8 -0
- package/dist/lib/node-version-guard.js +60 -0
- package/dist/lib/open-directory.d.ts +7 -0
- package/dist/lib/open-directory.js +41 -0
- package/dist/lib/repo-manager.d.ts +29 -0
- package/dist/lib/repo-manager.js +103 -0
- package/dist/lib/resilient-emitter.d.ts +53 -0
- package/dist/lib/resilient-emitter.js +160 -0
- package/dist/lib/rest-client.d.ts +164 -0
- package/dist/lib/rest-client.js +139 -0
- package/dist/lib/skill-manager.d.ts +23 -0
- package/dist/lib/skill-manager.js +167 -0
- package/dist/lib/socket-client.d.ts +68 -0
- package/dist/lib/socket-client.js +160 -0
- package/dist/mcp/mcp-probe.d.ts +11 -0
- package/dist/mcp/mcp-probe.js +106 -0
- package/dist/mcp/mcp-response.d.ts +29 -0
- package/dist/mcp/mcp-response.js +67 -0
- package/dist/mcp/mcp-server.d.ts +19 -0
- package/dist/mcp/mcp-server.js +76 -0
- package/dist/mcp/resources/project-conventions.d.ts +4 -0
- package/dist/mcp/resources/project-conventions.js +50 -0
- package/dist/mcp/resources/task-current.d.ts +4 -0
- package/dist/mcp/resources/task-current.js +51 -0
- package/dist/mcp/tools/forge-add-group-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-add-group-dependency.js +20 -0
- package/dist/mcp/tools/forge-add-task-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-add-task-dependency.js +20 -0
- package/dist/mcp/tools/forge-ask-human.d.ts +4 -0
- package/dist/mcp/tools/forge-ask-human.js +80 -0
- package/dist/mcp/tools/forge-column-done.d.ts +4 -0
- package/dist/mcp/tools/forge-column-done.js +46 -0
- package/dist/mcp/tools/forge-complete-step.d.ts +4 -0
- package/dist/mcp/tools/forge-complete-step.js +139 -0
- package/dist/mcp/tools/forge-create-task-group.d.ts +4 -0
- package/dist/mcp/tools/forge-create-task-group.js +22 -0
- package/dist/mcp/tools/forge-create-task.d.ts +4 -0
- package/dist/mcp/tools/forge-create-task.js +28 -0
- package/dist/mcp/tools/forge-delete-task-group.d.ts +4 -0
- package/dist/mcp/tools/forge-delete-task-group.js +17 -0
- package/dist/mcp/tools/forge-delete-task.d.ts +4 -0
- package/dist/mcp/tools/forge-delete-task.js +17 -0
- package/dist/mcp/tools/forge-get-task.d.ts +4 -0
- package/dist/mcp/tools/forge-get-task.js +17 -0
- package/dist/mcp/tools/forge-merge-back.d.ts +4 -0
- package/dist/mcp/tools/forge-merge-back.js +179 -0
- package/dist/mcp/tools/forge-release-merge.d.ts +4 -0
- package/dist/mcp/tools/forge-release-merge.js +159 -0
- package/dist/mcp/tools/forge-release-notes.d.ts +4 -0
- package/dist/mcp/tools/forge-release-notes.js +51 -0
- package/dist/mcp/tools/forge-release-tag.d.ts +4 -0
- package/dist/mcp/tools/forge-release-tag.js +125 -0
- package/dist/mcp/tools/forge-remove-group-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-remove-group-dependency.js +19 -0
- package/dist/mcp/tools/forge-remove-task-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-remove-task-dependency.js +19 -0
- package/dist/mcp/tools/forge-requeue.d.ts +4 -0
- package/dist/mcp/tools/forge-requeue.js +39 -0
- package/dist/mcp/tools/forge-submit-pr.d.ts +4 -0
- package/dist/mcp/tools/forge-submit-pr.js +68 -0
- package/dist/mcp/tools/forge-update-status.d.ts +4 -0
- package/dist/mcp/tools/forge-update-status.js +77 -0
- package/dist/mcp/tools/forge-update-task-group.d.ts +4 -0
- package/dist/mcp/tools/forge-update-task-group.js +21 -0
- package/dist/mcp/tools/forge-update-task.d.ts +4 -0
- package/dist/mcp/tools/forge-update-task.js +25 -0
- package/dist/mcp/tools/forge-validate-merge.d.ts +4 -0
- package/dist/mcp/tools/forge-validate-merge.js +194 -0
- package/dist/mcp/types.d.ts +39 -0
- package/dist/mcp/types.js +8 -0
- package/dist/methodology/artifact-manager.d.ts +14 -0
- package/dist/methodology/artifact-manager.js +55 -0
- package/dist/orchestrator/agent-registry.d.ts +53 -0
- package/dist/orchestrator/agent-registry.js +96 -0
- package/dist/orchestrator/agent-spawner.d.ts +112 -0
- package/dist/orchestrator/agent-spawner.js +1518 -0
- package/dist/orchestrator/connectors/claude-code-connector.d.ts +3 -0
- package/dist/orchestrator/connectors/claude-code-connector.js +60 -0
- package/dist/orchestrator/connectors/connector.d.ts +95 -0
- package/dist/orchestrator/connectors/connector.js +25 -0
- package/dist/orchestrator/connectors/docker-connector.d.ts +14 -0
- package/dist/orchestrator/connectors/docker-connector.js +337 -0
- package/dist/orchestrator/connectors/native-vm-connector.d.ts +11 -0
- package/dist/orchestrator/connectors/native-vm-connector.js +162 -0
- package/dist/orchestrator/connectors/opencode-connector.d.ts +3 -0
- package/dist/orchestrator/connectors/opencode-connector.js +39 -0
- package/dist/orchestrator/connectors/vm-connector.d.ts +3 -0
- package/dist/orchestrator/connectors/vm-connector.js +139 -0
- package/dist/orchestrator/dispatch-queue.d.ts +43 -0
- package/dist/orchestrator/dispatch-queue.js +93 -0
- package/dist/orchestrator/resume-handler.d.ts +34 -0
- package/dist/orchestrator/resume-handler.js +159 -0
- package/dist/orchestrator/stream-batcher.d.ts +16 -0
- package/dist/orchestrator/stream-batcher.js +50 -0
- package/dist/orchestrator/stream-json-extractor.d.ts +97 -0
- package/dist/orchestrator/stream-json-extractor.js +579 -0
- package/dist/vm/claude-token-manager.d.ts +36 -0
- package/dist/vm/claude-token-manager.js +223 -0
- package/dist/vm/docker-executor.d.ts +131 -0
- package/dist/vm/docker-executor.js +360 -0
- package/dist/vm/docker-process-proxy.d.ts +31 -0
- package/dist/vm/docker-process-proxy.js +105 -0
- package/dist/vm/forge-vm-agent.d.ts +11 -0
- package/dist/vm/forge-vm-agent.js +159 -0
- package/dist/vm/git-credentials.d.ts +43 -0
- package/dist/vm/git-credentials.js +53 -0
- package/dist/vm/image-manager.d.ts +7 -0
- package/dist/vm/image-manager.js +61 -0
- package/dist/vm/log.d.ts +9 -0
- package/dist/vm/log.js +18 -0
- package/dist/vm/mcp-bridge.d.ts +63 -0
- package/dist/vm/mcp-bridge.js +243 -0
- package/dist/vm/mcp-stub.d.ts +15 -0
- package/dist/vm/mcp-stub.js +67 -0
- package/dist/vm/native-addon.d.ts +69 -0
- package/dist/vm/native-addon.js +68 -0
- package/dist/vm/provider-resolver.d.ts +7 -0
- package/dist/vm/provider-resolver.js +27 -0
- package/dist/vm/providers/lima.d.ts +17 -0
- package/dist/vm/providers/lima.js +191 -0
- package/dist/vm/providers/qemu.d.ts +28 -0
- package/dist/vm/providers/qemu.js +260 -0
- package/dist/vm/providers/vfkit.d.ts +40 -0
- package/dist/vm/providers/vfkit.js +371 -0
- package/dist/vm/providers/wsl2.d.ts +27 -0
- package/dist/vm/providers/wsl2.js +226 -0
- package/dist/vm/session-manager.d.ts +130 -0
- package/dist/vm/session-manager.js +494 -0
- package/dist/vm/session-snapshot.d.ts +31 -0
- package/dist/vm/session-snapshot.js +180 -0
- package/dist/vm/types.d.ts +73 -0
- package/dist/vm/types.js +2 -0
- package/dist/vm/vm-helper-client.d.ts +81 -0
- package/dist/vm/vm-helper-client.js +335 -0
- package/dist/vm/vm-helper.d.ts +11 -0
- package/dist/vm/vm-helper.js +156 -0
- package/dist/vm/vm-ipc-protocol.d.ts +84 -0
- package/dist/vm/vm-ipc-protocol.js +9 -0
- package/dist/vm/vm-manager-facade.d.ts +23 -0
- package/dist/vm/vm-manager-facade.js +43 -0
- package/dist/vm/vm-manager.d.ts +74 -0
- package/dist/vm/vm-manager.js +515 -0
- package/dist/vm/vm-process-proxy.d.ts +34 -0
- package/dist/vm/vm-process-proxy.js +64 -0
- package/dist/vm/vsock-bridge.d.ts +44 -0
- package/dist/vm/vsock-bridge.js +140 -0
- package/dist/vm/vsock-protocol.d.ts +98 -0
- package/dist/vm/vsock-protocol.js +57 -0
- package/package.json +65 -0
- package/rootfs/Dockerfile +50 -0
- package/rootfs/forge-mcp-relay.mjs +158 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session snapshot: save and restore Claude Code session files for workflow resume.
|
|
3
|
+
*
|
|
4
|
+
* Before destroying a container after idle timeout:
|
|
5
|
+
* 1. Auto-save code: git add -A && git commit && git push
|
|
6
|
+
* 2. Tar Claude Code session files (~/.claude/projects/)
|
|
7
|
+
* 3. Upload encrypted snapshot to Cloud via socket event
|
|
8
|
+
*
|
|
9
|
+
* On resume in a new container:
|
|
10
|
+
* 1. Download snapshot from Cloud
|
|
11
|
+
* 2. Restore Claude Code session files
|
|
12
|
+
* 3. Launch `claude --continue` (full context restored)
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { execInContainer } from "./docker-executor.js";
|
|
17
|
+
import { gitCredentialConfigShellFragment, gitCredentialSecretEnv, } from "./git-credentials.js";
|
|
18
|
+
import { vmLog } from "./log.js";
|
|
19
|
+
/** Max snapshot size: 5MB compressed */
|
|
20
|
+
const MAX_SNAPSHOT_SIZE = 5 * 1024 * 1024;
|
|
21
|
+
/**
|
|
22
|
+
* Auto-save code changes in the container (best-effort git commit + push).
|
|
23
|
+
* Does not throw — failures are logged and ignored.
|
|
24
|
+
*/
|
|
25
|
+
export async function autoSaveCode(containerId, sessionId, credentials) {
|
|
26
|
+
// Network pushes need the credential re-supplied out-of-band: the repo was
|
|
27
|
+
// cloned with a clean URL, so nothing is embedded in .git/config. The helper
|
|
28
|
+
// reads the value from FORGE_GIT_* env vars provided via secretEnv.
|
|
29
|
+
const gitPush = credentials
|
|
30
|
+
? `git ${gitCredentialConfigShellFragment()} push`
|
|
31
|
+
: "git push";
|
|
32
|
+
const pushExecOpts = credentials
|
|
33
|
+
? { timeout: 30_000, secretEnv: gitCredentialSecretEnv(credentials) }
|
|
34
|
+
: { timeout: 30_000 };
|
|
35
|
+
try {
|
|
36
|
+
// 0. Log current branch and remote
|
|
37
|
+
const branchInfo = await execInContainer(containerId, [
|
|
38
|
+
"bash",
|
|
39
|
+
"-c",
|
|
40
|
+
"cd /session/workspace && echo \"branch=$(git rev-parse --abbrev-ref HEAD) remote=$(git remote get-url origin 2>/dev/null | sed 's/\\/\\/.*@/\\/\\//') \"",
|
|
41
|
+
], { timeout: 5_000 });
|
|
42
|
+
vmLog.dim("session-snapshot", branchInfo.stdout.trim(), sessionId);
|
|
43
|
+
// 1. Stage all changes
|
|
44
|
+
await execInContainer(containerId, ["bash", "-c", "cd /session/workspace && git add -A"], { timeout: 10_000 });
|
|
45
|
+
// 2. Check if there's anything to commit
|
|
46
|
+
const diffResult = await execInContainer(containerId, ["bash", "-c", "cd /session/workspace && git diff --cached --quiet"], { timeout: 10_000 });
|
|
47
|
+
if (diffResult.exitCode === 0) {
|
|
48
|
+
vmLog.dim("session-snapshot", "No changes to commit", sessionId);
|
|
49
|
+
// Still try to push (agent may have committed but not pushed)
|
|
50
|
+
const pushResult = await execInContainer(containerId, ["bash", "-c", `cd /session/workspace && ${gitPush} 2>&1`], pushExecOpts);
|
|
51
|
+
if (pushResult.exitCode === 0) {
|
|
52
|
+
const output = pushResult.stdout.trim();
|
|
53
|
+
if (output.includes("Everything up-to-date")) {
|
|
54
|
+
vmLog.dim("session-snapshot", "Nothing to push (up-to-date)", sessionId);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
vmLog.cyan("session-snapshot", `Pushed existing commits: ${output.slice(0, 100)}`, sessionId);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
vmLog.warn("session-snapshot", `Push failed: ${pushResult.stderr.slice(0, 200)}`, sessionId);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// 3. Commit staged changes
|
|
66
|
+
const commitResult = await execInContainer(containerId, [
|
|
67
|
+
"bash",
|
|
68
|
+
"-c",
|
|
69
|
+
'cd /session/workspace && git commit -m "wip: auto-save step progress" 2>&1',
|
|
70
|
+
], { timeout: 10_000 });
|
|
71
|
+
if (commitResult.exitCode !== 0) {
|
|
72
|
+
vmLog.warn("session-snapshot", `Commit failed: ${commitResult.stdout.slice(0, 200)}`, sessionId);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
vmLog.cyan("session-snapshot", "Committed changes", sessionId);
|
|
76
|
+
// 4. Push
|
|
77
|
+
const pushResult = await execInContainer(containerId, ["bash", "-c", `cd /session/workspace && ${gitPush} 2>&1`], pushExecOpts);
|
|
78
|
+
if (pushResult.exitCode !== 0) {
|
|
79
|
+
vmLog.error("session-snapshot", `Push FAILED: ${pushResult.stderr.slice(0, 200)} ${pushResult.stdout.slice(0, 200)}`, sessionId);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
vmLog.cyan("session-snapshot", "Auto-saved code (commit + push)", sessionId);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
vmLog.warn("session-snapshot", `Auto-save failed: ${err instanceof Error ? err.message : String(err)}`, sessionId);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Save Claude Code session files from the container to a base64 tar.gz string.
|
|
91
|
+
*
|
|
92
|
+
* Returns the base64-encoded tar.gz, or null if snapshot is unavailable or too large.
|
|
93
|
+
*/
|
|
94
|
+
export async function createSessionSnapshot(session) {
|
|
95
|
+
const sessionId = session.id;
|
|
96
|
+
try {
|
|
97
|
+
// Tar the entire Claude Code config directory (~/.claude/).
|
|
98
|
+
// --continue needs: history.jsonl (global session index) + projects/<encoded-path>/ (session JSONL files)
|
|
99
|
+
// Exclude settings.json and other non-session files to keep size small.
|
|
100
|
+
const tarResult = await execInContainer(session.containerId, [
|
|
101
|
+
"bash",
|
|
102
|
+
"-c",
|
|
103
|
+
[
|
|
104
|
+
"test -d /home/agent/.claude || exit 0",
|
|
105
|
+
"tar czf /session/claude-session.tar.gz -C /home/agent/.claude" +
|
|
106
|
+
" --exclude='settings.json'" +
|
|
107
|
+
" --exclude='credentials.json'" +
|
|
108
|
+
" --exclude='statsig'" +
|
|
109
|
+
" --exclude='commands'" +
|
|
110
|
+
" --exclude='todos'" +
|
|
111
|
+
" .",
|
|
112
|
+
].join(" && "),
|
|
113
|
+
], { timeout: 30_000 });
|
|
114
|
+
if (tarResult.exitCode !== 0) {
|
|
115
|
+
vmLog.warn("session-snapshot", `Tar failed (code ${tarResult.exitCode}): ${tarResult.stderr.slice(0, 200)}`, sessionId);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
// Read the tar from the host volume
|
|
119
|
+
const tarPath = join(session.sessionDir, "claude-session.tar.gz");
|
|
120
|
+
if (!existsSync(tarPath)) {
|
|
121
|
+
vmLog.dim("session-snapshot", "No Claude session directory found in container, skipping snapshot", sessionId);
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const tarBuffer = readFileSync(tarPath);
|
|
125
|
+
// Check size limit
|
|
126
|
+
if (tarBuffer.length > MAX_SNAPSHOT_SIZE) {
|
|
127
|
+
vmLog.warn("session-snapshot", `Snapshot too large (${(tarBuffer.length / 1024 / 1024).toFixed(1)}MB > 5MB), skipping`, sessionId);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
vmLog.cyan("session-snapshot", `Created snapshot (${(tarBuffer.length / 1024).toFixed(0)}KB compressed)`, sessionId);
|
|
131
|
+
return tarBuffer.toString("base64");
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
vmLog.warn("session-snapshot", `Snapshot creation failed: ${err instanceof Error ? err.message : String(err)}`, sessionId);
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Restore Claude Code session files into a container from a base64 tar.gz string.
|
|
140
|
+
*/
|
|
141
|
+
export async function restoreSessionSnapshot(session, snapshotBase64) {
|
|
142
|
+
const sessionId = session.id;
|
|
143
|
+
try {
|
|
144
|
+
// Write the tar to the host volume
|
|
145
|
+
const tarPath = join(session.sessionDir, "claude-session.tar.gz");
|
|
146
|
+
const { writeFileSync } = await import("node:fs");
|
|
147
|
+
writeFileSync(tarPath, Buffer.from(snapshotBase64, "base64"));
|
|
148
|
+
// Create the target directory and extract (entire ~/.claude/)
|
|
149
|
+
await execInContainer(session.containerId, [
|
|
150
|
+
"mkdir",
|
|
151
|
+
"-p",
|
|
152
|
+
"/home/agent/.claude",
|
|
153
|
+
]);
|
|
154
|
+
const extractResult = await execInContainer(session.containerId, [
|
|
155
|
+
"tar",
|
|
156
|
+
"xzf",
|
|
157
|
+
"/session/claude-session.tar.gz",
|
|
158
|
+
"-C",
|
|
159
|
+
"/home/agent/.claude",
|
|
160
|
+
], { timeout: 30_000 });
|
|
161
|
+
if (extractResult.exitCode !== 0) {
|
|
162
|
+
vmLog.warn("session-snapshot", `Tar extract failed (code ${extractResult.exitCode}): ${extractResult.stderr.slice(0, 200)}`, sessionId);
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
// Fix ownership (tar may have preserved root ownership from the build)
|
|
166
|
+
await execInContainer(session.containerId, [
|
|
167
|
+
"chown",
|
|
168
|
+
"-R",
|
|
169
|
+
"agent:agent",
|
|
170
|
+
"/home/agent/.claude",
|
|
171
|
+
]);
|
|
172
|
+
vmLog.cyan("session-snapshot", "Restored Claude session snapshot", sessionId);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
vmLog.warn("session-snapshot", `Snapshot restore failed: ${err instanceof Error ? err.message : String(err)}`, sessionId);
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=session-snapshot.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
export type NetworkRule = {
|
|
3
|
+
cidr: string;
|
|
4
|
+
port: number;
|
|
5
|
+
protocol: "tcp" | "udp";
|
|
6
|
+
};
|
|
7
|
+
export type VmConfig = {
|
|
8
|
+
cpus: number;
|
|
9
|
+
memoryMB: number;
|
|
10
|
+
rootfsPath: string;
|
|
11
|
+
sessionDir: string;
|
|
12
|
+
networkAllowlist: NetworkRule[];
|
|
13
|
+
vsockPort: number;
|
|
14
|
+
};
|
|
15
|
+
export type VmInfo = {
|
|
16
|
+
id: string;
|
|
17
|
+
status: "RUNNING" | "STOPPED" | "ERROR";
|
|
18
|
+
pid?: number;
|
|
19
|
+
vsockCid?: number;
|
|
20
|
+
};
|
|
21
|
+
export type ExecResult = {
|
|
22
|
+
stdout: string;
|
|
23
|
+
stderr: string;
|
|
24
|
+
exitCode: number;
|
|
25
|
+
};
|
|
26
|
+
export type VmProvider = {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
isAvailable(): Promise<boolean>;
|
|
29
|
+
start(id: string, config: VmConfig): Promise<VmInfo>;
|
|
30
|
+
exec(id: string, cmd: string, args?: string[]): Promise<ExecResult>;
|
|
31
|
+
/** Spawn a long-lived process with piped stdout/stderr (for agent execution via SSH) */
|
|
32
|
+
spawnStreaming(id: string, cmd: string, args: string[], cwd: string, options?: {
|
|
33
|
+
reverseTunnels?: Array<{
|
|
34
|
+
remotePort: number;
|
|
35
|
+
localPort: number;
|
|
36
|
+
}>;
|
|
37
|
+
}): {
|
|
38
|
+
pid: number;
|
|
39
|
+
process: ChildProcess;
|
|
40
|
+
};
|
|
41
|
+
stop(id: string): Promise<void>;
|
|
42
|
+
destroy(id: string): Promise<void>;
|
|
43
|
+
getInfo(id: string): Promise<VmInfo | null>;
|
|
44
|
+
/** Get the vsock socket path (Unix domain socket on macOS, TCP host:port on Linux/Windows) */
|
|
45
|
+
getVsockPath(id: string): string;
|
|
46
|
+
};
|
|
47
|
+
export type SessionConfig = {
|
|
48
|
+
repoUrl?: string;
|
|
49
|
+
branch?: string;
|
|
50
|
+
workflowBaseBranch?: string;
|
|
51
|
+
taskContext?: Record<string, unknown>;
|
|
52
|
+
workflowContext?: {
|
|
53
|
+
workflowId: string;
|
|
54
|
+
stepId: string;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
export type SessionFiles = {
|
|
58
|
+
contextJson: string;
|
|
59
|
+
mcpConfigJson: string;
|
|
60
|
+
skillFiles: Array<{
|
|
61
|
+
relativePath: string;
|
|
62
|
+
content: string;
|
|
63
|
+
}>;
|
|
64
|
+
};
|
|
65
|
+
export type SessionInfo = {
|
|
66
|
+
id: string;
|
|
67
|
+
taskId: string;
|
|
68
|
+
vmInfo: VmInfo | null;
|
|
69
|
+
sessionDir: string;
|
|
70
|
+
config: SessionConfig;
|
|
71
|
+
status: "CREATED" | "BOOTING" | "RUNNING" | "STOPPED" | "DESTROYED";
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/vm/types.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VmHelperClient — parent-side IPC client for the VM helper process.
|
|
3
|
+
*
|
|
4
|
+
* Provides the same API as SessionManager but delegates all native addon
|
|
5
|
+
* operations to a child process (node-vm) that has Apple Virtualization
|
|
6
|
+
* entitlements. Communication uses Node.js built-in IPC with structured
|
|
7
|
+
* clone for efficient Buffer transfer.
|
|
8
|
+
*/
|
|
9
|
+
import { EventEmitter } from "node:events";
|
|
10
|
+
import { PassThrough } from "node:stream";
|
|
11
|
+
import type { AgentProcess } from "../orchestrator/connectors/connector.js";
|
|
12
|
+
import type { SessionState } from "./vm-ipc-protocol.js";
|
|
13
|
+
/**
|
|
14
|
+
* Parent-side process proxy that receives streaming data via IPC events.
|
|
15
|
+
* Implements AgentProcess so the orchestrator can use it identically.
|
|
16
|
+
*/
|
|
17
|
+
export declare class RemoteVmProcessProxy extends EventEmitter implements AgentProcess {
|
|
18
|
+
private readonly taskId;
|
|
19
|
+
private readonly client;
|
|
20
|
+
readonly stdout: PassThrough;
|
|
21
|
+
readonly stderr: PassThrough;
|
|
22
|
+
pid: number;
|
|
23
|
+
exitCode: number | null;
|
|
24
|
+
killed: boolean;
|
|
25
|
+
constructor(taskId: string, client: VmHelperClient);
|
|
26
|
+
setPid(pid: number): void;
|
|
27
|
+
pushStdout(data: Buffer): void;
|
|
28
|
+
pushStderr(data: Buffer): void;
|
|
29
|
+
emitExit(code: number): void;
|
|
30
|
+
kill(signal?: string): boolean;
|
|
31
|
+
}
|
|
32
|
+
export declare class VmHelperClient {
|
|
33
|
+
private child;
|
|
34
|
+
private pending;
|
|
35
|
+
private proxies;
|
|
36
|
+
private exitListeners;
|
|
37
|
+
private _vmRunning;
|
|
38
|
+
/**
|
|
39
|
+
* Spawn the helper child process.
|
|
40
|
+
* Must be called before any other method.
|
|
41
|
+
*/
|
|
42
|
+
spawn(): Promise<void>;
|
|
43
|
+
ensureVmRunning(): Promise<void>;
|
|
44
|
+
createSession(taskId: string): Promise<string>;
|
|
45
|
+
cloneRepo(sessionId: string, repoUrl: string | undefined, branch?: string, baseBranch?: string): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Write session files to the VirtioFS-shared host directory.
|
|
48
|
+
* Stays in parent process — no IPC needed (pure filesystem I/O).
|
|
49
|
+
*/
|
|
50
|
+
prepareWorkspace(sessionId: string, files: {
|
|
51
|
+
contextJson: string;
|
|
52
|
+
mcpConfigJson: string;
|
|
53
|
+
skillFiles: Array<{
|
|
54
|
+
relativePath: string;
|
|
55
|
+
content: string;
|
|
56
|
+
}>;
|
|
57
|
+
}): void;
|
|
58
|
+
spawnAgent(taskId: string, cmd: string, args: string[], env?: Record<string, string>): Promise<RemoteVmProcessProxy>;
|
|
59
|
+
killAgent(taskIdOrSessionId: string, signal?: string): Promise<void>;
|
|
60
|
+
destroySession(taskId: string): Promise<void>;
|
|
61
|
+
exec(cmd: string, args: string[], cwd?: string): Promise<{
|
|
62
|
+
stdout: string;
|
|
63
|
+
stderr: string;
|
|
64
|
+
exitCode: number;
|
|
65
|
+
}>;
|
|
66
|
+
shutdown(): Promise<void>;
|
|
67
|
+
/** Send a request on a specific child reference (used during shutdown) */
|
|
68
|
+
private requestOnChild;
|
|
69
|
+
isVmRunning(): boolean;
|
|
70
|
+
getAllSessions(): SessionState[];
|
|
71
|
+
getSessionByTaskId(_taskId: string): SessionState | undefined;
|
|
72
|
+
onSessionExit(sessionId: string, callback: (code: number) => void): void;
|
|
73
|
+
private request;
|
|
74
|
+
private handleMessage;
|
|
75
|
+
private handleResponse;
|
|
76
|
+
private handleEvent;
|
|
77
|
+
private handleChildDeath;
|
|
78
|
+
}
|
|
79
|
+
export declare function initVmHelper(): VmHelperClient;
|
|
80
|
+
export declare function getVmHelper(): VmHelperClient;
|
|
81
|
+
//# sourceMappingURL=vm-helper-client.d.ts.map
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VmHelperClient — parent-side IPC client for the VM helper process.
|
|
3
|
+
*
|
|
4
|
+
* Provides the same API as SessionManager but delegates all native addon
|
|
5
|
+
* operations to a child process (node-vm) that has Apple Virtualization
|
|
6
|
+
* entitlements. Communication uses Node.js built-in IPC with structured
|
|
7
|
+
* clone for efficient Buffer transfer.
|
|
8
|
+
*/
|
|
9
|
+
import { fork } from "node:child_process";
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { EventEmitter } from "node:events";
|
|
12
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { join, resolve } from "node:path";
|
|
15
|
+
import { PassThrough } from "node:stream";
|
|
16
|
+
import { vmLog } from "./log.js";
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Paths
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
const SESSIONS_DIR = join(homedir(), ".forgeai", "vm", "sessions");
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// RemoteVmProcessProxy
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
/**
|
|
25
|
+
* Parent-side process proxy that receives streaming data via IPC events.
|
|
26
|
+
* Implements AgentProcess so the orchestrator can use it identically.
|
|
27
|
+
*/
|
|
28
|
+
export class RemoteVmProcessProxy extends EventEmitter {
|
|
29
|
+
taskId;
|
|
30
|
+
client;
|
|
31
|
+
stdout;
|
|
32
|
+
stderr;
|
|
33
|
+
pid = -1;
|
|
34
|
+
exitCode = null;
|
|
35
|
+
killed = false;
|
|
36
|
+
constructor(taskId, client) {
|
|
37
|
+
super();
|
|
38
|
+
this.taskId = taskId;
|
|
39
|
+
this.client = client;
|
|
40
|
+
this.stdout = new PassThrough();
|
|
41
|
+
this.stderr = new PassThrough();
|
|
42
|
+
}
|
|
43
|
+
setPid(pid) {
|
|
44
|
+
this.pid = pid;
|
|
45
|
+
}
|
|
46
|
+
pushStdout(data) {
|
|
47
|
+
this.stdout.push(data);
|
|
48
|
+
}
|
|
49
|
+
pushStderr(data) {
|
|
50
|
+
this.stderr.push(data);
|
|
51
|
+
}
|
|
52
|
+
emitExit(code) {
|
|
53
|
+
this.exitCode = code;
|
|
54
|
+
this.stdout.end();
|
|
55
|
+
this.stderr.end();
|
|
56
|
+
this.emit("exit", code, null);
|
|
57
|
+
this.emit("close", code, null);
|
|
58
|
+
}
|
|
59
|
+
kill(signal) {
|
|
60
|
+
if (this.exitCode !== null || this.killed)
|
|
61
|
+
return false;
|
|
62
|
+
this.killed = true;
|
|
63
|
+
this.client.killAgent(this.taskId, signal ?? "SIGTERM").catch(() => { });
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// VmHelperClient
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
export class VmHelperClient {
|
|
71
|
+
child = null;
|
|
72
|
+
pending = new Map();
|
|
73
|
+
proxies = new Map();
|
|
74
|
+
exitListeners = new Map();
|
|
75
|
+
_vmRunning = false;
|
|
76
|
+
/**
|
|
77
|
+
* Spawn the helper child process.
|
|
78
|
+
* Must be called before any other method.
|
|
79
|
+
*/
|
|
80
|
+
async spawn() {
|
|
81
|
+
const cliRoot = join(import.meta.dirname, "..", "..");
|
|
82
|
+
const nodeVmPath = join(cliRoot, "native", "node-vm");
|
|
83
|
+
const helperScript = join(import.meta.dirname, "vm-helper.js");
|
|
84
|
+
if (!existsSync(nodeVmPath)) {
|
|
85
|
+
throw new Error(`node-vm not found at ${nodeVmPath}. Build it with:\n` +
|
|
86
|
+
" cd packages/cli/native && cp $(which node) node-vm\n" +
|
|
87
|
+
' codesign --entitlements entitlements-vm-only.plist --force -s "Developer ID Application: ..." node-vm');
|
|
88
|
+
}
|
|
89
|
+
this.child = fork(helperScript, [], {
|
|
90
|
+
execPath: nodeVmPath,
|
|
91
|
+
serialization: "advanced",
|
|
92
|
+
stdio: ["pipe", "pipe", "pipe", "ipc"],
|
|
93
|
+
env: process.env,
|
|
94
|
+
});
|
|
95
|
+
// Forward child stdout/stderr to logs
|
|
96
|
+
this.child.stdout?.on("data", (chunk) => {
|
|
97
|
+
const text = chunk.toString("utf-8").trimEnd();
|
|
98
|
+
if (text)
|
|
99
|
+
vmLog.info("vm-helper:stdout", text);
|
|
100
|
+
});
|
|
101
|
+
this.child.stderr?.on("data", (chunk) => {
|
|
102
|
+
const text = chunk.toString("utf-8").trimEnd();
|
|
103
|
+
if (text)
|
|
104
|
+
vmLog.info("vm-helper:stderr", text);
|
|
105
|
+
});
|
|
106
|
+
// Handle IPC messages from child
|
|
107
|
+
this.child.on("message", (msg) => {
|
|
108
|
+
this.handleMessage(msg);
|
|
109
|
+
});
|
|
110
|
+
// Handle child crash
|
|
111
|
+
this.child.on("exit", (code, signal) => {
|
|
112
|
+
vmLog.error("vm-helper-client", `Helper process exited (code=${code}, signal=${signal})`);
|
|
113
|
+
this.handleChildDeath();
|
|
114
|
+
});
|
|
115
|
+
vmLog.info("vm-helper-client", "Helper process spawned");
|
|
116
|
+
}
|
|
117
|
+
// -------------------------------------------------------------------------
|
|
118
|
+
// SessionManager-compatible API
|
|
119
|
+
// -------------------------------------------------------------------------
|
|
120
|
+
async ensureVmRunning() {
|
|
121
|
+
await this.request({ method: "ensureVmRunning" }, 120_000);
|
|
122
|
+
this._vmRunning = true;
|
|
123
|
+
}
|
|
124
|
+
async createSession(taskId) {
|
|
125
|
+
return (await this.request({ method: "createSession", taskId }, 30_000));
|
|
126
|
+
}
|
|
127
|
+
async cloneRepo(sessionId, repoUrl, branch, baseBranch) {
|
|
128
|
+
await this.request({ method: "cloneRepo", sessionId, repoUrl, branch, baseBranch }, 600_000);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Write session files to the VirtioFS-shared host directory.
|
|
132
|
+
* Stays in parent process — no IPC needed (pure filesystem I/O).
|
|
133
|
+
*/
|
|
134
|
+
prepareWorkspace(sessionId, files) {
|
|
135
|
+
const sessionDir = join(SESSIONS_DIR, sessionId);
|
|
136
|
+
mkdirSync(sessionDir, { recursive: true, mode: 0o700 });
|
|
137
|
+
writeFileSync(join(sessionDir, "context.json"), files.contextJson, "utf-8");
|
|
138
|
+
writeFileSync(join(sessionDir, "mcp_config.json"), files.mcpConfigJson, "utf-8");
|
|
139
|
+
for (const file of files.skillFiles) {
|
|
140
|
+
const filePath = resolve(sessionDir, "skills", file.relativePath);
|
|
141
|
+
if (!filePath.startsWith(resolve(sessionDir))) {
|
|
142
|
+
throw new Error(`Path traversal detected: ${file.relativePath}`);
|
|
143
|
+
}
|
|
144
|
+
mkdirSync(join(filePath, ".."), { recursive: true });
|
|
145
|
+
writeFileSync(filePath, file.content, "utf-8");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async spawnAgent(taskId, cmd, args, env = {}) {
|
|
149
|
+
const proxy = new RemoteVmProcessProxy(taskId, this);
|
|
150
|
+
this.proxies.set(taskId, proxy);
|
|
151
|
+
const result = (await this.request({ method: "spawnAgent", taskId, cmd, args, env }, 30_000));
|
|
152
|
+
proxy.setPid(result.pid);
|
|
153
|
+
return proxy;
|
|
154
|
+
}
|
|
155
|
+
async killAgent(taskIdOrSessionId, signal = "SIGTERM") {
|
|
156
|
+
await this.request({ method: "killAgent", taskIdOrSessionId, signal }, 10_000);
|
|
157
|
+
}
|
|
158
|
+
async destroySession(taskId) {
|
|
159
|
+
await this.request({ method: "destroySession", taskId }, 30_000);
|
|
160
|
+
this.proxies.delete(taskId);
|
|
161
|
+
}
|
|
162
|
+
async exec(cmd, args, cwd) {
|
|
163
|
+
return (await this.request({ method: "exec", cmd, args, cwd }, 600_000));
|
|
164
|
+
}
|
|
165
|
+
async shutdown() {
|
|
166
|
+
if (!this.child)
|
|
167
|
+
return;
|
|
168
|
+
const child = this.child;
|
|
169
|
+
this.child = null;
|
|
170
|
+
this._vmRunning = false;
|
|
171
|
+
try {
|
|
172
|
+
if (child.connected) {
|
|
173
|
+
await this.requestOnChild(child, { method: "shutdown" }, 10_000);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
/* child may already be gone */
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
child.kill("SIGTERM");
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
/* already dead */
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** Send a request on a specific child reference (used during shutdown) */
|
|
187
|
+
requestOnChild(child, params, timeoutMs) {
|
|
188
|
+
if (!child.connected)
|
|
189
|
+
return Promise.resolve(null);
|
|
190
|
+
const reqId = randomUUID();
|
|
191
|
+
const msg = { type: "req", reqId, ...params };
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
const timer = setTimeout(() => {
|
|
194
|
+
this.pending.delete(reqId);
|
|
195
|
+
resolve(null); // Don't reject on shutdown timeout
|
|
196
|
+
}, timeoutMs);
|
|
197
|
+
this.pending.set(reqId, { resolve, reject, timer });
|
|
198
|
+
try {
|
|
199
|
+
child.send(msg);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
clearTimeout(timer);
|
|
203
|
+
this.pending.delete(reqId);
|
|
204
|
+
resolve(null);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
isVmRunning() {
|
|
209
|
+
return this._vmRunning && this.child !== null;
|
|
210
|
+
}
|
|
211
|
+
getAllSessions() {
|
|
212
|
+
// Synchronous — returns empty if not available (best effort)
|
|
213
|
+
// For accurate data, use the async version
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
getSessionByTaskId(_taskId) {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
onSessionExit(sessionId, callback) {
|
|
220
|
+
this.exitListeners.set(sessionId, callback);
|
|
221
|
+
}
|
|
222
|
+
// -------------------------------------------------------------------------
|
|
223
|
+
// IPC internals
|
|
224
|
+
// -------------------------------------------------------------------------
|
|
225
|
+
request(params, timeoutMs) {
|
|
226
|
+
if (!this.child?.connected) {
|
|
227
|
+
return Promise.reject(new Error("VM helper process not connected"));
|
|
228
|
+
}
|
|
229
|
+
const reqId = randomUUID();
|
|
230
|
+
const msg = { type: "req", reqId, ...params };
|
|
231
|
+
return new Promise((resolve, reject) => {
|
|
232
|
+
const timer = setTimeout(() => {
|
|
233
|
+
this.pending.delete(reqId);
|
|
234
|
+
reject(new Error(`IPC timeout (${timeoutMs}ms): ${params.method}`));
|
|
235
|
+
}, timeoutMs);
|
|
236
|
+
this.pending.set(reqId, { resolve, reject, timer });
|
|
237
|
+
this.child.send(msg);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
handleMessage(msg) {
|
|
241
|
+
switch (msg.type) {
|
|
242
|
+
case "res":
|
|
243
|
+
this.handleResponse(msg);
|
|
244
|
+
break;
|
|
245
|
+
case "stream":
|
|
246
|
+
case "exit":
|
|
247
|
+
case "log":
|
|
248
|
+
this.handleEvent(msg);
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
handleResponse(res) {
|
|
253
|
+
const pending = this.pending.get(res.reqId);
|
|
254
|
+
if (!pending)
|
|
255
|
+
return;
|
|
256
|
+
this.pending.delete(res.reqId);
|
|
257
|
+
clearTimeout(pending.timer);
|
|
258
|
+
if (res.ok) {
|
|
259
|
+
pending.resolve(res.value);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
pending.reject(new Error(res.error));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
handleEvent(event) {
|
|
266
|
+
switch (event.type) {
|
|
267
|
+
case "stream": {
|
|
268
|
+
const proxy = this.proxies.get(event.taskId);
|
|
269
|
+
if (!proxy)
|
|
270
|
+
break;
|
|
271
|
+
if (event.channel === "stdout") {
|
|
272
|
+
proxy.pushStdout(Buffer.from(event.data));
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
proxy.pushStderr(Buffer.from(event.data));
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
case "exit": {
|
|
280
|
+
const proxy = this.proxies.get(event.taskId);
|
|
281
|
+
if (proxy) {
|
|
282
|
+
proxy.emitExit(event.code);
|
|
283
|
+
this.proxies.delete(event.taskId);
|
|
284
|
+
}
|
|
285
|
+
// Also fire exit listener (used by onSessionExit registrations)
|
|
286
|
+
const listener = this.exitListeners.get(event.taskId);
|
|
287
|
+
if (listener) {
|
|
288
|
+
this.exitListeners.delete(event.taskId);
|
|
289
|
+
listener(event.code);
|
|
290
|
+
}
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
case "log": {
|
|
294
|
+
const logFn = event.level === "error"
|
|
295
|
+
? vmLog.error
|
|
296
|
+
: event.level === "warn"
|
|
297
|
+
? vmLog.warn
|
|
298
|
+
: vmLog.info;
|
|
299
|
+
logFn.call(vmLog, event.prefix, event.message);
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
handleChildDeath() {
|
|
305
|
+
// Reject all pending requests
|
|
306
|
+
for (const [reqId, pending] of this.pending) {
|
|
307
|
+
clearTimeout(pending.timer);
|
|
308
|
+
pending.reject(new Error("VM helper process died"));
|
|
309
|
+
this.pending.delete(reqId);
|
|
310
|
+
}
|
|
311
|
+
// Emit exit on all active proxies
|
|
312
|
+
for (const [taskId, proxy] of this.proxies) {
|
|
313
|
+
proxy.emitExit(-1);
|
|
314
|
+
this.proxies.delete(taskId);
|
|
315
|
+
}
|
|
316
|
+
this.child = null;
|
|
317
|
+
this._vmRunning = false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
// Singleton
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
let helperInstance = null;
|
|
324
|
+
export function initVmHelper() {
|
|
325
|
+
if (helperInstance)
|
|
326
|
+
return helperInstance;
|
|
327
|
+
helperInstance = new VmHelperClient();
|
|
328
|
+
return helperInstance;
|
|
329
|
+
}
|
|
330
|
+
export function getVmHelper() {
|
|
331
|
+
if (!helperInstance)
|
|
332
|
+
throw new Error("VmHelperClient not initialized");
|
|
333
|
+
return helperInstance;
|
|
334
|
+
}
|
|
335
|
+
//# sourceMappingURL=vm-helper-client.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VM Helper Process — child process entry point.
|
|
3
|
+
*
|
|
4
|
+
* Runs under node-vm (signed with com.apple.security.virtualization entitlement).
|
|
5
|
+
* Loads the native addon, creates a real SessionManager, and proxies commands
|
|
6
|
+
* from the parent CLI process over Node.js IPC.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node-vm dist/vm/vm-helper.js
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=vm-helper.d.ts.map
|