@olhapi/maestro 0.1.5-rc.11 → 0.1.5-rc.14

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/README.md CHANGED
@@ -29,7 +29,7 @@ The docs site is organized around the same operator flow the product uses:
29
29
 
30
30
  ### npm
31
31
 
32
- Current public npm install on supported platforms:
32
+ Install the launcher package:
33
33
 
34
34
  ```bash
35
35
  npm install -g @olhapi/maestro
@@ -43,19 +43,47 @@ npm install -g @olhapi/maestro@next
43
43
 
44
44
  The installed command name is still `maestro`.
45
45
 
46
- Official npm builds currently cover:
46
+ This package no longer ships native platform binaries. It installs a thin host
47
+ launcher that runs Maestro from the published Docker image, so Docker is now a
48
+ first-party runtime requirement for normal CLI use.
47
49
 
48
- | Platform | Arch | Notes |
49
- | --- | --- | --- |
50
- | macOS | arm64 | native package |
51
- | macOS | x64 | native package |
52
- | Linux | x64 | glibc only |
53
- | Linux | arm64 | glibc only |
54
- | Windows | x64 | native package |
50
+ The npm launcher requires Node 24 or newer because it uses the built-in
51
+ `node:sqlite` module to inspect the Maestro database before Docker starts.
55
52
 
56
- Linux npm packages currently target glibc only. Alpine and other musl-based distros should build from source or use Docker.
53
+ The launcher resolves its runtime image in this order:
57
54
 
58
- ### Docker
55
+ - `MAESTRO_IMAGE`
56
+ - the locally pinned image from `~/.maestro/launcher/runtime.json`
57
+ - the npm package version you installed
58
+
59
+ Pull and pin the latest runtime image explicitly:
60
+
61
+ ```bash
62
+ maestro self-update
63
+ ```
64
+
65
+ Pin a specific image version:
66
+
67
+ ```bash
68
+ maestro self-update --version 0.117.0
69
+ ```
70
+
71
+ Validate the local launcher install, Docker access, and runtime pin:
72
+
73
+ ```bash
74
+ maestro doctor install
75
+ ```
76
+
77
+ ### Curl Installer
78
+
79
+ If you want the same launcher without npm, install it with the repository
80
+ script:
81
+
82
+ ```bash
83
+ curl -fsSL https://raw.githubusercontent.com/olhapi/maestro/main/scripts/install_maestro.sh | sh
84
+ ```
85
+
86
+ ### Docker Runtime
59
87
 
60
88
  Published image:
61
89
 
@@ -77,7 +105,8 @@ docker run --rm -v ./repo:/repo -v ./data:/data ghcr.io/olhapi/maestro:latest ru
77
105
 
78
106
  ### Build From Source
79
107
 
80
- For local development or unsupported platforms:
108
+ For local development, contributor workflows, or environments where Docker is
109
+ not the right runtime:
81
110
 
82
111
  ```bash
83
112
  go build -o maestro ./cmd/maestro
@@ -89,7 +118,7 @@ development package for the standard `make build` / `make test` flow.
89
118
  Local contributor Docker build:
90
119
 
91
120
  ```bash
92
- docker build -t maestro-local .
121
+ docker build -t maestro-local --build-arg CODEX_VERSION="$(./scripts/codex_supported_version.sh)" .
93
122
  ```
94
123
 
95
124
  ## Quick Start
