proteum 2.5.5 → 2.5.7
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/agents/project/AGENTS.md +0 -1
- package/agents/project/root/AGENTS.md +0 -1
- package/cli/commands/configure.ts +63 -4
- package/cli/index.ts +24 -18
- package/cli/presentation/commands.ts +12 -7
- package/cli/runtime/monorepoCommands.ts +625 -0
- package/cli/runtime/worktreeBootstrap.ts +163 -0
- package/cli/utils/agents.ts +211 -43
- package/package.json +1 -1
- package/tests/agents-utils.test.cjs +165 -5
- package/tests/cli-mcp-command.test.cjs +60 -11
- package/tests/worktree-bootstrap.test.cjs +98 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
compactWorktreeBootstrapStatus,
|
|
6
|
+
runMonorepoWorktreeBootstrapCreate,
|
|
7
|
+
runMonorepoWorktreeBootstrapInit,
|
|
8
|
+
} from './worktreeBootstrap';
|
|
9
|
+
import { inspectDevPort } from './ports';
|
|
10
|
+
import {
|
|
11
|
+
resolveProteumAppRootContext,
|
|
12
|
+
type TProteumAppRootSummary,
|
|
13
|
+
} from '../utils/appRoots';
|
|
14
|
+
import { printJson, quoteCommandArgument } from '../utils/agentOutput';
|
|
15
|
+
|
|
16
|
+
export const monorepoFanoutChildEnv = 'PROTEUM_MONOREPO_FANOUT_CHILD';
|
|
17
|
+
|
|
18
|
+
type TMonorepoCommandResult = {
|
|
19
|
+
appRoot: string;
|
|
20
|
+
exitCode: number | null;
|
|
21
|
+
json?: unknown;
|
|
22
|
+
ok: boolean;
|
|
23
|
+
relativeAppRoot?: string;
|
|
24
|
+
stderr: string;
|
|
25
|
+
stdout: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type TParsedWorktreeArgs = {
|
|
29
|
+
action: string;
|
|
30
|
+
base?: string;
|
|
31
|
+
branch?: string;
|
|
32
|
+
json: boolean;
|
|
33
|
+
reason?: string;
|
|
34
|
+
refresh: boolean;
|
|
35
|
+
skipDeps: boolean;
|
|
36
|
+
source?: string;
|
|
37
|
+
target?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const defaultJsonCommandNames = new Set([
|
|
41
|
+
'connect',
|
|
42
|
+
'db',
|
|
43
|
+
'diagnose',
|
|
44
|
+
'doctor',
|
|
45
|
+
'explain',
|
|
46
|
+
'orient',
|
|
47
|
+
'perf',
|
|
48
|
+
'runtime',
|
|
49
|
+
'trace',
|
|
50
|
+
]);
|
|
51
|
+
const genericFanoutCommands = new Set([
|
|
52
|
+
'build',
|
|
53
|
+
'check',
|
|
54
|
+
'command',
|
|
55
|
+
'connect',
|
|
56
|
+
'db',
|
|
57
|
+
'diagnose',
|
|
58
|
+
'doctor',
|
|
59
|
+
'e2e',
|
|
60
|
+
'explain',
|
|
61
|
+
'lint',
|
|
62
|
+
'orient',
|
|
63
|
+
'perf',
|
|
64
|
+
'refresh',
|
|
65
|
+
'runtime',
|
|
66
|
+
'session',
|
|
67
|
+
'trace',
|
|
68
|
+
'typecheck',
|
|
69
|
+
]);
|
|
70
|
+
const optionNamesWithValue = new Set([
|
|
71
|
+
'--base',
|
|
72
|
+
'--branch',
|
|
73
|
+
'--cwd',
|
|
74
|
+
'--port',
|
|
75
|
+
'--reason',
|
|
76
|
+
'--session-file',
|
|
77
|
+
'--source',
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
export const isMonorepoFanoutChild = () => process.env[monorepoFanoutChildEnv] === '1';
|
|
81
|
+
|
|
82
|
+
const getCliBin = () => path.join(__dirname, '..', 'bin.js');
|
|
83
|
+
|
|
84
|
+
const hasFlag = (argv: string[], flag: string) => argv.includes(flag) || argv.some((arg) => arg.startsWith(`${flag}=`));
|
|
85
|
+
|
|
86
|
+
const getOptionValue = (argv: string[], optionName: string) => {
|
|
87
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
88
|
+
const arg = argv[index];
|
|
89
|
+
if (arg === optionName) return argv[index + 1];
|
|
90
|
+
if (arg.startsWith(`${optionName}=`)) return arg.slice(optionName.length + 1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return undefined;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const removeOptionsWithValues = (argv: string[], options: Set<string>) => {
|
|
97
|
+
const nextArgv: string[] = [];
|
|
98
|
+
|
|
99
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
100
|
+
const arg = argv[index];
|
|
101
|
+
const exactOption = options.has(arg);
|
|
102
|
+
const assignmentOption = [...options].some((option) => arg.startsWith(`${option}=`));
|
|
103
|
+
|
|
104
|
+
if (assignmentOption) continue;
|
|
105
|
+
if (exactOption) {
|
|
106
|
+
index += 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
nextArgv.push(arg);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return nextArgv;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const getPositionals = (argv: string[]) => {
|
|
117
|
+
const positionals: string[] = [];
|
|
118
|
+
|
|
119
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
120
|
+
const arg = argv[index];
|
|
121
|
+
if (arg.startsWith('-')) {
|
|
122
|
+
if (optionNamesWithValue.has(arg)) index += 1;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
positionals.push(arg);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return positionals;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const getCommandString = (argv: string[]) => ['proteum', ...argv].join(' ');
|
|
133
|
+
|
|
134
|
+
const shouldPrintJsonAggregate = (argv: string[]) => {
|
|
135
|
+
const commandName = argv[0] || '';
|
|
136
|
+
|
|
137
|
+
if (hasFlag(argv, '--human')) return false;
|
|
138
|
+
if (hasFlag(argv, '--json')) return true;
|
|
139
|
+
if (defaultJsonCommandNames.has(commandName)) return true;
|
|
140
|
+
if (commandName === 'dev' && getPositionals(argv.slice(1))[0] === 'list' && hasFlag(argv, '--json')) return true;
|
|
141
|
+
return false;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const parseJsonOutput = (stdout: string) => {
|
|
145
|
+
const trimmed = stdout.trim();
|
|
146
|
+
if (!trimmed) return undefined;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
return JSON.parse(trimmed);
|
|
150
|
+
} catch {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const runChildCommand = async ({
|
|
156
|
+
app,
|
|
157
|
+
argv,
|
|
158
|
+
}: {
|
|
159
|
+
app: TProteumAppRootSummary;
|
|
160
|
+
argv: string[];
|
|
161
|
+
}): Promise<TMonorepoCommandResult> =>
|
|
162
|
+
await new Promise((resolve) => {
|
|
163
|
+
const child = spawn(process.execPath, [getCliBin(), ...argv], {
|
|
164
|
+
cwd: app.appRoot,
|
|
165
|
+
env: {
|
|
166
|
+
...process.env,
|
|
167
|
+
[monorepoFanoutChildEnv]: '1',
|
|
168
|
+
},
|
|
169
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
170
|
+
});
|
|
171
|
+
const stdoutChunks: Buffer[] = [];
|
|
172
|
+
const stderrChunks: Buffer[] = [];
|
|
173
|
+
|
|
174
|
+
child.stdout.on('data', (chunk) => stdoutChunks.push(Buffer.from(chunk)));
|
|
175
|
+
child.stderr.on('data', (chunk) => stderrChunks.push(Buffer.from(chunk)));
|
|
176
|
+
child.on('error', (error) => {
|
|
177
|
+
resolve({
|
|
178
|
+
appRoot: app.appRoot,
|
|
179
|
+
exitCode: 1,
|
|
180
|
+
ok: false,
|
|
181
|
+
relativeAppRoot: app.relativeAppRoot,
|
|
182
|
+
stderr: error.message,
|
|
183
|
+
stdout: '',
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
child.on('close', (exitCode) => {
|
|
187
|
+
const stdout = Buffer.concat(stdoutChunks).toString('utf8');
|
|
188
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf8');
|
|
189
|
+
|
|
190
|
+
resolve({
|
|
191
|
+
appRoot: app.appRoot,
|
|
192
|
+
exitCode,
|
|
193
|
+
json: parseJsonOutput(stdout),
|
|
194
|
+
ok: exitCode === 0,
|
|
195
|
+
relativeAppRoot: app.relativeAppRoot,
|
|
196
|
+
stderr,
|
|
197
|
+
stdout,
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const printAggregateJson = ({
|
|
203
|
+
argv,
|
|
204
|
+
cwd,
|
|
205
|
+
results,
|
|
206
|
+
}: {
|
|
207
|
+
argv: string[];
|
|
208
|
+
cwd: string;
|
|
209
|
+
results: TMonorepoCommandResult[];
|
|
210
|
+
}) => {
|
|
211
|
+
const passed = results.filter((result) => result.ok).length;
|
|
212
|
+
|
|
213
|
+
printJson({
|
|
214
|
+
ok: passed === results.length,
|
|
215
|
+
format: 'proteum-agent-v1',
|
|
216
|
+
summary: `Monorepo ${getCommandString(argv)}: ${passed}/${results.length} apps passed.`,
|
|
217
|
+
data: {
|
|
218
|
+
cwd,
|
|
219
|
+
command: getCommandString(argv),
|
|
220
|
+
apps: results,
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const printAggregateHuman = ({
|
|
226
|
+
argv,
|
|
227
|
+
results,
|
|
228
|
+
}: {
|
|
229
|
+
argv: string[];
|
|
230
|
+
results: TMonorepoCommandResult[];
|
|
231
|
+
}) => {
|
|
232
|
+
const lines = [
|
|
233
|
+
`Proteum monorepo command: ${getCommandString(argv)}`,
|
|
234
|
+
`Apps: ${results.length}`,
|
|
235
|
+
'',
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
for (const result of results) {
|
|
239
|
+
lines.push(`## ${result.relativeAppRoot || result.appRoot} (exit ${result.exitCode ?? 'unknown'})`);
|
|
240
|
+
if (result.stdout.trim()) lines.push(result.stdout.trimEnd());
|
|
241
|
+
if (result.stderr.trim()) lines.push(result.stderr.trimEnd());
|
|
242
|
+
lines.push('');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
process.stdout.write(lines.join('\n'));
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const runGenericFanout = async ({
|
|
249
|
+
apps,
|
|
250
|
+
argv,
|
|
251
|
+
cwd,
|
|
252
|
+
}: {
|
|
253
|
+
apps: TProteumAppRootSummary[];
|
|
254
|
+
argv: string[];
|
|
255
|
+
cwd: string;
|
|
256
|
+
}) => {
|
|
257
|
+
const results: TMonorepoCommandResult[] = [];
|
|
258
|
+
|
|
259
|
+
for (const app of apps) {
|
|
260
|
+
results.push(await runChildCommand({ app, argv }));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (shouldPrintJsonAggregate(argv)) printAggregateJson({ argv, cwd, results });
|
|
264
|
+
else printAggregateHuman({ argv, results });
|
|
265
|
+
|
|
266
|
+
if (results.some((result) => !result.ok)) process.exitCode = 1;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const parsePort = (value: string | undefined) => {
|
|
270
|
+
if (!value) return undefined;
|
|
271
|
+
|
|
272
|
+
const port = Number(value);
|
|
273
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
274
|
+
throw new Error(`Invalid --port value "${value}". Expected an integer between 1 and 65535.`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return port;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const slugifyAppRoot = (relativeAppRoot: string | undefined, appRoot: string) =>
|
|
281
|
+
(relativeAppRoot || path.basename(appRoot) || 'app').replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'app';
|
|
282
|
+
|
|
283
|
+
const allocateDevPort = async ({
|
|
284
|
+
app,
|
|
285
|
+
explicitStartPort,
|
|
286
|
+
index,
|
|
287
|
+
usedPorts,
|
|
288
|
+
}: {
|
|
289
|
+
app: TProteumAppRootSummary;
|
|
290
|
+
explicitStartPort?: number;
|
|
291
|
+
index: number;
|
|
292
|
+
usedPorts: Set<number>;
|
|
293
|
+
}) => {
|
|
294
|
+
let port = explicitStartPort ? explicitStartPort + index * 2 : app.manifest?.routerPort || 3000 + index * 2;
|
|
295
|
+
|
|
296
|
+
for (let attempts = 0; attempts < 80; attempts += 1) {
|
|
297
|
+
if (usedPorts.has(port) || usedPorts.has(port + 1)) {
|
|
298
|
+
port += 2;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const inspection = await inspectDevPort({ appRoot: app.appRoot, port });
|
|
303
|
+
if (inspection.canStartOnConfiguredPort) {
|
|
304
|
+
usedPorts.add(port);
|
|
305
|
+
usedPorts.add(port + 1);
|
|
306
|
+
return port;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
port = inspection.recommendedPort && !usedPorts.has(inspection.recommendedPort)
|
|
310
|
+
? inspection.recommendedPort
|
|
311
|
+
: port + 2;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
throw new Error(`Could not find a free router/HMR port pair for ${app.relativeAppRoot || app.appRoot}.`);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const prefixOutput = ({
|
|
318
|
+
label,
|
|
319
|
+
stream,
|
|
320
|
+
target,
|
|
321
|
+
}: {
|
|
322
|
+
label: string;
|
|
323
|
+
stream: NodeJS.ReadableStream;
|
|
324
|
+
target: NodeJS.WritableStream;
|
|
325
|
+
}) => {
|
|
326
|
+
let pending = '';
|
|
327
|
+
|
|
328
|
+
stream.on('data', (chunk) => {
|
|
329
|
+
pending += Buffer.from(chunk).toString('utf8');
|
|
330
|
+
const lines = pending.split(/\r?\n/);
|
|
331
|
+
pending = lines.pop() || '';
|
|
332
|
+
|
|
333
|
+
for (const line of lines) target.write(`[${label}] ${line}\n`);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
stream.on('end', () => {
|
|
337
|
+
if (pending) target.write(`[${label}] ${pending}\n`);
|
|
338
|
+
});
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const runDevSupervisor = async ({
|
|
342
|
+
apps,
|
|
343
|
+
argv,
|
|
344
|
+
}: {
|
|
345
|
+
apps: TProteumAppRootSummary[];
|
|
346
|
+
argv: string[];
|
|
347
|
+
}) => {
|
|
348
|
+
const explicitPort = parsePort(getOptionValue(argv, '--port'));
|
|
349
|
+
const childArgvBase = removeOptionsWithValues(argv, new Set(['--cwd', '--port', '--session-file']));
|
|
350
|
+
const usedPorts = new Set<number>();
|
|
351
|
+
const children: Array<{ app: TProteumAppRootSummary; child: ReturnType<typeof spawn> }> = [];
|
|
352
|
+
|
|
353
|
+
for (let index = 0; index < apps.length; index += 1) {
|
|
354
|
+
const app = apps[index];
|
|
355
|
+
const port = await allocateDevPort({ app, explicitStartPort: explicitPort, index, usedPorts });
|
|
356
|
+
const slug = slugifyAppRoot(app.relativeAppRoot, app.appRoot);
|
|
357
|
+
const sessionFile = `var/run/proteum/dev/monorepo/${slug}.json`;
|
|
358
|
+
const childArgv = [...childArgvBase, '--port', String(port), '--session-file', sessionFile];
|
|
359
|
+
const child = spawn(process.execPath, [getCliBin(), ...childArgv], {
|
|
360
|
+
cwd: app.appRoot,
|
|
361
|
+
env: {
|
|
362
|
+
...process.env,
|
|
363
|
+
[monorepoFanoutChildEnv]: '1',
|
|
364
|
+
},
|
|
365
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
366
|
+
});
|
|
367
|
+
const label = app.relativeAppRoot || app.appRoot;
|
|
368
|
+
|
|
369
|
+
prefixOutput({ label, stream: child.stdout, target: process.stdout });
|
|
370
|
+
prefixOutput({ label, stream: child.stderr, target: process.stderr });
|
|
371
|
+
children.push({ app, child });
|
|
372
|
+
process.stdout.write(`[${label}] starting on port ${port} with session ${sessionFile}\n`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const stopChildren = () => {
|
|
376
|
+
for (const { child } of children) {
|
|
377
|
+
if (child.exitCode === null && child.signalCode === null) child.kill('SIGTERM');
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
process.once('SIGINT', () => {
|
|
382
|
+
stopChildren();
|
|
383
|
+
});
|
|
384
|
+
process.once('SIGTERM', () => {
|
|
385
|
+
stopChildren();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const results = await Promise.all(
|
|
389
|
+
children.map(
|
|
390
|
+
({ app, child }) =>
|
|
391
|
+
new Promise<TMonorepoCommandResult>((resolve) => {
|
|
392
|
+
child.on('close', (exitCode) => {
|
|
393
|
+
resolve({
|
|
394
|
+
appRoot: app.appRoot,
|
|
395
|
+
exitCode,
|
|
396
|
+
ok: exitCode === 0,
|
|
397
|
+
relativeAppRoot: app.relativeAppRoot,
|
|
398
|
+
stderr: '',
|
|
399
|
+
stdout: '',
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
}),
|
|
403
|
+
),
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
if (results.some((result) => !result.ok)) process.exitCode = 1;
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const parseWorktreeArgs = (argv: string[]): TParsedWorktreeArgs => {
|
|
410
|
+
const [, action = '', ...rest] = argv;
|
|
411
|
+
const positionals: string[] = [];
|
|
412
|
+
const parsed: TParsedWorktreeArgs = {
|
|
413
|
+
action,
|
|
414
|
+
json: hasFlag(argv, '--json'),
|
|
415
|
+
refresh: hasFlag(argv, '--refresh'),
|
|
416
|
+
skipDeps: hasFlag(argv, '--skip-deps'),
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
420
|
+
const arg = rest[index];
|
|
421
|
+
|
|
422
|
+
if (arg === '--source') {
|
|
423
|
+
parsed.source = rest[index + 1];
|
|
424
|
+
index += 1;
|
|
425
|
+
} else if (arg.startsWith('--source=')) {
|
|
426
|
+
parsed.source = arg.slice('--source='.length);
|
|
427
|
+
} else if (arg === '--branch') {
|
|
428
|
+
parsed.branch = rest[index + 1];
|
|
429
|
+
index += 1;
|
|
430
|
+
} else if (arg.startsWith('--branch=')) {
|
|
431
|
+
parsed.branch = arg.slice('--branch='.length);
|
|
432
|
+
} else if (arg === '--base') {
|
|
433
|
+
parsed.base = rest[index + 1];
|
|
434
|
+
index += 1;
|
|
435
|
+
} else if (arg.startsWith('--base=')) {
|
|
436
|
+
parsed.base = arg.slice('--base='.length);
|
|
437
|
+
} else if (arg === '--reason') {
|
|
438
|
+
parsed.reason = rest[index + 1];
|
|
439
|
+
index += 1;
|
|
440
|
+
} else if (arg.startsWith('--reason=')) {
|
|
441
|
+
parsed.reason = arg.slice('--reason='.length);
|
|
442
|
+
} else if (!arg.startsWith('-')) {
|
|
443
|
+
positionals.push(arg);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
parsed.target = positionals[0];
|
|
448
|
+
return parsed;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const printWorktreeResult = ({
|
|
452
|
+
data,
|
|
453
|
+
json,
|
|
454
|
+
ok,
|
|
455
|
+
summary,
|
|
456
|
+
}: {
|
|
457
|
+
data: object;
|
|
458
|
+
json: boolean;
|
|
459
|
+
ok: boolean;
|
|
460
|
+
summary: string;
|
|
461
|
+
}) => {
|
|
462
|
+
if (json) {
|
|
463
|
+
printJson({
|
|
464
|
+
ok,
|
|
465
|
+
format: 'proteum-agent-v1',
|
|
466
|
+
summary,
|
|
467
|
+
data,
|
|
468
|
+
});
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
printJson({
|
|
473
|
+
ok,
|
|
474
|
+
format: 'proteum-agent-v1',
|
|
475
|
+
summary,
|
|
476
|
+
data,
|
|
477
|
+
});
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const runWorktreeMonorepoCommand = async ({
|
|
481
|
+
apps,
|
|
482
|
+
argv,
|
|
483
|
+
cwd,
|
|
484
|
+
}: {
|
|
485
|
+
apps: TProteumAppRootSummary[];
|
|
486
|
+
argv: string[];
|
|
487
|
+
cwd: string;
|
|
488
|
+
}) => {
|
|
489
|
+
const parsed = parseWorktreeArgs(argv);
|
|
490
|
+
const common = {
|
|
491
|
+
appRoots: apps.map((app) => app.appRoot),
|
|
492
|
+
coreRoot: path.resolve(__dirname, '..', '..'),
|
|
493
|
+
json: parsed.json,
|
|
494
|
+
proteumVersion: require('../../package.json').version as string,
|
|
495
|
+
reason: parsed.reason,
|
|
496
|
+
refresh: parsed.refresh,
|
|
497
|
+
skipDeps: parsed.skipDeps,
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
if (parsed.action === 'init') {
|
|
501
|
+
const result = await runMonorepoWorktreeBootstrapInit({
|
|
502
|
+
...common,
|
|
503
|
+
monorepoRoot: cwd,
|
|
504
|
+
source: parsed.source ? path.resolve(cwd, parsed.source) : undefined,
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
printWorktreeResult({
|
|
508
|
+
data: {
|
|
509
|
+
...result,
|
|
510
|
+
apps: result.apps.map((app) => ({
|
|
511
|
+
...app,
|
|
512
|
+
status: app.status ? compactWorktreeBootstrapStatus(app.status) : undefined,
|
|
513
|
+
})),
|
|
514
|
+
},
|
|
515
|
+
json: parsed.json,
|
|
516
|
+
ok: result.ok,
|
|
517
|
+
summary: `Proteum monorepo worktree bootstrap completed for ${result.apps.length} app${result.apps.length === 1 ? '' : 's'}.`,
|
|
518
|
+
});
|
|
519
|
+
if (!result.ok) process.exitCode = 1;
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (parsed.action === 'create') {
|
|
524
|
+
if (!parsed.target) throw new Error('worktree create requires <target-repo-root>.');
|
|
525
|
+
|
|
526
|
+
const result = await runMonorepoWorktreeBootstrapCreate({
|
|
527
|
+
base: parsed.base,
|
|
528
|
+
branch: parsed.branch || '',
|
|
529
|
+
coreRoot: common.coreRoot,
|
|
530
|
+
json: common.json,
|
|
531
|
+
monorepoRoot: cwd,
|
|
532
|
+
proteumVersion: common.proteumVersion,
|
|
533
|
+
reason: common.reason,
|
|
534
|
+
refresh: common.refresh,
|
|
535
|
+
skipDeps: common.skipDeps,
|
|
536
|
+
source: parsed.source ? path.resolve(cwd, parsed.source) : cwd,
|
|
537
|
+
targetRepoRoot: path.resolve(cwd, parsed.target),
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
printWorktreeResult({
|
|
541
|
+
data: {
|
|
542
|
+
...result,
|
|
543
|
+
worktreeBootstrap: {
|
|
544
|
+
...result.worktreeBootstrap,
|
|
545
|
+
apps: result.worktreeBootstrap.apps.map((app) => ({
|
|
546
|
+
...app,
|
|
547
|
+
status: app.status ? compactWorktreeBootstrapStatus(app.status) : undefined,
|
|
548
|
+
})),
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
json: parsed.json,
|
|
552
|
+
ok: result.worktreeBootstrap.ok,
|
|
553
|
+
summary: `Created Proteum monorepo worktree at ${result.targetRepoRoot}.`,
|
|
554
|
+
});
|
|
555
|
+
if (!result.worktreeBootstrap.ok) process.exitCode = 1;
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
throw new Error('Usage: `proteum worktree init` or `proteum worktree create <target-repo-root>`.');
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
const isGenericFanoutCommand = (argv: string[]) => {
|
|
563
|
+
const commandName = argv[0] || '';
|
|
564
|
+
const commandPositionals = getPositionals(argv.slice(1));
|
|
565
|
+
const action = commandPositionals[0] || '';
|
|
566
|
+
|
|
567
|
+
if (commandName === 'build' && hasFlag(argv, '--analyze-serve')) return true;
|
|
568
|
+
if (commandName === 'dev') return action === 'list' || action === 'stop';
|
|
569
|
+
if (commandName === 'runtime') return action === '' || action === 'status';
|
|
570
|
+
if (commandName === 'verify') return action === 'owner' || action === 'request' || action === 'browser';
|
|
571
|
+
return genericFanoutCommands.has(commandName);
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
export const maybeRunMonorepoCommand = async (argv: string[]) => {
|
|
575
|
+
if (isMonorepoFanoutChild()) return false;
|
|
576
|
+
if (argv.length === 0) return false;
|
|
577
|
+
|
|
578
|
+
const commandName = argv[0] || '';
|
|
579
|
+
if ((commandName === 'dev' || commandName === 'e2e') && getOptionValue(argv, '--cwd')) return false;
|
|
580
|
+
|
|
581
|
+
const context = resolveProteumAppRootContext(process.cwd());
|
|
582
|
+
const apps = context.appCandidates;
|
|
583
|
+
if (!context.isWrapper || apps.length === 0) return false;
|
|
584
|
+
|
|
585
|
+
if (commandName === 'build' && hasFlag(argv, '--analyze-serve')) {
|
|
586
|
+
printJson({
|
|
587
|
+
ok: false,
|
|
588
|
+
format: 'proteum-agent-v1',
|
|
589
|
+
summary: '`proteum build --analyze-serve` cannot run as monorepo fan-out because analyzer servers stay open.',
|
|
590
|
+
data: { cwd: context.cwd, appCandidates: apps },
|
|
591
|
+
nextActions: apps.map((app) => ({
|
|
592
|
+
label: `Analyze ${app.relativeAppRoot || app.appRoot}`,
|
|
593
|
+
command: `cd ${quoteCommandArgument(app.relativeAppRoot || app.appRoot)} && proteum build --prod --analyze --analyze-serve --analyze-port auto`,
|
|
594
|
+
reason: 'Run one analyzer server from the target app root.',
|
|
595
|
+
})),
|
|
596
|
+
});
|
|
597
|
+
process.exitCode = 1;
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (commandName === 'configure' && getPositionals(argv.slice(1))[0] === 'agents') {
|
|
602
|
+
const { runConfigureAgentsMonorepoWizard } = await import('../commands/configure');
|
|
603
|
+
|
|
604
|
+
await runConfigureAgentsMonorepoWizard({
|
|
605
|
+
appRoots: apps.map((app) => app.appRoot),
|
|
606
|
+
monorepoRoot: context.cwd,
|
|
607
|
+
});
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (commandName === 'worktree') {
|
|
612
|
+
await runWorktreeMonorepoCommand({ apps, argv, cwd: context.cwd });
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (commandName === 'dev' && getPositionals(argv.slice(1)).length === 0) {
|
|
617
|
+
await runDevSupervisor({ apps, argv });
|
|
618
|
+
return true;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (!isGenericFanoutCommand(argv)) return false;
|
|
622
|
+
|
|
623
|
+
await runGenericFanout({ apps, argv, cwd: context.cwd });
|
|
624
|
+
return true;
|
|
625
|
+
};
|