proteum 2.5.5 → 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.
@@ -2,6 +2,7 @@ import cp from 'child_process';
2
2
  import crypto from 'crypto';
3
3
  import fs from 'fs-extra';
4
4
  import path from 'path';
5
+ import { findProteumAppRootsUnder } from '../utils/appRoots';
5
6
 
6
7
  /*----------------------------------
7
8
  - TYPES
@@ -124,6 +125,30 @@ export type TRunWorktreeBootstrapCreateOptions = TRunWorktreeBootstrapInitOption
124
125
  targetRepoRoot: string;
125
126
  };
126
127
 
128
+ export type TMonorepoWorktreeBootstrapAppResult = {
129
+ appRoot: string;
130
+ error?: string;
131
+ markerFilepath?: string;
132
+ ok: boolean;
133
+ refresh?: string;
134
+ relativeAppRoot: string;
135
+ runtimeStatus?: string;
136
+ sourceAppRoot?: string;
137
+ status?: TWorktreeBootstrapStatus;
138
+ };
139
+
140
+ export type TRunMonorepoWorktreeBootstrapInitOptions = Omit<TRunWorktreeBootstrapInitOptions, 'appRoot' | 'source'> & {
141
+ appRoots?: string[];
142
+ monorepoRoot: string;
143
+ source?: string;
144
+ };
145
+
146
+ export type TRunMonorepoWorktreeBootstrapCreateOptions = TRunMonorepoWorktreeBootstrapInitOptions & {
147
+ base?: string;
148
+ branch: string;
149
+ targetRepoRoot: string;
150
+ };
151
+
127
152
  /*----------------------------------
128
153
  - CONSTANTS
129
154
  ----------------------------------*/
@@ -139,6 +164,16 @@ const codexWorktreeSegment = `${path.sep}.codex${path.sep}worktrees${path.sep}`;
139
164
 
140
165
  const normalizePath = (value: string) => path.normalize(path.resolve(value));
141
166
 
167
+ const normalizeExistingPath = (value: string) => {
168
+ const normalized = normalizePath(value);
169
+
170
+ try {
171
+ return path.normalize(fs.realpathSync(normalized));
172
+ } catch {
173
+ return normalized;
174
+ }
175
+ };
176
+
142
177
  const isTruthyEnv = (value: string | undefined) => value === '1' || value === 'true' || value === 'yes';
143
178
 
144
179
  const nowIso = () => new Date().toISOString();
@@ -699,3 +734,131 @@ export const runWorktreeBootstrapCreate = async ({
699
734
  worktreeBootstrap: initResult,
700
735
  };
701
736
  };