@@ -132,6 +161,12 @@ When `--db` is omitted, Maestro uses `~/.maestro/maestro.db` by default. When `-
132
161
 
133
162
  Running `maestro run` without `repo_path` starts the shared daemon for the current database. It does not infer the repo from your shell working directory.
134
163
 
164
+ Before Docker starts, `maestro run` performs a one-time preflight pass. When a
165
+ database file exists, the launcher reads the discovered workflow files, resolves
166
+ their `workspace.root` values, and mounts the repo and workspace directories up
167
+ front. If the database file does not exist yet, it skips discovery so fresh
168
+ bootstrap flows still work.
169
+
135
170
  Issue images are stored next to the active database under `assets/images`. With the default database path, that means `~/.maestro/assets/images`. If you run with `--db /custom/path/maestro.db`, image assets move to `/custom/path/assets/images`.
136
171
 
137
172
  The preview warning on `run` is intentional. Pass `--i-understand-that-this-will-be-running-without-the-usual-guardrails` only when unattended Codex execution is actually what you want.
@@ -286,7 +321,7 @@ Fresh `maestro workflow init --defaults` output currently defaults to:
286
321
 
287
322
  - `tracker.kind: kanban`
288
323
  - `polling.interval_ms: 10000`
289
- - `workspace.root: ./workspaces`
324
+ - `workspace.root: ~/.maestro/worktrees`
290
325
  - `agent.max_concurrent_agents: 3`
291
326
  - `agent.max_turns: 4`
292
327
  - `agent.max_retry_backoff_ms: 60000`
@@ -303,7 +338,11 @@ Fresh `maestro workflow init --defaults` output currently defaults to:
303
338
 
304
339
  `initial_collaboration_mode: default` keeps unattended runs execution-first for a fresh `app_server` thread. Use `plan` only when you explicitly want a plan-gated startup mode. Interactive approvals and `requestUserInput` prompts still depend on using a non-`never` approval policy, and those prompts are queued through the dashboard's global interrupt panel. Resumed threads and `stdio` runs do not use that startup-mode path.
305
340
 
306
- Interactive `maestro workflow init` now walks through `workspace.root`, `codex.command`, `agent.mode`, `agent.dispatch_mode`, `agent.max_concurrent_agents`, `agent.max_turns`, and `agent.max_automatic_retries`, then asks for `codex.approval_policy` and `codex.initial_collaboration_mode` only for `app_server`. Those extra tuning knobs remain interactive-only; `--defaults` stays the stable scripted path, and the existing flags still override only the workspace root, Codex command, and agent mode.
341
+ Interactive `maestro workflow init` now walks through `workspace.root`, `codex.command`, `agent.mode`, `agent.dispatch_mode`, `agent.max_concurrent_agents`, `agent.max_turns`, and `agent.max_automatic_retries`, then asks for `codex.approval_policy` and `codex.initial_collaboration_mode` only for `app_server`.
342
+
343
+ Enum prompts now render numbered menus. You can press Enter to keep the default, or enter the number, an alias, a unique prefix, or the full value. Examples: `server` for `app_server`, `serial` or `pps` for `per_project_serial`, `req` for `on-request`, and `def` for `default`. Ambiguous prefixes such as `on` are rejected and reprompted.
344
+
345
+ `--defaults` remains the stable scripted path, and the same setup knobs are available as flags: `--workspace-root`, `--codex-command`, `--agent-mode`, `--dispatch-mode`, `--max-concurrent-agents`, `--max-turns`, `--max-automatic-retries`, `--approval-policy`, and `--initial-collaboration-mode`.
307
346
 
308
347
  Supported prompt-template variables are:
309
348
 
@@ -378,9 +417,10 @@ Repo-managed Git hooks stay targeted:
378
417
  - staged website changes run Astro checks and website tests
379
418
  - staged workspace and hook changes run the full `pnpm verify` suite
380
419
  - `pnpm verify` runs the JS lint/test/check/smoke flow, npm packaging unit test, and Go build/test/coverage/race gates
381
- - `pnpm run verify:pre-push` adds current-host npm packaging smoke, the shared retry stress test, and the full retry-safety harness on top of `pnpm verify`
420
+ - `pnpm run verify:pre-push` adds real Docker image build smoke, tarball install smoke, local-registry install smoke, curl-installer smoke, the shared retry stress test, and the full retry-safety harness on top of `pnpm verify`
421
+ - `pnpm run verify:ci` is the lean GitHub Actions gate: web verification, launcher packaging tests, and `go test ./...`
382
422
  - package-scoped root commands such as `pnpm run frontend:test` and `pnpm run website:build` now go through `turbo --filter=...` so they benefit from task caching too
383
- - `pre-push` now runs `pnpm run verify:pre-push`, leaving GitHub Actions with the cross-platform packaging matrix and registry smoke coverage
423
+ - `pre-push` now runs `pnpm run verify:pre-push`, while GitHub Actions stays on the smaller `pnpm run verify:ci` gate
384
424
 
385
425
  ## License
386
426
 
package/bin/maestro ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env sh
2
+
3
+ set -eu
4
+
5
+ SCRIPT_PATH="$0"
6
+ while [ -L "$SCRIPT_PATH" ]; do
7
+ LINK_TARGET=$(readlink "$SCRIPT_PATH")
8
+ case "$LINK_TARGET" in
9
+ /*) SCRIPT_PATH="$LINK_TARGET" ;;
10
+ *) SCRIPT_PATH=$(dirname "$SCRIPT_PATH")/$LINK_TARGET ;;
11
+ esac
12
+ done
13
+
14
+ SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$SCRIPT_PATH")" && pwd)
15
+ NODE_BIN="${MAESTRO_NODE_BIN:-node}"
16
+ exec "$NODE_BIN" "$SCRIPT_DIR/maestro.js" "$@"
@@ -0,0 +1,8 @@
1
+ @ECHO OFF
2
+ SETLOCAL
3
+ SET SCRIPT_DIR=%~dp0
4
+ IF DEFINED MAESTRO_NODE_BIN (
5
+ "%MAESTRO_NODE_BIN%" "%SCRIPT_DIR%maestro.js" %*
6
+ ) ELSE (
7
+ node "%SCRIPT_DIR%maestro.js" %*
8
+ )
package/bin/maestro.js CHANGED
@@ -1,26 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { spawnSync } = require("node:child_process");
4
- const { getExePath } = require("../lib/get-exe-path");
5
-
6
- let exePath;
7
- try {
8
- exePath = getExePath();
9
- } catch (error) {
10
- const message = error instanceof Error ? error.message : String(error);
11
- process.stderr.write(`${message}\n`);
3
+ const nodeMajor = Number.parseInt(process.versions.node.split(".")[0], 10);
4
+ if (!Number.isInteger(nodeMajor) || nodeMajor < 24) {
5
+ process.stderr.write(`Maestro's npm launcher requires Node 24 or newer; found ${process.versions.node}\n`);
12
6
  process.exit(1);
