agent-dev-env 0.1.0
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/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 +153 -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,179 @@
|
|
|
1
|
+
// commands/delete.ts — `agent-dev-env delete <platform> [--yes]
|
|
2
|
+
// [--pristine]`: stop the sandbox (delegating to the same flow as
|
|
3
|
+
// `stop`), then remove the VM/state. macOS: `tart delete` the working VM
|
|
4
|
+
// (+ the pristine image with --pristine). VMware (Ubuntu + Windows) and
|
|
5
|
+
// QEMU (Windows): `rm -rf` the state dir (extracted base + working clone
|
|
6
|
+
// or overlay/TPM/NVRAM + pulled cache) — the next run re-pulls and
|
|
7
|
+
// re-clones.
|
|
8
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
9
|
+
import { run } from '../lib/exec.js';
|
|
10
|
+
import { logger } from '../lib/logger.js';
|
|
11
|
+
import { imageRootDir } from '../lib/paths.js';
|
|
12
|
+
import { qemuStateDir } from '../lib/qemu.js';
|
|
13
|
+
import { deleteVm, stopVm, tartAvailable, vmExists, vmState } from '../lib/tart.js';
|
|
14
|
+
import { findVmrun } from '../lib/vmrun.js';
|
|
15
|
+
import { confirm } from '../lib/prompt.js';
|
|
16
|
+
import { resolveRunOptions } from '../runners/options.js';
|
|
17
|
+
import { stopMacos, stopQemuSandbox, stopVmware } from './stop.js';
|
|
18
|
+
/** Deletes a sandbox platform.
|
|
19
|
+
*
|
|
20
|
+
* @param platform - The platform to delete.
|
|
21
|
+
* @param options - --yes / --pristine flags.
|
|
22
|
+
* @returns The process exit code.
|
|
23
|
+
*/
|
|
24
|
+
export async function deleteCmd(platform, options) {
|
|
25
|
+
if (platform === 'macos') {
|
|
26
|
+
await deleteMacos(options);
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
if (platform === 'windows-qemu') {
|
|
30
|
+
await deleteQemu(options);
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
await deleteVmware(platform, options);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
/** The macOS delete flow (working VM, optionally the pristine image). */
|
|
37
|
+
async function deleteMacos(options) {
|
|
38
|
+
if (!tartAvailable()) {
|
|
39
|
+
logger.die("tart is not installed — run 'brew install cirruslabs/cli/tart' first.");
|
|
40
|
+
}
|
|
41
|
+
const runOptions = resolveRunOptions('macos', { yes: options.yes });
|
|
42
|
+
const { vm, image } = runOptions;
|
|
43
|
+
const yes = options.yes === true;
|
|
44
|
+
logger.title(`Deleting macOS sandbox: ${vm}`);
|
|
45
|
+
logger.step('Stopping the sandbox');
|
|
46
|
+
await stopMacos();
|
|
47
|
+
logger.step('Deleting the working VM');
|
|
48
|
+
if (await vmExists(vm)) {
|
|
49
|
+
const ask = `Delete the working VM '${vm}'? This stops and removes it — the next ` +
|
|
50
|
+
`run re-clones it from '${image}'.`;
|
|
51
|
+
if (yes || (await confirm(ask, { default: 'y' }))) {
|
|
52
|
+
logger.cmd(`tart delete ${vm}`);
|
|
53
|
+
const res = await deleteVm(vm);
|
|
54
|
+
if (res.code === 0) {
|
|
55
|
+
if (await vmExists(vm)) {
|
|
56
|
+
logger.warn(`VM '${vm}' still exists after 'tart delete'.`);
|
|
57
|
+
}
|
|
58
|
+
logger.ok(`Working VM '${vm}' deleted.`);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
logger.warn(`'tart delete ${vm}' failed — is it running?`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
logger.info(`Kept '${vm}' — nothing was deleted.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
logger.info(`Working VM '${vm}' does not exist (already deleted?) — nothing to delete.`);
|
|
70
|
+
}
|
|
71
|
+
await maybeDeletePristine(image, yes, options.pristine === true);
|
|
72
|
+
logger.step('Sandbox deleted');
|
|
73
|
+
logger.info(`Working VM: ${vm} — deleted if it existed (next run re-clones from '${image}').`);
|
|
74
|
+
logger.info('Pristine image: ' + image + ' — kept or deleted per the flags above.');
|
|
75
|
+
logger.info('Next run: agent-dev-env run macos');
|
|
76
|
+
}
|
|
77
|
+
/** The VMware delete flow — stop first, then remove the state dir
|
|
78
|
+
* (shared by the Ubuntu and Windows backends).
|
|
79
|
+
*
|
|
80
|
+
* @param platform - The VMware platform to delete.
|
|
81
|
+
* @param options - --yes flag.
|
|
82
|
+
*/
|
|
83
|
+
async function deleteVmware(platform, options) {
|
|
84
|
+
if (!findVmrun()) {
|
|
85
|
+
logger.die('vmrun not found — install VMware Fusion (free for personal use) or set FUSION_APP_PATH.');
|
|
86
|
+
}
|
|
87
|
+
const runOptions = resolveRunOptions(platform, { yes: options.yes });
|
|
88
|
+
const stateDir = imageRootDir(platform, runOptions.image);
|
|
89
|
+
const yes = options.yes === true;
|
|
90
|
+
const label = platform === 'ubuntu-vmware' ? 'Ubuntu' : 'Windows';
|
|
91
|
+
logger.title(`Deleting ${label} VMware sandbox: ${runOptions.image}`);
|
|
92
|
+
logger.step('Stopping the sandbox');
|
|
93
|
+
await stopVmware(platform);
|
|
94
|
+
logger.step('Deleting the state');
|
|
95
|
+
if (!existsSync(stateDir)) {
|
|
96
|
+
logger.info(`No state at ${stateDir} (already deleted?) — nothing to delete.`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const size = await dirSizeHuman(stateDir);
|
|
100
|
+
const ask = `Delete the sandbox state at '${stateDir}' (${size}; re-pulled on the next run)? ` +
|
|
101
|
+
`This removes the pristine base and the working clone of '${runOptions.image}'.`;
|
|
102
|
+
if (yes || (await confirm(ask, { default: 'y' }))) {
|
|
103
|
+
logger.cmd(`rm -rf ${stateDir}`);
|
|
104
|
+
rmSync(stateDir, { recursive: true, force: true });
|
|
105
|
+
logger.ok(`State deleted: ${stateDir} (${size} freed).`);
|
|
106
|
+
logger.warn("Fusion's VM library may still list the deleted working VM — remove the stale entry in the Fusion UI (harmless).");
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
logger.info(`Kept '${stateDir}' — nothing was deleted.`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
logger.step('Sandbox deleted');
|
|
113
|
+
logger.info(`State: ${stateDir}`);
|
|
114
|
+
logger.info(`Next run: agent-dev-env run ${platform} (re-pulls the archive)`);
|
|
115
|
+
}
|
|
116
|
+
/** The QEMU delete flow — stop first (delegating to the stop flow), then
|
|
117
|
+
* remove the state dir (working disk overlay + TPM + EFI NVRAM + the
|
|
118
|
+
* pulled image cache). The next run re-pulls the image and starts fresh.
|
|
119
|
+
*
|
|
120
|
+
* @param options - --yes flag.
|
|
121
|
+
*/
|
|
122
|
+
async function deleteQemu(options) {
|
|
123
|
+
const runOptions = resolveRunOptions('windows-qemu', { yes: options.yes });
|
|
124
|
+
const stateDir = qemuStateDir(runOptions.image);
|
|
125
|
+
const yes = options.yes === true;
|
|
126
|
+
logger.title(`Deleting Windows QEMU sandbox: ${runOptions.image}`);
|
|
127
|
+
logger.step('Stopping the sandbox');
|
|
128
|
+
await stopQemuSandbox();
|
|
129
|
+
logger.step('Deleting the state');
|
|
130
|
+
if (!existsSync(stateDir)) {
|
|
131
|
+
logger.info(`No state at ${stateDir} (already deleted?) — nothing to delete.`);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
const size = await dirSizeHuman(stateDir);
|
|
135
|
+
const ask = `Delete the sandbox state at '${stateDir}' (${size}; re-pulled on the next run)? ` +
|
|
136
|
+
`This removes the working overlay + TPM + EFI NVRAM and the pulled image of '${runOptions.image}'.`;
|
|
137
|
+
if (yes || (await confirm(ask, { default: 'y' }))) {
|
|
138
|
+
logger.cmd(`rm -rf ${stateDir}`);
|
|
139
|
+
rmSync(stateDir, { recursive: true, force: true });
|
|
140
|
+
logger.ok(`State deleted: ${stateDir} (${size} freed).`);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
logger.info(`Kept '${stateDir}' — nothing was deleted.`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
logger.step('Sandbox deleted');
|
|
147
|
+
logger.info(`State: ${stateDir}`);
|
|
148
|
+
logger.info(`Next run: agent-dev-env run windows-qemu (re-pulls the image)`);
|
|
149
|
+
}
|
|
150
|
+
/** @internal — `du -sh`-style human size ("12G", "812M"); '?' on failure. */
|
|
151
|
+
export async function dirSizeHuman(dir) {
|
|
152
|
+
const res = await run('du', ['-sh', dir]);
|
|
153
|
+
return res.code === 0 ? res.stdout.trim().split('\t')[0] : '?';
|
|
154
|
+
}
|
|
155
|
+
/** The pristine-image deletion — always opt-in (never implied). */
|
|
156
|
+
async function maybeDeletePristine(image, yes, pristine) {
|
|
157
|
+
const ask = `Also delete the pristine image '${image}' (frees ~50 GB; re-pulled from ` +
|
|
158
|
+
'GHCR on the next run)?';
|
|
159
|
+
if (!pristine && !(yes || (await confirm(ask, { default: 'n' })))) {
|
|
160
|
+
logger.info(`Kept the pristine image '${image}'.`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (!(await vmExists(image))) {
|
|
164
|
+
logger.info(`Pristine image '${image}' does not exist — nothing to delete.`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if ((await vmState(image)) === 'running') {
|
|
168
|
+
logger.cmd(`tart stop ${image}`);
|
|
169
|
+
await stopVm(image);
|
|
170
|
+
}
|
|
171
|
+
logger.cmd(`tart delete ${image}`);
|
|
172
|
+
const res = await deleteVm(image);
|
|
173
|
+
if (res.code === 0) {
|
|
174
|
+
logger.ok(`Pristine image '${image}' deleted.`);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
logger.warn(`'tart delete ${image}' failed — is it running?`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// commands/doctor.ts — `agent-dev-env doctor [--platform P]`: prereq table
|
|
2
|
+
// with install hints and a free-disk estimate (against the vars files'
|
|
3
|
+
// disk_size + the ~50 GB base image for macOS builds). Supersedes the
|
|
4
|
+
// scattered require_cmd blocks; read-only.
|
|
5
|
+
//
|
|
6
|
+
// Exit code: 1 when any required check fails, 0 otherwise. Optional
|
|
7
|
+
// (bridge/build-only) tooling is reported as such and does not fail the
|
|
8
|
+
// check.
|
|
9
|
+
import { statfsSync } from 'node:fs';
|
|
10
|
+
import { listImages, varsFor } from '../lifecycle/catalog.js';
|
|
11
|
+
import { commandExists } from '../lib/exec.js';
|
|
12
|
+
import { logger } from '../lib/logger.js';
|
|
13
|
+
import { PLATFORM_DEFAULTS, PLATFORMS } from '../lib/platform.js';
|
|
14
|
+
import { findVmrun } from '../lib/vmrun.js';
|
|
15
|
+
/** Overhead for the macOS base image (~50 GB, pulled for the build). */
|
|
16
|
+
const BASE_IMAGE_GB = 50;
|
|
17
|
+
/** Runs the prerequisite + free-disk check for one or all platforms.
|
|
18
|
+
*
|
|
19
|
+
* @param options - Platform scoping (all platforms by default).
|
|
20
|
+
* @returns Exit code: 0 when all required checks pass, 1 otherwise.
|
|
21
|
+
*/
|
|
22
|
+
export async function doctorCmd(options = {}) {
|
|
23
|
+
const targets = options.platform ? [options.platform] : [...PLATFORMS];
|
|
24
|
+
let anyFailed = false;
|
|
25
|
+
for (const [i, platform] of targets.entries()) {
|
|
26
|
+
if (i > 0) {
|
|
27
|
+
logger.out('');
|
|
28
|
+
}
|
|
29
|
+
anyFailed = (await printDoctor(platform)) || anyFailed;
|
|
30
|
+
}
|
|
31
|
+
return anyFailed ? 1 : 0;
|
|
32
|
+
}
|
|
33
|
+
async function printDoctor(platform) {
|
|
34
|
+
logger.title(`Doctor: ${platform} (${PLATFORM_DEFAULTS[platform].image})`);
|
|
35
|
+
const checks = await checksFor(platform);
|
|
36
|
+
const width = Math.max(...checks.map((c) => c.label.length), 'Requirement'.length);
|
|
37
|
+
const statusText = (c) => (c.ok ? 'ok' : c.required ? 'missing' : 'optional');
|
|
38
|
+
logger.out(` ${'Requirement'.padEnd(width)} Status Hint`);
|
|
39
|
+
for (const check of checks) {
|
|
40
|
+
const status = statusText(check);
|
|
41
|
+
const colored = status === 'ok'
|
|
42
|
+
? logger.color('green') + status + logger.reset()
|
|
43
|
+
: logger.color('red') + status + logger.reset();
|
|
44
|
+
const line = `${check.label.padEnd(width)} ${colored.padEnd(status.length)} ${check.hint}`;
|
|
45
|
+
logger.out(line.replace(/\s+$/, ''));
|
|
46
|
+
}
|
|
47
|
+
const failed = checks.filter((c) => c.required && !c.ok);
|
|
48
|
+
if (failed.length === 0) {
|
|
49
|
+
logger.ok('All checks passed.');
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
logger.warn(`${failed.length} required check(s) failed.`);
|
|
53
|
+
}
|
|
54
|
+
return failed.length > 0;
|
|
55
|
+
}
|
|
56
|
+
async function checksFor(platform) {
|
|
57
|
+
const platformChecks = platform === 'macos'
|
|
58
|
+
? macosChecks()
|
|
59
|
+
: platform === 'windows-qemu'
|
|
60
|
+
? qemuChecks()
|
|
61
|
+
: vmwareChecks();
|
|
62
|
+
return [...hostChecks(platform), ...platformChecks];
|
|
63
|
+
}
|
|
64
|
+
/** Checks shared by every platform: host, arch, free disk. */
|
|
65
|
+
function hostChecks(platform) {
|
|
66
|
+
const needed = diskNeededGb(platform);
|
|
67
|
+
const free = freeDiskGb();
|
|
68
|
+
return [
|
|
69
|
+
{
|
|
70
|
+
label: 'macOS host',
|
|
71
|
+
ok: process.platform === 'darwin',
|
|
72
|
+
required: true,
|
|
73
|
+
hint: 'the sandbox runners run on macOS only',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
label: 'Apple Silicon',
|
|
77
|
+
ok: process.arch === 'arm64',
|
|
78
|
+
required: true,
|
|
79
|
+
hint: 'Tart/QEMU/Fusion can only virtualize ARM64 guests (Intel unsupported)',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
label: 'free disk',
|
|
83
|
+
ok: free !== undefined && free >= needed,
|
|
84
|
+
required: true,
|
|
85
|
+
hint: free === undefined
|
|
86
|
+
? 'could not determine free disk'
|
|
87
|
+
: `free ${free.toFixed(0)} GB vs ${needed.toFixed(0)} GB needed ` +
|
|
88
|
+
`(disk_size + ~${BASE_IMAGE_GB} GB base image)`,
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
function macosChecks() {
|
|
93
|
+
return [
|
|
94
|
+
{
|
|
95
|
+
label: 'tart',
|
|
96
|
+
ok: commandExists('tart'),
|
|
97
|
+
required: true,
|
|
98
|
+
hint: 'brew install cirruslabs/cli/tart',
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
function qemuChecks() {
|
|
103
|
+
return [
|
|
104
|
+
{
|
|
105
|
+
label: 'qemu',
|
|
106
|
+
ok: commandExists('qemu-system-aarch64'),
|
|
107
|
+
required: true,
|
|
108
|
+
hint: 'brew install qemu',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
label: 'qemu-img',
|
|
112
|
+
ok: commandExists('qemu-img'),
|
|
113
|
+
required: true,
|
|
114
|
+
hint: 'brew install qemu',
|
|
115
|
+
},
|
|
116
|
+
{ label: 'swtpm', ok: commandExists('swtpm'), required: true, hint: 'brew install swtpm' },
|
|
117
|
+
{
|
|
118
|
+
label: 'oras',
|
|
119
|
+
ok: commandExists('oras'),
|
|
120
|
+
required: true,
|
|
121
|
+
hint: 'brew install oras — needed to pull images',
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
function vmwareChecks() {
|
|
126
|
+
return [
|
|
127
|
+
{
|
|
128
|
+
label: 'vmrun',
|
|
129
|
+
ok: findVmrun() !== undefined,
|
|
130
|
+
required: true,
|
|
131
|
+
hint: 'install VMware Fusion (free for personal use) or set FUSION_APP_PATH',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
label: 'oras',
|
|
135
|
+
ok: commandExists('oras'),
|
|
136
|
+
required: true,
|
|
137
|
+
hint: 'brew install oras — needed to pull images',
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
/** Largest disk_size among the platform's images + the base-image
|
|
142
|
+
* overhead. */
|
|
143
|
+
function diskNeededGb(platform) {
|
|
144
|
+
let maxSize = 0;
|
|
145
|
+
try {
|
|
146
|
+
const sizes = listImages()
|
|
147
|
+
.filter((image) => image.platform === platform)
|
|
148
|
+
.map((image) => varsFor(image).disk_size)
|
|
149
|
+
.filter((size) => typeof size === 'number');
|
|
150
|
+
maxSize = sizes.length > 0 ? Math.max(...sizes) : 0;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// no image in the catalog — fall back to the default estimate
|
|
154
|
+
}
|
|
155
|
+
return (maxSize || 100) + BASE_IMAGE_GB;
|
|
156
|
+
}
|
|
157
|
+
function freeDiskGb() {
|
|
158
|
+
try {
|
|
159
|
+
const s = statfsSync('/');
|
|
160
|
+
return (s.bavail * s.bsize) / 1e9;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// commands/list.ts — `agent-dev-env list`: the bundled images (name,
|
|
2
|
+
// platform, image_version) from the per-image vars files, replacing
|
|
3
|
+
// list_images()/find_vars_file() in build.sh/deploy.sh/tag.sh.
|
|
4
|
+
import { imageVersion, listImages } from '../lifecycle/catalog.js';
|
|
5
|
+
import { logger } from '../lib/logger.js';
|
|
6
|
+
/** Prints the bundled images as a NAME / PLATFORM / VERSION table.
|
|
7
|
+
*
|
|
8
|
+
* @returns Exit code: 0 on success, 1 when no images are in the catalog.
|
|
9
|
+
*/
|
|
10
|
+
export async function listCmd() {
|
|
11
|
+
const images = listImages();
|
|
12
|
+
if (images.length === 0) {
|
|
13
|
+
logger.warn('No images found: no vars files under images/*/vars/.');
|
|
14
|
+
return 1;
|
|
15
|
+
}
|
|
16
|
+
const header = ['NAME', 'PLATFORM', 'VERSION'];
|
|
17
|
+
const rows = images.map((image) => {
|
|
18
|
+
let version = '?';
|
|
19
|
+
try {
|
|
20
|
+
version = imageVersion(image);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// vars file without image_version — show '?' and let the caller
|
|
24
|
+
// (deploy/tag) fail with the precise error.
|
|
25
|
+
}
|
|
26
|
+
return [image.name, image.platform, version];
|
|
27
|
+
});
|
|
28
|
+
const widths = header.map((h, i) => Math.max(h.length, ...rows.map((row) => row[i].length)));
|
|
29
|
+
const fmt = (cols) => cols
|
|
30
|
+
.map((c, i) => c.padEnd(widths[i]))
|
|
31
|
+
.join(' ')
|
|
32
|
+
.replace(/\s+$/, '');
|
|
33
|
+
logger.out(fmt(header));
|
|
34
|
+
for (const row of rows) {
|
|
35
|
+
logger.out(fmt(row));
|
|
36
|
+
}
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// commands/not-yet.ts — the placeholder for the remaining unsupported
|
|
2
|
+
// command/platform combo: `sync` on the Windows platforms (the settings
|
|
3
|
+
// copy is tart/ssh2-based, like macOS/Ubuntu, and Windows has no such
|
|
4
|
+
// transport yet).
|
|
5
|
+
import { logger } from '../lib/logger.js';
|
|
6
|
+
/** Reports that a command is not supported for the platform.
|
|
7
|
+
*
|
|
8
|
+
* @param command - The command name (run/stop/delete/sync).
|
|
9
|
+
* @param platform - The platform that is not supported.
|
|
10
|
+
* @returns The exit code to use (1).
|
|
11
|
+
*/
|
|
12
|
+
export function notYet(command, platform) {
|
|
13
|
+
logger.warn(`${command} is not supported for '${platform}'.`);
|
|
14
|
+
return 1;
|
|
15
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// commands/register.ts — the commander surface. Splitting the CLI into
|
|
2
|
+
// small register functions keeps every function under the oxlint
|
|
3
|
+
// max-lines-per-function gate (mirrors src/cli/register-commands.ts in
|
|
4
|
+
// ameshkov/mcp-compress-router).
|
|
5
|
+
import { deleteCmd } from './delete.js';
|
|
6
|
+
import { doctorCmd } from './doctor.js';
|
|
7
|
+
import { listCmd } from './list.js';
|
|
8
|
+
import { runCmd } from './run.js';
|
|
9
|
+
import { statusCmd } from './status.js';
|
|
10
|
+
import { stopCmd } from './stop.js';
|
|
11
|
+
import { syncCmd } from './sync.js';
|
|
12
|
+
import { buildCmd } from '../lifecycle/build.js';
|
|
13
|
+
import { deployCmd } from '../lifecycle/deploy.js';
|
|
14
|
+
import { tagCmd } from '../lifecycle/tag.js';
|
|
15
|
+
import { watchBuildCmd } from '../lifecycle/watch-build.js';
|
|
16
|
+
import { logger } from '../lib/logger.js';
|
|
17
|
+
import { isPlatform } from '../lib/platform.js';
|
|
18
|
+
const PLATFORM_CHOICE = 'macos|windows-qemu|windows-vmware|ubuntu-vmware';
|
|
19
|
+
/** Validates the platform argument, dying when unknown (same message shape
|
|
20
|
+
* as the shell's per-script usage errors). */
|
|
21
|
+
function parsePlatform(value) {
|
|
22
|
+
if (!isPlatform(value)) {
|
|
23
|
+
logger.die(`unknown platform '${value}' (expected ${PLATFORM_CHOICE})`);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
/** Registers run/stop/delete/sync — the per-platform VM commands.
|
|
28
|
+
*
|
|
29
|
+
* @param program - The commander program to wire commands onto.
|
|
30
|
+
*/
|
|
31
|
+
export function registerVmCommands(program) {
|
|
32
|
+
program
|
|
33
|
+
.command('run')
|
|
34
|
+
.argument('<platform>', PLATFORM_CHOICE, parsePlatform)
|
|
35
|
+
.description('Run (and wire up) a sandbox VM: macos | windows-qemu | windows-vmware | ubuntu-vmware')
|
|
36
|
+
.option('--headless', 'run without a window (tart run --no-graphics)')
|
|
37
|
+
.option('--foreground', 'keep the terminal attached and block until the VM stops')
|
|
38
|
+
.option('--no-agent', 'skip the SSH agent bridge setup')
|
|
39
|
+
.option('--no-docker', 'skip the Docker engine bridge setup')
|
|
40
|
+
.option('--no-settings', 'skip copying the host user settings into the guest')
|
|
41
|
+
.option('--work-dir <path>', 'host dir to share into the guest (SANDBOX_WORK_DIR)')
|
|
42
|
+
.option('--reset', 'delete the working VM first (fresh clone on this run)')
|
|
43
|
+
.option('--image <image>', 'pristine image VM to pull/clone from (SANDBOX_IMAGE)')
|
|
44
|
+
.option('--owner <owner>', 'GHCR owner for pulls (GHCR_OWNER)')
|
|
45
|
+
.option('--yes', 'skip confirmation prompts')
|
|
46
|
+
.action(async (platform, options) => {
|
|
47
|
+
process.exitCode = await runCmd(platform, options);
|
|
48
|
+
});
|
|
49
|
+
program
|
|
50
|
+
.command('stop')
|
|
51
|
+
.argument('<platform>', PLATFORM_CHOICE, parsePlatform)
|
|
52
|
+
.description('Stop the sandbox VM and its host bridges')
|
|
53
|
+
.action(async (platform) => {
|
|
54
|
+
process.exitCode = await stopCmd(platform);
|
|
55
|
+
});
|
|
56
|
+
program
|
|
57
|
+
.command('delete')
|
|
58
|
+
.argument('<platform>', PLATFORM_CHOICE, parsePlatform)
|
|
59
|
+
.option('--yes', 'do not ask for confirmation')
|
|
60
|
+
.option('--pristine', 'also delete the pristine image (macOS only, with tart delete)')
|
|
61
|
+
.description('Delete the sandbox VM/state (pristine image too with --pristine)')
|
|
62
|
+
.action(async (platform, options) => {
|
|
63
|
+
process.exitCode = await deleteCmd(platform, options);
|
|
64
|
+
});
|
|
65
|
+
program
|
|
66
|
+
.command('sync')
|
|
67
|
+
.argument('<platform>', PLATFORM_CHOICE, parsePlatform)
|
|
68
|
+
.option('--yes', 'do not ask for confirmation')
|
|
69
|
+
.description('Copy the host user settings into the guest (macos | ubuntu-vmware)')
|
|
70
|
+
.action(async (platform, options) => {
|
|
71
|
+
process.exitCode = await syncCmd(platform, options);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** Registers status/list — the live-info commands (implemented).
|
|
75
|
+
*
|
|
76
|
+
* @param program - The commander program to wire commands onto.
|
|
77
|
+
*/
|
|
78
|
+
export function registerStatusCommands(program) {
|
|
79
|
+
program
|
|
80
|
+
.command('status')
|
|
81
|
+
.argument('[platform]', PLATFORM_CHOICE, parsePlatform)
|
|
82
|
+
.description('Live status of one or all platforms')
|
|
83
|
+
.action(async (platform) => {
|
|
84
|
+
process.exitCode = await statusCmd(platform);
|
|
85
|
+
});
|
|
86
|
+
program
|
|
87
|
+
.command('list')
|
|
88
|
+
.description('Bundled images: name, platform, image_version')
|
|
89
|
+
.action(async () => {
|
|
90
|
+
process.exitCode = await listCmd();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/** Registers build/deploy/tag — the image lifecycle (Phase 7).
|
|
94
|
+
*
|
|
95
|
+
* @param program - The commander program to wire commands onto.
|
|
96
|
+
*/
|
|
97
|
+
export function registerLifecycleCommands(program) {
|
|
98
|
+
program
|
|
99
|
+
.command('build')
|
|
100
|
+
.argument('[image...]', 'image names')
|
|
101
|
+
.option('--force', 'force a rebuild (packer -force)')
|
|
102
|
+
.option('--no-watchdog', 'skip the VNC build watchdog')
|
|
103
|
+
.description('Build sandbox images with Packer')
|
|
104
|
+
.action(async (images, options) => {
|
|
105
|
+
process.exitCode = await buildCmd(images, {
|
|
106
|
+
force: options.force,
|
|
107
|
+
watchdog: options.watchdog !== false,
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
program
|
|
111
|
+
.command('deploy')
|
|
112
|
+
.argument('[image...]', 'image names')
|
|
113
|
+
.option('--owner <owner>', 'GHCR owner override')
|
|
114
|
+
.description('Push locally built images to GHCR')
|
|
115
|
+
.action(async (images, options) => {
|
|
116
|
+
process.exitCode = await deployCmd(images, { owner: options.owner });
|
|
117
|
+
});
|
|
118
|
+
program
|
|
119
|
+
.command('tag')
|
|
120
|
+
.argument('[image...]', 'image names')
|
|
121
|
+
.option('--repo <path>', 'repo checkout to tag from (default: the current checkout)')
|
|
122
|
+
.description('Create and push the git release tag')
|
|
123
|
+
.action(async (images, options) => {
|
|
124
|
+
process.exitCode = await tagCmd(images, { repo: options.repo });
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/** Registers doctor — plus the hidden build watch-build command.
|
|
128
|
+
*
|
|
129
|
+
* @param program - The commander program to wire commands onto.
|
|
130
|
+
*/
|
|
131
|
+
export function registerDoctorCommands(program) {
|
|
132
|
+
program
|
|
133
|
+
.command('doctor')
|
|
134
|
+
.option('--platform <platform>', PLATFORM_CHOICE, parsePlatform)
|
|
135
|
+
.description('Prerequisite + disk check for one or all platforms')
|
|
136
|
+
.action(async (options) => {
|
|
137
|
+
process.exitCode = await doctorCmd({ platform: options.platform });
|
|
138
|
+
});
|
|
139
|
+
program
|
|
140
|
+
.command('watch-build', { hidden: true })
|
|
141
|
+
.argument('<vnc-port>', 'VNC port of the Packer build (builder pins 5901)')
|
|
142
|
+
.argument('[outdir]', 'frame/output directory')
|
|
143
|
+
.option('--no-ocr', 'skip the optional macOS OCR fallback')
|
|
144
|
+
.description('Drive a VNC build watchdog against a build window')
|
|
145
|
+
.action(async (vncPort, outdir, options) => {
|
|
146
|
+
process.exitCode = await watchBuildCmd(Number.parseInt(vncPort, 10), outdir, {
|
|
147
|
+
noOcr: options.noOcr,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// commands/run.ts — `agent-dev-env run <platform>`: resolve the options
|
|
2
|
+
// (flags > SANDBOX_* env > platform defaults), pick the backend, run the
|
|
3
|
+
// shared flow (runners/framework.ts). Phase 3 shipped the macOS backend,
|
|
4
|
+
// Phase 4 the Ubuntu VMware backend, Phase 5 the Windows VMware backend,
|
|
5
|
+
// Phase 6 the Windows QEMU backend — all four platforms are green.
|
|
6
|
+
import { logger } from '../lib/logger.js';
|
|
7
|
+
import { runSandbox } from '../runners/framework.js';
|
|
8
|
+
import { macosBackend } from '../runners/macos.js';
|
|
9
|
+
import { resolveRunOptions } from '../runners/options.js';
|
|
10
|
+
import { ubuntuBackend } from '../runners/ubuntu.js';
|
|
11
|
+
import { windowsBackend } from '../runners/windows.js';
|
|
12
|
+
import { windowsQemuBackend } from '../runners/windows-qemu.js';
|
|
13
|
+
/** Runs a sandbox VM platform.
|
|
14
|
+
*
|
|
15
|
+
* @param platform - The platform to run.
|
|
16
|
+
* @param flags - The `run` command flags.
|
|
17
|
+
* @returns The process exit code.
|
|
18
|
+
*/
|
|
19
|
+
export async function runCmd(platform, flags) {
|
|
20
|
+
const options = resolveRunOptions(platform, flags);
|
|
21
|
+
const context = buildContext(options);
|
|
22
|
+
const title = platform === 'macos'
|
|
23
|
+
? `macOS sandbox: ${context.vm} (image: ${context.image}, ` +
|
|
24
|
+
`${options.headless ? 'headless' : 'gui'} mode)`
|
|
25
|
+
: platform === 'windows-vmware'
|
|
26
|
+
? `Windows VMware sandbox: ${context.vm} (image: ${context.image})`
|
|
27
|
+
: platform === 'windows-qemu'
|
|
28
|
+
? `Windows QEMU sandbox: ${context.vm} (image: ${context.image})`
|
|
29
|
+
: `Ubuntu VMware sandbox: ${context.vm} (image: ${context.image})`;
|
|
30
|
+
logger.title(title);
|
|
31
|
+
const backend = platform === 'macos'
|
|
32
|
+
? macosBackend
|
|
33
|
+
: platform === 'windows-vmware'
|
|
34
|
+
? windowsBackend
|
|
35
|
+
: platform === 'windows-qemu'
|
|
36
|
+
? windowsQemuBackend
|
|
37
|
+
: ubuntuBackend;
|
|
38
|
+
await runSandbox(backend, context);
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
/** @internal — folds the resolved options into the runner context (the
|
|
42
|
+
* guest mount path is the per-platform convention: Tart's
|
|
43
|
+
* /Volumes/My Shared Files/ on macOS, /mnt/hgfs/work on VMware). */
|
|
44
|
+
export function buildContext(options) {
|
|
45
|
+
const guestMount = options.platform === 'macos'
|
|
46
|
+
? `/Volumes/My Shared Files/${options.mountName}`
|
|
47
|
+
: '/mnt/hgfs/work';
|
|
48
|
+
return {
|
|
49
|
+
platform: options.platform,
|
|
50
|
+
options,
|
|
51
|
+
vm: options.vm,
|
|
52
|
+
image: options.image,
|
|
53
|
+
workDir: options.workDir,
|
|
54
|
+
mountName: options.mountName,
|
|
55
|
+
guestMount,
|
|
56
|
+
agentPort: options.agentPort,
|
|
57
|
+
dockerPort: options.dockerPort,
|
|
58
|
+
openchamberPort: options.openchamberPort,
|
|
59
|
+
sshPort: options.sshPort,
|
|
60
|
+
rdpPort: options.rdpPort,
|
|
61
|
+
winrmPort: options.winrmPort,
|
|
62
|
+
cpuCount: options.cpuCount,
|
|
63
|
+
memoryMb: options.memoryMb,
|
|
64
|
+
};
|
|
65
|
+
}
|