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
package/dist/lib/ghcr.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// ghcr.ts — GHCR owner resolution and registry refs.
|
|
2
|
+
//
|
|
3
|
+
// Port of the owner discovery in scripts/deploy.sh / the runners, with one
|
|
4
|
+
// planned change: the fallback is the default owner constant (ameshkov)
|
|
5
|
+
// instead of dying — the CLI is a published package and cannot require a
|
|
6
|
+
// checkout.
|
|
7
|
+
//
|
|
8
|
+
// Order: GHCR_OWNER env -> --owner flag -> git remote (inside a checkout)
|
|
9
|
+
// -> DEFAULT_GHCR_OWNER.
|
|
10
|
+
import { gitConfigGet } from './git.js';
|
|
11
|
+
/** @internal */
|
|
12
|
+
export const DEFAULT_GHCR_OWNER = 'ameshkov';
|
|
13
|
+
/** @internal */
|
|
14
|
+
export const GHCR_REGISTRY = 'ghcr.io';
|
|
15
|
+
/** Extracts the owner from a git remote URL. Mirrors the sed in
|
|
16
|
+
* scripts/deploy.sh:
|
|
17
|
+
*
|
|
18
|
+
* s#^(https?://[^/]+/|git@[^:]+:|ssh://[^/]+/)## (strip the host part)
|
|
19
|
+
* s#/[^/]*$## (strip the repo name)
|
|
20
|
+
*/
|
|
21
|
+
/** @internal — Extracts the owner from a git remote URL (sed parity).
|
|
22
|
+
* @param remoteUrl - The remote.url value.
|
|
23
|
+
* @returns The owner slug, or undefined when it cannot be extracted.
|
|
24
|
+
*/
|
|
25
|
+
export function ownerFromGitRemote(remoteUrl) {
|
|
26
|
+
const afterHost = remoteUrl.replace(/^(https?:\/\/[^/]+\/|git@[^:]+:|ssh:\/\/[^/]+\/)/, '');
|
|
27
|
+
const owner = afterHost.replace(/\/[^/]*$/, '');
|
|
28
|
+
// A real owner is a plain slug; the sed would emit garbage for a URL
|
|
29
|
+
// without a path (e.g. "https://github.com" -> "https:/"), which is
|
|
30
|
+
// never a valid GHCR owner — guard instead of shipping it.
|
|
31
|
+
return /^[A-Za-z0-9_.-]+$/.test(owner) ? owner : undefined;
|
|
32
|
+
}
|
|
33
|
+
/** @internal — Pure resolution given the three inputs; synchronous and
|
|
34
|
+
* testable.
|
|
35
|
+
* @param input - env / flag owner / remoteUrl.
|
|
36
|
+
* @returns The resolved owner (defaults to DEFAULT_GHCR_OWNER).
|
|
37
|
+
*/
|
|
38
|
+
export function resolveOwnerFrom(input = {}) {
|
|
39
|
+
const env = input.env ?? process.env;
|
|
40
|
+
const parts = [];
|
|
41
|
+
if (input.owner)
|
|
42
|
+
parts.push('flag');
|
|
43
|
+
if (env.GHCR_OWNER)
|
|
44
|
+
parts.push('env');
|
|
45
|
+
if (env.GHCR_OWNER) {
|
|
46
|
+
return env.GHCR_OWNER;
|
|
47
|
+
}
|
|
48
|
+
if (input.owner) {
|
|
49
|
+
return input.owner;
|
|
50
|
+
}
|
|
51
|
+
if (input.remoteUrl) {
|
|
52
|
+
const owner = ownerFromGitRemote(input.remoteUrl);
|
|
53
|
+
if (owner) {
|
|
54
|
+
return owner;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return DEFAULT_GHCR_OWNER;
|
|
58
|
+
}
|
|
59
|
+
/** Full resolution: env -> flag -> git remote in the repo ->
|
|
60
|
+
* default.
|
|
61
|
+
* @param options - Flag owner / repo root / env overrides.
|
|
62
|
+
* @returns The resolved owner.
|
|
63
|
+
*/
|
|
64
|
+
export async function resolveOwner(options = {}) {
|
|
65
|
+
const remoteUrl = options.repoRoot
|
|
66
|
+
? await gitConfigGet('remote.origin.url', { repoRoot: options.repoRoot })
|
|
67
|
+
: await gitConfigGet('remote.origin.url');
|
|
68
|
+
return resolveOwnerFrom({
|
|
69
|
+
env: options.env ?? process.env,
|
|
70
|
+
owner: options.owner,
|
|
71
|
+
remoteUrl,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** ghcr.io/<owner>/<image>:<tag>
|
|
75
|
+
* @param image - The image name.
|
|
76
|
+
* @param tag - The tag.
|
|
77
|
+
* @param owner - The GHCR owner.
|
|
78
|
+
* @returns The full registry ref.
|
|
79
|
+
*/
|
|
80
|
+
export function registryRef(image, tag, owner) {
|
|
81
|
+
return `${GHCR_REGISTRY}/${owner}/${image}:${tag}`;
|
|
82
|
+
}
|
package/dist/lib/git.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// git.ts — git helpers. Phase 1 landed the repo-root walk and git config
|
|
2
|
+
// reads (GHCR owner discovery); Phase 7 adds the release-tag helpers
|
|
3
|
+
// (clean-tree check, annotated tag creation/push, CHANGELOG entry
|
|
4
|
+
// validation) — the port of scripts/tag.sh.
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { dirname, join, resolve } from 'node:path';
|
|
7
|
+
import { run } from './exec.js';
|
|
8
|
+
/** Walks up from startDir to the first directory with a .git entry (a
|
|
9
|
+
* checkout or a worktree).
|
|
10
|
+
*
|
|
11
|
+
* @param startDir - Directory to start the walk from.
|
|
12
|
+
* @returns The repo root, or null when nothing found.
|
|
13
|
+
*/
|
|
14
|
+
export function findRepoRoot(startDir = process.cwd()) {
|
|
15
|
+
let dir = resolve(startDir);
|
|
16
|
+
for (;;) {
|
|
17
|
+
if (existsSync(join(dir, '.git'))) {
|
|
18
|
+
return dir;
|
|
19
|
+
}
|
|
20
|
+
const parent = dirname(dir);
|
|
21
|
+
if (parent === dir) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
dir = parent;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** `git config --get <key>` in the repo, first line;
|
|
28
|
+
* undefined when the key is unset or git is unavailable. */
|
|
29
|
+
export async function gitConfigGet(key, options = {}) {
|
|
30
|
+
const root = options.repoRoot ?? findRepoRoot();
|
|
31
|
+
if (!root) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const res = await run('git', ['-C', root, 'config', '--get', key]);
|
|
35
|
+
return res.code === 0 ? res.stdout.trim().split('\n')[0] : undefined;
|
|
36
|
+
}
|
|
37
|
+
/** Whether the repo has no uncommitted changes (worktree and index) —
|
|
38
|
+
* `git diff --quiet` + `git diff --cached --quiet`.
|
|
39
|
+
*
|
|
40
|
+
* @param repoRoot - The repo to check.
|
|
41
|
+
* @returns True when the tree is clean.
|
|
42
|
+
*/
|
|
43
|
+
export async function isWorkingTreeClean(repoRoot) {
|
|
44
|
+
const dirty = await run('git', ['-C', repoRoot, 'diff', '--quiet']);
|
|
45
|
+
if (dirty.code !== 0) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const staged = await run('git', ['-C', repoRoot, 'diff', '--cached', '--quiet']);
|
|
49
|
+
return staged.code === 0;
|
|
50
|
+
}
|
|
51
|
+
/** Whether a tag already exists (`git rev-parse --verify --quiet`).
|
|
52
|
+
*
|
|
53
|
+
* @param repoRoot - The repo to check.
|
|
54
|
+
* @param tag - The tag name.
|
|
55
|
+
* @returns True when the tag exists.
|
|
56
|
+
*/
|
|
57
|
+
export async function tagExists(repoRoot, tag) {
|
|
58
|
+
const res = await run('git', [
|
|
59
|
+
'-C',
|
|
60
|
+
repoRoot,
|
|
61
|
+
'rev-parse',
|
|
62
|
+
'--verify',
|
|
63
|
+
'--quiet',
|
|
64
|
+
`refs/tags/${tag}`,
|
|
65
|
+
]);
|
|
66
|
+
return res.code === 0;
|
|
67
|
+
}
|
|
68
|
+
/** The shell's `grep -q "## [<tag>]"` check: the changelog must contain a
|
|
69
|
+
* `## [<tag>]` heading line.
|
|
70
|
+
*
|
|
71
|
+
* @param changelogContent - The changelog file content.
|
|
72
|
+
* @param tag - The tag name to look for.
|
|
73
|
+
* @returns True when the changelog has the entry.
|
|
74
|
+
*/
|
|
75
|
+
export function changelogHasTagEntry(changelogContent, tag) {
|
|
76
|
+
return changelogContent.includes(`## [${tag}]`);
|
|
77
|
+
}
|
|
78
|
+
/** Creates an annotated tag (`git tag -a <tag> -m <message>`).
|
|
79
|
+
*
|
|
80
|
+
* @param repoRoot - The repo to tag.
|
|
81
|
+
* @param tag - The tag name.
|
|
82
|
+
* @param message - The annotation message.
|
|
83
|
+
*/
|
|
84
|
+
export async function createAnnotatedTag(repoRoot, tag, message) {
|
|
85
|
+
const res = await run('git', ['-C', repoRoot, 'tag', '-a', tag, '-m', message]);
|
|
86
|
+
if (res.code !== 0) {
|
|
87
|
+
throw new Error(`git tag failed: ${res.stderr.trim() || res.stdout.trim()}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Pushes a tag to origin (`git push origin <tag>`); throws on failure.
|
|
91
|
+
*
|
|
92
|
+
* @param repoRoot - The repo with the tag.
|
|
93
|
+
* @param tag - The tag name to push.
|
|
94
|
+
*/
|
|
95
|
+
export async function pushTag(repoRoot, tag) {
|
|
96
|
+
const res = await run('git', ['-C', repoRoot, 'push', 'origin', tag]);
|
|
97
|
+
if (res.code !== 0) {
|
|
98
|
+
throw new Error(`git push origin ${tag} failed: ${res.stderr.trim() || res.stdout.trim()}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// logger.ts — output helpers for the CLI, ported 1:1 from the shell
|
|
2
|
+
// scripts' shared color/step helpers (scripts/lib/macos-settings.sh and the
|
|
3
|
+
// copies in every runner).
|
|
4
|
+
//
|
|
5
|
+
// Conventions (kept identical to the shell):
|
|
6
|
+
// - stdout colors: bold, dim, green, yellow, blue, reset
|
|
7
|
+
// - stderr colors: bold, red, yellow, reset
|
|
8
|
+
// - colors only when the stream is a TTY and NO_COLOR is unset
|
|
9
|
+
// - die(): "<name>: <msg>" (bold red) on stderr, exit 1
|
|
10
|
+
// - warn(): "<name>: warning: <msg>" (bold yellow) on stderr
|
|
11
|
+
// - title/step/info/cmd/ok() as in the shell runners (info is plain,
|
|
12
|
+
// 4-space indented; cmd is dim; ok is green)
|
|
13
|
+
import { env as processEnv, stderr as processStderr, stdout as processStdout } from 'node:process';
|
|
14
|
+
const ANSI = {
|
|
15
|
+
bold: '\u001b[1m',
|
|
16
|
+
dim: '\u001b[2m',
|
|
17
|
+
green: '\u001b[32m',
|
|
18
|
+
yellow: '\u001b[33m',
|
|
19
|
+
blue: '\u001b[34m',
|
|
20
|
+
red: '\u001b[31m',
|
|
21
|
+
};
|
|
22
|
+
const RESET = '\u001b[0m';
|
|
23
|
+
const DEFAULT_NAME = 'agent-dev-env';
|
|
24
|
+
/** @internal — the public surface is the `logger` instance below. */
|
|
25
|
+
export class Logger {
|
|
26
|
+
name;
|
|
27
|
+
stdout;
|
|
28
|
+
stderr;
|
|
29
|
+
env;
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
this.name = options.name ?? DEFAULT_NAME;
|
|
32
|
+
this.stdout = options.stdout ?? processStdout;
|
|
33
|
+
this.stderr = options.stderr ?? processStderr;
|
|
34
|
+
this.env = options.env ?? processEnv;
|
|
35
|
+
}
|
|
36
|
+
/** Whether this stream should carry color (TTY + NO_COLOR unset). */
|
|
37
|
+
enabled(stream) {
|
|
38
|
+
return Boolean(stream.isTTY) && !this.env.NO_COLOR;
|
|
39
|
+
}
|
|
40
|
+
/** ANSI code for the color on the given stream, or '' when disabled. */
|
|
41
|
+
color(name, stream = this.stdout) {
|
|
42
|
+
return this.enabled(stream) ? ANSI[name] : '';
|
|
43
|
+
}
|
|
44
|
+
/** Reset code for the given stream ('' when disabled). */
|
|
45
|
+
reset(stream = this.stdout) {
|
|
46
|
+
return this.enabled(stream) ? RESET : '';
|
|
47
|
+
}
|
|
48
|
+
/** Bold text for the default stream — used by prompt() like the shell's
|
|
49
|
+
* `${c_bold}${prompt}${c_reset}`. */
|
|
50
|
+
bold(text) {
|
|
51
|
+
return `${this.color('bold')}${text}${this.reset()}`;
|
|
52
|
+
}
|
|
53
|
+
die(message) {
|
|
54
|
+
const c = (name) => this.color(name, this.stderr);
|
|
55
|
+
this.stderr.write(`${c('bold')}${c('red')}${this.name}: ${message}${this.reset(this.stderr)}\n`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
warn(message) {
|
|
59
|
+
const c = (name) => this.color(name, this.stderr);
|
|
60
|
+
this.stderr.write(`${c('bold')}${c('yellow')}${this.name}: warning: ${message}${this.reset(this.stderr)}\n`);
|
|
61
|
+
}
|
|
62
|
+
title(message) {
|
|
63
|
+
this.stdout.write(`${this.color('bold')}${message}${this.reset()}\n`);
|
|
64
|
+
}
|
|
65
|
+
step(message) {
|
|
66
|
+
this.stdout.write(`${this.color('bold')}${this.color('blue')}==> ${message}${this.reset()}\n`);
|
|
67
|
+
}
|
|
68
|
+
info(message) {
|
|
69
|
+
this.stdout.write(` ${message}\n`);
|
|
70
|
+
}
|
|
71
|
+
cmd(message) {
|
|
72
|
+
this.stdout.write(`${this.color('dim')} ${message}${this.reset()}\n`);
|
|
73
|
+
}
|
|
74
|
+
ok(message) {
|
|
75
|
+
this.stdout.write(`${this.color('green')} ${message}${this.reset()}\n`);
|
|
76
|
+
}
|
|
77
|
+
/** Raw line on stdout (table output, no indent/color). */
|
|
78
|
+
out(message) {
|
|
79
|
+
this.stdout.write(`${message}\n`);
|
|
80
|
+
}
|
|
81
|
+
/** Raw line on stderr (diagnostics not prefixed by the script name). */
|
|
82
|
+
err(message) {
|
|
83
|
+
this.stderr.write(`${message}\n`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** The default CLI logger (name: agent-dev-env). */
|
|
87
|
+
export const logger = new Logger();
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// lib/network.ts — NAT-segment address resolution for the VMware runners
|
|
2
|
+
// (the shell's find_host_alias + the x.y.z.1 fallback). VMware Fusion's
|
|
3
|
+
// NAT is userspace (vmnetd): the guest's gateway is x.y.z.2 and the
|
|
4
|
+
// host's own interface on that segment is x.y.z.1 (a dynamically named
|
|
5
|
+
// bridge — there is no fixed vmnet8 device). The guest reaches the host
|
|
6
|
+
// directly at that address, so the host-side bridges bind it.
|
|
7
|
+
//
|
|
8
|
+
// The resolver is kept pure (ifconfig text in → host IP out) so the
|
|
9
|
+
// parsing is unit-tested with a fixture; the runner wraps it with the
|
|
10
|
+
// ifconfig invocation and the fallback.
|
|
11
|
+
import { run } from './exec.js';
|
|
12
|
+
/** @internal — extracts the first host IPv4 address on the guest's /24
|
|
13
|
+
* from `ifconfig` output, excluding the guest IP itself (the awk port:
|
|
14
|
+
* scan `inet` lines, keep addresses sharing the first three octets,
|
|
15
|
+
* drop the guest's own address, take the first).
|
|
16
|
+
*
|
|
17
|
+
* @param output - The raw `ifconfig` output.
|
|
18
|
+
* @param guestIp - The guest's IP (as reported by vmrun).
|
|
19
|
+
* @returns The host address, or undefined when none matches.
|
|
20
|
+
*/
|
|
21
|
+
export function hostAliasFromIfconfig(output, guestIp) {
|
|
22
|
+
const guest = guestIp.split('.');
|
|
23
|
+
if (guest.length !== 4) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const candidates = new Set();
|
|
27
|
+
for (const line of output.split('\n')) {
|
|
28
|
+
if (!/\binet\s/.test(line)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const match = /\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b/.exec(line);
|
|
32
|
+
if (!match) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const ip = match[1];
|
|
36
|
+
const parts = ip.split('.');
|
|
37
|
+
if (parts[0] === guest[0] && parts[1] === guest[1] && parts[2] === guest[2]) {
|
|
38
|
+
candidates.add(ip);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const ip of candidates) {
|
|
42
|
+
if (ip !== guestIp) {
|
|
43
|
+
return ip;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
/** The host's address on the guest's NAT segment: scan the host
|
|
49
|
+
* interfaces for the /24, fall back to x.y.z.1 (the legacy default when
|
|
50
|
+
* no interface matches).
|
|
51
|
+
*
|
|
52
|
+
* @param guestIp - The guest's IP.
|
|
53
|
+
* @returns The host alias, or undefined when the guest IP is not a
|
|
54
|
+
* dotted-quad (no segment to derive a host address from).
|
|
55
|
+
*/
|
|
56
|
+
export async function findHostAlias(guestIp) {
|
|
57
|
+
if (guestIp.split('.').length !== 4) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const res = await run('ifconfig');
|
|
61
|
+
const alias = res.code === 0 ? hostAliasFromIfconfig(res.stdout, guestIp) : undefined;
|
|
62
|
+
return alias ?? `${guestIp.split('.').slice(0, 3).join('.')}.1`;
|
|
63
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// paths.ts — the on-disk state layout, designed from scratch (XDG-aware,
|
|
2
|
+
// no migration from the legacy agent-sandbox paths).
|
|
3
|
+
//
|
|
4
|
+
// Resolution order (highest first):
|
|
5
|
+
//
|
|
6
|
+
// 1. AGENT_DEV_ENV_DATA_HOME / AGENT_DEV_ENV_LOG_DIR / AGENT_DEV_ENV_CACHE_DIR
|
|
7
|
+
// 2. XDG_DATA_HOME / XDG_STATE_HOME / XDG_CACHE_HOME — when set (on any OS)
|
|
8
|
+
// 3. Platform defaults:
|
|
9
|
+
//
|
|
10
|
+
// Role | macOS | Linux
|
|
11
|
+
// ------ | ------------------------------------ | ----------------------------
|
|
12
|
+
// Data | ~/Library/Application Support/agent-dev-env | ~/.local/share/agent-dev-env
|
|
13
|
+
// Logs | ~/Library/Logs/agent-dev-env | ~/.local/state/agent-dev-env
|
|
14
|
+
// Cache | ~/Library/Caches/agent-dev-env | ~/.cache/agent-dev-env
|
|
15
|
+
//
|
|
16
|
+
// An empty value counts as unset (the shell's `${VAR:-default}` semantics),
|
|
17
|
+
// so "export XDG_DATA_HOME=" still falls back to the platform default.
|
|
18
|
+
//
|
|
19
|
+
// Data layout under <data> (the plan's canonical layout):
|
|
20
|
+
//
|
|
21
|
+
// <data>/
|
|
22
|
+
// build/<platform>/ packer build outputs (built image +
|
|
23
|
+
// packer_cache + staged drivers)
|
|
24
|
+
// build-context/<platform>/ materialized packer context (writable
|
|
25
|
+
// copy of images/<platform>)
|
|
26
|
+
// windows-qemu/<image>/ image/ (pristine qcow2), working/ (overlay,
|
|
27
|
+
// efivars.fd, tpm/, pids, socks)
|
|
28
|
+
// windows-vmware/<image>/ image/, base/, working/
|
|
29
|
+
// ubuntu-vmware/<image>/ image/, base/, working/
|
|
30
|
+
//
|
|
31
|
+
// macOS has no data footprint by design: Tart owns the image + working VM
|
|
32
|
+
// (github.com/cirruslabs/tart) — only ~/Library/Logs/agent-dev-env/tart-*.log
|
|
33
|
+
// is ours. Guest-side markers (~/.config/agent-dev-env/…) live inside the
|
|
34
|
+
// guests; XDG_CONFIG_HOME is a documented future hook, no host config file
|
|
35
|
+
// yet.
|
|
36
|
+
import { homedir } from 'node:os';
|
|
37
|
+
import { join } from 'node:path';
|
|
38
|
+
function firstNonEmpty(...values) {
|
|
39
|
+
for (const v of values) {
|
|
40
|
+
if (v) {
|
|
41
|
+
return v;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
/** @internal — Resolves the three roots; deterministic and injectable for
|
|
47
|
+
* tests.
|
|
48
|
+
* @param options - Env / home / platform overrides (tests).
|
|
49
|
+
* @returns The data, logs, and cache roots.
|
|
50
|
+
*/
|
|
51
|
+
export function resolvePaths(options = {}) {
|
|
52
|
+
const env = options.env ?? process.env;
|
|
53
|
+
const home = options.home ?? homedir();
|
|
54
|
+
const isDarwin = (options.platform ?? process.platform) === 'darwin';
|
|
55
|
+
const defaultData = isDarwin
|
|
56
|
+
? join(home, 'Library', 'Application Support', 'agent-dev-env')
|
|
57
|
+
: join(home, '.local', 'share', 'agent-dev-env');
|
|
58
|
+
const defaultLogs = isDarwin
|
|
59
|
+
? join(home, 'Library', 'Logs', 'agent-dev-env')
|
|
60
|
+
: join(home, '.local', 'state', 'agent-dev-env');
|
|
61
|
+
const defaultCache = isDarwin
|
|
62
|
+
? join(home, 'Library', 'Caches', 'agent-dev-env')
|
|
63
|
+
: join(home, '.cache', 'agent-dev-env');
|
|
64
|
+
return {
|
|
65
|
+
data: firstNonEmpty(env.AGENT_DEV_ENV_DATA_HOME, env.XDG_DATA_HOME) ?? defaultData,
|
|
66
|
+
logs: firstNonEmpty(env.AGENT_DEV_ENV_LOG_DIR, env.XDG_STATE_HOME) ?? defaultLogs,
|
|
67
|
+
cache: firstNonEmpty(env.AGENT_DEV_ENV_CACHE_DIR, env.XDG_CACHE_HOME) ?? defaultCache,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** The resolved roots for this process. */
|
|
71
|
+
export const paths = resolvePaths();
|
|
72
|
+
/** @internal — <data>/<platform>, the platform's state root (unused by
|
|
73
|
+
* macOS).
|
|
74
|
+
* @param platform - The platform id.
|
|
75
|
+
* @returns The platform's data dir.
|
|
76
|
+
*/
|
|
77
|
+
export function platformDir(platform) {
|
|
78
|
+
return join(paths.data, platform);
|
|
79
|
+
}
|
|
80
|
+
/** <data>/build/<platform>: packer build contexts + outputs (deployable
|
|
81
|
+
* artifacts; lives under data, not cache, because deploy consumes it).
|
|
82
|
+
* @param platform - The platform id.
|
|
83
|
+
* @returns The platform's build dir.
|
|
84
|
+
*/
|
|
85
|
+
export function buildDir(platform) {
|
|
86
|
+
return join(paths.data, 'build', platform);
|
|
87
|
+
}
|
|
88
|
+
/** <data>/<platform>/<image> — the per-image state root.
|
|
89
|
+
*
|
|
90
|
+
* @param platform - The platform id.
|
|
91
|
+
* @param image - The image name.
|
|
92
|
+
* @returns The image's state dir.
|
|
93
|
+
*/
|
|
94
|
+
export function imageRootDir(platform, image) {
|
|
95
|
+
return join(paths.data, platform, image);
|
|
96
|
+
}
|
|
97
|
+
/** <data>/<platform>/<image>/image/<image>.tar.gz — the cached VMware
|
|
98
|
+
* archive (the runner's pull cache; status/deploy read it too).
|
|
99
|
+
*
|
|
100
|
+
* @param platform - The platform id.
|
|
101
|
+
* @param image - The image name.
|
|
102
|
+
* @returns The cached archive path.
|
|
103
|
+
*/
|
|
104
|
+
export function vmwareArchivePath(platform, image) {
|
|
105
|
+
return join(imageRootDir(platform, image), 'image', `${image}.tar.gz`);
|
|
106
|
+
}
|
|
107
|
+
/** <data>/<platform>/<image>/working/<image>.vmx — the VMware working
|
|
108
|
+
* clone (the vmx path the runners/stop/delete flows derive).
|
|
109
|
+
*
|
|
110
|
+
* @param platform - The platform id.
|
|
111
|
+
* @param image - The image name.
|
|
112
|
+
* @returns The working vmx path.
|
|
113
|
+
*/
|
|
114
|
+
export function workingVmxPath(platform, image) {
|
|
115
|
+
return join(imageRootDir(platform, image), 'working', `${image}.vmx`);
|
|
116
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// platform.ts — the platform registry: platform ids, the images/ directory
|
|
2
|
+
// name for each, and the run-time defaults the shell scripts hardcoded
|
|
3
|
+
// (VM/image names, bridge ports, VM resources, download hints).
|
|
4
|
+
export const PLATFORMS = ['macos', 'windows-qemu', 'windows-vmware', 'ubuntu-vmware'];
|
|
5
|
+
/** images/<dir> name -> platform id. The image-name/platform naming scheme
|
|
6
|
+
* is fixed (AGENTS.md conventions); never introduce a separate one. */
|
|
7
|
+
export const PLATFORM_DIR_MAP = {
|
|
8
|
+
mac: 'macos',
|
|
9
|
+
'windows-arm64-qemu': 'windows-qemu',
|
|
10
|
+
'windows-arm64-vmware': 'windows-vmware',
|
|
11
|
+
'ubuntu-arm64-vmware': 'ubuntu-vmware',
|
|
12
|
+
};
|
|
13
|
+
export const PLATFORM_DEFAULTS = {
|
|
14
|
+
macos: {
|
|
15
|
+
image: 'sandbox-macos-tahoe',
|
|
16
|
+
vmName: 'sandbox-macos',
|
|
17
|
+
stateDir: 'macos',
|
|
18
|
+
agentPort: 4100,
|
|
19
|
+
dockerPort: 4101,
|
|
20
|
+
openchamberPort: 4000,
|
|
21
|
+
cpuCount: 8,
|
|
22
|
+
memoryMb: 16384,
|
|
23
|
+
workDir: '/Volumes/dev',
|
|
24
|
+
mountName: 'dev',
|
|
25
|
+
supportsAgentRules: true,
|
|
26
|
+
supportsSettings: true,
|
|
27
|
+
supportsSync: true,
|
|
28
|
+
supportsPristineDelete: true,
|
|
29
|
+
downloadHint: '~50 GB',
|
|
30
|
+
},
|
|
31
|
+
'windows-qemu': {
|
|
32
|
+
image: 'sandbox-windows-11-arm64-qemu',
|
|
33
|
+
vmName: 'sandbox-windows-11-arm64-qemu',
|
|
34
|
+
stateDir: 'windows-qemu',
|
|
35
|
+
agentPort: 4200,
|
|
36
|
+
dockerPort: 4201,
|
|
37
|
+
openchamberPort: 4000,
|
|
38
|
+
sshPort: 2222,
|
|
39
|
+
rdpPort: 3389,
|
|
40
|
+
winrmPort: 5985,
|
|
41
|
+
cpuCount: 4,
|
|
42
|
+
memoryMb: 8192,
|
|
43
|
+
supportsAgentRules: false,
|
|
44
|
+
supportsSettings: false,
|
|
45
|
+
supportsSync: false,
|
|
46
|
+
supportsPristineDelete: false,
|
|
47
|
+
downloadHint: '~14 GB',
|
|
48
|
+
},
|
|
49
|
+
'windows-vmware': {
|
|
50
|
+
image: 'sandbox-windows-11-arm64-vmware',
|
|
51
|
+
vmName: 'agent-sandbox-windows-11-arm64-vmware',
|
|
52
|
+
stateDir: 'windows-vmware',
|
|
53
|
+
agentPort: 4300,
|
|
54
|
+
dockerPort: 4301,
|
|
55
|
+
openchamberPort: 4000,
|
|
56
|
+
cpuCount: 4,
|
|
57
|
+
memoryMb: 8192,
|
|
58
|
+
supportsAgentRules: false,
|
|
59
|
+
supportsSettings: false,
|
|
60
|
+
supportsSync: false,
|
|
61
|
+
supportsPristineDelete: false,
|
|
62
|
+
downloadHint: '~20 GB',
|
|
63
|
+
},
|
|
64
|
+
'ubuntu-vmware': {
|
|
65
|
+
image: 'sandbox-ubuntu-24-04-arm64-vmware',
|
|
66
|
+
vmName: 'agent-sandbox-ubuntu-24-04-arm64-vmware',
|
|
67
|
+
stateDir: 'ubuntu-vmware',
|
|
68
|
+
agentPort: 4400,
|
|
69
|
+
dockerPort: 4401,
|
|
70
|
+
openchamberPort: 4000,
|
|
71
|
+
cpuCount: 4,
|
|
72
|
+
memoryMb: 8192,
|
|
73
|
+
supportsAgentRules: true,
|
|
74
|
+
supportsSettings: true,
|
|
75
|
+
supportsSync: true,
|
|
76
|
+
supportsPristineDelete: false,
|
|
77
|
+
downloadHint: '~15 GB',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
/** Whether the string names a supported platform.
|
|
81
|
+
*
|
|
82
|
+
* @param value - The candidate platform id.
|
|
83
|
+
* @returns True when it is a known platform.
|
|
84
|
+
*/
|
|
85
|
+
export function isPlatform(value) {
|
|
86
|
+
return PLATFORMS.includes(value);
|
|
87
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// prompt.ts — interactive confirm, ported 1:1 from the shell confirm():
|
|
2
|
+
//
|
|
3
|
+
// prompt "…" [y|n] -> "… [Y/n] " (or "… [y/N] ")
|
|
4
|
+
// yes answers: y | Y | yes | YES
|
|
5
|
+
// empty answer: the default (y -> yes, n -> no)
|
|
6
|
+
// anything else: no
|
|
7
|
+
// EOF (non-interactive input that ran dry): no — the shell's `read`
|
|
8
|
+
// fails, which returns 1 (no), always.
|
|
9
|
+
//
|
|
10
|
+
// The shell also prints a newline right after the prompt when stdin is not
|
|
11
|
+
// a TTY, because nothing will echo it otherwise — kept here.
|
|
12
|
+
import { createInterface } from 'node:readline';
|
|
13
|
+
import { stdin as processStdin, stdout as processStdout } from 'node:process';
|
|
14
|
+
import { logger } from './logger.js';
|
|
15
|
+
const YES_ANSWERS = new Set(['y', 'Y', 'yes', 'YES']);
|
|
16
|
+
/** Asks for a yes/no confirmation; resolves true when the user confirms.
|
|
17
|
+
*
|
|
18
|
+
* @param prompt - The question to display.
|
|
19
|
+
* @param options - Default answer + injectable input/output (tests).
|
|
20
|
+
* @returns True when confirmed, false otherwise (EOF is always false).
|
|
21
|
+
*/
|
|
22
|
+
export async function confirm(prompt, options = {}) {
|
|
23
|
+
const def = options.default ?? 'n';
|
|
24
|
+
const hint = def === 'y' ? 'Y/n' : 'y/N';
|
|
25
|
+
const output = options.output ?? processStdout;
|
|
26
|
+
const input = options.input ?? processStdin;
|
|
27
|
+
output.write(`${logger.bold(prompt)} [${hint}] `);
|
|
28
|
+
if (!output.isTTY) {
|
|
29
|
+
output.write('\n');
|
|
30
|
+
}
|
|
31
|
+
const rl = createInterface({ input, crlfDelay: Infinity });
|
|
32
|
+
const answer = await readFirstLine(rl);
|
|
33
|
+
rl.close();
|
|
34
|
+
if (answer === null) {
|
|
35
|
+
return false; // EOF — mirrors `read` failing (no).
|
|
36
|
+
}
|
|
37
|
+
if (YES_ANSWERS.has(answer.trim())) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
if (answer.trim() === '') {
|
|
41
|
+
return def === 'y';
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
/** First line read, or null on EOF. */
|
|
46
|
+
async function readFirstLine(rl) {
|
|
47
|
+
for await (const line of rl) {
|
|
48
|
+
return line;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/** The `--yes` semantics used by the runners: skip the prompt and accept
|
|
53
|
+
* the prompt's default — not an unconditional yes. A default-n prompt
|
|
54
|
+
* (e.g. "restart the running VM?") stays a no with --yes.
|
|
55
|
+
*
|
|
56
|
+
* @param message - The question to display (when asking).
|
|
57
|
+
* @param options - Default answer + --yes flag; the rest (injectable
|
|
58
|
+
* input/output) is ConfirmOptions for tests.
|
|
59
|
+
* @returns The (possibly defaulted) answer.
|
|
60
|
+
*/
|
|
61
|
+
export async function confirmDefault(message, options) {
|
|
62
|
+
if (options.yes) {
|
|
63
|
+
return options.default === 'y';
|
|
64
|
+
}
|
|
65
|
+
return confirm(message, { ...options, default: options.default });
|
|
66
|
+
}
|