13
7
  }
14
8
 
15
- const result = spawnSync(exePath, process.argv.slice(2), { stdio: "inherit" });
16
- if (result.error) {
17
- process.stderr.write(`${result.error.message}\n`);
9
+ const { main } = require("../lib/cli");
10
+
11
+ main(process.argv.slice(2)).catch((error) => {
12
+ const message = error instanceof Error ? error.message : String(error);
13
+ process.stderr.write(`${message}\n`);
18
14
  process.exit(1);
19
- }
20
- if (typeof result.status === "number") {
21
- process.exit(result.status);
22
- }
23
- if (result.signal) {
24
- process.stderr.write(`maestro terminated with signal ${result.signal}\n`);
25
- }
26
- process.exit(1);
15
+ });
@@ -0,0 +1,4 @@
1
+ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
2
+ $NodeBin = if ($env:MAESTRO_NODE_BIN) { $env:MAESTRO_NODE_BIN } else { "node" }
3
+ & $NodeBin (Join-Path $ScriptDir "maestro.js") @args
4
+ exit $LASTEXITCODE
package/lib/browser.js ADDED
@@ -0,0 +1,79 @@
1
+ const { spawn } = require("node:child_process");
2
+
3
+ const DEFAULT_BROWSER_TIMEOUT_MS = 3000;
4
+ const DEFAULT_BROWSER_POLL_INTERVAL_MS = 50;
5
+
6
+ function terminalsInteractive(streams = process) {
7
+ return Boolean(streams.stdout && streams.stdout.isTTY && streams.stderr && streams.stderr.isTTY);
8
+ }
9
+
10
+ function browserOpenDisabled(env = process.env) {
11
+ const value = env && env.MAESTRO_DISABLE_BROWSER_OPEN;
12
+ return typeof value === "string" && value.trim() !== "";
13
+ }
14
+
15
+ function browserCommandFor(platform, url) {
16
+ switch (platform) {
17
+ case "darwin":
18
+ return ["open", [url]];
19
+ case "linux":
20
+ case "freebsd":
21
+ case "openbsd":
22
+ case "netbsd":
23
+ return ["xdg-open", [url]];
24
+ case "win32":
25
+ return ["rundll32", ["url.dll,FileProtocolHandler", url]];
26
+ default:
27
+ throw new Error(`unsupported platform ${platform}`);
28
+ }
29
+ }
30
+
31
+ async function waitForHealthy(url, options = {}) {
32
+ const timeoutMs = options.timeoutMs || DEFAULT_BROWSER_TIMEOUT_MS;
33
+ const pollIntervalMs = options.pollIntervalMs || DEFAULT_BROWSER_POLL_INTERVAL_MS;
34
+ const fetchImpl = options.fetchImpl || fetch;
35
+ const deadline = Date.now() + timeoutMs;
36
+ let lastError = null;
37
+
38
+ while (Date.now() < deadline) {
39
+ try {
40
+ const response = await fetchImpl(url);
41
+ if (response.ok) {
42
+ await response.arrayBuffer();
43
+ return;
44
+ }
45
+ lastError = new Error(`health returned ${response.status}`);
46
+ } catch (error) {
47
+ lastError = error;
48
+ }
49
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
50
+ }
51
+
52
+ if (lastError) {
53
+ throw lastError;
54
+ }
55
+ throw new Error(`timed out waiting for ${url}`);
56
+ }
57
+
58
+ async function openDashboardWhenReady(baseURL, options = {}) {
59
+ if (!baseURL || browserOpenDisabled(options.env || process.env) || !terminalsInteractive(options.streams || process)) {
60
+ return;
61
+ }
62
+
63
+ const normalizedBaseURL = String(baseURL).replace(/\/+$/, "");
64
+ await waitForHealthy(`${normalizedBaseURL}/health`, options);
65
+ const [command, args] = browserCommandFor(options.platform || process.platform, normalizedBaseURL);
66
+ const child = spawn(command, args, {
67
+ detached: true,
68
+ stdio: "ignore",
69
+ });
70
+ child.unref();
71
+ }
72
+
73
+ module.exports = {
74
+ browserOpenDisabled,
75
+ browserCommandFor,
76
+ openDashboardWhenReady,
77
+ terminalsInteractive,
78
+ waitForHealthy,
79
+ };
package/lib/cli.js ADDED
@@ -0,0 +1,306 @@
1
+ const fs = require("node:fs");
2
+ const os = require("node:os");
3
+ const path = require("node:path");
4
+ const { spawn, spawnSync } = require("node:child_process");
5
+
6
+ const { browserOpenDisabled, openDashboardWhenReady } = require("./browser");
7
+ const { planDockerInvocation } = require("./docker-plan");
8
+ const { installBundledSkills } = require("./install-skills");
9
+ const {
10
+ DEFAULT_IMAGE_REPOSITORY,
11
+ imageRefForVersion,
12
+ resolveImageRef,
13
+ sanitizeVersion,
14
+ writeRuntimeState,
15
+ } = require("./runtime-state");
16
+
17
+ const MINIMUM_LAUNCHER_NODE_MAJOR = 24;
18
+
19
+ function packageVersion() {
20
+ const packageJSON = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
21
+ return packageJSON.version;
22
+ }
23
+
24
+ function ensureSupportedNodeVersion(nodeVersion = process.versions.node) {
25
+ const major = Number.parseInt(String(nodeVersion || "").split(".")[0], 10);
26
+ if (!Number.isInteger(major) || major < MINIMUM_LAUNCHER_NODE_MAJOR) {
27
+ throw new Error(`Maestro's npm launcher requires Node ${MINIMUM_LAUNCHER_NODE_MAJOR} or newer; found ${nodeVersion}`);
28
+ }
29
+ }
30
+
31
+ async function main(argv = process.argv.slice(2), options = {}) {
32
+ ensureSupportedNodeVersion();
33
+ const deps = createDeps(options);
34
+ if (argv[0] === "self-update") {
35
+ return handleSelfUpdate(argv.slice(1), deps);
36
+ }
37
+ if (argv[0] === "doctor" && argv[1] === "install") {
38
+ return handleDoctorInstall(argv.slice(2), deps);
39
+ }
40
+ if (argv[0] === "install" && argv.includes("--skills") && !argv.includes("--help") && !argv.includes("-h")) {
41
+ return handleInstallSkills(deps);
42
+ }
43
+ return runContainerized(argv, deps);
44
+ }
45
+
46
+ function createDeps(options) {
47
+ return {
48
+ cwd: options.cwd || process.cwd(),
49
+ env: options.env || process.env,
50
+ fs: options.fs || fs,
51
+ gid: options.gid ?? (typeof process.getgid === "function" ? process.getgid() : undefined),
52
+ baseDir: options.baseDir || path.join(__dirname, ".."),
53
+ homeDir: options.homeDir || os.homedir(),
54
+ packageVersion: options.packageVersion || packageVersion(),
55
+ platform: options.platform || process.platform,
56
+ spawn: options.spawn || spawn,
57
+ spawnSync: options.spawnSync || spawnSync,
58
+ stdout: options.stdout || process.stdout,
59
+ stderr: options.stderr || process.stderr,
60
+ uid: options.uid ?? (typeof process.getuid === "function" ? process.getuid() : undefined),
61
+ exit: options.exit || ((code) => process.exit(code)),
62
+ openDashboardWhenReady: options.openDashboardWhenReady || openDashboardWhenReady,
63
+ planDockerInvocation: options.planDockerInvocation || planDockerInvocation,
64
+ };
65
+ }
66
+
67
+ function dockerBinary(env) {
68
+ if (typeof env.MAESTRO_DOCKER_BIN === "string" && env.MAESTRO_DOCKER_BIN.trim() !== "") {
69
+ return env.MAESTRO_DOCKER_BIN.trim();
70
+ }
71
+ return "docker";
72
+ }
73
+
74
+ function ensureDockerAvailable(deps) {
75
+ const result = deps.spawnSync(dockerBinary(deps.env), ["version", "--format", "{{.Server.Version}}"], {
76
+ encoding: "utf8",
77
+ stdio: ["ignore", "pipe", "pipe"],
78
+ });
79
+ if (result.error) {
80
+ throw new Error(`failed to start docker: ${result.error.message}`);
81
+ }
82
+ if (result.status !== 0) {
83
+ const stderr = String(result.stderr || "").trim();
84
+ throw new Error(stderr || "docker is required to run the Maestro launcher");
85
+ }
86
+ }
87
+
88
+ function ensureImageAvailable(imageRef, deps) {
89
+ const inspect = deps.spawnSync(dockerBinary(deps.env), ["image", "inspect", imageRef], {
90
+ encoding: "utf8",
91
+ stdio: ["ignore", "ignore", "pipe"],
92
+ });
93
+ if (inspect.error) {
94
+ throw new Error(`failed to inspect docker image ${imageRef}: ${inspect.error.message}`);
95
+ }
96
+ if (inspect.status === 0) {
97
+ return;
98
+ }
99
+
100
+ const pull = deps.spawnSync(dockerBinary(deps.env), ["pull", imageRef], {
101
+ encoding: "utf8",
102
+ stdio: "inherit",
103
+ });
104
+ if (pull.error) {
105
+ throw new Error(`failed to pull docker image ${imageRef}: ${pull.error.message}`);
106
+ }
107
+ if (pull.status !== 0) {
108
+ throw new Error(`docker pull ${imageRef} failed`);
109
+ }
110
+ }
111
+
112
+ async function runContainerized(argv, deps) {
113
+ ensureDockerAvailable(deps);
114
+ const imageRef = resolveImageRef({
115
+ env: deps.env,
116
+ homeDir: deps.homeDir,
117
+ packageVersion: deps.packageVersion,
118
+ });
119
+ ensureImageAvailable(imageRef, deps);
120
+
121
+ const plan = await deps.planDockerInvocation(argv, {
122
+ cwd: deps.cwd,
123
+ env: deps.env,
124
+ fs: deps.fs,
125
+ gid: deps.gid,
126
+ homeDir: deps.homeDir,
127
+ imageRef,
128
+ platform: deps.platform,
129
+ uid: deps.uid,
130
+ });
131
+
132
+ const child = deps.spawn(dockerBinary(deps.env), plan.dockerArgs, {
133
+ cwd: deps.cwd,
134
+ stdio: "inherit",
135
+ });
136
+
137
+ let browserPromise = Promise.resolve();
138
+ if (plan.commandPath[0] === "run" && plan.hostBaseURL && !browserOpenDisabled(deps.env)) {
139
+ browserPromise = deps.openDashboardWhenReady(plan.hostBaseURL, {
140
+ env: deps.env,
141
+ streams: { stdout: deps.stdout, stderr: deps.stderr },
142
+ platform: deps.platform,
143
+ }).catch(() => {});
144
+ }
145
+
146
+ const signalHandlers = [];
147
+ for (const signal of ["SIGINT", "SIGTERM"]) {
148
+ const handler = () => {
149
+ if (!child.killed) {
150
+ child.kill(signal);
151
+ }
152
+ };
153
+ process.on(signal, handler);
154
+ signalHandlers.push([signal, handler]);
155
+ }
156
+
157
+ const exitCode = await new Promise((resolve, reject) => {
158
+ child.on("error", reject);
159
+ child.on("exit", (code, signal) => {
160
+ if (signal) {
161
+ deps.stderr.write(`maestro terminated with signal ${signal}\n`);
162
+ resolve(1);
163
+ return;
164
+ }
165
+ resolve(typeof code === "number" ? code : 1);
166
+ });
167
+ });
168
+
169
+ for (const [signal, handler] of signalHandlers) {
170
+ process.off(signal, handler);
171
+ }
172
+ await browserPromise;
173
+ deps.exit(exitCode);
174
+ }
175
+
176
+ function printInstalledTargets(targets, stdout) {
177
+ stdout.write("Installed Maestro skill bundle:\n");
178
+ for (const target of targets) {
179
+ stdout.write(` - ${target}\n`);
180
+ }
181
+ }
182
+
183
+ function handleInstallSkills(deps) {
184
+ const targets = installBundledSkills({
185
+ baseDir: deps.baseDir,
186
+ fs: deps.fs,
187
+ homeDir: deps.homeDir,
188
+ });
189
+ printInstalledTargets(targets, deps.stdout);
190
+ }
191
+
192
+ function parseSelfUpdateArgs(argv) {
193
+ let version = "";
194
+ for (let i = 0; i < argv.length; i += 1) {
195
+ const token = argv[i];
196
+ if (token === "--help" || token === "-h") {
197
+ return { help: true, version: "" };
198
+ }
199
+ if (token === "--version" && typeof argv[i + 1] === "string") {
200
+ version = argv[i + 1];
201
+ i += 1;
202
+ continue;
203
+ }
204
+ if (token.startsWith("--version=")) {
205
+ version = token.slice("--version=".length);
206
+ continue;
207
+ }
208
+ throw new Error(`unknown argument for self-update: ${token}`);
209
+ }
210
+ return { help: false, version };
211
+ }
212
+
213
+ function handleSelfUpdate(argv, deps) {
214
+ const parsed = parseSelfUpdateArgs(argv);
215
+ if (parsed.help) {
216
+ deps.stdout.write("Usage: maestro self-update [--version <tag>]\n");
217
+ return;
218
+ }
219
+
220
+ ensureDockerAvailable(deps);
221
+ const selectedRef = parsed.version
222
+ ? imageRefForVersion(parsed.version, DEFAULT_IMAGE_REPOSITORY)
223
+ : `${DEFAULT_IMAGE_REPOSITORY}:latest`;
224
+ const pull = deps.spawnSync(dockerBinary(deps.env), ["pull", selectedRef], {
225
+ encoding: "utf8",
226
+ stdio: "inherit",
227
+ });
228
+ if (pull.error) {
229
+ throw new Error(`failed to pull docker image ${selectedRef}: ${pull.error.message}`);
230
+ }
231
+ if (pull.status !== 0) {
232
+ throw new Error(`docker pull ${selectedRef} failed`);
233
+ }
234
+ writeRuntimeState(selectedRef, {
235
+ fs: deps.fs,
236
+ homeDir: deps.homeDir,
237
+ });
238
+ deps.stdout.write(`Pinned Maestro runtime image to ${selectedRef}\n`);
239
+ }
240
+
241
+ function handleDoctorInstall(argv, deps) {
242
+ if (argv.some((arg) => arg === "--help" || arg === "-h")) {
243
+ deps.stdout.write("Usage: maestro doctor install [--json]\n");
244
+ return;
245
+ }
246
+ const jsonMode = argv.includes("--json");
247
+ const checks = [];
248
+
249
+ try {
250
+ ensureDockerAvailable(deps);
251
+ checks.push({ name: "docker", status: "ok" });
252
+ } catch (error) {
253
+ checks.push({ name: "docker", status: "fail", detail: error.message });
254
+ }
255
+
256
+ const homeDir = deps.homeDir;
257
+ const runtimeImage = resolveImageRef({
258
+ env: deps.env,
259
+ homeDir,
260
+ packageVersion: deps.packageVersion,
261
+ });
262
+ checks.push({ name: "runtime_image", status: "ok", detail: runtimeImage });
263
+
264
+ const codexConfigDir = path.join(homeDir, ".codex");
265
+ checks.push({
266
+ name: "codex_config",
267
+ status: deps.fs.existsSync(codexConfigDir) ? "ok" : "warn",
268
+ detail: codexConfigDir,
269
+ });
270
+
271
+ const daemonRegistryDir = path.join(homeDir, ".maestro", "launcher", "daemons");
272
+ checks.push({
273
+ name: "daemon_registry",
274
+ status: "ok",
275
+ detail: daemonRegistryDir,
276
+ });
277
+
278
+ if (jsonMode) {
279
+ deps.stdout.write(`${JSON.stringify({ checks }, null, 2)}\n`);
280
+ return;
281
+ }
282
+
283
+ deps.stdout.write("INSTALL DOCTOR\n");
284
+ for (const check of checks) {
285
+ deps.stdout.write(`${check.name}:\t${check.status}`);
286
+ if (check.detail) {
287
+ deps.stdout.write(`\t${check.detail}`);
288
+ }
289
+ deps.stdout.write("\n");
290
+ }
291
+ }
292
+
293
+ module.exports = {
294
+ createDeps,
295
+ dockerBinary,
296
+ ensureDockerAvailable,
297
+ ensureImageAvailable,
298
+ ensureSupportedNodeVersion,
299
+ handleDoctorInstall,
300
+ handleInstallSkills,
301
+ handleSelfUpdate,
302
+ main,
303
+ parseSelfUpdateArgs,
304
+ printInstalledTargets,
305
+ runContainerized,
306
+ };