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.
- 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 +156 -38
- package/package.json +1 -1
- package/tests/agents-utils.test.cjs +63 -3
- package/tests/cli-mcp-command.test.cjs +60 -11
- package/tests/worktree-bootstrap.test.cjs +98 -0
|
@@ -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
|
+
};
|
package/cli/utils/agents.ts
CHANGED
|
@@ -12,14 +12,30 @@ import { createStartDevCommand, findProteumAppRootsUnder, readProteumAppRootSumm
|
|
|
12
12
|
- TYPES
|
|
13
13
|
----------------------------------*/
|
|
14
14
|
|
|
15
|
-
type TProjectInstructionArgs = {
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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.
|
|
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 =
|
|
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({
|
|
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
|
|
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.
|
|
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 {
|
|
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', () => {
|