agent-dev-env 0.1.0-canary.2.fbbd044
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 +21 -0
- package/README.md +106 -0
- package/dist/assets/bridge/bridge.js +202 -0
- package/dist/assets/guest/guest-agent-mac.js +586 -0
- package/dist/assets/guest/guest-agent-ubuntu.js +481 -0
- package/dist/assets/guest/guest-agent-windows.js +466 -0
- package/dist/assets/images/mac/CHANGELOG.md +265 -0
- package/dist/assets/images/mac/README.md +63 -0
- package/dist/assets/images/mac/sandbox.pkr.hcl +468 -0
- package/dist/assets/images/mac/vars/sandbox-macos-tahoe.pkrvars.hcl +39 -0
- package/dist/assets/images/ubuntu-arm64-vmware/CHANGELOG.md +239 -0
- package/dist/assets/images/ubuntu-arm64-vmware/README.md +177 -0
- package/dist/assets/images/ubuntu-arm64-vmware/autoinstall/meta-data +2 -0
- package/dist/assets/images/ubuntu-arm64-vmware/autoinstall/user-data +34 -0
- package/dist/assets/images/ubuntu-arm64-vmware/sandbox.pkr.hcl +776 -0
- package/dist/assets/images/ubuntu-arm64-vmware/vars/sandbox-ubuntu-24-04-arm64-vmware.pkrvars.hcl +71 -0
- package/dist/assets/images/windows-arm64-qemu/CHANGELOG.md +209 -0
- package/dist/assets/images/windows-arm64-qemu/README.md +142 -0
- package/dist/assets/images/windows-arm64-qemu/autounattend.xml +325 -0
- package/dist/assets/images/windows-arm64-qemu/qemu-with-tpm.sh +170 -0
- package/dist/assets/images/windows-arm64-qemu/sandbox.pkr.hcl +1012 -0
- package/dist/assets/images/windows-arm64-qemu/vars/sandbox-windows-11-arm64-qemu.pkrvars.hcl +83 -0
- package/dist/assets/images/windows-arm64-vmware/CHANGELOG.md +270 -0
- package/dist/assets/images/windows-arm64-vmware/README.md +171 -0
- package/dist/assets/images/windows-arm64-vmware/autounattend.xml +311 -0
- package/dist/assets/images/windows-arm64-vmware/sandbox.pkr.hcl +1036 -0
- package/dist/assets/images/windows-arm64-vmware/vars/sandbox-windows-11-arm64-vmware.pkrvars.hcl +83 -0
- package/dist/assets/rules/agent-rules-linux.md +68 -0
- package/dist/assets/rules/agent-rules.md +71 -0
- package/dist/assets/watchdog/watch-build-ocr.swift +40 -0
- package/dist/assets/watchdog/watch-build.py +275 -0
- package/dist/cli.js +52 -0
- package/dist/commands/delete.js +179 -0
- package/dist/commands/doctor.js +165 -0
- package/dist/commands/list.js +38 -0
- package/dist/commands/not-yet.js +15 -0
- package/dist/commands/register.js +150 -0
- package/dist/commands/run.js +65 -0
- package/dist/commands/status.js +142 -0
- package/dist/commands/stop.js +186 -0
- package/dist/commands/sync.js +98 -0
- package/dist/lib/exec.js +250 -0
- package/dist/lib/ghcr.js +82 -0
- package/dist/lib/git.js +100 -0
- package/dist/lib/logger.js +87 -0
- package/dist/lib/network.js +63 -0
- package/dist/lib/paths.js +116 -0
- package/dist/lib/platform.js +87 -0
- package/dist/lib/prompt.js +66 -0
- package/dist/lib/qemu.js +373 -0
- package/dist/lib/regex.js +9 -0
- package/dist/lib/ssh.js +230 -0
- package/dist/lib/tart.js +210 -0
- package/dist/lib/template.js +35 -0
- package/dist/lib/vars.js +99 -0
- package/dist/lib/vmrun.js +334 -0
- package/dist/lifecycle/build-macos.js +23 -0
- package/dist/lifecycle/build-qemu.js +204 -0
- package/dist/lifecycle/build-shared.js +355 -0
- package/dist/lifecycle/build-ubuntu.js +184 -0
- package/dist/lifecycle/build-watchdog.js +146 -0
- package/dist/lifecycle/build-windows-vmware.js +112 -0
- package/dist/lifecycle/build.js +55 -0
- package/dist/lifecycle/catalog.js +165 -0
- package/dist/lifecycle/deploy.js +161 -0
- package/dist/lifecycle/tag.js +77 -0
- package/dist/lifecycle/watch-build.js +64 -0
- package/dist/runners/bridges.js +156 -0
- package/dist/runners/framework.js +66 -0
- package/dist/runners/macos-bridges.js +143 -0
- package/dist/runners/macos-guest.js +76 -0
- package/dist/runners/macos-rules.js +77 -0
- package/dist/runners/macos-summary.js +145 -0
- package/dist/runners/macos.js +258 -0
- package/dist/runners/openchamber.js +60 -0
- package/dist/runners/options.js +56 -0
- package/dist/runners/qemu-image.js +183 -0
- package/dist/runners/rules.js +49 -0
- package/dist/runners/ubuntu-bridges.js +154 -0
- package/dist/runners/ubuntu-guest.js +136 -0
- package/dist/runners/ubuntu-image.js +26 -0
- package/dist/runners/ubuntu-rules.js +86 -0
- package/dist/runners/ubuntu-shared.js +65 -0
- package/dist/runners/ubuntu-summary.js +141 -0
- package/dist/runners/ubuntu.js +117 -0
- package/dist/runners/vmware-common.js +158 -0
- package/dist/runners/vmware-image.js +242 -0
- package/dist/runners/windows-autologon.js +119 -0
- package/dist/runners/windows-bridges.js +136 -0
- package/dist/runners/windows-guest.js +213 -0
- package/dist/runners/windows-image.js +23 -0
- package/dist/runners/windows-qemu-summary.js +85 -0
- package/dist/runners/windows-qemu.js +189 -0
- package/dist/runners/windows-shared.js +68 -0
- package/dist/runners/windows-summary.js +104 -0
- package/dist/runners/windows.js +96 -0
- package/dist/settings/common.js +107 -0
- package/dist/settings/macos-copy.js +223 -0
- package/dist/settings/macos.js +41 -0
- package/dist/settings/ubuntu-copy.js +223 -0
- package/dist/settings/ubuntu.js +68 -0
- package/package.json +52 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// runners/ubuntu-shared.ts — step 4 for the Ubuntu backend: the shared
|
|
2
|
+
// host directory over HGFS (open-vm-tools' vmhgfs-fuse). Best-effort —
|
|
3
|
+
// the sandbox works without it, failures warn only, like the shell's
|
|
4
|
+
// setup_shared_folder. The vmrun side (tools gate + addSharedFolder with
|
|
5
|
+
// the "Already exists" case) is here; the guest mount runs over the ssh2
|
|
6
|
+
// session with the password piped into sudo -S.
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { logger } from '../lib/logger.js';
|
|
9
|
+
import { openSshSession } from '../lib/ssh.js';
|
|
10
|
+
import { enableSharedFolders, isVmRunning } from '../lib/vmrun.js';
|
|
11
|
+
import { addSharedFolderRetry, waitForTools } from './vmware-common.js';
|
|
12
|
+
/** The shared-folder step: register + enable the share with vmrun and
|
|
13
|
+
* mount it in the guest (skipped silently when no --work-dir / missing
|
|
14
|
+
* host dir / VM down — the shell's early-outs).
|
|
15
|
+
*
|
|
16
|
+
* @param context - The run context.
|
|
17
|
+
* @param workVmx - The working VM vmx.
|
|
18
|
+
* @param creds - The guest credentials (mount over ssh).
|
|
19
|
+
*/
|
|
20
|
+
export async function setupSharedFolder(context, workVmx, creds) {
|
|
21
|
+
if (!context.workDir) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (!existsSync(context.workDir)) {
|
|
25
|
+
logger.warn(`work directory '${context.workDir}' does not exist — skipping the shared-directory share.`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (!(await isVmRunning(workVmx))) {
|
|
29
|
+
logger.warn('shared folder skipped — the VM is not running.');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
logger.info(`Sharing ${context.workDir} into the guest as 'work' (/mnt/hgfs/work).`);
|
|
33
|
+
if (!(await waitForTools(workVmx))) {
|
|
34
|
+
logger.warn('VMware Tools did not report running — skipping the shared-directory share (re-run to retry).');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
await addSharedFolderRetry(workVmx, 'work', context.workDir);
|
|
38
|
+
await enableSharedFolders(workVmx);
|
|
39
|
+
await mountSharedFolder(creds);
|
|
40
|
+
}
|
|
41
|
+
/** Mounts the registered shares in the guest (vmhgfs-fuse needs root —
|
|
42
|
+
* sudo -S reads the password from the session's stdin).
|
|
43
|
+
*
|
|
44
|
+
* @param creds - The guest credentials.
|
|
45
|
+
*/
|
|
46
|
+
async function mountSharedFolder(creds) {
|
|
47
|
+
const session = await openSshSession(creds);
|
|
48
|
+
try {
|
|
49
|
+
const command = 'mkdir -p /mnt/hgfs/work && sudo -S vmhgfs-fuse .host:/ /mnt/hgfs ' +
|
|
50
|
+
'-o allow_other,default_permissions,uid=$(id -u),gid=$(id -g)';
|
|
51
|
+
const res = await session.exec(command, { input: `${creds.password}\n`, timeoutMs: 60_000 });
|
|
52
|
+
if (res.code !== 0) {
|
|
53
|
+
logger.warn('could not mount the shared folder in the guest — is vmhgfs-fuse available (open-vm-tools)?');
|
|
54
|
+
logger.warn('try manually: sudo vmhgfs-fuse .host:/ /mnt/hgfs -o allow_other');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
logger.ok('Shared folder mounted at /mnt/hgfs/work.');
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
logger.warn(`could not mount the shared folder in the guest: ${err.message}`);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
session.end();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// runners/ubuntu-summary.ts — the Ubuntu "Sandbox is ready" block (port
|
|
2
|
+
// of the shell's print_summary in run-ubuntu-vmware-sandbox.sh: same
|
|
3
|
+
// labels, same wording, same color scheme — with the CLI's 12-char label
|
|
4
|
+
// column, like the macOS summary).
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { logger } from '../lib/logger.js';
|
|
7
|
+
import { imageRootDir, workingVmxPath } from '../lib/paths.js';
|
|
8
|
+
import { resolveGuestCredentials } from './ubuntu-guest.js';
|
|
9
|
+
/** Prints the Ubuntu summary block (image/VM/IP/SSH/shared dir/bridges/
|
|
10
|
+
* rules/settings/OpenChamber/state/stop hints).
|
|
11
|
+
*
|
|
12
|
+
* @param context - The run context.
|
|
13
|
+
* @param state - The accumulated run state.
|
|
14
|
+
*/
|
|
15
|
+
export async function printUbuntuSummary(context, state) {
|
|
16
|
+
const workVmx = workingVmxPath(context.platform, context.image);
|
|
17
|
+
const stateDir = imageRootDir(context.platform, context.image);
|
|
18
|
+
logger.step('Sandbox is ready');
|
|
19
|
+
summaryLine('Image:', state.imageArchive ?? 'not available');
|
|
20
|
+
summaryLine('VM:', workVmx);
|
|
21
|
+
summaryLine('Guest IP:', state.vmIp ? `${state.vmIp} (Fusion NAT, vmnet8)` : 'unavailable');
|
|
22
|
+
const creds = state.vmIp
|
|
23
|
+
? resolveGuestCredentials(context.image, context.options.env, state.vmIp)
|
|
24
|
+
: undefined;
|
|
25
|
+
summaryLine('SSH:', `ssh ${creds?.username ?? 'admin'}@${state.vmIp ?? '?'} (password: ${creds?.password ?? 'sandbox'})`);
|
|
26
|
+
if (context.workDir && existsSync(context.workDir)) {
|
|
27
|
+
summaryLine('Shared:', `/mnt/hgfs/work -> ${context.workDir}`);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
summaryLine('Shared:', 'not shared');
|
|
31
|
+
}
|
|
32
|
+
printBridgeLines(context, state);
|
|
33
|
+
printRulesLine(state.rules);
|
|
34
|
+
printSettingsLine(state.settings);
|
|
35
|
+
printOpenchamberLine(context, state);
|
|
36
|
+
summaryLine('State:', `${stateDir} (extracted base + working clone; --reset re-clones)`);
|
|
37
|
+
printStopLines(context, state);
|
|
38
|
+
}
|
|
39
|
+
/** One ` LABEL........ VALUE` line. */
|
|
40
|
+
function summaryLine(label, value) {
|
|
41
|
+
logger.out(` ${label.padEnd(12)}${value}`);
|
|
42
|
+
}
|
|
43
|
+
function printBridgeLines(context, state) {
|
|
44
|
+
const agent = state.bridges.agent;
|
|
45
|
+
if (agent.bridged) {
|
|
46
|
+
const value = agent.guestUp
|
|
47
|
+
? `host agent -> TCP ${context.agentPort} -> guest (/tmp/ssh-agent.sock)`
|
|
48
|
+
: `host bridge up (TCP ${context.agentPort}), guest bridge not running`;
|
|
49
|
+
summaryLine('SSH agent:', agent.guestUp ? loggerOk(value) : loggerWarn(value));
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
summaryLine('SSH agent:', 'not bridged');
|
|
53
|
+
}
|
|
54
|
+
const docker = state.bridges.docker;
|
|
55
|
+
if (docker.bridged) {
|
|
56
|
+
if (docker.engineUp) {
|
|
57
|
+
summaryLine('Docker:', loggerOk(`host engine (v${docker.serverVersion}) -> TCP ${context.dockerPort} -> guest (context 'host')`));
|
|
58
|
+
}
|
|
59
|
+
else if (docker.guestUp) {
|
|
60
|
+
summaryLine('Docker:', loggerWarn('bridge up, engine not reachable in the guest — is Docker running on the host?'));
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
summaryLine('Docker:', loggerWarn(`host bridge up (TCP ${context.dockerPort}), guest bridge not running`));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
summaryLine('Docker:', 'not bridged');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function printRulesLine(rules) {
|
|
71
|
+
switch (rules) {
|
|
72
|
+
case 'installed':
|
|
73
|
+
summaryLine('Rules:', loggerOk('installed for opencode + Copilot'));
|
|
74
|
+
break;
|
|
75
|
+
case 'updated':
|
|
76
|
+
case 'overwritten':
|
|
77
|
+
summaryLine('Rules:', loggerOk('updated for opencode + Copilot'));
|
|
78
|
+
break;
|
|
79
|
+
case 'uptodate':
|
|
80
|
+
summaryLine('Rules:', 'up to date in the guest');
|
|
81
|
+
break;
|
|
82
|
+
case 'kept':
|
|
83
|
+
summaryLine('Rules:', 'guest rules kept (not overwritten)');
|
|
84
|
+
break;
|
|
85
|
+
case 'failed':
|
|
86
|
+
summaryLine('Rules:', loggerWarn('could not be installed'));
|
|
87
|
+
break;
|
|
88
|
+
default:
|
|
89
|
+
summaryLine('Rules:', 'not installed');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function printSettingsLine(settings) {
|
|
93
|
+
switch (settings) {
|
|
94
|
+
case 'copied':
|
|
95
|
+
summaryLine('Settings:', loggerOk('host user settings copied into the guest'));
|
|
96
|
+
break;
|
|
97
|
+
case 'uptodate':
|
|
98
|
+
summaryLine('Settings:', 'already in the guest');
|
|
99
|
+
break;
|
|
100
|
+
case 'skipped':
|
|
101
|
+
summaryLine('Settings:', 'not copied (--no-settings)');
|
|
102
|
+
break;
|
|
103
|
+
case 'declined':
|
|
104
|
+
summaryLine('Settings:', 'not copied (declined)');
|
|
105
|
+
break;
|
|
106
|
+
case 'none':
|
|
107
|
+
summaryLine('Settings:', 'no host settings found');
|
|
108
|
+
break;
|
|
109
|
+
case 'failed':
|
|
110
|
+
summaryLine('Settings:', loggerWarn('copy failed — re-run to retry'));
|
|
111
|
+
break;
|
|
112
|
+
default:
|
|
113
|
+
summaryLine('Settings:', 'not copied');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function printOpenchamberLine(context, state) {
|
|
117
|
+
if (state.openchamberUp && state.openchamberUrl) {
|
|
118
|
+
summaryLine('OpenChamber:', loggerOk(`${state.openchamberUrl} (password: sandbox)`));
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
summaryLine('OpenChamber:', loggerWarn(`not responding on http://${state.vmIp ?? '?'}:${context.openchamberPort}`));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function printStopLines(context, state) {
|
|
125
|
+
summaryLine('Stop:', 'run "agent-dev-env stop ubuntu-vmware"');
|
|
126
|
+
if (!context.options.foreground) {
|
|
127
|
+
if (state.bridges.agent.bridged) {
|
|
128
|
+
summaryLine('Bridge:', `host listener on TCP ${context.agentPort} stays up — stop it with: agent-dev-env stop ubuntu-vmware`);
|
|
129
|
+
}
|
|
130
|
+
if (state.bridges.docker.bridged) {
|
|
131
|
+
summaryLine('Bridge:', `host Docker listener on TCP ${context.dockerPort} stays up — stop it with: agent-dev-env stop ubuntu-vmware`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// color helpers — the shell's ${c_green}/${c_yellow} inline.
|
|
136
|
+
function loggerOk(text) {
|
|
137
|
+
return logger.color('green') + text + logger.reset();
|
|
138
|
+
}
|
|
139
|
+
function loggerWarn(text) {
|
|
140
|
+
return logger.color('yellow') + text + logger.reset();
|
|
141
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// runners/ubuntu.ts — the Ubuntu 24.04 (ARM64) VMware backend (Phase 4 of
|
|
2
|
+
// the port): vmrun-driven image/base/clone (ubuntu-image.ts), boot with
|
|
3
|
+
// the guest-IP + sshd waits and the HGFS share (ubuntu-shared.ts), the
|
|
4
|
+
// NAT-segment bridges + guest agent over ssh2 (ubuntu-bridges.ts), the
|
|
5
|
+
// settings copy over ssh2 (settings/ubuntu-copy.ts) and the summary.
|
|
6
|
+
// Port of run-ubuntu-vmware-sandbox.sh §steps 0-8.
|
|
7
|
+
import { rmSync } from 'node:fs';
|
|
8
|
+
import { logger } from '../lib/logger.js';
|
|
9
|
+
import { imageRootDir } from '../lib/paths.js';
|
|
10
|
+
import { openSshSession, waitForSshd } from '../lib/ssh.js';
|
|
11
|
+
import { startVm } from '../lib/vmrun.js';
|
|
12
|
+
import { ensureUserSettings, restartOpenchamber } from '../settings/ubuntu-copy.js';
|
|
13
|
+
import { ensureBridgeDir } from './bridges.js';
|
|
14
|
+
import { offerOpenInBrowser, waitForOpenchamber } from './openchamber.js';
|
|
15
|
+
import { ensureUbuntuImage, ubuntuWorkingVmx } from './ubuntu-image.js';
|
|
16
|
+
import { ubuntuBridges } from './ubuntu-bridges.js';
|
|
17
|
+
import { resolveGuestCredentials } from './ubuntu-guest.js';
|
|
18
|
+
import { setupSharedFolder } from './ubuntu-shared.js';
|
|
19
|
+
import { printUbuntuSummary } from './ubuntu-summary.js';
|
|
20
|
+
import { cleanupRunBridge, requireVmrun, stopRunningVm, waitForForegroundVmStop, waitGuestIp, } from './vmware-common.js';
|
|
21
|
+
const PLATFORM = 'ubuntu-vmware';
|
|
22
|
+
/** The Ubuntu backend — vmrun + ssh2 (no Tart; the state lives under the
|
|
23
|
+
* CLI's data dir, unlike the macOS backend's no-footprint design).
|
|
24
|
+
*/
|
|
25
|
+
export const ubuntuBackend = {
|
|
26
|
+
preflight: (context) => preflight(context),
|
|
27
|
+
ensureImageAndVm: (context, state) => ensureUbuntuImage(context, state),
|
|
28
|
+
boot: boot,
|
|
29
|
+
setupBridges: (context, state) => ubuntuBridges(context, state),
|
|
30
|
+
setupSettings: (context, state) => setupSettings(context, state),
|
|
31
|
+
verifyOpenchamber: (context, state) => verifyOpenchamber(context, state),
|
|
32
|
+
summarize: (context, state) => printUbuntuSummary(context, state),
|
|
33
|
+
finish: finish,
|
|
34
|
+
};
|
|
35
|
+
async function preflight(context) {
|
|
36
|
+
requireVmrun();
|
|
37
|
+
ensureBridgeDir();
|
|
38
|
+
if (context.options.reset) {
|
|
39
|
+
const root = imageRootDir(PLATFORM, context.image);
|
|
40
|
+
logger.info(`Resetting the working VM (--reset) — deleting the extracted base and the working clone.`);
|
|
41
|
+
rmSync(root, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// --- step 2: boot -----------------------------------------------------------
|
|
45
|
+
async function boot(context, state) {
|
|
46
|
+
const workVmx = ubuntuWorkingVmx(context.image);
|
|
47
|
+
await stopRunningVm(context, workVmx, PLATFORM);
|
|
48
|
+
logger.cmd(`vmrun -T fusion start ${workVmx} ${context.options.headless ? 'nogui' : 'gui'}`);
|
|
49
|
+
const start = await startVm(workVmx, context.options.headless ? 'nogui' : 'gui');
|
|
50
|
+
if (start.code !== 0) {
|
|
51
|
+
throw new Error(`vmrun start failed:\n${start.stderr.trim()}`);
|
|
52
|
+
}
|
|
53
|
+
logger.ok('VM started.');
|
|
54
|
+
state.vmIp = await waitGuestIp(workVmx);
|
|
55
|
+
logger.ok(`Guest IP: ${state.vmIp}`);
|
|
56
|
+
const creds = resolveGuestCredentials(context.image, context.options.env, state.vmIp);
|
|
57
|
+
process.stdout.write(' Waiting for the guest to boot (up to 10 min)');
|
|
58
|
+
if (await waitForSshd(creds)) {
|
|
59
|
+
process.stdout.write(` ${logger.color('green')}done${logger.reset()}\n`);
|
|
60
|
+
logger.ok(`VM is up: ssh ${creds.username}@${state.vmIp}`);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
process.stdout.write(` ${logger.color('yellow')}failed${logger.reset()}\n`);
|
|
64
|
+
logger.die(`timed out waiting for the guest to boot (no SSH on ${state.vmIp}:22).`);
|
|
65
|
+
}
|
|
66
|
+
await setupSharedFolder(context, workVmx, creds);
|
|
67
|
+
}
|
|
68
|
+
// --- step 4/5 hooks ---------------------------------------------------------
|
|
69
|
+
async function setupSettings(context, state) {
|
|
70
|
+
if (context.options.noSettings) {
|
|
71
|
+
logger.info('Skipping user settings copy (--no-settings).');
|
|
72
|
+
state.settings = 'skipped';
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (!state.vmIp) {
|
|
76
|
+
logger.warn('no guest IP — skipping the user settings copy.');
|
|
77
|
+
state.settings = 'failed';
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const creds = resolveGuestCredentials(context.image, context.options.env, state.vmIp);
|
|
81
|
+
const session = await openSshSession(creds);
|
|
82
|
+
try {
|
|
83
|
+
state.settings = await ensureUserSettings(session, context.options.home, context.options.yes, creds.password);
|
|
84
|
+
if (state.settings === 'copied') {
|
|
85
|
+
await restartOpenchamber(session);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
state.settings = 'failed';
|
|
90
|
+
logger.warn(`settings copy failed: ${err.message}`);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
session.end();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function verifyOpenchamber(context, state) {
|
|
97
|
+
if (!state.vmIp) {
|
|
98
|
+
logger.warn('OpenChamber: guest IP unavailable — cannot probe.');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
state.openchamberUrl = `http://${state.vmIp}:${context.openchamberPort}`;
|
|
102
|
+
state.openchamberUp = await waitForOpenchamber(state.openchamberUrl);
|
|
103
|
+
if (state.openchamberUp) {
|
|
104
|
+
await offerOpenInBrowser(state.openchamberUrl, context.options.yes);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// --- finish: foreground wait + bridge cleanup --------------------------------
|
|
108
|
+
async function finish(context, state) {
|
|
109
|
+
if (!context.options.foreground) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
await waitForForegroundVmStop(ubuntuWorkingVmx(context.image));
|
|
113
|
+
// The VM stopped (or Cmd+C was pressed) — kill the bridges this run
|
|
114
|
+
// started; in background mode they stay up, outliving the CLI by design.
|
|
115
|
+
await cleanupRunBridge(state.bridges.agent.pid, 'agent bridge');
|
|
116
|
+
await cleanupRunBridge(state.bridges.docker.pid, 'Docker bridge');
|
|
117
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// runners/vmware-common.ts — the run-time helpers the two VMware backends
|
|
2
|
+
// (ubuntu-vmware and windows-vmware) share: the vmrun prereq, the
|
|
3
|
+
// stop-running-VM restart flow, the bounded guest-IP wait, the
|
|
4
|
+
// VMware-Tools wait and the bridge cleanup. Extracted from the ubuntu
|
|
5
|
+
// backend when the windows backend landed (Phase 5) — one implementation
|
|
6
|
+
// for both, with the platform label for the runners' messages.
|
|
7
|
+
import { killTree, sleep } from '../lib/exec.js';
|
|
8
|
+
import { logger } from '../lib/logger.js';
|
|
9
|
+
import { confirmDefault } from '../lib/prompt.js';
|
|
10
|
+
import { addSharedFolder, checkToolsState, findVmrun, getGuestIpAddress, isVmRunning, sharedFolderAlreadyExists, stopVm, waitForVmNotRunning, } from '../lib/vmrun.js';
|
|
11
|
+
/** vmrun prereq (same message as doctor/status).
|
|
12
|
+
*
|
|
13
|
+
* @returns The resolved vmrun path.
|
|
14
|
+
*/
|
|
15
|
+
export function requireVmrun() {
|
|
16
|
+
if (!findVmrun()) {
|
|
17
|
+
logger.die('vmrun not found — install VMware Fusion (free for personal use) or set FUSION_APP_PATH.');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Stops a VM left running by a previous run before the boot (the old
|
|
21
|
+
* VM holds the clone's disks; the legacy stop_running_vm — restart
|
|
22
|
+
* confirm defaults to no).
|
|
23
|
+
*
|
|
24
|
+
* @param context - The run context.
|
|
25
|
+
* @param workVmx - The working VM vmx.
|
|
26
|
+
* @param platform - The platform id (stop-hint message).
|
|
27
|
+
*/
|
|
28
|
+
export async function stopRunningVm(context, workVmx, platform) {
|
|
29
|
+
if (!(await isVmRunning(workVmx))) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (!(await confirmDefault('The sandbox VM is already running — restart it?', {
|
|
33
|
+
default: 'n',
|
|
34
|
+
yes: context.options.yes,
|
|
35
|
+
}))) {
|
|
36
|
+
logger.die(`aborted — the VM is already running. Stop it with: agent-dev-env stop ${platform}`);
|
|
37
|
+
}
|
|
38
|
+
logger.cmd(`vmrun -T fusion stop ${workVmx}`);
|
|
39
|
+
await stopVm(workVmx);
|
|
40
|
+
process.stdout.write(' Waiting for the VM to stop');
|
|
41
|
+
if (await waitForVmNotRunning(workVmx, 60, 2000)) {
|
|
42
|
+
process.stdout.write(` ${logger.color('green')}stopped${logger.reset()}\n`);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
process.stdout.write(` ${logger.color('yellow')}failed${logger.reset()}\n`);
|
|
46
|
+
logger.warn(`'${workVmx}' is still running — stop it manually with 'vmrun -T fusion stop'.`);
|
|
47
|
+
}
|
|
48
|
+
await sleep(1000);
|
|
49
|
+
}
|
|
50
|
+
/** Polls vmrun getGuestIPAddress (bounded per call; strictly a dotted
|
|
51
|
+
* quad — the "Tools not running" error text must never become the IP).
|
|
52
|
+
*
|
|
53
|
+
* @param workVmx - The working VM vmx.
|
|
54
|
+
* @returns The guest IP.
|
|
55
|
+
*/
|
|
56
|
+
export async function waitGuestIp(workVmx) {
|
|
57
|
+
process.stdout.write(' Waiting for the guest IP (open-vm-tools; up to 15 min)');
|
|
58
|
+
for (let attempt = 0; attempt < 180; attempt += 1) {
|
|
59
|
+
const ip = await getGuestIpAddress(workVmx);
|
|
60
|
+
if (ip) {
|
|
61
|
+
process.stdout.write(` ${logger.color('green')}done${logger.reset()}\n`);
|
|
62
|
+
return ip;
|
|
63
|
+
}
|
|
64
|
+
process.stdout.write('.');
|
|
65
|
+
await sleep(5000);
|
|
66
|
+
}
|
|
67
|
+
process.stdout.write(` ${logger.color('yellow')}failed${logger.reset()}\n`);
|
|
68
|
+
return logger.die('timed out waiting for the guest IP — are open-vm-tools running in the guest?');
|
|
69
|
+
}
|
|
70
|
+
/** Polls checkToolsState until vmrun reports "running" (the tools can
|
|
71
|
+
* still be starting when the IP and sshd already answer — vmrun
|
|
72
|
+
* addSharedFolder then fails).
|
|
73
|
+
*
|
|
74
|
+
* @param workVmx - The working VM vmx.
|
|
75
|
+
* @returns True when the tools report running within the window.
|
|
76
|
+
*/
|
|
77
|
+
export async function waitForTools(workVmx) {
|
|
78
|
+
process.stdout.write(' Waiting for VMware Tools to be running');
|
|
79
|
+
for (let attempt = 0; attempt < 60; attempt += 1) {
|
|
80
|
+
if ((await checkToolsState(workVmx)) === 'running') {
|
|
81
|
+
process.stdout.write(` ${logger.color('green')}done${logger.reset()}\n`);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
process.stdout.write('.');
|
|
85
|
+
await sleep(5000);
|
|
86
|
+
}
|
|
87
|
+
process.stdout.write(` ${logger.color('yellow')}failed${logger.reset()}\n`);
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
/** Registers the share (4 x 10 s retries — the tools state can flip back
|
|
91
|
+
* after waitForTools; "Already exists" from a previous run is ok).
|
|
92
|
+
*
|
|
93
|
+
* @param workVmx - The working VM vmx.
|
|
94
|
+
* @param name - The share name.
|
|
95
|
+
* @param path - The host directory.
|
|
96
|
+
*/
|
|
97
|
+
export async function addSharedFolderRetry(workVmx, name, path) {
|
|
98
|
+
for (let attempt = 1; attempt <= 4; attempt += 1) {
|
|
99
|
+
const res = await addSharedFolder(workVmx, name, path);
|
|
100
|
+
if (res.code === 0) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (sharedFolderAlreadyExists(`${res.stdout}${res.stderr}`)) {
|
|
104
|
+
logger.ok('Shared folder already registered (from a previous run — the share persists in the vmx).');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (attempt < 4) {
|
|
108
|
+
logger.warn(`addSharedFolder failed (${res.stderr.trim()}) — retrying in 10 s (attempt ${attempt}/4).`);
|
|
109
|
+
await sleep(10_000);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
logger.warn('addSharedFolder failed after 4 attempts — is VMware Tools up? Continuing.');
|
|
113
|
+
}
|
|
114
|
+
/** The foreground wait: poll vmrun list until the VM stops; a Cmd+C stops
|
|
115
|
+
* it through the same vmrun path as the shell's EXIT trap. The VM must
|
|
116
|
+
* already be up (the working vmx exists).
|
|
117
|
+
*
|
|
118
|
+
* @param workVmx - The working VM vmx.
|
|
119
|
+
*/
|
|
120
|
+
export async function waitForForegroundVmStop(workVmx) {
|
|
121
|
+
let stopping = false;
|
|
122
|
+
const onSignal = () => {
|
|
123
|
+
if (stopping) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
stopping = true;
|
|
127
|
+
logger.info('Stopping the VM (Cmd+C)...');
|
|
128
|
+
// Plain `vmrun stop` — the shell's EXIT trap parity (hard power-off;
|
|
129
|
+
// the poll loop waits for the running list to clear).
|
|
130
|
+
void stopVm(workVmx, 'hard');
|
|
131
|
+
};
|
|
132
|
+
process.on('SIGINT', onSignal);
|
|
133
|
+
process.on('SIGTERM', onSignal);
|
|
134
|
+
logger.info('Waiting for the VM to stop (Cmd+C to stop it now)...');
|
|
135
|
+
try {
|
|
136
|
+
while (await isVmRunning(workVmx)) {
|
|
137
|
+
await sleep(3000);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
process.removeListener('SIGINT', onSignal);
|
|
142
|
+
process.removeListener('SIGTERM', onSignal);
|
|
143
|
+
}
|
|
144
|
+
logger.info('VM stopped.');
|
|
145
|
+
}
|
|
146
|
+
/** Kills one of this run's bridges (the foreground-mode teardown; in
|
|
147
|
+
* background mode the bridges stay up, outliving the CLI by design).
|
|
148
|
+
*
|
|
149
|
+
* @param pid - The bridge pid this run started (undefined = not ours).
|
|
150
|
+
* @param label - The bridge label (agent bridge / Docker bridge).
|
|
151
|
+
*/
|
|
152
|
+
export async function cleanupRunBridge(pid, label) {
|
|
153
|
+
if (pid === undefined) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
await killTree(pid);
|
|
157
|
+
logger.info(`Stopped the host ${label} (pid ${pid}).`);
|
|
158
|
+
}
|