737
+
738
+ const findBootstrapInstallRoot = (appRoot: string) => {
739
+ const packageLockFilepath = findNearestExistingPath(appRoot, 'package-lock.json');
740
+ return packageLockFilepath ? path.dirname(packageLockFilepath) : normalizePath(appRoot);
741
+ };
742
+
743
+ const createSharedDependencyRunner = (runDependencies: (appRoot: string) => Promise<void> = runNpmInstall) => {
744
+ const completedInstallRoots = new Set<string>();
745
+
746
+ return async (appRoot: string) => {
747
+ const installRoot = findBootstrapInstallRoot(appRoot);
748
+ if (completedInstallRoots.has(installRoot)) return;
749
+
750
+ completedInstallRoots.add(installRoot);
751
+ await runDependencies(installRoot);
752
+ };
753
+ };
754
+
755
+ const resolveSourceAppRoot = ({
756
+ relativeAppRoot,
757
+ sourceRoot,
758
+ }: {
759
+ relativeAppRoot: string;
760
+ sourceRoot?: string;
761
+ }) => {
762
+ if (!sourceRoot) return undefined;
763
+
764
+ const normalizedSourceRoot = normalizeExistingPath(sourceRoot);
765
+ const sourceAppRoot = path.join(normalizedSourceRoot, relativeAppRoot);
766
+
767
+ if (fs.existsSync(sourceAppRoot)) return sourceAppRoot;
768
+
769
+ return undefined;
770
+ };
771
+
772
+ export const runMonorepoWorktreeBootstrapInit = async ({
773
+ appRoots,
774
+ monorepoRoot,
775
+ runDependencies,
776
+ source,
777
+ ...initOptions
778
+ }: TRunMonorepoWorktreeBootstrapInitOptions) => {
779
+ const normalizedMonorepoRoot = normalizeExistingPath(monorepoRoot);
780
+ const targetAppRoots = (appRoots || findProteumAppRootsUnder(normalizedMonorepoRoot))
781
+ .map((appRoot) => normalizeExistingPath(appRoot))
782
+ .sort((left, right) => left.localeCompare(right));
783
+ const sharedDependencyRunner = createSharedDependencyRunner(runDependencies);
784
+ const apps: TMonorepoWorktreeBootstrapAppResult[] = [];
785
+
786
+ if (targetAppRoots.length === 0) throw new Error(`No Proteum app roots were found under ${normalizedMonorepoRoot}.`);
787
+
788
+ for (const appRoot of targetAppRoots) {
789
+ const relativeAppRoot = path.relative(normalizedMonorepoRoot, appRoot) || '.';
790
+ const sourceAppRoot = resolveSourceAppRoot({
791
+ relativeAppRoot,
792
+ sourceRoot: source,
793
+ });
794
+
795
+ try {
796
+ const result = await runWorktreeBootstrapInit({
797
+ ...initOptions,
798
+ appRoot,
799
+ runDependencies: sharedDependencyRunner,
800
+ source: sourceAppRoot,
801
+ });
802
+
803
+ apps.push({
804
+ appRoot,
805
+ markerFilepath: result.markerFilepath,
806
+ ok: true,
807
+ refresh: result.refresh,
808
+ relativeAppRoot,
809
+ runtimeStatus: result.runtimeStatus,
810
+ sourceAppRoot,
811
+ status: result.status,
812
+ });
813
+ } catch (error) {
814
+ apps.push({
815
+ appRoot,
816
+ error: error instanceof Error ? error.message : String(error),
817
+ ok: false,
818
+ relativeAppRoot,
819
+ sourceAppRoot,
820
+ });
821
+ }
822
+ }
823
+
824
+ return {
825
+ appRoots: targetAppRoots,
826
+ apps,
827
+ failed: apps.filter((entry) => !entry.ok).length,
828
+ monorepoRoot: normalizedMonorepoRoot,
829
+ ok: apps.every((entry) => entry.ok),
830
+ sourceRoot: source ? normalizeExistingPath(source) : undefined,
831
+ };
832
+ };
833
+
834
+ export const runMonorepoWorktreeBootstrapCreate = async ({
835
+ base = 'HEAD',
836
+ branch,
837
+ source,
838
+ targetRepoRoot,
839
+ ...initOptions
840
+ }: TRunMonorepoWorktreeBootstrapCreateOptions) => {
841
+ if (!branch.trim()) throw new Error('worktree create requires --branch <branch>.');
842
+ if (!targetRepoRoot.trim()) throw new Error('worktree create requires <target-repo-root>.');
843
+
844
+ const normalizedSourceRoot = normalizeExistingPath(source || initOptions.monorepoRoot);
845
+ const normalizedTargetRepoRoot = path.resolve(targetRepoRoot);
846
+ const sourceRepoRoot = await findGitRepoRoot(normalizedSourceRoot);
847
+
848
+ await runCapture('git', ['worktree', 'add', '-b', branch, normalizedTargetRepoRoot, base], { cwd: sourceRepoRoot });
849
+
850
+ const initResult = await runMonorepoWorktreeBootstrapInit({
851
+ ...initOptions,
852
+ monorepoRoot: normalizedTargetRepoRoot,
853
+ refresh: true,
854
+ source: normalizedSourceRoot,
855
+ });
856
+
857
+ return {
858
+ branch,
859
+ sourceMonorepoRoot: normalizedSourceRoot,
860
+ sourceRepoRoot,
861
+ targetRepoRoot: normalizedTargetRepoRoot,
862
+ worktreeBootstrap: initResult,
863
+ };
864
+ };
@@ -12,14 +12,30 @@ import { createStartDevCommand, findProteumAppRootsUnder, readProteumAppRootSumm
12
12
  - TYPES
13
13
  ----------------------------------*/
14
14
 
15
- type TProjectInstructionArgs = { appRoot?: string; coreRoot: string; includeMonorepoRegistry?: boolean; monorepoRoot?: string };
15
+ type TProjectInstructionArgs = {
16
+ appRoot?: string;
17
+ coreRoot: string;
18
+ includeMonorepoRegistry?: boolean;
19
+ monorepoRegistryCurrentAppRoot?: string;
20
+ monorepoRoot?: string;
21
+ };
16
22
  type TConfigureProjectAgentInstructionsArgs = {
17
23
  appRoot: string;
18
24
  coreRoot: string;
19
25
  dryRun?: boolean;
26
+ includeAppInstructions?: boolean;
27
+ includeRootInstructions?: boolean;
28
+ markCurrentAppInMonorepoRegistry?: boolean;
20
29
  monorepoRoot?: string;
21
30
  overwriteBlockedPaths?: string[];
22
31
  };
32
+ type TConfigureMonorepoProjectAgentInstructionsArgs = {
33
+ appRoots: string[];
34
+ coreRoot: string;
35
+ dryRun?: boolean;
36
+ monorepoRoot: string;
37
+ overwriteBlockedPaths?: string[];
38
+ };
23
39
 
24
40
  type TAgentInstructionDefinition = {
25
41
  projectPath: string;
@@ -49,6 +65,12 @@ export type TConfigureProjectAgentInstructionsResult = {
49
65
  updatedGitignores: string[];
50
66
  };
51
67
 
68
+ export type TConfigureMonorepoProjectAgentInstructionsResult = Omit<TConfigureProjectAgentInstructionsResult, 'appRoot'> & {
69
+ appRoots: string[];
70
+ monorepoRoot: string;
71
+ mode: 'monorepo';
72
+ };
73
+
52
74
  /*----------------------------------
53
75
  - CONSTANTS
54
76
  ----------------------------------*/
@@ -115,6 +137,9 @@ export function configureProjectAgentInstructions({
115
137
  appRoot,
116
138
  coreRoot,
117
139
  dryRun = false,
140
+ includeAppInstructions = true,
141
+ includeRootInstructions = true,
142
+ markCurrentAppInMonorepoRegistry = true,
118
143
  monorepoRoot,
119
144
  overwriteBlockedPaths = [],
120
145
  }: TConfigureProjectAgentInstructionsArgs): TConfigureProjectAgentInstructionsResult {
@@ -147,11 +172,12 @@ export function configureProjectAgentInstructions({
147
172
  appRoot: normalizedAppRoot,
148
173
  coreRoot,
149
174
  includeMonorepoRegistry: true,
175
+ monorepoRegistryCurrentAppRoot: markCurrentAppInMonorepoRegistry ? normalizedAppRoot : undefined,
150
176
  monorepoRoot: normalizedMonorepoRoot,
151
177
  })
152
178
  : appEmbeddedInstructions;
153
179
 
154
- if (mode === 'monorepo' && normalizedMonorepoRoot) {
180
+ if (includeRootInstructions && mode === 'monorepo' && normalizedMonorepoRoot) {
155
181
  result.monorepoRoot = normalizedMonorepoRoot;
156
182
 
157
183
  const rootInstructions = getRootAgentInstructionDefinitions();
@@ -172,52 +198,127 @@ export function configureProjectAgentInstructions({
172
198
  result.updatedGitignores.push(path.join(normalizedMonorepoRoot, '.gitignore'));
173
199
  }
174
200
 
175
- const appInstructions = getAppAgentInstructionDefinitions({ mode });
176
- const appFiles = ensureInstructionFiles(
177
- normalizedAppRoot,
178
- appInstructions,
179
- '[agents]',
180
- path.join(coreRoot, 'agents', 'project'),
181
- appEmbeddedInstructions,
182
- {
183
- dryRun,
184
- overwriteBlockedPaths: normalizedOverwriteBlockedPaths,
185
- },
186
- );
187
- mergeInstructionResults(result, appFiles, normalizedAppRoot);
188
-
189
- if (mode === 'monorepo') {
190
- const retiredAppRootFiles = removeManagedInstructionFiles(
201
+ if (includeAppInstructions) {
202
+ const appInstructions = getAppAgentInstructionDefinitions({ mode });
203
+ const appFiles = ensureInstructionFiles(
191
204
  normalizedAppRoot,
192
- [...sharedRootDocumentInstructionDefinitions, ...sharedTestAgentInstructionDefinitions],
205
+ appInstructions,
193
206
  '[agents]',
194
207
  path.join(coreRoot, 'agents', 'project'),
208
+ appEmbeddedInstructions,
195
209
  {
196
210
  dryRun,
211
+ overwriteBlockedPaths: normalizedOverwriteBlockedPaths,
197
212
  },
198
213
  );
199
- mergeInstructionResults(result, retiredAppRootFiles, normalizedAppRoot);
200
- }
214
+ mergeInstructionResults(result, appFiles, normalizedAppRoot);
215
+
216
+ if (mode === 'monorepo') {
217
+ const retiredAppRootFiles = removeManagedInstructionFiles(
218
+ normalizedAppRoot,
219
+ [...sharedRootDocumentInstructionDefinitions, ...sharedTestAgentInstructionDefinitions],
220
+ '[agents]',
221
+ path.join(coreRoot, 'agents', 'project'),
222
+ {
223
+ dryRun,
224
+ },
225
+ );
226
+ mergeInstructionResults(result, retiredAppRootFiles, normalizedAppRoot);
227
+ }
201
228
 
202
- const appGitignoreCleanupInstructions =
203
- mode === 'monorepo'
204
- ? [...appInstructions, ...sharedRootDocumentInstructionDefinitions, ...sharedTestAgentInstructionDefinitions]
205
- : appInstructions;
206
-
207
- if (
208
- !dryRun &&
209
- removeInstructionGitignoreEntries({
210
- rootDir: normalizedAppRoot,
211
- instructionDefinitions: appGitignoreCleanupInstructions,
212
- })
213
- )
214
- result.updatedGitignores.push(path.join(normalizedAppRoot, '.gitignore'));
229
+ const appGitignoreCleanupInstructions =
230
+ mode === 'monorepo'
231
+ ? [...appInstructions, ...sharedRootDocumentInstructionDefinitions, ...sharedTestAgentInstructionDefinitions]
232
+ : appInstructions;
233
+
234
+ if (
235
+ !dryRun &&
236
+ removeInstructionGitignoreEntries({
237
+ rootDir: normalizedAppRoot,
238
+ instructionDefinitions: appGitignoreCleanupInstructions,
239
+ })
240
+ )
241
+ result.updatedGitignores.push(path.join(normalizedAppRoot, '.gitignore'));
242
+ }
215
243
 
216
244
  return result;
217
245
  }
218
246
 
219
247
  export const configureProjectAgentSymlinks = configureProjectAgentInstructions;
220
248
 
249
+ export function configureMonorepoProjectAgentInstructions({
250
+ appRoots,
251
+ coreRoot,
252
+ dryRun = false,
253
+ monorepoRoot,
254
+ overwriteBlockedPaths = [],
255
+ }: TConfigureMonorepoProjectAgentInstructionsArgs): TConfigureMonorepoProjectAgentInstructionsResult {
256
+ const normalizedMonorepoRoot = path.resolve(monorepoRoot);
257
+ const normalizedAppRoots = [...new Set(appRoots.map((appRoot) => path.resolve(appRoot)))].sort((left, right) =>
258
+ left.localeCompare(right),
259
+ );
260
+ const [firstAppRoot] = normalizedAppRoots;
261
+
262
+ if (!firstAppRoot) throw new Error('No Proteum app roots were found under the monorepo root.');
263
+
264
+ const result: TConfigureMonorepoProjectAgentInstructionsResult = {
265
+ appRoots: normalizedAppRoots,
266
+ blocked: [],
267
+ created: [],
268
+ mode: 'monorepo',
269
+ monorepoRoot: normalizedMonorepoRoot,
270
+ overwritten: [],
271
+ removed: [],
272
+ skipped: [],
273
+ updated: [],
274
+ updatedGitignores: [],
275
+ };
276
+ const mergeProjectResult = (next: TConfigureProjectAgentInstructionsResult) => {
277
+ result.blocked = [...new Set([...result.blocked, ...next.blocked])];
278
+ result.created = [...new Set([...result.created, ...next.created])];
279
+ result.overwritten = [...new Set([...result.overwritten, ...next.overwritten])];
280
+ result.removed = [...new Set([...result.removed, ...next.removed])];
281
+ result.skipped = [...new Set([...result.skipped, ...next.skipped])];
282
+ result.updated = [...new Set([...result.updated, ...next.updated])];
283
+ result.updatedGitignores = [...new Set([...result.updatedGitignores, ...next.updatedGitignores])];
284
+ };
285
+
286
+ mergeProjectResult(
287
+ configureProjectAgentInstructions({
288
+ appRoot: firstAppRoot,
289
+ coreRoot,
290
+ dryRun,
291
+ includeAppInstructions: false,
292
+ markCurrentAppInMonorepoRegistry: false,
293
+ monorepoRoot: normalizedMonorepoRoot,
294
+ overwriteBlockedPaths,
295
+ }),
296
+ );
297
+
298
+ for (const appRoot of normalizedAppRoots) {
299
+ mergeProjectResult(
300
+ configureProjectAgentInstructions({
301
+ appRoot,
302
+ coreRoot,
303
+ dryRun,
304
+ includeRootInstructions: false,
305
+ monorepoRoot: normalizedMonorepoRoot,
306
+ overwriteBlockedPaths,
307
+ }),
308
+ );
309
+ }
310
+
311
+ result.blocked = [...new Set(result.blocked)];
312
+ result.created = [...new Set(result.created)];
313
+ result.overwritten = [...new Set(result.overwritten)];
314
+ result.removed = [...new Set(result.removed)];
315
+ result.skipped = [...new Set(result.skipped)];
316
+ result.updated = [...new Set(result.updated)];
317
+ result.updatedGitignores = [...new Set(result.updatedGitignores)];
318
+
319
+ return result;
320
+ }
321
+
221
322
  export function resolveProjectAgentMonorepoRoot(appRoot: string) {
222
323
  const normalizedAppRoot = resolveCanonicalPath(appRoot);
223
324
  const likelyRepoRoot = findLikelyRepoRoot(normalizedAppRoot);
@@ -831,9 +932,11 @@ function renderSingleProjectInstruction({
831
932
 
832
933
  function renderMonorepoAppRegistry({
833
934
  appRoot,
935
+ currentAppRoot,
834
936
  monorepoRoot,
835
937
  }: {
836
938
  appRoot?: string;
939
+ currentAppRoot?: string;
837
940
  monorepoRoot?: string;
838
941
  }) {
839
942
  if (!monorepoRoot || !appRoot || path.resolve(monorepoRoot) === path.resolve(appRoot)) return [];
@@ -846,10 +949,13 @@ function renderMonorepoAppRegistry({
846
949
  return [
847
950
  '## Known Proteum Apps',
848
951
  '',
849
- 'This is a monorepo root wrapper. Do not start `npx proteum dev` from this root; start it from one app root below.',
952
+ 'This is a monorepo root wrapper. Eligible Proteum commands run across the apps below from this root; use an app root when you need to target exactly one app.',
850
953
  '',
851
954
  ...summaries.map((summary) => {
852
- const marker = path.resolve(summary.appRoot) === path.resolve(appRoot) ? ' (current configured app)' : '';
955
+ const marker =
956
+ currentAppRoot && path.resolve(summary.appRoot) === path.resolve(currentAppRoot)
957
+ ? ' (current configured app)'
958
+ : '';
853
959
  const port = summary.manifest?.routerPort ? `, default port ${summary.manifest.routerPort}` : '';
854
960
  const command = createStartDevCommand({
855
961
  appRoot: summary.appRoot,
@@ -863,7 +969,13 @@ function renderMonorepoAppRegistry({
863
969
  ];
864
970
  }
865
971
 
866
- function renderEmbeddedProjectInstructions({ appRoot, coreRoot, includeMonorepoRegistry = false, monorepoRoot }: TProjectInstructionArgs) {
972
+ function renderEmbeddedProjectInstructions({
973
+ appRoot,
974
+ coreRoot,
975
+ includeMonorepoRegistry = false,
976
+ monorepoRegistryCurrentAppRoot,
977
+ monorepoRoot,
978
+ }: TProjectInstructionArgs) {
867
979
  const agentSourceRoot = path.join(coreRoot, 'agents', 'project');
868
980
  if (!fs.existsSync(agentSourceRoot)) throw new Error(`Missing project instruction source root: ${agentSourceRoot}`);
869
981
 
@@ -895,7 +1007,13 @@ function renderEmbeddedProjectInstructions({ appRoot, coreRoot, includeMonorepoR
895
1007
  '',
896
1008
  'CLI remains the reproducible surface for `dev`, `build`, `check`, `verify`, migrations, and final command evidence. MCP remains read-only and returns compact `proteum-mcp-v1` JSON.',
897
1009
  '',
898
- ...(includeMonorepoRegistry ? renderMonorepoAppRegistry({ appRoot, monorepoRoot }) : []),
1010
+ ...(includeMonorepoRegistry
1011
+ ? renderMonorepoAppRegistry({
1012
+ appRoot,
1013
+ currentAppRoot: monorepoRegistryCurrentAppRoot,
1014
+ monorepoRoot,
1015
+ })
1016
+ : []),
899
1017
  '## Always-On Safety',
900
1018
  '',
901
1019
  '- Never edit generated files under `.proteum`.',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "proteum",
3
3
  "description": "LLM-first Opinionated Typescript Framework for web applications.",
4
- "version": "2.5.5",
4
+ "version": "2.5.6",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/proteum.git",
7
7
  "license": "MIT",
@@ -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', () => {