agent-dev-env 0.1.0-canary.2.fbbd044

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/assets/bridge/bridge.js +202 -0
  4. package/dist/assets/guest/guest-agent-mac.js +586 -0
  5. package/dist/assets/guest/guest-agent-ubuntu.js +481 -0
  6. package/dist/assets/guest/guest-agent-windows.js +466 -0
  7. package/dist/assets/images/mac/CHANGELOG.md +265 -0
  8. package/dist/assets/images/mac/README.md +63 -0
  9. package/dist/assets/images/mac/sandbox.pkr.hcl +468 -0
  10. package/dist/assets/images/mac/vars/sandbox-macos-tahoe.pkrvars.hcl +39 -0
  11. package/dist/assets/images/ubuntu-arm64-vmware/CHANGELOG.md +239 -0
  12. package/dist/assets/images/ubuntu-arm64-vmware/README.md +177 -0
  13. package/dist/assets/images/ubuntu-arm64-vmware/autoinstall/meta-data +2 -0
  14. package/dist/assets/images/ubuntu-arm64-vmware/autoinstall/user-data +34 -0
  15. package/dist/assets/images/ubuntu-arm64-vmware/sandbox.pkr.hcl +776 -0
  16. package/dist/assets/images/ubuntu-arm64-vmware/vars/sandbox-ubuntu-24-04-arm64-vmware.pkrvars.hcl +71 -0
  17. package/dist/assets/images/windows-arm64-qemu/CHANGELOG.md +209 -0
  18. package/dist/assets/images/windows-arm64-qemu/README.md +142 -0
  19. package/dist/assets/images/windows-arm64-qemu/autounattend.xml +325 -0
  20. package/dist/assets/images/windows-arm64-qemu/qemu-with-tpm.sh +170 -0
  21. package/dist/assets/images/windows-arm64-qemu/sandbox.pkr.hcl +1012 -0
  22. package/dist/assets/images/windows-arm64-qemu/vars/sandbox-windows-11-arm64-qemu.pkrvars.hcl +83 -0
  23. package/dist/assets/images/windows-arm64-vmware/CHANGELOG.md +270 -0
  24. package/dist/assets/images/windows-arm64-vmware/README.md +171 -0
  25. package/dist/assets/images/windows-arm64-vmware/autounattend.xml +311 -0
  26. package/dist/assets/images/windows-arm64-vmware/sandbox.pkr.hcl +1036 -0
  27. package/dist/assets/images/windows-arm64-vmware/vars/sandbox-windows-11-arm64-vmware.pkrvars.hcl +83 -0
  28. package/dist/assets/rules/agent-rules-linux.md +68 -0
  29. package/dist/assets/rules/agent-rules.md +71 -0
  30. package/dist/assets/watchdog/watch-build-ocr.swift +40 -0
  31. package/dist/assets/watchdog/watch-build.py +275 -0
  32. package/dist/cli.js +52 -0
  33. package/dist/commands/delete.js +179 -0
  34. package/dist/commands/doctor.js +165 -0
  35. package/dist/commands/list.js +38 -0
  36. package/dist/commands/not-yet.js +15 -0
  37. package/dist/commands/register.js +150 -0
  38. package/dist/commands/run.js +65 -0
  39. package/dist/commands/status.js +142 -0
  40. package/dist/commands/stop.js +186 -0
  41. package/dist/commands/sync.js +98 -0
  42. package/dist/lib/exec.js +250 -0
  43. package/dist/lib/ghcr.js +82 -0
  44. package/dist/lib/git.js +100 -0
  45. package/dist/lib/logger.js +87 -0
  46. package/dist/lib/network.js +63 -0
  47. package/dist/lib/paths.js +116 -0
  48. package/dist/lib/platform.js +87 -0
  49. package/dist/lib/prompt.js +66 -0
  50. package/dist/lib/qemu.js +373 -0
  51. package/dist/lib/regex.js +9 -0
  52. package/dist/lib/ssh.js +230 -0
  53. package/dist/lib/tart.js +210 -0
  54. package/dist/lib/template.js +35 -0
  55. package/dist/lib/vars.js +99 -0
  56. package/dist/lib/vmrun.js +334 -0
  57. package/dist/lifecycle/build-macos.js +23 -0
  58. package/dist/lifecycle/build-qemu.js +204 -0
  59. package/dist/lifecycle/build-shared.js +355 -0
  60. package/dist/lifecycle/build-ubuntu.js +184 -0
  61. package/dist/lifecycle/build-watchdog.js +146 -0
  62. package/dist/lifecycle/build-windows-vmware.js +112 -0
  63. package/dist/lifecycle/build.js +55 -0
  64. package/dist/lifecycle/catalog.js +165 -0
  65. package/dist/lifecycle/deploy.js +161 -0
  66. package/dist/lifecycle/tag.js +77 -0
  67. package/dist/lifecycle/watch-build.js +64 -0
  68. package/dist/runners/bridges.js +156 -0
  69. package/dist/runners/framework.js +66 -0
  70. package/dist/runners/macos-bridges.js +143 -0
  71. package/dist/runners/macos-guest.js +76 -0
  72. package/dist/runners/macos-rules.js +77 -0
  73. package/dist/runners/macos-summary.js +145 -0
  74. package/dist/runners/macos.js +258 -0
  75. package/dist/runners/openchamber.js +60 -0
  76. package/dist/runners/options.js +56 -0
  77. package/dist/runners/qemu-image.js +183 -0
  78. package/dist/runners/rules.js +49 -0
  79. package/dist/runners/ubuntu-bridges.js +154 -0
  80. package/dist/runners/ubuntu-guest.js +136 -0
  81. package/dist/runners/ubuntu-image.js +26 -0
  82. package/dist/runners/ubuntu-rules.js +86 -0
  83. package/dist/runners/ubuntu-shared.js +65 -0
  84. package/dist/runners/ubuntu-summary.js +141 -0
  85. package/dist/runners/ubuntu.js +117 -0
  86. package/dist/runners/vmware-common.js +158 -0
  87. package/dist/runners/vmware-image.js +242 -0
  88. package/dist/runners/windows-autologon.js +119 -0
  89. package/dist/runners/windows-bridges.js +136 -0
  90. package/dist/runners/windows-guest.js +213 -0
  91. package/dist/runners/windows-image.js +23 -0
  92. package/dist/runners/windows-qemu-summary.js +85 -0
  93. package/dist/runners/windows-qemu.js +189 -0
  94. package/dist/runners/windows-shared.js +68 -0
  95. package/dist/runners/windows-summary.js +104 -0
  96. package/dist/runners/windows.js +96 -0
  97. package/dist/settings/common.js +107 -0
  98. package/dist/settings/macos-copy.js +223 -0
  99. package/dist/settings/macos.js +41 -0
  100. package/dist/settings/ubuntu-copy.js +223 -0
  101. package/dist/settings/ubuntu.js +68 -0
  102. package/package.json +52 -0
@@ -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
+ }
@@ -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 legacy 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-dev-env-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-dev-env-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
+ }