@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
|
@@ -0,0 +1,941 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const net = require("node:net");
|
|
3
|
+
const os = require("node:os");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { DatabaseSync } = require("node:sqlite");
|
|
6
|
+
const { pathToFileURL } = require("node:url");
|
|
7
|
+
|
|
8
|
+
const DEFAULT_HTTP_PORT = "8787";
|
|
9
|
+
const DEFAULT_DATABASE_PATH = "~/.maestro/maestro.db";
|
|
10
|
+
const DEFAULT_WORKSPACE_ROOT = "~/.maestro/worktrees";
|
|
11
|
+
const HOST_GATEWAY_NAME = "host.docker.internal";
|
|
12
|
+
const UNRESOLVED_ENV_TOKEN_PATTERN = /\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/;
|
|
13
|
+
const POSIX_PATH_FLAGS = new Map([
|
|
14
|
+
["--db", { usage: "file-parent" }],
|
|
15
|
+
["--workflow", { usage: "file-parent" }],
|
|
16
|
+
["--extensions", { usage: "file-parent" }],
|
|
17
|
+
["--logs-root", { usage: "dir" }],
|
|
18
|
+
["--repo", { usage: "dir" }],
|
|
19
|
+
["--attach", { usage: "file-parent" }],
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const ROOT_FLAGS_WITH_VALUE = new Set(["--db", "--api-url", "--log-level"]);
|
|
23
|
+
const ROOT_BOOL_FLAGS = new Set(["--json", "--wide", "--quiet"]);
|
|
24
|
+
|
|
25
|
+
function pathModuleForPlatform(platform = process.platform) {
|
|
26
|
+
return platform === "win32" ? path.win32 : path.posix;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function expandPathValue(value, options = {}) {
|
|
30
|
+
const env = options.env || process.env;
|
|
31
|
+
const homeDir = options.homeDir || os.homedir();
|
|
32
|
+
const platform = options.platform || process.platform;
|
|
33
|
+
const pathModule = pathModuleForPlatform(platform);
|
|
34
|
+
let result = String(value || "").trim();
|
|
35
|
+
if (!result) {
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
result = result.replace(/\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g, (match, braced, bare) => {
|
|
40
|
+
const name = braced || bare;
|
|
41
|
+
const resolved = env[name];
|
|
42
|
+
if (typeof resolved === "string" && resolved.trim() !== "") {
|
|
43
|
+
return resolved;
|
|
44
|
+
}
|
|
45
|
+
return match;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (result === "~") {
|
|
49
|
+
return homeDir;
|
|
50
|
+
}
|
|
51
|
+
if (result.startsWith("~/") || result.startsWith("~\\")) {
|
|
52
|
+
return pathModule.join(homeDir, result.slice(2));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolvePathValue(baseDir, raw, fallback, options = {}) {
|
|
59
|
+
const platform = options.platform || process.platform;
|
|
60
|
+
const pathModule = pathModuleForPlatform(platform);
|
|
61
|
+
let value = String(raw || "").trim();
|
|
62
|
+
if (value === "") {
|
|
63
|
+
value = String(fallback || "").trim();
|
|
64
|
+
}
|
|
65
|
+
if (value === "") {
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
value = expandPathValue(value, options);
|
|
69
|
+
if (value === "") {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
if (value.startsWith("$")) {
|
|
73
|
+
return pathModule.normalize(value);
|
|
74
|
+
}
|
|
75
|
+
if (pathModule.isAbsolute(value)) {
|
|
76
|
+
return pathModule.normalize(value);
|
|
77
|
+
}
|
|
78
|
+
if (!baseDir) {
|
|
79
|
+
baseDir = options.cwd || process.cwd();
|
|
80
|
+
}
|
|
81
|
+
return pathModule.normalize(pathModule.join(baseDir, value));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function hasUnresolvedExpandedEnvPath(_rawPath, resolvedPath, _options = {}) {
|
|
85
|
+
return UNRESOLVED_ENV_TOKEN_PATTERN.test(String(resolvedPath || "").trim());
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeHostPath(rawPath, options = {}) {
|
|
89
|
+
return resolvePathValue(options.cwd || process.cwd(), rawPath, "", options);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isWithinPath(parentPath, candidatePath, pathModule = path.posix) {
|
|
93
|
+
const relative = pathModule.relative(parentPath, candidatePath);
|
|
94
|
+
return relative === "" || (!relative.startsWith("..") && !pathModule.isAbsolute(relative));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function containerHomeDir(options = {}) {
|
|
98
|
+
const platform = options.platform || process.platform;
|
|
99
|
+
const homeDir = options.homeDir || os.homedir();
|
|
100
|
+
if (platform !== "win32") {
|
|
101
|
+
return homeDir;
|
|
102
|
+
}
|
|
103
|
+
return "/maestro-host/home";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function toContainerPath(hostPath, options = {}) {
|
|
107
|
+
const platform = options.platform || process.platform;
|
|
108
|
+
const homeDir = options.homeDir || os.homedir();
|
|
109
|
+
const hostPathModule = pathModuleForPlatform(platform);
|
|
110
|
+
|
|
111
|
+
if (platform !== "win32") {
|
|
112
|
+
return hostPath;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const normalized = hostPathModule.resolve(hostPath);
|
|
116
|
+
const normalizedHome = hostPathModule.resolve(homeDir);
|
|
117
|
+
if (isWithinPath(normalizedHome, normalized, hostPathModule)) {
|
|
118
|
+
const relative = hostPathModule.relative(normalizedHome, normalized);
|
|
119
|
+
const segments = relative === "" ? [] : relative.split(hostPathModule.sep);
|
|
120
|
+
return path.posix.join("/maestro-host/home", ...segments);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const drive = normalized.slice(0, 1).toLowerCase();
|
|
124
|
+
const remainder = normalized.slice(2).replace(/^\\+/, "");
|
|
125
|
+
const segments = remainder === "" ? [] : remainder.split(hostPathModule.sep);
|
|
126
|
+
return path.posix.join("/maestro-host", drive, ...segments);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function createMountCollector(options = {}) {
|
|
130
|
+
const platform = options.platform || process.platform;
|
|
131
|
+
const homeDir = options.homeDir || os.homedir();
|
|
132
|
+
const fsModule = options.fs || fs;
|
|
133
|
+
const hostPathModule = pathModuleForPlatform(platform);
|
|
134
|
+
const mounts = new Map();
|
|
135
|
+
|
|
136
|
+
function add(sourcePath, mountOptions = {}) {
|
|
137
|
+
let resolvedSource = sourcePath;
|
|
138
|
+
let resolvedTarget = toContainerPath(sourcePath, { platform, homeDir });
|
|
139
|
+
if (mountOptions.usage === "file-parent") {
|
|
140
|
+
resolvedSource = hostPathModule.dirname(resolvedSource);
|
|
141
|
+
resolvedTarget = path.posix.dirname(resolvedTarget);
|
|
142
|
+
fsModule.mkdirSync(resolvedSource, { recursive: true });
|
|
143
|
+
} else if (mountOptions.usage === "dir") {
|
|
144
|
+
fsModule.mkdirSync(resolvedSource, { recursive: true });
|
|
145
|
+
} else if (mountOptions.usage === "exact-file") {
|
|
146
|
+
if (!fsModule.existsSync(resolvedSource)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const existing of mounts.values()) {
|
|
152
|
+
if (isWithinPath(existing.source, resolvedSource, hostPathModule) && isWithinPath(existing.target, resolvedTarget, path.posix)) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (const [key, existing] of mounts) {
|
|
157
|
+
if (isWithinPath(resolvedSource, existing.source, hostPathModule) && isWithinPath(resolvedTarget, existing.target, path.posix)) {
|
|
158
|
+
mounts.delete(key);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const key = resolvedTarget;
|
|
163
|
+
const next = {
|
|
164
|
+
source: resolvedSource,
|
|
165
|
+
target: resolvedTarget,
|
|
166
|
+
readOnly: Boolean(mountOptions.readOnly),
|
|
167
|
+
};
|
|
168
|
+
if (mounts.has(key)) {
|
|
169
|
+
const existing = mounts.get(key);
|
|
170
|
+
existing.readOnly = existing.readOnly && next.readOnly;
|
|
171
|
+
mounts.set(key, existing);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
mounts.set(key, next);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
add,
|
|
179
|
+
list() {
|
|
180
|
+
return Array.from(mounts.values()).sort((left, right) => left.target.localeCompare(right.target));
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function rewriteLocalAPIURL(rawValue) {
|
|
186
|
+
const value = String(rawValue || "").trim();
|
|
187
|
+
if (!value) {
|
|
188
|
+
return { value, usesHostGateway: false };
|
|
189
|
+
}
|
|
190
|
+
const hadScheme = /^[a-z]+:\/\//i.test(value);
|
|
191
|
+
const prefixed = hadScheme ? value : `http://${value}`;
|
|
192
|
+
let parsed;
|
|
193
|
+
try {
|
|
194
|
+
parsed = new URL(prefixed);
|
|
195
|
+
} catch {
|
|
196
|
+
return { value, usesHostGateway: false };
|
|
197
|
+
}
|
|
198
|
+
if (!["127.0.0.1", "localhost", "::1", "[::1]"].includes(parsed.hostname)) {
|
|
199
|
+
return { value, usesHostGateway: false };
|
|
200
|
+
}
|
|
201
|
+
parsed.hostname = HOST_GATEWAY_NAME;
|
|
202
|
+
return {
|
|
203
|
+
value: hadScheme ? parsed.toString() : `${parsed.host}${parsed.pathname}${parsed.search}${parsed.hash}`,
|
|
204
|
+
usesHostGateway: true,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function parsePublishedPort(rawValue) {
|
|
209
|
+
const value = String(rawValue || "").trim();
|
|
210
|
+
if (!value) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
if (/^\d+$/.test(value)) {
|
|
214
|
+
return {
|
|
215
|
+
hostBinding: `127.0.0.1:${value}:${value}`,
|
|
216
|
+
containerPortFlag: `0.0.0.0:${value}`,
|
|
217
|
+
hostBaseURL: `http://127.0.0.1:${value}`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const hostPortMatch = value.match(/^(?<host>[^:]+|\[[^\]]+\]):(?<port>\d+)$/);
|
|
222
|
+
if (!hostPortMatch) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const rawHost = hostPortMatch.groups.host;
|
|
227
|
+
const host = rawHost.replace(/^\[(.*)\]$/, "$1");
|
|
228
|
+
const port = hostPortMatch.groups.port;
|
|
229
|
+
const publishedHost = rawHost.startsWith("[") ? rawHost : host;
|
|
230
|
+
let bindHost = publishedHost;
|
|
231
|
+
if (host === "0.0.0.0") {
|
|
232
|
+
bindHost = "127.0.0.1";
|
|
233
|
+
} else if (host === "::") {
|
|
234
|
+
bindHost = "[::1]";
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
hostBinding: `${publishedHost}:${port}:${port}`,
|
|
238
|
+
containerPortFlag: `0.0.0.0:${port}`,
|
|
239
|
+
hostBaseURL: `http://${bindHost}:${port}`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function flagsWithInlineValues(flagSet, token) {
|
|
244
|
+
for (const flag of flagSet) {
|
|
245
|
+
if (token === flag || token.startsWith(`${flag}=`)) {
|
|
246
|
+
return flag;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function stripRootFlags(argv) {
|
|
253
|
+
const entries = [];
|
|
254
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
255
|
+
const token = argv[i];
|
|
256
|
+
if (token === "--") {
|
|
257
|
+
entries.push({ index: i, token });
|
|
258
|
+
for (let j = i + 1; j < argv.length; j += 1) {
|
|
259
|
+
entries.push({ index: j, token: argv[j] });
|
|
260
|
+
}
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
const valueFlag = flagsWithInlineValues(ROOT_FLAGS_WITH_VALUE, token);
|
|
264
|
+
if (valueFlag) {
|
|
265
|
+
if (token === valueFlag) {
|
|
266
|
+
i += 1;
|
|
267
|
+
}
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (ROOT_BOOL_FLAGS.has(token)) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
entries.push({ index: i, token });
|
|
274
|
+
}
|
|
275
|
+
return entries;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function parseCommandPath(argv) {
|
|
279
|
+
const entries = stripRootFlags(argv).filter((entry) => entry.token !== "--");
|
|
280
|
+
if (entries.length === 0) {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const first = entries[0]?.token;
|
|
285
|
+
const second = entries[1]?.token;
|
|
286
|
+
const third = entries[2]?.token;
|
|
287
|
+
|
|
288
|
+
if (first === "workflow" && second && !second.startsWith("-")) {
|
|
289
|
+
return ["workflow", second];
|
|
290
|
+
}
|
|
291
|
+
if (first === "issue" && second && !second.startsWith("-")) {
|
|
292
|
+
if (["assets", "images", "comments", "blockers"].includes(second) && third && !third.startsWith("-")) {
|
|
293
|
+
return ["issue", second, third];
|
|
294
|
+
}
|
|
295
|
+
return ["issue", second];
|
|
296
|
+
}
|
|
297
|
+
if (first === "project" && second && !second.startsWith("-")) {
|
|
298
|
+
return ["project", second];
|
|
299
|
+
}
|
|
300
|
+
return [first];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function collectCommandPositionals(argv, commandPath) {
|
|
304
|
+
const entries = stripRootFlags(argv);
|
|
305
|
+
const localFlagsWithValue = new Set();
|
|
306
|
+
const localBoolFlags = new Set();
|
|
307
|
+
|
|
308
|
+
if (commandPath[0] === "run") {
|
|
309
|
+
for (const flag of ["--workflow", "--extensions", "--logs-root", "--port", "--log-max-bytes", "--log-max-files"]) {
|
|
310
|
+
localFlagsWithValue.add(flag);
|
|
311
|
+
}
|
|
312
|
+
localBoolFlags.add("--i-understand-that-this-will-be-running-without-the-usual-guardrails");
|
|
313
|
+
} else if (commandPath[0] === "init" || (commandPath[0] === "workflow" && commandPath[1] === "init")) {
|
|
314
|
+
for (const flag of [
|
|
315
|
+
"--workspace-root",
|
|
316
|
+
"--codex-command",
|
|
317
|
+
"--agent-mode",
|
|
318
|
+
"--dispatch-mode",
|
|
319
|
+
"--max-concurrent-agents",
|
|
320
|
+
"--max-turns",
|
|
321
|
+
"--max-automatic-retries",
|
|
322
|
+
"--approval-policy",
|
|
323
|
+
"--initial-collaboration-mode",
|
|
324
|
+
]) {
|
|
325
|
+
localFlagsWithValue.add(flag);
|
|
326
|
+
}
|
|
327
|
+
localBoolFlags.add("--force");
|
|
328
|
+
localBoolFlags.add("--defaults");
|
|
329
|
+
} else if (commandPath[0] === "issue" && commandPath[1] === "comments") {
|
|
330
|
+
for (const flag of ["--body", "--parent", "--attach", "--remove-attachment"]) {
|
|
331
|
+
localFlagsWithValue.add(flag);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const positionals = [];
|
|
336
|
+
for (let i = commandPath.length; i < entries.length; i += 1) {
|
|
337
|
+
const { token } = entries[i];
|
|
338
|
+
if (token === "--") {
|
|
339
|
+
for (let j = i + 1; j < entries.length; j += 1) {
|
|
340
|
+
positionals.push(entries[j]);
|
|
341
|
+
}
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
const valueFlag = flagsWithInlineValues(localFlagsWithValue, token);
|
|
345
|
+
if (valueFlag) {
|
|
346
|
+
if (token === valueFlag) {
|
|
347
|
+
i += 1;
|
|
348
|
+
}
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (localBoolFlags.has(token)) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (token.startsWith("-")) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
positionals.push(entries[i]);
|
|
358
|
+
}
|
|
359
|
+
return positionals;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function findFlagValue(argv, flag) {
|
|
363
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
364
|
+
const token = argv[i];
|
|
365
|
+
if (token === flag) {
|
|
366
|
+
return typeof argv[i + 1] === "string" ? argv[i + 1] : "";
|
|
367
|
+
}
|
|
368
|
+
if (token.startsWith(`${flag}=`)) {
|
|
369
|
+
return token.slice(flag.length + 1);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return "";
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function parseWorkflowFrontMatter(content) {
|
|
376
|
+
const normalized = String(content || "").replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
377
|
+
if (!normalized.startsWith("---\n")) {
|
|
378
|
+
return "";
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const end = normalized.indexOf("\n---\n", 4);
|
|
382
|
+
return end === -1 ? normalized.slice(4) : normalized.slice(4, end);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function parseWorkflowScalar(rawValue, workflowPath, keyName) {
|
|
386
|
+
let value = String(rawValue || "").trim();
|
|
387
|
+
if (!value) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
if (value.startsWith("[") || value.startsWith("{") || value === "|" || value === ">") {
|
|
391
|
+
throw new Error(`${keyName} must be a string at ${workflowPath}`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (value.startsWith('"')) {
|
|
395
|
+
if (!value.endsWith('"')) {
|
|
396
|
+
throw new Error(`${keyName} must be a string at ${workflowPath}`);
|
|
397
|
+
}
|
|
398
|
+
value = value.slice(1, -1).replace(/\\(["\\/bfnrt])/g, (match, escape) => {
|
|
399
|
+
switch (escape) {
|
|
400
|
+
case "b":
|
|
401
|
+
return "\b";
|
|
402
|
+
case "f":
|
|
403
|
+
return "\f";
|
|
404
|
+
case "n":
|
|
405
|
+
return "\n";
|
|
406
|
+
case "r":
|
|
407
|
+
return "\r";
|
|
408
|
+
case "t":
|
|
409
|
+
return "\t";
|
|
410
|
+
default:
|
|
411
|
+
return escape;
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
return value;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (value.startsWith("'")) {
|
|
418
|
+
if (!value.endsWith("'")) {
|
|
419
|
+
throw new Error(`${keyName} must be a string at ${workflowPath}`);
|
|
420
|
+
}
|
|
421
|
+
return value.slice(1, -1).replace(/''/g, "'");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return value;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function nextSignificantLine(lines, startIndex) {
|
|
428
|
+
for (let i = startIndex; i < lines.length; i += 1) {
|
|
429
|
+
const trimmed = lines[i].trim();
|
|
430
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
indent: lines[i].match(/^[ \t]*/)?.[0].length || 0,
|
|
435
|
+
index: i,
|
|
436
|
+
trimmed,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function parseWorkflowWorkspaceRoot(workflowPath, content, options = {}) {
|
|
443
|
+
const pathModule = pathModuleForPlatform(options.platform || process.platform);
|
|
444
|
+
const workflowDir = pathModule.dirname(workflowPath);
|
|
445
|
+
const frontMatter = parseWorkflowFrontMatter(content);
|
|
446
|
+
if (!frontMatter) {
|
|
447
|
+
return resolvePathValue(workflowDir, "", DEFAULT_WORKSPACE_ROOT, options);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const lines = frontMatter.split("\n");
|
|
451
|
+
let workspaceRoot = null;
|
|
452
|
+
let inWorkspaceBlock = false;
|
|
453
|
+
let workspaceIndent = 0;
|
|
454
|
+
|
|
455
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
456
|
+
const line = lines[i];
|
|
457
|
+
const trimmed = line.trim();
|
|
458
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const indent = line.match(/^[ \t]*/)?.[0].length || 0;
|
|
463
|
+
if (indent === 0) {
|
|
464
|
+
inWorkspaceBlock = false;
|
|
465
|
+
|
|
466
|
+
const workspaceRootMatch = trimmed.match(/^workspace_root\s*:\s*(.*)$/);
|
|
467
|
+
if (workspaceRootMatch) {
|
|
468
|
+
workspaceRoot = parseWorkflowScalar(workspaceRootMatch[1], workflowPath, "workspace.root");
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const workspaceMatch = trimmed.match(/^workspace\s*:\s*(.*)$/);
|
|
473
|
+
if (!workspaceMatch) {
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const inline = workspaceMatch[1].trim();
|
|
478
|
+
if (!inline) {
|
|
479
|
+
inWorkspaceBlock = true;
|
|
480
|
+
workspaceIndent = indent;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const inlineRootMatch = inline.match(/^\{\s*root\s*:\s*(.*?)\s*\}$/) || inline.match(/^root\s*:\s*(.*)$/);
|
|
485
|
+
if (inlineRootMatch) {
|
|
486
|
+
workspaceRoot = parseWorkflowScalar(inlineRootMatch[1], workflowPath, "workspace.root");
|
|
487
|
+
}
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (!inWorkspaceBlock || indent <= workspaceIndent) {
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const rootMatch = trimmed.match(/^root\s*:\s*(.*)$/);
|
|
496
|
+
if (!rootMatch) {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const rawRootValue = rootMatch[1];
|
|
501
|
+
if (!rawRootValue.trim()) {
|
|
502
|
+
const next = nextSignificantLine(lines, i + 1);
|
|
503
|
+
if (next && next.indent > indent) {
|
|
504
|
+
throw new Error(`workspace.root must be a string at ${workflowPath}`);
|
|
505
|
+
}
|
|
506
|
+
workspaceRoot = null;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
workspaceRoot = parseWorkflowScalar(rawRootValue, workflowPath, "workspace.root");
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const resolved = resolvePathValue(workflowDir, workspaceRoot, DEFAULT_WORKSPACE_ROOT, options);
|
|
514
|
+
if (workspaceRoot != null && hasUnresolvedExpandedEnvPath(workspaceRoot, resolved, options)) {
|
|
515
|
+
throw new Error(`failed to resolve workspace root: unresolved environment variable in ${JSON.stringify(resolved)}`);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return resolved;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function resolveWorkflowWorkspaceRoot(workflowPath, options = {}) {
|
|
522
|
+
const fsModule = options.fs || fs;
|
|
523
|
+
const pathModule = pathModuleForPlatform(options.platform || process.platform);
|
|
524
|
+
const workflowDir = pathModule.dirname(workflowPath);
|
|
525
|
+
const defaultRoot = resolvePathValue(workflowDir, "", DEFAULT_WORKSPACE_ROOT, options);
|
|
526
|
+
|
|
527
|
+
let stat;
|
|
528
|
+
try {
|
|
529
|
+
stat = fsModule.statSync(workflowPath);
|
|
530
|
+
} catch (error) {
|
|
531
|
+
if (error && error.code === "ENOENT") {
|
|
532
|
+
return defaultRoot;
|
|
533
|
+
}
|
|
534
|
+
throw new Error(`failed to inspect workflow file ${workflowPath}: ${error.message}`);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (!stat.isFile()) {
|
|
538
|
+
throw new Error(`workflow path is not a file: ${workflowPath}`);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
let content;
|
|
542
|
+
try {
|
|
543
|
+
content = fsModule.readFileSync(workflowPath, "utf8");
|
|
544
|
+
} catch (error) {
|
|
545
|
+
throw new Error(`failed to read workflow file ${workflowPath}: ${error.message}`);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return parseWorkflowWorkspaceRoot(workflowPath, content, options);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function resolveDatabasePath(argv, options = {}) {
|
|
552
|
+
const rawDbPath = findFlagValue(argv, "--db");
|
|
553
|
+
const cwd = options.cwd || process.cwd();
|
|
554
|
+
const resolved = resolvePathValue(cwd, rawDbPath, DEFAULT_DATABASE_PATH, options);
|
|
555
|
+
if (hasUnresolvedExpandedEnvPath(rawDbPath, resolved, options)) {
|
|
556
|
+
throw new Error(`failed to resolve database path: unresolved environment variable in ${JSON.stringify(resolved)}`);
|
|
557
|
+
}
|
|
558
|
+
return resolved;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function shouldSkipRunDatabaseDiscovery(error) {
|
|
562
|
+
if (!error || typeof error !== "object") {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
if (error.code !== "ERR_SQLITE_ERROR") {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
return /no such table: projects/.test(String(error.message || ""));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function discoverProjectWorkflowsFromDatabase(dbPath, options = {}) {
|
|
572
|
+
let db;
|
|
573
|
+
try {
|
|
574
|
+
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
575
|
+
const pathModule = pathModuleForPlatform(options.platform || process.platform);
|
|
576
|
+
const tableInfo = db.prepare("PRAGMA table_info(projects)").all();
|
|
577
|
+
const columns = new Set(tableInfo.map((row) => String(row.name || "").trim()).filter(Boolean));
|
|
578
|
+
if (!columns.has("repo_path")) {
|
|
579
|
+
return [];
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const projectWorkflows = [];
|
|
583
|
+
const rows = columns.has("workflow_path")
|
|
584
|
+
? db.prepare("SELECT repo_path, workflow_path FROM projects").all()
|
|
585
|
+
: db.prepare("SELECT repo_path FROM projects").all();
|
|
586
|
+
for (const row of rows) {
|
|
587
|
+
const repoPath = String(row.repo_path || "").trim();
|
|
588
|
+
if (!repoPath) {
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
const workflowPath = columns.has("workflow_path")
|
|
592
|
+
? String(row.workflow_path || "").trim() || pathModule.join(repoPath, "WORKFLOW.md")
|
|
593
|
+
: pathModule.join(repoPath, "WORKFLOW.md");
|
|
594
|
+
projectWorkflows.push({ repoPath, workflowPath });
|
|
595
|
+
}
|
|
596
|
+
return projectWorkflows;
|
|
597
|
+
} catch (error) {
|
|
598
|
+
if (shouldSkipRunDatabaseDiscovery(error)) {
|
|
599
|
+
return [];
|
|
600
|
+
}
|
|
601
|
+
throw new Error(`failed to inspect Maestro database at ${dbPath}: ${error.message}`);
|
|
602
|
+
} finally {
|
|
603
|
+
if (db) {
|
|
604
|
+
db.close();
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function addWorkflowMounts(workflowPath, mountCollector, options = {}) {
|
|
610
|
+
const fsModule = options.fs || fs;
|
|
611
|
+
const pathModule = pathModuleForPlatform(options.platform || process.platform);
|
|
612
|
+
const workflowDir = pathModule.dirname(workflowPath);
|
|
613
|
+
mountCollector.add(workflowDir, { usage: "dir" });
|
|
614
|
+
const root = resolveWorkflowWorkspaceRoot(workflowPath, {
|
|
615
|
+
cwd: options.cwd,
|
|
616
|
+
env: options.env,
|
|
617
|
+
fs: fsModule,
|
|
618
|
+
homeDir: options.homeDir,
|
|
619
|
+
platform: options.platform,
|
|
620
|
+
});
|
|
621
|
+
mountCollector.add(root, { usage: "dir" });
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function preflightRunMounts(argv, commandPath, mountCollector, options = {}) {
|
|
625
|
+
if (commandPath[0] !== "run") {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const fsModule = options.fs || fs;
|
|
630
|
+
const cwd = options.cwd || process.cwd();
|
|
631
|
+
const env = options.env || process.env;
|
|
632
|
+
const homeDir = options.homeDir || os.homedir();
|
|
633
|
+
const platform = options.platform || process.platform;
|
|
634
|
+
const positionals = collectCommandPositionals(argv, commandPath);
|
|
635
|
+
const scopedRepoPath = positionals[0] ? normalizeHostPath(positionals[0].token, { cwd, env, homeDir, platform }) : "";
|
|
636
|
+
const rawWorkflowPath = findFlagValue(argv, "--workflow").trim();
|
|
637
|
+
const explicitWorkflowPath = rawWorkflowPath ? normalizeHostPath(rawWorkflowPath, { cwd, env, homeDir, platform }) : "";
|
|
638
|
+
const pathModule = pathModuleForPlatform(platform);
|
|
639
|
+
|
|
640
|
+
if (scopedRepoPath) {
|
|
641
|
+
const workflowPath = explicitWorkflowPath || pathModule.join(scopedRepoPath, "WORKFLOW.md");
|
|
642
|
+
mountCollector.add(scopedRepoPath, { usage: "dir" });
|
|
643
|
+
addWorkflowMounts(workflowPath, mountCollector, { cwd, env, fs: fsModule, homeDir, platform });
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (explicitWorkflowPath) {
|
|
648
|
+
addWorkflowMounts(explicitWorkflowPath, mountCollector, { cwd, env, fs: fsModule, homeDir, platform });
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const dbPath = resolveDatabasePath(argv, { cwd, env, homeDir, platform });
|
|
652
|
+
if (!fsModule.existsSync(dbPath)) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
for (const project of discoverProjectWorkflowsFromDatabase(dbPath, { platform })) {
|
|
657
|
+
const repoPath = normalizeHostPath(project.repoPath, { cwd, env, homeDir, platform });
|
|
658
|
+
if (!repoPath) {
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
mountCollector.add(repoPath, { usage: "dir" });
|
|
662
|
+
addWorkflowMounts(normalizeHostPath(project.workflowPath, { cwd, env, homeDir, platform }), mountCollector, {
|
|
663
|
+
cwd,
|
|
664
|
+
env,
|
|
665
|
+
fs: fsModule,
|
|
666
|
+
homeDir,
|
|
667
|
+
platform,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
async function reservePort() {
|
|
673
|
+
return await new Promise((resolve, reject) => {
|
|
674
|
+
const server = net.createServer();
|
|
675
|
+
server.once("error", reject);
|
|
676
|
+
server.listen(0, "127.0.0.1", () => {
|
|
677
|
+
const address = server.address();
|
|
678
|
+
if (!address || typeof address === "string") {
|
|
679
|
+
server.close(() => reject(new Error("failed to allocate an ephemeral port")));
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
server.close((error) => {
|
|
683
|
+
if (error) {
|
|
684
|
+
reject(error);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
resolve(address.port);
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function createArgRewriter(argv) {
|
|
694
|
+
const replacements = new Map();
|
|
695
|
+
const appended = [];
|
|
696
|
+
return {
|
|
697
|
+
replaceValue(index, value) {
|
|
698
|
+
replacements.set(index, value);
|
|
699
|
+
},
|
|
700
|
+
replaceInline(index, flag, value) {
|
|
701
|
+
replacements.set(index, `${flag}=${value}`);
|
|
702
|
+
},
|
|
703
|
+
append(...tokens) {
|
|
704
|
+
appended.push(...tokens);
|
|
705
|
+
},
|
|
706
|
+
apply() {
|
|
707
|
+
return argv.map((token, index) => (replacements.has(index) ? replacements.get(index) : token)).concat(appended);
|
|
708
|
+
},
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async function planDockerInvocation(argv, options = {}) {
|
|
713
|
+
const cwd = options.cwd || process.cwd();
|
|
714
|
+
const env = options.env || process.env;
|
|
715
|
+
const fsModule = options.fs || fs;
|
|
716
|
+
const platform = options.platform || process.platform;
|
|
717
|
+
const homeDir = options.homeDir || os.homedir();
|
|
718
|
+
const imageRef = options.imageRef;
|
|
719
|
+
const uid = options.uid;
|
|
720
|
+
const gid = options.gid;
|
|
721
|
+
|
|
722
|
+
if (!imageRef) {
|
|
723
|
+
throw new Error("image ref is required");
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const commandPath = parseCommandPath(argv);
|
|
727
|
+
const mountCollector = createMountCollector({ fs: fsModule, platform, homeDir });
|
|
728
|
+
const rewriter = createArgRewriter(argv);
|
|
729
|
+
const containerHome = containerHomeDir({ platform, homeDir });
|
|
730
|
+
const envVars = new Map([
|
|
731
|
+
["HOME", containerHome],
|
|
732
|
+
["MAESTRO_DISABLE_BROWSER_OPEN", "1"],
|
|
733
|
+
]);
|
|
734
|
+
const dockerArgs = ["run", "--rm", "--init"];
|
|
735
|
+
let usesHostGateway = platform === "linux";
|
|
736
|
+
|
|
737
|
+
if (platform !== "win32" && Number.isInteger(uid) && Number.isInteger(gid)) {
|
|
738
|
+
dockerArgs.push("--user", `${uid}:${gid}`);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (process.stdin.isTTY || !process.stdin.isTTY) {
|
|
742
|
+
dockerArgs.push("-i");
|
|
743
|
+
}
|
|
744
|
+
if (process.stdin.isTTY && process.stdout.isTTY && process.stderr.isTTY && commandPath[0] !== "mcp") {
|
|
745
|
+
dockerArgs.push("-t");
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const hostMaestroDir = path.join(homeDir, ".maestro");
|
|
749
|
+
fsModule.mkdirSync(hostMaestroDir, { recursive: true });
|
|
750
|
+
mountCollector.add(hostMaestroDir, { usage: "dir" });
|
|
751
|
+
|
|
752
|
+
const hostDaemonRegistryDir = path.join(hostMaestroDir, "launcher", "daemons");
|
|
753
|
+
fsModule.mkdirSync(hostDaemonRegistryDir, { recursive: true });
|
|
754
|
+
envVars.set("MAESTRO_DAEMON_REGISTRY_DIR", toContainerPath(hostDaemonRegistryDir, { platform, homeDir }));
|
|
755
|
+
|
|
756
|
+
const hostCodexDir = path.join(homeDir, ".codex");
|
|
757
|
+
if (fsModule.existsSync(hostCodexDir)) {
|
|
758
|
+
mountCollector.add(hostCodexDir, { usage: "dir" });
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
for (const maybeFile of [".gitconfig", ".config/git"]) {
|
|
762
|
+
const absolute = path.join(homeDir, maybeFile);
|
|
763
|
+
if (fsModule.existsSync(absolute)) {
|
|
764
|
+
mountCollector.add(absolute, { usage: fsModule.statSync(absolute).isDirectory() ? "dir" : "exact-file" });
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
const sshDir = path.join(homeDir, ".ssh");
|
|
768
|
+
if (fsModule.existsSync(sshDir)) {
|
|
769
|
+
mountCollector.add(sshDir, { usage: "dir" });
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (typeof env.SSH_AUTH_SOCK === "string" && env.SSH_AUTH_SOCK.trim() !== "") {
|
|
773
|
+
const resolvedSocket = normalizeHostPath(env.SSH_AUTH_SOCK, { cwd, env, homeDir, platform });
|
|
774
|
+
if (fsModule.existsSync(resolvedSocket)) {
|
|
775
|
+
mountCollector.add(path.dirname(resolvedSocket), { usage: "dir" });
|
|
776
|
+
envVars.set("SSH_AUTH_SOCK", toContainerPath(resolvedSocket, { platform, homeDir }));
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
mountCollector.add(cwd, { usage: "dir" });
|
|
781
|
+
preflightRunMounts(argv, commandPath, mountCollector, {
|
|
782
|
+
cwd,
|
|
783
|
+
env,
|
|
784
|
+
fs: fsModule,
|
|
785
|
+
homeDir,
|
|
786
|
+
platform,
|
|
787
|
+
});
|
|
788
|
+
const containerCwd = toContainerPath(cwd, { platform, homeDir });
|
|
789
|
+
dockerArgs.push("--workdir", containerCwd);
|
|
790
|
+
|
|
791
|
+
for (const key of ["OPENAI_API_KEY", "OPENAI_BASE_URL", "OPENAI_ORG_ID", "OPENAI_PROJECT_ID", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY"]) {
|
|
792
|
+
if (typeof env[key] === "string" && env[key] !== "") {
|
|
793
|
+
envVars.set(key, env[key]);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
798
|
+
const token = argv[i];
|
|
799
|
+
const pathFlag = flagsWithInlineValues(new Set(POSIX_PATH_FLAGS.keys()), token);
|
|
800
|
+
if (pathFlag) {
|
|
801
|
+
const descriptor = POSIX_PATH_FLAGS.get(pathFlag);
|
|
802
|
+
if (token === pathFlag) {
|
|
803
|
+
const next = argv[i + 1];
|
|
804
|
+
if (typeof next === "string") {
|
|
805
|
+
const resolved = normalizeHostPath(next, { cwd, env, homeDir, platform });
|
|
806
|
+
mountCollector.add(resolved, { usage: descriptor.usage });
|
|
807
|
+
rewriter.replaceValue(i + 1, toContainerPath(resolved, { platform, homeDir }));
|
|
808
|
+
i += 1;
|
|
809
|
+
}
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
const rawValue = token.slice(`${pathFlag}=`.length);
|
|
813
|
+
const resolved = normalizeHostPath(rawValue, { cwd, env, homeDir, platform });
|
|
814
|
+
mountCollector.add(resolved, { usage: descriptor.usage });
|
|
815
|
+
rewriter.replaceInline(i, pathFlag, toContainerPath(resolved, { platform, homeDir }));
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const apiFlag = flagsWithInlineValues(new Set(["--api-url"]), token);
|
|
820
|
+
if (apiFlag) {
|
|
821
|
+
if (token === apiFlag) {
|
|
822
|
+
const next = argv[i + 1];
|
|
823
|
+
if (typeof next === "string") {
|
|
824
|
+
const rewritten = rewriteLocalAPIURL(next);
|
|
825
|
+
if (rewritten.usesHostGateway) {
|
|
826
|
+
usesHostGateway = true;
|
|
827
|
+
}
|
|
828
|
+
rewriter.replaceValue(i + 1, rewritten.value);
|
|
829
|
+
i += 1;
|
|
830
|
+
}
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
const rewritten = rewriteLocalAPIURL(token.slice("--api-url=".length));
|
|
834
|
+
if (rewritten.usesHostGateway) {
|
|
835
|
+
usesHostGateway = true;
|
|
836
|
+
}
|
|
837
|
+
rewriter.replaceInline(i, "--api-url", rewritten.value);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const positionals = collectCommandPositionals(argv, commandPath);
|
|
842
|
+
if (commandPath[0] === "run" && positionals[0]) {
|
|
843
|
+
const resolvedRepoPath = normalizeHostPath(positionals[0].token, { cwd, env, homeDir, platform });
|
|
844
|
+
mountCollector.add(resolvedRepoPath, { usage: fsModule.existsSync(resolvedRepoPath) ? "dir" : "file-parent" });
|
|
845
|
+
rewriter.replaceValue(positionals[0].index, toContainerPath(resolvedRepoPath, { platform, homeDir }));
|
|
846
|
+
}
|
|
847
|
+
if ((commandPath[0] === "init" || (commandPath[0] === "workflow" && commandPath[1] === "init")) && positionals[0]) {
|
|
848
|
+
const resolvedRepoPath = normalizeHostPath(positionals[0].token, { cwd, env, homeDir, platform });
|
|
849
|
+
const usage = fsModule.existsSync(resolvedRepoPath) && fsModule.statSync(resolvedRepoPath).isDirectory() ? "dir" : "file-parent";
|
|
850
|
+
mountCollector.add(resolvedRepoPath, { usage });
|
|
851
|
+
rewriter.replaceValue(positionals[0].index, toContainerPath(resolvedRepoPath, { platform, homeDir }));
|
|
852
|
+
}
|
|
853
|
+
if (commandPath[0] === "issue" && ["assets", "images"].includes(commandPath[1]) && commandPath[2] === "add" && positionals[1]) {
|
|
854
|
+
const resolvedAssetPath = normalizeHostPath(positionals[1].token, { cwd, env, homeDir, platform });
|
|
855
|
+
mountCollector.add(resolvedAssetPath, { usage: "file-parent" });
|
|
856
|
+
rewriter.replaceValue(positionals[1].index, toContainerPath(resolvedAssetPath, { platform, homeDir }));
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
let hostBaseURL = "";
|
|
860
|
+
if (commandPath[0] === "run") {
|
|
861
|
+
let portTokenIndex = -1;
|
|
862
|
+
let portValue = DEFAULT_HTTP_PORT;
|
|
863
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
864
|
+
const token = argv[i];
|
|
865
|
+
if (token === "--port" && typeof argv[i + 1] === "string") {
|
|
866
|
+
portTokenIndex = i + 1;
|
|
867
|
+
portValue = argv[i + 1];
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
870
|
+
if (token.startsWith("--port=")) {
|
|
871
|
+
portTokenIndex = i;
|
|
872
|
+
portValue = token.slice("--port=".length);
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const portPlan = parsePublishedPort(portValue);
|
|
877
|
+
if (portPlan) {
|
|
878
|
+
hostBaseURL = portPlan.hostBaseURL;
|
|
879
|
+
dockerArgs.push("-p", portPlan.hostBinding);
|
|
880
|
+
if (portTokenIndex === -1) {
|
|
881
|
+
rewriter.append("--port", portPlan.containerPortFlag);
|
|
882
|
+
} else if (argv[portTokenIndex].startsWith("--port=")) {
|
|
883
|
+
rewriter.replaceInline(portTokenIndex, "--port", portPlan.containerPortFlag);
|
|
884
|
+
} else {
|
|
885
|
+
rewriter.replaceValue(portTokenIndex, portPlan.containerPortFlag);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const mcpPort = await reservePort();
|
|
890
|
+
dockerArgs.push("-p", `127.0.0.1:${mcpPort}:${mcpPort}`);
|
|
891
|
+
envVars.set("MAESTRO_MCP_LISTEN_ADDR", `0.0.0.0:${mcpPort}`);
|
|
892
|
+
envVars.set("MAESTRO_MCP_ADVERTISED_URL", `http://${HOST_GATEWAY_NAME}:${mcpPort}/mcp`);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (usesHostGateway && platform === "linux") {
|
|
896
|
+
dockerArgs.push("--add-host", `${HOST_GATEWAY_NAME}:host-gateway`);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
for (const mount of mountCollector.list()) {
|
|
900
|
+
const suffix = mount.readOnly ? ":ro" : "";
|
|
901
|
+
dockerArgs.push("-v", `${mount.source}:${mount.target}${suffix}`);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
for (const [key, value] of envVars) {
|
|
905
|
+
dockerArgs.push("-e", `${key}=${value}`);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const rewrittenArgv = rewriter.apply();
|
|
909
|
+
dockerArgs.push(imageRef, ...rewrittenArgv);
|
|
910
|
+
|
|
911
|
+
return {
|
|
912
|
+
commandPath,
|
|
913
|
+
dockerArgs,
|
|
914
|
+
hostBaseURL,
|
|
915
|
+
rewrittenArgv,
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function resolveBinPathFromInstallDir(installDir, platform = process.platform) {
|
|
920
|
+
const suffix = platform === "win32" ? "maestro.cmd" : "maestro";
|
|
921
|
+
return path.join(installDir, "node_modules", ".bin", suffix);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function isMainModule(metaURL) {
|
|
925
|
+
return Boolean(process.argv[1]) && pathToFileURL(process.argv[1]).href === metaURL;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
module.exports = {
|
|
929
|
+
DEFAULT_HTTP_PORT,
|
|
930
|
+
HOST_GATEWAY_NAME,
|
|
931
|
+
collectCommandPositionals,
|
|
932
|
+
containerHomeDir,
|
|
933
|
+
isMainModule,
|
|
934
|
+
normalizeHostPath,
|
|
935
|
+
parseCommandPath,
|
|
936
|
+
parsePublishedPort,
|
|
937
|
+
planDockerInvocation,
|
|
938
|
+
resolveBinPathFromInstallDir,
|
|
939
|
+
rewriteLocalAPIURL,
|
|
940
|
+
toContainerPath,
|
|
941
|
+
};
|