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
|
@@ -8,7 +8,11 @@ process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
|
|
|
8
8
|
process.env.TS_NODE_TRANSPILE_ONLY = '1';
|
|
9
9
|
require('ts-node/register/transpile-only');
|
|
10
10
|
|
|
11
|
-
const {
|
|
11
|
+
const {
|
|
12
|
+
configureMonorepoProjectAgentInstructions,
|
|
13
|
+
configureProjectAgentInstructions,
|
|
14
|
+
resolveProjectAgentMonorepoRoot,
|
|
15
|
+
} = require('../cli/utils/agents.ts');
|
|
12
16
|
|
|
13
17
|
const writeFile = (filepath, content) => {
|
|
14
18
|
fs.mkdirSync(path.dirname(filepath), { recursive: true });
|
|
@@ -34,8 +38,9 @@ const pathEntryExists = (filepath) => {
|
|
|
34
38
|
|
|
35
39
|
const makeTempRoot = () => fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-agents-'));
|
|
36
40
|
|
|
37
|
-
const
|
|
38
|
-
|
|
41
|
+
const normalizePath = (value) => value.replace(/\\/g, '/');
|
|
42
|
+
|
|
43
|
+
const createCoreFixture = (root = makeTempRoot()) => {
|
|
39
44
|
const agentsRoot = path.join(root, 'agents', 'project');
|
|
40
45
|
|
|
41
46
|
writeFile(path.join(agentsRoot, 'AGENTS.md'), '# Root Contract\n\n- Root rule\n');
|
|
@@ -57,6 +62,20 @@ const createCoreFixture = () => {
|
|
|
57
62
|
return root;
|
|
58
63
|
};
|
|
59
64
|
|
|
65
|
+
const expectedSourceMapPath = ({ coreRoot, instructionRoot, projectPath }) =>
|
|
66
|
+
normalizePath(path.relative(instructionRoot, path.join(coreRoot, 'agents', 'project', projectPath)));
|
|
67
|
+
|
|
68
|
+
const assertSourceMapPath = ({ content, coreRoot, instructionRoot, label, projectPath }) => {
|
|
69
|
+
assert.equal(
|
|
70
|
+
content.includes(`- ${label}: ${expectedSourceMapPath({ coreRoot, instructionRoot, projectPath })}`),
|
|
71
|
+
true,
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const assertNoAbsoluteCoreSourceMapPath = ({ content, coreRoot }) => {
|
|
76
|
+
assert.equal(content.includes(normalizePath(path.join(coreRoot, 'agents', 'project'))), false);
|
|
77
|
+
};
|
|
78
|
+
|
|
60
79
|
const createAppFixture = () => {
|
|
61
80
|
const appRoot = makeTempRoot();
|
|
62
81
|
|
|
@@ -169,6 +188,53 @@ test('standalone configure creates tracked instruction files with routing contra
|
|
|
169
188
|
assert.doesNotMatch(gitignoreContent, /^\/DOCUMENTATION\.md$/m);
|
|
170
189
|
});
|
|
171
190
|
|
|
191
|
+
test('standalone configure writes install-relative source map fallbacks', () => {
|
|
192
|
+
const appRoot = createAppFixture();
|
|
193
|
+
const coreRoot = createCoreFixture(path.join(appRoot, 'node_modules', 'proteum'));
|
|
194
|
+
const result = configureProjectAgentInstructions({ appRoot, coreRoot });
|
|
195
|
+
const agentsContent = fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8');
|
|
196
|
+
|
|
197
|
+
assert.equal(result.blocked.length, 0);
|
|
198
|
+
assertSourceMapPath({
|
|
199
|
+
content: agentsContent,
|
|
200
|
+
coreRoot,
|
|
201
|
+
instructionRoot: appRoot,
|
|
202
|
+
label: 'Root contract fallback',
|
|
203
|
+
projectPath: 'AGENTS.md',
|
|
204
|
+
});
|
|
205
|
+
assertSourceMapPath({
|
|
206
|
+
content: agentsContent,
|
|
207
|
+
coreRoot,
|
|
208
|
+
instructionRoot: appRoot,
|
|
209
|
+
label: 'Documentation fallback',
|
|
210
|
+
projectPath: 'DOCUMENTATION.md',
|
|
211
|
+
});
|
|
212
|
+
assertNoAbsoluteCoreSourceMapPath({ content: agentsContent, coreRoot });
|
|
213
|
+
assert.match(agentsContent, /Root contract fallback: node_modules\/proteum\/agents\/project\/AGENTS\.md/);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('standalone configure source map prefers project install over active external core', () => {
|
|
217
|
+
const activeCoreRoot = createCoreFixture();
|
|
218
|
+
const appRoot = createAppFixture();
|
|
219
|
+
createCoreFixture(path.join(appRoot, 'node_modules', 'proteum'));
|
|
220
|
+
|
|
221
|
+
configureProjectAgentInstructions({ appRoot, coreRoot: activeCoreRoot });
|
|
222
|
+
|
|
223
|
+
const agentsContent = fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8');
|
|
224
|
+
assert.match(agentsContent, /Root contract fallback: node_modules\/proteum\/agents\/project\/AGENTS\.md/);
|
|
225
|
+
assert.equal(
|
|
226
|
+
agentsContent.includes(
|
|
227
|
+
`Root contract fallback: ${expectedSourceMapPath({
|
|
228
|
+
coreRoot: activeCoreRoot,
|
|
229
|
+
instructionRoot: appRoot,
|
|
230
|
+
projectPath: 'AGENTS.md',
|
|
231
|
+
})}`,
|
|
232
|
+
),
|
|
233
|
+
false,
|
|
234
|
+
);
|
|
235
|
+
assertNoAbsoluteCoreSourceMapPath({ content: agentsContent, coreRoot: activeCoreRoot });
|
|
236
|
+
});
|
|
237
|
+
|
|
172
238
|
test('configure preserves project content outside the managed section', () => {
|
|
173
239
|
const coreRoot = createCoreFixture();
|
|
174
240
|
const appRoot = createAppFixture();
|
|
@@ -271,7 +337,7 @@ test('monorepo configure writes root and app instruction files', () => {
|
|
|
271
337
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /## Agent Routing Contract/);
|
|
272
338
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /## Known Proteum Apps/);
|
|
273
339
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /apps\/product/);
|
|
274
|
-
assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /
|
|
340
|
+
assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /Eligible Proteum commands run across the apps below/);
|
|
275
341
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /Worktree Preflight/);
|
|
276
342
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'CODING_STYLE.md'), 'utf8'), /## Source: CODING_STYLE\.md/);
|
|
277
343
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'DOCUMENTATION.md'), 'utf8'), /## Source: DOCUMENTATION\.md/);
|
|
@@ -291,7 +357,23 @@ test('monorepo configure writes root and app instruction files', () => {
|
|
|
291
357
|
const appAgentsContent = fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8');
|
|
292
358
|
assert.match(appAgentsContent, /## Agent Routing Contract/);
|
|
293
359
|
assert.doesNotMatch(appAgentsContent, /## Known Proteum Apps/);
|
|
294
|
-
assert.doesNotMatch(appAgentsContent, /
|
|
360
|
+
assert.doesNotMatch(appAgentsContent, /Eligible Proteum commands run across the apps below/);
|
|
361
|
+
assertSourceMapPath({
|
|
362
|
+
content: fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'),
|
|
363
|
+
coreRoot,
|
|
364
|
+
instructionRoot: monorepoRoot,
|
|
365
|
+
label: 'Root contract fallback',
|
|
366
|
+
projectPath: 'AGENTS.md',
|
|
367
|
+
});
|
|
368
|
+
assertSourceMapPath({
|
|
369
|
+
content: appAgentsContent,
|
|
370
|
+
coreRoot,
|
|
371
|
+
instructionRoot: appRoot,
|
|
372
|
+
label: 'Root contract fallback',
|
|
373
|
+
projectPath: 'AGENTS.md',
|
|
374
|
+
});
|
|
375
|
+
assertNoAbsoluteCoreSourceMapPath({ content: fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), coreRoot });
|
|
376
|
+
assertNoAbsoluteCoreSourceMapPath({ content: appAgentsContent, coreRoot });
|
|
295
377
|
assert.match(fs.readFileSync(path.join(appRoot, 'client', 'AGENTS.md'), 'utf8'), /## Source: client\/AGENTS\.md/);
|
|
296
378
|
assertClaudeSymlink(appRoot);
|
|
297
379
|
assertClaudeSymlink(appRoot, 'client');
|
|
@@ -302,6 +384,84 @@ test('monorepo configure writes root and app instruction files', () => {
|
|
|
302
384
|
assert.equal(result.removed.some((entry) => entry.endsWith('/apps/product/CODING_STYLE.md')), true);
|
|
303
385
|
});
|
|
304
386
|
|
|
387
|
+
test('monorepo configure source map uses workspace install from root and nested apps', () => {
|
|
388
|
+
const activeCoreRoot = createCoreFixture();
|
|
389
|
+
const monorepoRoot = makeTempRoot();
|
|
390
|
+
const appRoot = path.join(monorepoRoot, 'apps', 'product');
|
|
391
|
+
|
|
392
|
+
createCoreFixture(path.join(monorepoRoot, 'node_modules', 'proteum'));
|
|
393
|
+
fs.mkdirSync(path.join(monorepoRoot, '.git'));
|
|
394
|
+
fs.mkdirSync(path.join(appRoot, 'client'), { recursive: true });
|
|
395
|
+
writeFile(path.join(appRoot, 'package.json'), '{"name":"product"}\n');
|
|
396
|
+
writeFile(path.join(appRoot, 'identity.config.ts'), 'export default {};\n');
|
|
397
|
+
writeFile(path.join(appRoot, 'proteum.config.ts'), 'export default {};\n');
|
|
398
|
+
|
|
399
|
+
configureProjectAgentInstructions({ appRoot, coreRoot: activeCoreRoot, monorepoRoot });
|
|
400
|
+
|
|
401
|
+
const rootAgentsContent = fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8');
|
|
402
|
+
const appAgentsContent = fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8');
|
|
403
|
+
assert.match(rootAgentsContent, /Root contract fallback: node_modules\/proteum\/agents\/project\/AGENTS\.md/);
|
|
404
|
+
assert.match(appAgentsContent, /Root contract fallback: \.\.\/\.\.\/node_modules\/proteum\/agents\/project\/AGENTS\.md/);
|
|
405
|
+
assertNoAbsoluteCoreSourceMapPath({ content: rootAgentsContent, coreRoot: activeCoreRoot });
|
|
406
|
+
assertNoAbsoluteCoreSourceMapPath({ content: appAgentsContent, coreRoot: activeCoreRoot });
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test('monorepo-wide configure writes shared root once and all app instruction files', () => {
|
|
410
|
+
const coreRoot = createCoreFixture();
|
|
411
|
+
const monorepoRoot = makeTempRoot();
|
|
412
|
+
const productRoot = path.join(monorepoRoot, 'apps', 'product');
|
|
413
|
+
const websiteRoot = path.join(monorepoRoot, 'apps', 'website');
|
|
414
|
+
|
|
415
|
+
for (const appRoot of [productRoot, websiteRoot]) {
|
|
416
|
+
fs.mkdirSync(path.join(appRoot, 'client'), { recursive: true });
|
|
417
|
+
fs.mkdirSync(path.join(appRoot, 'server'), { recursive: true });
|
|
418
|
+
writeFile(path.join(appRoot, 'package.json'), '{"name":"fixture"}\n');
|
|
419
|
+
writeFile(path.join(appRoot, 'identity.config.ts'), 'export default {};\n');
|
|
420
|
+
writeFile(path.join(appRoot, 'proteum.config.ts'), 'export default {};\n');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const result = configureMonorepoProjectAgentInstructions({
|
|
424
|
+
appRoots: [websiteRoot, productRoot],
|
|
425
|
+
coreRoot,
|
|
426
|
+
monorepoRoot,
|
|
427
|
+
});
|
|
428
|
+
const rootAgentsContent = fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8');
|
|
429
|
+
|
|
430
|
+
assert.equal(result.mode, 'monorepo');
|
|
431
|
+
assert.deepEqual(result.appRoots, [productRoot, websiteRoot]);
|
|
432
|
+
assert.match(rootAgentsContent, /apps\/product/);
|
|
433
|
+
assert.match(rootAgentsContent, /apps\/website/);
|
|
434
|
+
assert.doesNotMatch(rootAgentsContent, /current configured app/);
|
|
435
|
+
assert.match(fs.readFileSync(path.join(productRoot, 'AGENTS.md'), 'utf8'), /## Agent Routing Contract/);
|
|
436
|
+
assert.match(fs.readFileSync(path.join(websiteRoot, 'AGENTS.md'), 'utf8'), /## Agent Routing Contract/);
|
|
437
|
+
assert.equal(fs.existsSync(path.join(productRoot, 'CODING_STYLE.md')), false);
|
|
438
|
+
assert.equal(fs.existsSync(path.join(websiteRoot, 'CODING_STYLE.md')), false);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test('monorepo-wide configure dedupes app roots and blocked paths', () => {
|
|
442
|
+
const coreRoot = createCoreFixture();
|
|
443
|
+
const monorepoRoot = makeTempRoot();
|
|
444
|
+
const productRoot = path.join(monorepoRoot, 'apps', 'product');
|
|
445
|
+
const blockedClaudePath = path.join(productRoot, 'CLAUDE.md');
|
|
446
|
+
|
|
447
|
+
fs.mkdirSync(path.join(productRoot, 'client'), { recursive: true });
|
|
448
|
+
fs.mkdirSync(path.join(productRoot, 'server'), { recursive: true });
|
|
449
|
+
writeFile(path.join(productRoot, 'package.json'), '{"name":"fixture"}\n');
|
|
450
|
+
writeFile(path.join(productRoot, 'identity.config.ts'), 'export default {};\n');
|
|
451
|
+
writeFile(path.join(productRoot, 'proteum.config.ts'), 'export default {};\n');
|
|
452
|
+
writeFile(blockedClaudePath, '# Local Claude Notes\n');
|
|
453
|
+
|
|
454
|
+
const result = configureMonorepoProjectAgentInstructions({
|
|
455
|
+
appRoots: [productRoot, productRoot],
|
|
456
|
+
coreRoot,
|
|
457
|
+
dryRun: true,
|
|
458
|
+
monorepoRoot,
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
assert.deepEqual(result.appRoots, [productRoot]);
|
|
462
|
+
assert.equal(result.blocked.filter((entry) => entry === blockedClaudePath).length, 1);
|
|
463
|
+
});
|
|
464
|
+
|
|
305
465
|
test('monorepo configure preserves local app-root documents', () => {
|
|
306
466
|
const coreRoot = createCoreFixture();
|
|
307
467
|
const monorepoRoot = makeTempRoot();
|
|
@@ -16,8 +16,32 @@ const writeFile = (filepath, content) => {
|
|
|
16
16
|
|
|
17
17
|
const createProteumApp = (appRoot, { routerPort = 3020 } = {}) => {
|
|
18
18
|
writeFile(path.join(appRoot, 'package.json'), '{"name":"fixture"}\n');
|
|
19
|
-
writeFile(
|
|
19
|
+
writeFile(
|
|
20
|
+
path.join(appRoot, 'identity.config.ts'),
|
|
21
|
+
`export default {
|
|
22
|
+
name: 'Product',
|
|
23
|
+
identifier: 'ProductApp',
|
|
24
|
+
description: 'Fixture app',
|
|
25
|
+
author: { name: 'Test', url: 'https://example.com', email: 'test@example.com' },
|
|
26
|
+
language: 'en',
|
|
27
|
+
maincolor: '#000000',
|
|
28
|
+
web: {
|
|
29
|
+
title: 'Product',
|
|
30
|
+
titleSuffix: 'Product',
|
|
31
|
+
fullTitle: 'Product',
|
|
32
|
+
description: 'Fixture app',
|
|
33
|
+
version: '1.0.0',
|
|
34
|
+
metas: {},
|
|
35
|
+
jsonld: {},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
`,
|
|
39
|
+
);
|
|
20
40
|
writeFile(path.join(appRoot, 'proteum.config.ts'), 'export default {};\n');
|
|
41
|
+
writeFile(
|
|
42
|
+
path.join(appRoot, '.env'),
|
|
43
|
+
`ENV_NAME=local\nENV_PROFILE=dev\nPORT=${routerPort}\nURL=http://localhost:${routerPort}\nURL_INTERNAL=http://localhost:${routerPort}\n`,
|
|
44
|
+
);
|
|
21
45
|
fs.mkdirSync(path.join(appRoot, 'client'), { recursive: true });
|
|
22
46
|
fs.mkdirSync(path.join(appRoot, 'server'), { recursive: true });
|
|
23
47
|
writeFile(
|
|
@@ -293,7 +317,7 @@ test('explain help describes compact section summaries', () => {
|
|
|
293
317
|
assert.match(output, /Explicit section flags summarize those sections by default/);
|
|
294
318
|
});
|
|
295
319
|
|
|
296
|
-
test('runtime status from a monorepo wrapper
|
|
320
|
+
test('runtime status from a monorepo wrapper aggregates app runtime status', () => {
|
|
297
321
|
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-cli-wrapper-'));
|
|
298
322
|
createProteumApp(path.join(repoRoot, 'apps', 'product'));
|
|
299
323
|
|
|
@@ -303,18 +327,38 @@ test('runtime status from a monorepo wrapper returns app candidates instead of t
|
|
|
303
327
|
});
|
|
304
328
|
const payload = JSON.parse(result.stdout);
|
|
305
329
|
|
|
306
|
-
assert.equal(result.status,
|
|
307
|
-
assert.equal(payload.ok,
|
|
308
|
-
assert.equal(payload.data.
|
|
309
|
-
assert.
|
|
310
|
-
assert.
|
|
330
|
+
assert.equal(result.status, 0, result.stderr);
|
|
331
|
+
assert.equal(payload.ok, true);
|
|
332
|
+
assert.equal(payload.data.apps.length, 1);
|
|
333
|
+
assert.equal(payload.data.apps[0].relativeAppRoot, 'apps/product');
|
|
334
|
+
assert.equal(payload.data.apps[0].ok, true);
|
|
335
|
+
assert.equal(payload.data.apps[0].json.data.appRoot, fs.realpathSync(path.join(repoRoot, 'apps', 'product')));
|
|
311
336
|
});
|
|
312
337
|
|
|
313
|
-
test('dev from a monorepo wrapper
|
|
338
|
+
test('dev list from a monorepo wrapper aggregates app session lists', () => {
|
|
314
339
|
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-cli-dev-wrapper-'));
|
|
315
340
|
createProteumApp(path.join(repoRoot, 'apps', 'product'));
|
|
316
341
|
|
|
317
|
-
const result = spawnSync(process.execPath, [cliBin, 'dev', 'list'], {
|
|
342
|
+
const result = spawnSync(process.execPath, [cliBin, 'dev', 'list', '--json'], {
|
|
343
|
+
cwd: repoRoot,
|
|
344
|
+
encoding: 'utf8',
|
|
345
|
+
});
|
|
346
|
+
const payload = JSON.parse(result.stdout);
|
|
347
|
+
|
|
348
|
+
assert.equal(result.status, 0, result.stderr);
|
|
349
|
+
assert.equal(payload.ok, true);
|
|
350
|
+
assert.equal(payload.data.apps.length, 1);
|
|
351
|
+
assert.equal(payload.data.apps[0].relativeAppRoot, 'apps/product');
|
|
352
|
+
assert.equal(payload.data.apps[0].json.sessions.length, 0);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('monorepo command fan-out continues after app command failures', () => {
|
|
356
|
+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-cli-dev-wrapper-failure-'));
|
|
357
|
+
createProteumApp(path.join(repoRoot, 'apps', 'product'));
|
|
358
|
+
createProteumApp(path.join(repoRoot, 'apps', 'website'));
|
|
359
|
+
writeFile(path.join(repoRoot, 'apps', 'website', 'identity.config.ts'), 'export default {};\n');
|
|
360
|
+
|
|
361
|
+
const result = spawnSync(process.execPath, [cliBin, 'dev', 'list', '--json'], {
|
|
318
362
|
cwd: repoRoot,
|
|
319
363
|
encoding: 'utf8',
|
|
320
364
|
});
|
|
@@ -322,8 +366,13 @@ test('dev from a monorepo wrapper returns exact app-root start command', () => {
|
|
|
322
366
|
|
|
323
367
|
assert.equal(result.status, 1);
|
|
324
368
|
assert.equal(payload.ok, false);
|
|
325
|
-
assert.
|
|
326
|
-
|
|
369
|
+
assert.deepEqual(
|
|
370
|
+
payload.data.apps.map((app) => app.relativeAppRoot),
|
|
371
|
+
['apps/product', 'apps/website'],
|
|
372
|
+
);
|
|
373
|
+
assert.equal(payload.data.apps[0].ok, true);
|
|
374
|
+
assert.equal(payload.data.apps[1].ok, false);
|
|
375
|
+
assert.match(payload.data.apps[1].stdout, /Invalid author/);
|
|
327
376
|
});
|
|
328
377
|
|
|
329
378
|
test('runtime status manifest guard points to explain manifest', () => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const assert = require('node:assert/strict');
|
|
2
|
+
const { spawnSync } = require('node:child_process');
|
|
2
3
|
const fs = require('node:fs');
|
|
3
4
|
const os = require('node:os');
|
|
4
5
|
const path = require('node:path');
|
|
@@ -11,6 +12,8 @@ require('ts-node/register/transpile-only');
|
|
|
11
12
|
const {
|
|
12
13
|
createWorktreeBootstrapDiagnostics,
|
|
13
14
|
getWorktreeBootstrapStatus,
|
|
15
|
+
runMonorepoWorktreeBootstrapCreate,
|
|
16
|
+
runMonorepoWorktreeBootstrapInit,
|
|
14
17
|
runWorktreeBootstrapInit,
|
|
15
18
|
worktreeBootstrapMarkerRelativePath,
|
|
16
19
|
} = require('../cli/runtime/worktreeBootstrap.ts');
|
|
@@ -20,6 +23,12 @@ const writeFile = (filepath, content) => {
|
|
|
20
23
|
fs.writeFileSync(filepath, content);
|
|
21
24
|
};
|
|
22
25
|
|
|
26
|
+
const runGit = (cwd, args) => {
|
|
27
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
28
|
+
|
|
29
|
+
assert.equal(result.status, 0, `${result.stderr}\n${result.stdout}`);
|
|
30
|
+
};
|
|
31
|
+
|
|
23
32
|
const createCodexAppRoot = () => {
|
|
24
33
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-worktree-bootstrap-'));
|
|
25
34
|
const appRoot = path.join(root, '.codex', 'worktrees', 'fixture-app');
|
|
@@ -38,6 +47,19 @@ const writeBootstrapFixture = (appRoot, { env = true, manifest = true, nodeModul
|
|
|
38
47
|
if (nodeModules) fs.mkdirSync(path.join(appRoot, 'node_modules'), { recursive: true });
|
|
39
48
|
};
|
|
40
49
|
|
|
50
|
+
const writeProteumAppRootFixture = (appRoot, { env = true } = {}) => {
|
|
51
|
+
writeFile(path.join(appRoot, 'package.json'), '{"name":"fixture"}\n');
|
|
52
|
+
writeFile(path.join(appRoot, 'identity.config.ts'), 'export default {};\n');
|
|
53
|
+
writeFile(path.join(appRoot, 'proteum.config.ts'), 'export default {};\n');
|
|
54
|
+
writeFile(path.join(appRoot, 'AGENTS.md'), '# Agents\n');
|
|
55
|
+
if (env) writeFile(path.join(appRoot, '.env'), 'PORT=3020\n');
|
|
56
|
+
writeFile(path.join(appRoot, '.proteum', 'manifest.json'), '{"version":10}\n');
|
|
57
|
+
fs.mkdirSync(path.join(appRoot, 'client'), { recursive: true });
|
|
58
|
+
fs.mkdirSync(path.join(appRoot, 'server'), { recursive: true });
|
|
59
|
+
writeFile(path.join(appRoot, 'client', '.keep'), '');
|
|
60
|
+
writeFile(path.join(appRoot, 'server', '.keep'), '');
|
|
61
|
+
};
|
|
62
|
+
|
|
41
63
|
const noOpRefresh = async () => ({ stdout: '', stderr: '', summary: 'refresh ok' });
|
|
42
64
|
const noOpRuntime = async () => ({ stdout: '', stderr: '', summary: 'runtime ok' });
|
|
43
65
|
const noOpDeps = async () => {};
|
|
@@ -228,6 +250,82 @@ test('worktree bootstrap falls back to source app env for missing workspace root
|
|
|
228
250
|
assert.equal(result.status.blocking, false);
|
|
229
251
|
});
|
|
230
252
|
|
|
253
|
+
test('monorepo worktree bootstrap initializes every app and dedupes shared dependency install', async () => {
|
|
254
|
+
const targetRepoRoot = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-worktree-monorepo-all-')), '.codex', 'worktrees', 'fixture-repo');
|
|
255
|
+
const sourceRepoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-worktree-monorepo-source-'));
|
|
256
|
+
const targetProductRoot = path.join(targetRepoRoot, 'apps', 'product');
|
|
257
|
+
const targetWebsiteRoot = path.join(targetRepoRoot, 'apps', 'website');
|
|
258
|
+
|
|
259
|
+
writeFile(path.join(targetRepoRoot, 'package.json'), '{"workspaces":["apps/*"]}\n');
|
|
260
|
+
writeFile(path.join(targetRepoRoot, 'package-lock.json'), '{"lockfileVersion":3}\n');
|
|
261
|
+
writeProteumAppRootFixture(targetProductRoot, { env: false });
|
|
262
|
+
writeProteumAppRootFixture(targetWebsiteRoot, { env: false });
|
|
263
|
+
|
|
264
|
+
writeFile(path.join(sourceRepoRoot, 'package.json'), '{"workspaces":["apps/*"]}\n');
|
|
265
|
+
writeFile(path.join(sourceRepoRoot, 'package-lock.json'), '{"lockfileVersion":3}\n');
|
|
266
|
+
writeProteumAppRootFixture(path.join(sourceRepoRoot, 'apps', 'product'));
|
|
267
|
+
writeProteumAppRootFixture(path.join(sourceRepoRoot, 'apps', 'website'));
|
|
268
|
+
|
|
269
|
+
const installRoots = [];
|
|
270
|
+
const result = await runMonorepoWorktreeBootstrapInit({
|
|
271
|
+
coreRoot,
|
|
272
|
+
monorepoRoot: targetRepoRoot,
|
|
273
|
+
proteumVersion: 'test',
|
|
274
|
+
runDependencies: async (installRoot) => {
|
|
275
|
+
installRoots.push(installRoot);
|
|
276
|
+
fs.mkdirSync(path.join(installRoot, 'node_modules'), { recursive: true });
|
|
277
|
+
},
|
|
278
|
+
runRefresh: noOpRefresh,
|
|
279
|
+
runRuntimeStatus: noOpRuntime,
|
|
280
|
+
source: sourceRepoRoot,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
assert.equal(result.ok, true);
|
|
284
|
+
assert.equal(result.apps.length, 2);
|
|
285
|
+
assert.deepEqual(installRoots, [fs.realpathSync(targetRepoRoot)]);
|
|
286
|
+
assert.equal(fs.existsSync(path.join(targetProductRoot, worktreeBootstrapMarkerRelativePath)), true);
|
|
287
|
+
assert.equal(fs.existsSync(path.join(targetWebsiteRoot, worktreeBootstrapMarkerRelativePath)), true);
|
|
288
|
+
assert.equal(fs.readFileSync(path.join(targetProductRoot, '.env'), 'utf8'), 'PORT=3020\n');
|
|
289
|
+
assert.equal(fs.readFileSync(path.join(targetWebsiteRoot, '.env'), 'utf8'), 'PORT=3020\n');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('monorepo worktree create adds one git worktree and bootstraps target apps', async () => {
|
|
293
|
+
const sourceRepoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-worktree-create-source-'));
|
|
294
|
+
const targetRepoRoot = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-worktree-create-target-')), 'repo-worktree');
|
|
295
|
+
|
|
296
|
+
writeFile(path.join(sourceRepoRoot, 'package.json'), '{"workspaces":["apps/*"]}\n');
|
|
297
|
+
writeFile(path.join(sourceRepoRoot, 'package-lock.json'), '{"lockfileVersion":3}\n');
|
|
298
|
+
writeProteumAppRootFixture(path.join(sourceRepoRoot, 'apps', 'product'));
|
|
299
|
+
writeProteumAppRootFixture(path.join(sourceRepoRoot, 'apps', 'website'));
|
|
300
|
+
runGit(sourceRepoRoot, ['init']);
|
|
301
|
+
runGit(sourceRepoRoot, ['config', 'user.email', 'test@example.com']);
|
|
302
|
+
runGit(sourceRepoRoot, ['config', 'user.name', 'Test']);
|
|
303
|
+
runGit(sourceRepoRoot, ['add', '.']);
|
|
304
|
+
runGit(sourceRepoRoot, ['commit', '-m', 'init']);
|
|
305
|
+
|
|
306
|
+
const installRoots = [];
|
|
307
|
+
const result = await runMonorepoWorktreeBootstrapCreate({
|
|
308
|
+
branch: 'test/monorepo-create',
|
|
309
|
+
coreRoot,
|
|
310
|
+
monorepoRoot: sourceRepoRoot,
|
|
311
|
+
proteumVersion: 'test',
|
|
312
|
+
runDependencies: async (installRoot) => {
|
|
313
|
+
installRoots.push(installRoot);
|
|
314
|
+
fs.mkdirSync(path.join(installRoot, 'node_modules'), { recursive: true });
|
|
315
|
+
},
|
|
316
|
+
runRefresh: noOpRefresh,
|
|
317
|
+
runRuntimeStatus: noOpRuntime,
|
|
318
|
+
targetRepoRoot,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
assert.equal(result.targetRepoRoot, targetRepoRoot);
|
|
322
|
+
assert.equal(result.worktreeBootstrap.ok, true);
|
|
323
|
+
assert.equal(result.worktreeBootstrap.apps.length, 2);
|
|
324
|
+
assert.deepEqual(installRoots, [fs.realpathSync(targetRepoRoot)]);
|
|
325
|
+
assert.equal(fs.existsSync(path.join(targetRepoRoot, 'apps', 'product', worktreeBootstrapMarkerRelativePath)), true);
|
|
326
|
+
assert.equal(fs.existsSync(path.join(targetRepoRoot, 'apps', 'website', worktreeBootstrapMarkerRelativePath)), true);
|
|
327
|
+
});
|
|
328
|
+
|
|
231
329
|
test('worktree bootstrap detects missing env manifest node_modules and version changes', async () => {
|
|
232
330
|
const appRoot = createCodexAppRoot();
|
|
233
331
|
writeBootstrapFixture(appRoot);
|