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,242 @@
|
|
|
1
|
+
// runners/vmware-image.ts — step 1 for the VMware backends (ubuntu-vmware
|
|
2
|
+
// and windows-vmware, Phase 4 + Phase 5): pick the image archive (env
|
|
3
|
+
// override → local build output → cached pull → oras pull), extract the
|
|
4
|
+
// pristine base (identity marker = path|size|mtime, so a rebuild over the
|
|
5
|
+
// same path is detected), clone the working VM + set its display name,
|
|
6
|
+
// and upgrade it once per hardware version (.hw-version). Port of
|
|
7
|
+
// run-{ubuntu,windows}-vmware-sandbox.sh §step 1/2; the CLI's data dir
|
|
8
|
+
// replaces the legacy agent-sandbox one. The per-platform pieces (platform
|
|
9
|
+
// id, the archive override env var) are parameters, so the two VMware
|
|
10
|
+
// backends share one implementation.
|
|
11
|
+
//
|
|
12
|
+
// The thin per-platform wrappers (ubuntu-image.ts / windows-image.ts)
|
|
13
|
+
// bind the platform id + override var; everything else is here.
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
import { commandExists, run } from '../lib/exec.js';
|
|
17
|
+
import { registryRef, resolveOwner } from '../lib/ghcr.js';
|
|
18
|
+
import { logger } from '../lib/logger.js';
|
|
19
|
+
import { buildDir, imageRootDir, vmwareArchivePath, workingVmxPath } from '../lib/paths.js';
|
|
20
|
+
import { confirmDefault } from '../lib/prompt.js';
|
|
21
|
+
import { cloneVm, setVmDisplayName, upgradeVmHardware, vmwareHwVersion } from '../lib/vmrun.js';
|
|
22
|
+
/** @internal — the pristine-archive identity (path|size|mtime; the same
|
|
23
|
+
* scheme as the QEMU runner's backing-image marker — a rebuild packs the
|
|
24
|
+
* new image at the SAME path, so the path alone misses it).
|
|
25
|
+
*
|
|
26
|
+
* @param path - The archive path.
|
|
27
|
+
* @param size - File size in bytes.
|
|
28
|
+
* @param mtimeMs - File mtime (ms since epoch).
|
|
29
|
+
* @returns The identity string.
|
|
30
|
+
*/
|
|
31
|
+
export function archiveIdentity(path, size, mtimeMs) {
|
|
32
|
+
return `${path}|${size}|${Math.floor(mtimeMs / 1000)}`;
|
|
33
|
+
}
|
|
34
|
+
/** The base directory of the pristine extraction. */
|
|
35
|
+
function baseDir(platform, image) {
|
|
36
|
+
return join(imageRootDir(platform, image), 'base');
|
|
37
|
+
}
|
|
38
|
+
/** The pristine base vmx. */
|
|
39
|
+
function baseVmx(platform, image) {
|
|
40
|
+
return join(baseDir(platform, image), `${image}.vmx`);
|
|
41
|
+
}
|
|
42
|
+
/** The archive identity marker path. */
|
|
43
|
+
function baseMarker(platform, image) {
|
|
44
|
+
return join(imageRootDir(platform, image), 'base-archive.txt');
|
|
45
|
+
}
|
|
46
|
+
/** The working clone's vmx. */
|
|
47
|
+
export function vmwareWorkingVmx(platform, image) {
|
|
48
|
+
return workingVmxPath(platform, image);
|
|
49
|
+
}
|
|
50
|
+
/** The .hw-version marker (records the upgraded hardware version). */
|
|
51
|
+
function hwMarker(platform, image) {
|
|
52
|
+
return join(dirname(vmwareWorkingVmx(platform, image)), '.hw-version');
|
|
53
|
+
}
|
|
54
|
+
/** Step 1: select the archive, extract the base, clone the working VM
|
|
55
|
+
* and upgrade it if the installed Fusion supports a newer hardware
|
|
56
|
+
* version.
|
|
57
|
+
*
|
|
58
|
+
* @param platform - The target platform (state dir naming).
|
|
59
|
+
* @param overrideEnv - The env var holding a local archive override
|
|
60
|
+
* (UBUNTU_VMWARE_IMAGE / WINDOWS_VMWARE_IMAGE).
|
|
61
|
+
* @param context - The run context.
|
|
62
|
+
* @param state - The accumulated run state (imageArchive set here).
|
|
63
|
+
*/
|
|
64
|
+
export async function ensureVmwareImage(platform, overrideEnv, context, state) {
|
|
65
|
+
const archive = await pickImage(platform, overrideEnv, context);
|
|
66
|
+
state.imageArchive = archive;
|
|
67
|
+
logger.ok(`Using archive: ${archive}`);
|
|
68
|
+
await ensureBase(platform, context, archive);
|
|
69
|
+
if (!(await ensureWorkingVm(platform, context))) {
|
|
70
|
+
logger.die('could not clone the working VM (see above). Check Fusion\u2019s VM library path and re-run.');
|
|
71
|
+
}
|
|
72
|
+
await upgradeWorkingVm(platform, context);
|
|
73
|
+
}
|
|
74
|
+
/** The shell's pick_image: <OVERRIDE_ENV> → local build output (packed on
|
|
75
|
+
* demand) → cached pull → oras pull with the owner chain.
|
|
76
|
+
*
|
|
77
|
+
* @param platform - The target platform.
|
|
78
|
+
* @param overrideEnv - The environment override variable name.
|
|
79
|
+
* @param context - The run context.
|
|
80
|
+
* @returns The archive path to run.
|
|
81
|
+
*/
|
|
82
|
+
async function pickImage(platform, overrideEnv, context) {
|
|
83
|
+
const env = context.options.env;
|
|
84
|
+
const image = context.image;
|
|
85
|
+
const override = env[overrideEnv];
|
|
86
|
+
if (override) {
|
|
87
|
+
if (!existsSync(override)) {
|
|
88
|
+
logger.die(`${overrideEnv} points to a file that does not exist: ${override}`);
|
|
89
|
+
}
|
|
90
|
+
return override;
|
|
91
|
+
}
|
|
92
|
+
const outputDir = join(buildDir(platform), 'output');
|
|
93
|
+
const local = join(outputDir, `${image}.tar.gz`);
|
|
94
|
+
if (existsSync(join(outputDir, `${image}.vmx`)) && !existsSync(local)) {
|
|
95
|
+
logger.info(`No archive yet — packing the local build output into ${local}`);
|
|
96
|
+
const vmdks = readdirSync(outputDir)
|
|
97
|
+
.filter((file) => file.endsWith('.vmdk'))
|
|
98
|
+
.map((file) => join(outputDir, file));
|
|
99
|
+
mkdirSync(dirname(local), { recursive: true });
|
|
100
|
+
const packed = await run('tar', ['-czf', local, '--exclude=*.log', `${image}.vmx`, `${image}.nvram`, ...vmdks], { cwd: outputDir });
|
|
101
|
+
if (packed.code !== 0) {
|
|
102
|
+
logger.die(`failed to pack the local build output:\n${packed.stderr.trim()}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (existsSync(local)) {
|
|
106
|
+
return local;
|
|
107
|
+
}
|
|
108
|
+
const cached = vmwareArchivePath(platform, image);
|
|
109
|
+
if (existsSync(cached)) {
|
|
110
|
+
return cached;
|
|
111
|
+
}
|
|
112
|
+
return pullImage(platform, overrideEnv, context, cached);
|
|
113
|
+
}
|
|
114
|
+
/** The oras pull into the image/ cache dir (owner chain + confirm +
|
|
115
|
+
* archive presence check).
|
|
116
|
+
*
|
|
117
|
+
* @param platform - The target platform.
|
|
118
|
+
* @param overrideEnv - The environment override variable name (prompts).
|
|
119
|
+
* @param context - The run context.
|
|
120
|
+
* @param cached - The destination archive path.
|
|
121
|
+
* @returns The pulled archive path.
|
|
122
|
+
*/
|
|
123
|
+
async function pullImage(platform, overrideEnv, context, cached) {
|
|
124
|
+
if (!commandExists('oras')) {
|
|
125
|
+
logger.die('oras is not installed — needed to pull the image (brew install oras). ' +
|
|
126
|
+
`Set ${overrideEnv} to a local archive to skip.`);
|
|
127
|
+
}
|
|
128
|
+
const owner = await resolveOwner({ owner: context.options.owner, env: context.options.env });
|
|
129
|
+
const ref = registryRef(context.image, 'latest', owner);
|
|
130
|
+
const hint = platform === 'windows-vmware' ? '~20 GB' : '~15 GB';
|
|
131
|
+
if (!(await confirmDefault(`Pull ${ref} (one-time, ${hint} download)?`, {
|
|
132
|
+
default: 'y',
|
|
133
|
+
yes: context.options.yes,
|
|
134
|
+
}))) {
|
|
135
|
+
logger.die(`aborted — no sandbox image available. Set ${overrideEnv} to a local archive or pull manually.`);
|
|
136
|
+
}
|
|
137
|
+
mkdirSync(dirname(cached), { recursive: true });
|
|
138
|
+
logger.info(`Pulling ${ref} (one-time, ${hint} download)...`);
|
|
139
|
+
const res = await run('oras', ['pull', ref], { cwd: dirname(cached) });
|
|
140
|
+
if (res.code !== 0) {
|
|
141
|
+
logger.die('oras pull failed — check your network connection (public GHCR images pull without a login).');
|
|
142
|
+
}
|
|
143
|
+
if (!existsSync(cached)) {
|
|
144
|
+
logger.die(`oras pull produced no ${cached} — is the image published under ${ref}?`);
|
|
145
|
+
}
|
|
146
|
+
return cached;
|
|
147
|
+
}
|
|
148
|
+
/** Extracts the pristine archive into base/ (identity-marker gated; an
|
|
149
|
+
* archive change drops base + the working clone).
|
|
150
|
+
*
|
|
151
|
+
* @param platform - The target platform.
|
|
152
|
+
* @param context - The run context.
|
|
153
|
+
* @param archive - The archive to extract.
|
|
154
|
+
*/
|
|
155
|
+
async function ensureBase(platform, context, archive) {
|
|
156
|
+
const image = context.image;
|
|
157
|
+
const markerPath = baseMarker(platform, image);
|
|
158
|
+
const baseDirPath = baseDir(platform, image);
|
|
159
|
+
const baseVmxPath = baseVmx(platform, image);
|
|
160
|
+
const stat = statSync(archive);
|
|
161
|
+
const id = archiveIdentity(archive, stat.size, stat.mtimeMs);
|
|
162
|
+
const hadMarker = existsSync(markerPath);
|
|
163
|
+
const marker = hadMarker ? readFileSync(markerPath, 'utf8').trim() : '';
|
|
164
|
+
if (hadMarker && marker !== id) {
|
|
165
|
+
logger.warn('The archive changed (new build or pull) — re-extracting the pristine VM and dropping the working clone.');
|
|
166
|
+
logger.warn('The working clone\u2019s guest state (installs, config, agent files) is lost with it.');
|
|
167
|
+
rmSync(baseDirPath, { recursive: true, force: true });
|
|
168
|
+
rmSync(join(imageRootDir(platform, image), 'working'), { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
if (marker === id && existsSync(baseVmxPath)) {
|
|
171
|
+
logger.ok(`Pristine VM extracted (${baseDirPath}).`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
mkdirSync(baseDirPath, { recursive: true });
|
|
175
|
+
logger.cmd(`tar -xzf ${archive} -C ${baseDirPath}`);
|
|
176
|
+
const res = await run('tar', ['-xzf', archive, '-C', baseDirPath]);
|
|
177
|
+
if (res.code !== 0) {
|
|
178
|
+
logger.die(`archive extraction failed:\n${res.stderr.trim()}`);
|
|
179
|
+
}
|
|
180
|
+
writeFileSync(markerPath, id);
|
|
181
|
+
if (!existsSync(baseVmxPath)) {
|
|
182
|
+
logger.die(`archive extraction produced no ${baseVmxPath} (is the archive valid?)`);
|
|
183
|
+
}
|
|
184
|
+
logger.ok(`Pristine VM extracted (${baseVmxPath}).`);
|
|
185
|
+
}
|
|
186
|
+
/** Clones the pristine base into the working VM + sets the display name
|
|
187
|
+
* (clone inherits the base's — the working VM would show under the
|
|
188
|
+
* base's name in Fusion's library otherwise).
|
|
189
|
+
*
|
|
190
|
+
* @param platform - The target platform.
|
|
191
|
+
* @param context - The run context.
|
|
192
|
+
* @returns True when the clone exists.
|
|
193
|
+
*/
|
|
194
|
+
async function ensureWorkingVm(platform, context) {
|
|
195
|
+
const image = context.image;
|
|
196
|
+
const wVmx = vmwareWorkingVmx(platform, image);
|
|
197
|
+
if (existsSync(wVmx)) {
|
|
198
|
+
logger.ok(`Working VM exists (${wVmx}).`);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
mkdirSync(dirname(wVmx), { recursive: true });
|
|
202
|
+
logger.cmd(`vmrun -T fusion clone ${baseVmx(platform, image)} ${wVmx} full`);
|
|
203
|
+
const res = await cloneVm(baseVmx(platform, image), wVmx);
|
|
204
|
+
if (res.code !== 0) {
|
|
205
|
+
logger.warn('full clone failed (Fusion may have rejected the destination path).');
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
logger.cmd(`set displayName "${context.vm}" in ${wVmx}`);
|
|
209
|
+
if (!setVmDisplayName(wVmx, context.vm)) {
|
|
210
|
+
logger.warn('could not set the working VM\u2019s display name (Fusion will show the base\u2019s name).');
|
|
211
|
+
}
|
|
212
|
+
logger.ok(`Working VM cloned (${wVmx}; display name '${context.vm}').`);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
/** Upgrades the working clone once per hardware version (vmrun
|
|
216
|
+
* upgradevm — the no-op hang is why it runs only when the marker
|
|
217
|
+
* differs).
|
|
218
|
+
*
|
|
219
|
+
* @param platform - The target platform.
|
|
220
|
+
* @param context - The run context.
|
|
221
|
+
*/
|
|
222
|
+
async function upgradeWorkingVm(platform, context) {
|
|
223
|
+
const wVmx = vmwareWorkingVmx(platform, context.image);
|
|
224
|
+
const before = vmwareHwVersion(wVmx);
|
|
225
|
+
if (!before) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const markerPath = hwMarker(platform, context.image);
|
|
229
|
+
const marker = existsSync(markerPath) ? readFileSync(markerPath, 'utf8').trim() : '';
|
|
230
|
+
if (marker === before) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const after = await upgradeVmHardware(wVmx);
|
|
234
|
+
if (!after) {
|
|
235
|
+
logger.warn('could not upgrade the working VM (vmrun missing?) — the first GUI start may prompt.');
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
writeFileSync(markerPath, after);
|
|
239
|
+
if (after !== before) {
|
|
240
|
+
logger.ok(`Working VM upgraded to hardware version ${after} (the installed Fusion\u2019s current).`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// runners/windows-autologon.ts — the one-time auto-logon step of the
|
|
2
|
+
// two Windows boots (the legacy ensure_autologon): the image's
|
|
3
|
+
// autounattend.xml sets LogonCount=1 (exactly one auto-login — the OOBE
|
|
4
|
+
// boot), after which Windows clears AutoAdminLogon, so every later boot
|
|
5
|
+
// lands on the lock screen and the OpenChamber ONLOGON task never fires.
|
|
6
|
+
// Re-enable it once (the registry keys persist in the working VM) and
|
|
7
|
+
// reboot the guest so the task runs at the auto-logon. The registry
|
|
8
|
+
// snippets are ASCII-only (the packed-agents rule) and ride the ssh2
|
|
9
|
+
// PowerShell transport (windows-guest.ts). The check/enable core
|
|
10
|
+
// (configureAutologon) is shared by the VMware + QEMU backends — only
|
|
11
|
+
// the reboot wait differs (the VMware NAT IP can change; the QEMU
|
|
12
|
+
// hostfwd target is fixed).
|
|
13
|
+
import { logger } from '../lib/logger.js';
|
|
14
|
+
import { confirmDefault } from '../lib/prompt.js';
|
|
15
|
+
import { openSshSession, waitForSshd } from '../lib/ssh.js';
|
|
16
|
+
import { waitGuestIp } from './vmware-common.js';
|
|
17
|
+
import { psExec } from './windows-guest.js';
|
|
18
|
+
const WINLOGON_KEY = 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon';
|
|
19
|
+
/** @internal — escapes a value for a single-quoted PowerShell literal. */
|
|
20
|
+
function escapePs(value) {
|
|
21
|
+
return value.replace(/'/g, "''");
|
|
22
|
+
}
|
|
23
|
+
/** @internal — the auto-logon state check ('enabled' | 'disabled' on
|
|
24
|
+
* stdout): enabled only when AutoAdminLogon is set, targets the right
|
|
25
|
+
* user and has a password (the image's LogonCount clears the flag).
|
|
26
|
+
*
|
|
27
|
+
* @param username - The expected auto-logon user.
|
|
28
|
+
* @returns The PowerShell script (ASCII).
|
|
29
|
+
*/
|
|
30
|
+
export function autologonCheckScript(username) {
|
|
31
|
+
return [
|
|
32
|
+
`$w = '${WINLOGON_KEY}'`,
|
|
33
|
+
'$prop = Get-ItemProperty -Path $w -ErrorAction SilentlyContinue',
|
|
34
|
+
`if ($prop.AutoAdminLogon -eq '1' -and $prop.DefaultUserName -eq '${escapePs(username)}' -and $prop.DefaultPassword) {`,
|
|
35
|
+
" Write-Output 'enabled'",
|
|
36
|
+
'} else {',
|
|
37
|
+
" Write-Output 'disabled'",
|
|
38
|
+
'}',
|
|
39
|
+
].join('\n');
|
|
40
|
+
}
|
|
41
|
+
/** @internal — sets the auto-logon registry keys and reboots the guest.
|
|
42
|
+
*
|
|
43
|
+
* @param username - The user to log in as.
|
|
44
|
+
* @param password - The user's password (the Winlogon DefaultPassword).
|
|
45
|
+
* @returns The PowerShell script (ASCII).
|
|
46
|
+
*/
|
|
47
|
+
export function autologonEnableScript(username, password) {
|
|
48
|
+
return [
|
|
49
|
+
`$w = '${WINLOGON_KEY}'`,
|
|
50
|
+
'New-Item -Path $w -Force | Out-Null',
|
|
51
|
+
"Set-ItemProperty -Path $w -Name AutoAdminLogon -Value '1'",
|
|
52
|
+
`Set-ItemProperty -Path $w -Name DefaultUserName -Value '${escapePs(username)}'`,
|
|
53
|
+
`Set-ItemProperty -Path $w -Name DefaultPassword -Value '${escapePs(password)}'`,
|
|
54
|
+
'shutdown /r /t 0',
|
|
55
|
+
].join('\n');
|
|
56
|
+
}
|
|
57
|
+
/** The auto-logon check/enable core (shared by the VMware and QEMU
|
|
58
|
+
* Windows backends): probe the registry, offer the enable+reboot and
|
|
59
|
+
* apply it. Returns true when a reboot was initiated — the caller
|
|
60
|
+
* waits for sshd (the wait differs per backend: the VMware IP can
|
|
61
|
+
* change, the QEMU hostfwd target is fixed).
|
|
62
|
+
*
|
|
63
|
+
* @param context - The run context (yes flag).
|
|
64
|
+
* @param creds - The guest credentials.
|
|
65
|
+
* @returns True when the guest was told to reboot with auto-logon.
|
|
66
|
+
*/
|
|
67
|
+
export async function configureAutologon(context, creds) {
|
|
68
|
+
const session = await openSshSession(creds);
|
|
69
|
+
try {
|
|
70
|
+
const check = await psExec(session, autologonCheckScript(creds.username), 60_000);
|
|
71
|
+
if (/enabled/.test(check.stdout)) {
|
|
72
|
+
logger.ok('Guest auto-logon is enabled — OpenChamber starts at logon.');
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
logger.info('Guest auto-logon is disabled (the image allows one OOBE logon only).');
|
|
76
|
+
if (!(await confirmDefault('Enable auto-logon and reboot the guest so OpenChamber starts at boot?', { default: 'y', yes: context.options.yes }))) {
|
|
77
|
+
logger.info('OpenChamber will not start until someone logs in via RDP or the console.');
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
await psExec(session, autologonEnableScript(creds.username, creds.password), 60_000);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
logger.warn(`auto-logon setup failed: ${err.message}`);
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
session.end();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** The VMware auto-logon flow: configure, then wait for the rebooted
|
|
92
|
+
* guest (the auto-logon makes the OpenChamber ONLOGON task fire at
|
|
93
|
+
* boot). The reboot can land on a new NAT address, so the sshd wait
|
|
94
|
+
* targets the refreshed IP (the shell's wait_guest_ip re-set the
|
|
95
|
+
* global).
|
|
96
|
+
*
|
|
97
|
+
* @param context - The run context (yes flag).
|
|
98
|
+
* @param state - The accumulated run state (vmIp refreshed after reboot).
|
|
99
|
+
* @param workVmx - The working VM vmx.
|
|
100
|
+
* @param creds - The guest credentials.
|
|
101
|
+
*/
|
|
102
|
+
export async function ensureAutologon(context, state, workVmx, creds) {
|
|
103
|
+
if (!(await configureAutologon(context, creds))) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
logger.info('Rebooting the guest (a minute or two)...');
|
|
107
|
+
const ip = await waitGuestIp(workVmx);
|
|
108
|
+
state.vmIp = ip;
|
|
109
|
+
const rebooted = { ...creds, host: ip };
|
|
110
|
+
process.stdout.write(' Waiting for the guest to reboot (up to 10 min)');
|
|
111
|
+
if (await waitForSshd(rebooted)) {
|
|
112
|
+
process.stdout.write(` ${logger.color('green')}done${logger.reset()}\n`);
|
|
113
|
+
logger.ok('Guest rebooted with auto-logon enabled.');
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
process.stdout.write(` ${logger.color('yellow')}failed${logger.reset()}\n`);
|
|
117
|
+
logger.die(`timed out waiting for the guest to reboot (no SSH on ${ip}:22).`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// runners/windows-bridges.ts — step 3 for the two Windows backends: the
|
|
2
|
+
// host side of the SSH agent + Docker bridges. VMware (Fusion's NAT is
|
|
3
|
+
// userspace — the guest gateway x.y.z.2 is vmnetd and does not forward to
|
|
4
|
+
// the host loopback, so the bridges bind the host's x.y.z.1 like the
|
|
5
|
+
// Ubuntu backend); QEMU (user-mode networking — the guest reaches the
|
|
6
|
+
// host loopback at 10.0.2.2, so the listeners bind 127.0.0.1). The guest
|
|
7
|
+
// side (upload/install/status/docker over the ssh2 PowerShell transport)
|
|
8
|
+
// lives in windows-guest.ts.
|
|
9
|
+
import { QEMU_HOST_ALIAS } from '../lib/qemu.js';
|
|
10
|
+
import { logger } from '../lib/logger.js';
|
|
11
|
+
import { findHostAlias } from '../lib/network.js';
|
|
12
|
+
import { openSshSession } from '../lib/ssh.js';
|
|
13
|
+
import { findHostAgentSocket, findHostDockerSocket, startHostBridge } from './bridges.js';
|
|
14
|
+
import { ensureGuestAgent, findGuestNode, readGuestStatus, resolveGuestCredentials, verifyGuestDocker, } from './windows-guest.js';
|
|
15
|
+
/** The Windows step 3: host alias → host bridges → guest agent → guest
|
|
16
|
+
* bridges → docker verification. The guest-visible alias and the bind
|
|
17
|
+
* address differ per backend (QEMU: 10.0.2.2 / 127.0.0.1; VMware: the
|
|
18
|
+
* NAT-segment x.y.z.1 / itself), resolved once and reused.
|
|
19
|
+
*/
|
|
20
|
+
export async function windowsBridges(context, state) {
|
|
21
|
+
const ip = state.vmIp;
|
|
22
|
+
if (!ip) {
|
|
23
|
+
logger.warn('no guest IP — skipping the host bridges and guest setup.');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const isQemu = context.platform === 'windows-qemu';
|
|
27
|
+
const guestAlias = isQemu ? QEMU_HOST_ALIAS : await findHostAlias(ip);
|
|
28
|
+
if (!guestAlias) {
|
|
29
|
+
logger.warn('could not determine the host NAT-segment address — skipping the bridges.');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const bindHost = isQemu ? '127.0.0.1' : guestAlias;
|
|
33
|
+
const creds = resolveGuestCredentials(context.image, context.options.env, ip, context.options.sshPort);
|
|
34
|
+
const session = await openSshSession(creds);
|
|
35
|
+
try {
|
|
36
|
+
const node = await findGuestNode(session);
|
|
37
|
+
if (!node) {
|
|
38
|
+
logger.warn('node not found in the guest — skipping the guest bridges.');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (context.options.noAgent) {
|
|
42
|
+
logger.info('Skipping SSH agent bridge setup (--no-agent).');
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
await setupSshAgent(context, state, bindHost, guestAlias);
|
|
46
|
+
}
|
|
47
|
+
if (context.options.noDocker) {
|
|
48
|
+
logger.info('Skipping Docker bridge setup (--no-docker).');
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
await setupDockerBridge(context, state, bindHost, guestAlias);
|
|
52
|
+
}
|
|
53
|
+
const anyBridged = state.bridges.agent.bridged || state.bridges.docker.bridged;
|
|
54
|
+
if (anyBridged) {
|
|
55
|
+
await ensureGuestAgent(session, node, guestAlias, context);
|
|
56
|
+
await readGuestStatus(session, state, node);
|
|
57
|
+
reportBridgeState(context, state);
|
|
58
|
+
if (state.bridges.docker.bridged && state.bridges.docker.guestUp) {
|
|
59
|
+
await verifyGuestDocker(session, state);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
session.end();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function setupSshAgent(context, state, bindHost, guestAlias) {
|
|
68
|
+
const sock = findHostAgentSocket(context.options.env, context.options.home);
|
|
69
|
+
if (!sock) {
|
|
70
|
+
logger.info('No SSH agent override detected — using the default macOS agent.');
|
|
71
|
+
logger.info('To share a password manager\u2019s agent (Bitwarden, 1Password, ...), ' +
|
|
72
|
+
'enable its SSH agent on the host and re-run.');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
logger.ok(`Host SSH agent socket found: ${sock}`);
|
|
76
|
+
logger.info(`Bridging it into the guest on TCP port ${context.agentPort} (see docs/ssh-agent.md).`);
|
|
77
|
+
logger.ok(`Guest reaches the host at ${guestAlias}`);
|
|
78
|
+
if (!(await startBridge(context, state, 'ssh-agent', sock, bindHost))) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function setupDockerBridge(context, state, bindHost, guestAlias) {
|
|
83
|
+
const sock = findHostDockerSocket(context.options.home);
|
|
84
|
+
if (!sock) {
|
|
85
|
+
logger.info('No Docker engine socket found on the host (Docker Desktop, Colima, OrbStack).');
|
|
86
|
+
logger.info('Start an engine on the host and re-run to bridge it into the guest.');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
logger.ok(`Host Docker engine socket found: ${sock}`);
|
|
90
|
+
logger.info(`Bridging it into the guest on TCP port ${context.dockerPort}.`);
|
|
91
|
+
logger.ok(`Guest reaches the host at ${guestAlias}`);
|
|
92
|
+
if (!(await startBridge(context, state, 'docker', sock, bindHost))) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** The shared bridge start: the host spawn + state bookkeeping (the
|
|
97
|
+
* shell's start_host_bridge; pidfile + detached bridge.js via
|
|
98
|
+
* runners/bridges.ts — no socat). Returns false when skipped.
|
|
99
|
+
*/
|
|
100
|
+
async function startBridge(context, state, role, socket, bindHost) {
|
|
101
|
+
const result = await startHostBridge({
|
|
102
|
+
role,
|
|
103
|
+
bindHost,
|
|
104
|
+
port: role === 'ssh-agent' ? context.agentPort : context.dockerPort,
|
|
105
|
+
forwardSocket: socket,
|
|
106
|
+
});
|
|
107
|
+
if (result.state === 'failed') {
|
|
108
|
+
logger.warn(`skipping the ${role === 'ssh-agent' ? 'SSH agent' : 'Docker'} bridge.`);
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const bridge = role === 'ssh-agent' ? state.bridges.agent : state.bridges.docker;
|
|
112
|
+
bridge.bridged = true;
|
|
113
|
+
bridge.socket = socket;
|
|
114
|
+
bridge.pid = result.state === 'started' ? result.pid : undefined;
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
function reportBridgeState(context, state) {
|
|
118
|
+
const agent = state.bridges.agent;
|
|
119
|
+
if (agent.bridged) {
|
|
120
|
+
if (agent.guestUp) {
|
|
121
|
+
logger.ok(`Guest bridge is up: \\\\.\\pipe\\openssh-ssh-agent -> host TCP ${context.agentPort}`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
logger.warn('guest bridge did not start — run the guest commands from docs/ssh-agent.md manually.');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const docker = state.bridges.docker;
|
|
128
|
+
if (docker.bridged) {
|
|
129
|
+
if (docker.guestUp) {
|
|
130
|
+
logger.ok(`Guest Docker bridge is up: \\\\.\\pipe\\docker_engine -> host TCP ${context.dockerPort}`);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
logger.warn('guest Docker bridge did not start — check the guest agent install.');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|