proteum 2.5.4 → 2.5.6
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.md +1 -1
- package/README.md +11 -10
- package/agents/project/AGENTS.md +6 -6
- package/agents/project/CODING_STYLE.md +1 -1
- package/agents/project/diagnostics.md +2 -2
- package/agents/project/root/AGENTS.md +6 -6
- package/agents/project/tests/AGENTS.md +1 -1
- package/cli/commands/configure.ts +63 -4
- package/cli/index.ts +24 -18
- package/cli/mcp/router.ts +61 -15
- package/cli/presentation/commands.ts +13 -8
- package/cli/runtime/freshCopyPreflight.ts +767 -0
- package/cli/runtime/monorepoCommands.ts +625 -0
- package/cli/runtime/worktreeBootstrap.ts +163 -0
- package/cli/utils/agents.ts +156 -38
- package/docs/agent-routing.md +3 -3
- package/docs/diagnostics.md +2 -2
- package/docs/mcp.md +7 -6
- package/docs/request-tracing.md +1 -1
- package/package.json +1 -1
- package/tests/agents-utils.test.cjs +63 -3
- package/tests/cli-mcp-command.test.cjs +60 -11
- package/tests/mcp.test.cjs +98 -1
- 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 });
|
|
@@ -271,7 +275,7 @@ test('monorepo configure writes root and app instruction files', () => {
|
|
|
271
275
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /## Agent Routing Contract/);
|
|
272
276
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /## Known Proteum Apps/);
|
|
273
277
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /apps\/product/);
|
|
274
|
-
assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /
|
|
278
|
+
assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /Eligible Proteum commands run across the apps below/);
|
|
275
279
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /Worktree Preflight/);
|
|
276
280
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'CODING_STYLE.md'), 'utf8'), /## Source: CODING_STYLE\.md/);
|
|
277
281
|
assert.match(fs.readFileSync(path.join(monorepoRoot, 'DOCUMENTATION.md'), 'utf8'), /## Source: DOCUMENTATION\.md/);
|
|
@@ -291,7 +295,7 @@ test('monorepo configure writes root and app instruction files', () => {
|
|
|
291
295
|
const appAgentsContent = fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8');
|
|
292
296
|
assert.match(appAgentsContent, /## Agent Routing Contract/);
|
|
293
297
|
assert.doesNotMatch(appAgentsContent, /## Known Proteum Apps/);
|
|
294
|
-
assert.doesNotMatch(appAgentsContent, /
|
|
298
|
+
assert.doesNotMatch(appAgentsContent, /Eligible Proteum commands run across the apps below/);
|
|
295
299
|
assert.match(fs.readFileSync(path.join(appRoot, 'client', 'AGENTS.md'), 'utf8'), /## Source: client\/AGENTS\.md/);
|
|
296
300
|
assertClaudeSymlink(appRoot);
|
|
297
301
|
assertClaudeSymlink(appRoot, 'client');
|
|
@@ -302,6 +306,62 @@ test('monorepo configure writes root and app instruction files', () => {
|
|
|
302
306
|
assert.equal(result.removed.some((entry) => entry.endsWith('/apps/product/CODING_STYLE.md')), true);
|
|
303
307
|
});
|
|
304
308
|
|
|
309
|
+
test('monorepo-wide configure writes shared root once and all app instruction files', () => {
|
|
310
|
+
const coreRoot = createCoreFixture();
|
|
311
|
+
const monorepoRoot = makeTempRoot();
|
|
312
|
+
const productRoot = path.join(monorepoRoot, 'apps', 'product');
|
|
313
|
+
const websiteRoot = path.join(monorepoRoot, 'apps', 'website');
|
|
314
|
+
|
|
315
|
+
for (const appRoot of [productRoot, websiteRoot]) {
|
|
316
|
+
fs.mkdirSync(path.join(appRoot, 'client'), { recursive: true });
|
|
317
|
+
fs.mkdirSync(path.join(appRoot, 'server'), { recursive: true });
|
|
318
|
+
writeFile(path.join(appRoot, 'package.json'), '{"name":"fixture"}\n');
|
|
319
|
+
writeFile(path.join(appRoot, 'identity.config.ts'), 'export default {};\n');
|
|
320
|
+
writeFile(path.join(appRoot, 'proteum.config.ts'), 'export default {};\n');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const result = configureMonorepoProjectAgentInstructions({
|
|
324
|
+
appRoots: [websiteRoot, productRoot],
|
|
325
|
+
coreRoot,
|
|
326
|
+
monorepoRoot,
|
|
327
|
+
});
|
|
328
|
+
const rootAgentsContent = fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8');
|
|
329
|
+
|
|
330
|
+
assert.equal(result.mode, 'monorepo');
|
|
331
|
+
assert.deepEqual(result.appRoots, [productRoot, websiteRoot]);
|
|
332
|
+
assert.match(rootAgentsContent, /apps\/product/);
|
|
333
|
+
assert.match(rootAgentsContent, /apps\/website/);
|
|
334
|
+
assert.doesNotMatch(rootAgentsContent, /current configured app/);
|
|
335
|
+
assert.match(fs.readFileSync(path.join(productRoot, 'AGENTS.md'), 'utf8'), /## Agent Routing Contract/);
|
|
336
|
+
assert.match(fs.readFileSync(path.join(websiteRoot, 'AGENTS.md'), 'utf8'), /## Agent Routing Contract/);
|
|
337
|
+
assert.equal(fs.existsSync(path.join(productRoot, 'CODING_STYLE.md')), false);
|
|
338
|
+
assert.equal(fs.existsSync(path.join(websiteRoot, 'CODING_STYLE.md')), false);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('monorepo-wide configure dedupes app roots and blocked paths', () => {
|
|
342
|
+
const coreRoot = createCoreFixture();
|
|
343
|
+
const monorepoRoot = makeTempRoot();
|
|
344
|
+
const productRoot = path.join(monorepoRoot, 'apps', 'product');
|
|
345
|
+
const blockedClaudePath = path.join(productRoot, 'CLAUDE.md');
|
|
346
|
+
|
|
347
|
+
fs.mkdirSync(path.join(productRoot, 'client'), { recursive: true });
|
|
348
|
+
fs.mkdirSync(path.join(productRoot, 'server'), { recursive: true });
|
|
349
|
+
writeFile(path.join(productRoot, 'package.json'), '{"name":"fixture"}\n');
|
|
350
|
+
writeFile(path.join(productRoot, 'identity.config.ts'), 'export default {};\n');
|
|
351
|
+
writeFile(path.join(productRoot, 'proteum.config.ts'), 'export default {};\n');
|
|
352
|
+
writeFile(blockedClaudePath, '# Local Claude Notes\n');
|
|
353
|
+
|
|
354
|
+
const result = configureMonorepoProjectAgentInstructions({
|
|
355
|
+
appRoots: [productRoot, productRoot],
|
|
356
|
+
coreRoot,
|
|
357
|
+
dryRun: true,
|
|
358
|
+
monorepoRoot,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
assert.deepEqual(result.appRoots, [productRoot]);
|
|
362
|
+
assert.equal(result.blocked.filter((entry) => entry === blockedClaudePath).length, 1);
|
|
363
|
+
});
|
|
364
|
+
|
|
305
365
|
test('monorepo configure preserves local app-root documents', () => {
|
|
306
366
|
const coreRoot = createCoreFixture();
|
|
307
367
|
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', () => {
|
package/tests/mcp.test.cjs
CHANGED
|
@@ -99,8 +99,53 @@ const createManifest = (appRoot, overrides = {}) => ({
|
|
|
99
99
|
|
|
100
100
|
const writeProteumAppFixture = (appRoot, manifestOverrides = {}) => {
|
|
101
101
|
writeFile(path.join(appRoot, 'package.json'), '{"name":"fixture"}\n');
|
|
102
|
+
writeFile(path.join(appRoot, 'package-lock.json'), '{"lockfileVersion":3}\n');
|
|
103
|
+
writeFile(
|
|
104
|
+
path.join(appRoot, '.env'),
|
|
105
|
+
[
|
|
106
|
+
'ENV_NAME=local',
|
|
107
|
+
'ENV_PROFILE=dev',
|
|
108
|
+
`PORT=${manifestOverrides.routerPort || 3104}`,
|
|
109
|
+
`URL=http://localhost:${manifestOverrides.routerPort || 3104}`,
|
|
110
|
+
`URL_INTERNAL=http://localhost:${manifestOverrides.routerPort || 3104}`,
|
|
111
|
+
'',
|
|
112
|
+
].join('\n'),
|
|
113
|
+
);
|
|
114
|
+
fs.mkdirSync(path.join(appRoot, 'node_modules'), { recursive: true });
|
|
115
|
+
writeFile(path.join(appRoot, 'identity.config.ts'), 'export default {};\n');
|
|
116
|
+
writeFile(path.join(appRoot, 'proteum.config.ts'), 'export default {};\n');
|
|
117
|
+
writeFile(path.join(appRoot, 'client', 'AGENTS.md'), '# Client\n');
|
|
118
|
+
writeFile(path.join(appRoot, 'client', 'pages', 'AGENTS.md'), '# Pages\n');
|
|
119
|
+
writeFile(path.join(appRoot, 'server', 'AGENTS.md'), '# Server\n');
|
|
120
|
+
writeFile(path.join(appRoot, 'server', 'routes', 'AGENTS.md'), '# Routes\n');
|
|
121
|
+
writeFile(path.join(appRoot, 'AGENTS.md'), '# App\n');
|
|
122
|
+
writeFile(path.join(appRoot, 'diagnostics.md'), '# Diagnostics\n');
|
|
123
|
+
writeFile(path.join(appRoot, '.proteum', 'manifest.json'), JSON.stringify(createManifest(appRoot, manifestOverrides), null, 2));
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const writeFreshCopyFixture = (appRoot, manifestOverrides = {}) => {
|
|
127
|
+
writeFile(path.join(appRoot, 'package.json'), '{"name":"fresh-copy"}\n');
|
|
128
|
+
writeFile(path.join(appRoot, 'package-lock.json'), '{"lockfileVersion":3}\n');
|
|
129
|
+
writeFile(
|
|
130
|
+
path.join(appRoot, '.env.example'),
|
|
131
|
+
[
|
|
132
|
+
'ENV_NAME=local',
|
|
133
|
+
'ENV_PROFILE=dev',
|
|
134
|
+
'PORT=3020',
|
|
135
|
+
'URL=http://localhost:3020',
|
|
136
|
+
'URL_INTERNAL=http://localhost:3020',
|
|
137
|
+
'DATABASE_URL=mysql://user:pass@localhost:3306/app',
|
|
138
|
+
'',
|
|
139
|
+
].join('\n'),
|
|
140
|
+
);
|
|
102
141
|
writeFile(path.join(appRoot, 'identity.config.ts'), 'export default {};\n');
|
|
103
142
|
writeFile(path.join(appRoot, 'proteum.config.ts'), 'export default {};\n');
|
|
143
|
+
writeFile(
|
|
144
|
+
path.join(appRoot, 'prisma', 'schema.prisma'),
|
|
145
|
+
['generator client {', ' provider = "prisma-client-js"', ' output = "../var/prisma"', '}', '', 'datasource db {', ' provider = "mysql"', '}'].join(
|
|
146
|
+
'\n',
|
|
147
|
+
),
|
|
148
|
+
);
|
|
104
149
|
writeFile(path.join(appRoot, 'client', 'AGENTS.md'), '# Client\n');
|
|
105
150
|
writeFile(path.join(appRoot, 'client', 'pages', 'AGENTS.md'), '# Pages\n');
|
|
106
151
|
writeFile(path.join(appRoot, 'server', 'AGENTS.md'), '# Server\n');
|
|
@@ -700,7 +745,10 @@ test('machine MCP router resolves projects by cwd and bootstraps workflow withou
|
|
|
700
745
|
assert.equal(forwardedCall.name, 'workflow_start');
|
|
701
746
|
assert.deepEqual(forwardedCall.arguments, { route: '/domains', task: 'read-only runtime health pass' });
|
|
702
747
|
assert.equal(workflowPayload.data.project.projectId, productMachineRecord.projectId);
|
|
703
|
-
assert.equal(
|
|
748
|
+
assert.equal(
|
|
749
|
+
workflowPayload.nextActions.find((action) => action.tool === 'diagnose').toolArgs.projectId,
|
|
750
|
+
productMachineRecord.projectId,
|
|
751
|
+
);
|
|
704
752
|
|
|
705
753
|
await client.close();
|
|
706
754
|
await server.close();
|
|
@@ -785,6 +833,55 @@ test('machine MCP router resolves offline monorepo app candidates before dev is
|
|
|
785
833
|
await server.close();
|
|
786
834
|
});
|
|
787
835
|
|
|
836
|
+
test('machine MCP workflow_start reports fresh-copy setup blockers before dev start', async (t) => {
|
|
837
|
+
const previousRegistryDir = process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
|
|
838
|
+
const registryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-fresh-copy-registry-'));
|
|
839
|
+
process.env.PROTEUM_MACHINE_DEV_SESSION_DIR = registryDir;
|
|
840
|
+
t.onTestFinished(() => {
|
|
841
|
+
if (previousRegistryDir === undefined) delete process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
|
|
842
|
+
else process.env.PROTEUM_MACHINE_DEV_SESSION_DIR = previousRegistryDir;
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-fresh-copy-'));
|
|
846
|
+
const appRoot = path.join(repoRoot, 'apps', 'product');
|
|
847
|
+
writeFreshCopyFixture(appRoot, {
|
|
848
|
+
identifier: 'FreshCopyApp',
|
|
849
|
+
name: 'Fresh Copy',
|
|
850
|
+
routerPort: 3022,
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
const server = createProteumMachineMcpServer({ version: 'test' });
|
|
854
|
+
const client = new Client({ name: 'machine-mcp-test', version: '1.0.0' });
|
|
855
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
856
|
+
|
|
857
|
+
await server.connect(serverTransport);
|
|
858
|
+
await client.connect(clientTransport);
|
|
859
|
+
|
|
860
|
+
const workflow = await client.callTool({
|
|
861
|
+
name: 'workflow_start',
|
|
862
|
+
arguments: { cwd: appRoot, task: 'prepare fresh copy' },
|
|
863
|
+
});
|
|
864
|
+
const payload = JSON.parse(workflow.content[0].text);
|
|
865
|
+
const actionLabels = payload.nextActions.map((action) => action.label);
|
|
866
|
+
|
|
867
|
+
assert.equal(payload.ok, true);
|
|
868
|
+
assert.equal(payload.data.readiness.state, 'blocked');
|
|
869
|
+
assert.equal(payload.data.readiness.env.app.present, false);
|
|
870
|
+
assert.equal(payload.data.readiness.dependencies.nodeModulesPresent, false);
|
|
871
|
+
assert.equal(payload.data.readiness.database.detected, true);
|
|
872
|
+
assert.equal(payload.data.readiness.database.generatedClientPresent, false);
|
|
873
|
+
assert.equal(actionLabels.includes('Copy App Env Example'), true);
|
|
874
|
+
assert.equal(actionLabels.includes('Install Dependencies'), true);
|
|
875
|
+
assert.equal(actionLabels.includes('Generate Prisma Client'), true);
|
|
876
|
+
assert.equal(actionLabels.includes('Start Dev'), true);
|
|
877
|
+
assert.match(payload.nextActions.find((action) => action.label === 'Install Dependencies').command, /npm install/);
|
|
878
|
+
assert.match(payload.nextActions.find((action) => action.label === 'Generate Prisma Client').command, /prisma generate/);
|
|
879
|
+
assert.doesNotMatch(workflow.content[0].text, /mysql:\/\/user:pass/);
|
|
880
|
+
|
|
881
|
+
await client.close();
|
|
882
|
+
await server.close();
|
|
883
|
+
});
|
|
884
|
+
|
|
788
885
|
test('machine MCP workflow_start blocks offline unbootstrapped Codex worktrees', async (t) => {
|
|
789
886
|
const previousRegistryDir = process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
|
|
790
887
|
const registryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-worktree-offline-'));
|
|
@@ -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);
|