@olhapi/maestro 0.1.5-rc.12 → 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 +51 -15
- package/bin/maestro +16 -0
- package/bin/maestro.cmd +8 -0
- package/bin/maestro.js +9 -20
- package/bin/maestro.ps1 +4 -0
- package/lib/browser.js +79 -0
- package/lib/cli.js +306 -0
- package/lib/docker-plan.js +941 -0
- package/lib/install-skills.js +110 -0
- package/lib/runtime-state.js +82 -0
- package/package.json +15 -11
- package/share/skills/maestro/SKILL.md +38 -0
- package/share/skills/maestro/references/operations.md +21 -0
- package/share/skills/maestro/references/project-work.md +44 -0
- package/share/skills/maestro/references/readiness.md +18 -0
- package/share/skills/maestro/references/setup.md +45 -0
- package/lib/get-exe-path.js +0 -118
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
+
The launcher resolves its runtime image in this order:
|
|
57
54
|
|
|
58
|
-
|
|
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
|
|
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.
|
|
@@ -382,9 +417,10 @@ Repo-managed Git hooks stay targeted:
|
|
|
382
417
|
- staged website changes run Astro checks and website tests
|
|
383
418
|
- staged workspace and hook changes run the full `pnpm verify` suite
|
|
384
419
|
- `pnpm verify` runs the JS lint/test/check/smoke flow, npm packaging unit test, and Go build/test/coverage/race gates
|
|
385
|
-
- `pnpm run verify:pre-push` adds
|
|
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 ./...`
|
|
386
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
|
|
387
|
-
- `pre-push` now runs `pnpm run verify:pre-push`,
|
|
423
|
+
- `pre-push` now runs `pnpm run verify:pre-push`, while GitHub Actions stays on the smaller `pnpm run verify:ci` gate
|
|
388
424
|
|
|
389
425
|
## License
|
|
390
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" "$@"
|
package/bin/maestro.cmd
ADDED
package/bin/maestro.js
CHANGED
|
@@ -1,26 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
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
|
+
});
|
package/bin/maestro.ps1
ADDED
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
|
+
};
|