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.
@@ -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 { configureProjectAgentInstructions, resolveProjectAgentMonorepoRoot } = require('../cli/utils/agents.ts');
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'), /Do not start `npx proteum dev` from this root/);
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, /Do not start `npx proteum dev` from this root/);
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(path.join(appRoot, 'identity.config.ts'), 'export default {};\n');
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 returns app candidates instead of treating wrapper as app', () => {
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, 1);
307
- assert.equal(payload.ok, false);
308
- assert.equal(payload.data.appCandidates.length, 1);
309
- assert.match(payload.nextActions[0].command, /cd "apps\/product"/);
310
- assert.match(payload.nextActions[0].command, /npx proteum runtime status/);
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 returns exact app-root start command', () => {
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.match(payload.nextActions[0].command, /cd "apps\/product"/);
326
- assert.match(payload.nextActions[0].command, /npx proteum dev --session-file/);
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', () => {
@@ -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(workflowPayload.nextActions[0].toolArgs.projectId, productMachineRecord.projectId);
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);