proteum 2.5.1 → 2.5.3
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 +3 -3
- package/README.md +12 -9
- package/agents/project/AGENTS.md +10 -8
- package/agents/project/CODING_STYLE.md +1 -1
- package/agents/project/diagnostics.md +2 -2
- package/agents/project/root/AGENTS.md +10 -8
- package/agents/project/tests/AGENTS.md +1 -1
- package/cli/commands/configure.ts +5 -5
- package/cli/commands/dev.ts +3 -3
- package/cli/commands/verify.ts +117 -4
- package/cli/compiler/artifacts/controllerHelper.ts +66 -0
- package/cli/compiler/artifacts/controllers.ts +3 -0
- package/cli/compiler/artifacts/services.ts +14 -8
- package/cli/compiler/common/generatedRouteModules.ts +270 -53
- package/cli/presentation/commands.ts +14 -3
- package/cli/presentation/help.ts +1 -1
- package/cli/runtime/commands.ts +6 -0
- package/cli/scaffold/templates.ts +11 -3
- package/cli/utils/agents.ts +271 -2
- package/cli/verification/changed.ts +460 -0
- package/client/app/index.ts +1 -1
- package/client/services/router/index.tsx +1 -1
- package/common/applicationConfig.ts +177 -0
- package/common/applicationConfigLoader.ts +33 -1
- package/common/dev/contractsDoctor.ts +16 -0
- package/config.ts +5 -1
- package/docs/migration-2.5.md +54 -11
- package/eslint.js +42 -8
- package/package.json +1 -1
- package/tests/agents-utils.test.cjs +72 -0
- package/tests/cli-mcp-command.test.cjs +14 -0
- package/tests/contracts-doctor.test.cjs +98 -0
- package/tests/definition-contracts.test.cjs +129 -0
- package/tests/eslint-rules.test.cjs +100 -0
- package/tests/scaffold-templates.test.cjs +27 -2
- package/tests/verify-changed.test.cjs +200 -0
package/cli/utils/agents.ts
CHANGED
|
@@ -60,6 +60,9 @@ const managedInstructionSectionHeader = '# Proteum Instructions';
|
|
|
60
60
|
const managedInstructionSectionStart = '<!-- proteum-instructions:start -->';
|
|
61
61
|
const managedInstructionSectionEnd = '<!-- proteum-instructions:end -->';
|
|
62
62
|
const managedInstructionSectionIntro = 'This section is managed by `proteum configure agents`.';
|
|
63
|
+
const agentInstructionFilename = 'AGENTS.md';
|
|
64
|
+
const claudeInstructionFilename = 'CLAUDE.md';
|
|
65
|
+
const claudeInstructionPointerContent = `@${agentInstructionFilename}`;
|
|
63
66
|
|
|
64
67
|
const sharedRootDocumentInstructionDefinitions: TAgentInstructionDefinition[] = [
|
|
65
68
|
{ projectPath: 'DOCUMENTATION.md', content: 'source' },
|
|
@@ -243,6 +246,45 @@ function getRootAgentInstructionDefinitions() {
|
|
|
243
246
|
return monorepoRootAgentInstructionDefinitions.map((instructionDefinition) => ({ ...instructionDefinition }));
|
|
244
247
|
}
|
|
245
248
|
|
|
249
|
+
function createEmptyInstructionResult(): TEnsureInstructionFilesResult {
|
|
250
|
+
return {
|
|
251
|
+
blocked: [],
|
|
252
|
+
created: [],
|
|
253
|
+
overwritten: [],
|
|
254
|
+
removed: [],
|
|
255
|
+
skipped: [],
|
|
256
|
+
updated: [],
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function mergeEnsureInstructionResults(result: TEnsureInstructionFilesResult, next: TEnsureInstructionFilesResult) {
|
|
261
|
+
result.created.push(...next.created);
|
|
262
|
+
result.overwritten.push(...next.overwritten);
|
|
263
|
+
result.removed.push(...next.removed);
|
|
264
|
+
result.updated.push(...next.updated);
|
|
265
|
+
result.skipped.push(...next.skipped);
|
|
266
|
+
result.blocked.push(...next.blocked);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function getManagedInstructionProjectPaths(instructionDefinitions: TAgentInstructionDefinition[]) {
|
|
270
|
+
const projectPaths: string[] = [];
|
|
271
|
+
|
|
272
|
+
for (const instructionDefinition of instructionDefinitions) {
|
|
273
|
+
projectPaths.push(instructionDefinition.projectPath);
|
|
274
|
+
|
|
275
|
+
const claudeProjectPath = getClaudeCompanionProjectPath(instructionDefinition.projectPath);
|
|
276
|
+
if (claudeProjectPath) projectPaths.push(claudeProjectPath);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return projectPaths;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function getClaudeCompanionProjectPath(projectPath: string) {
|
|
283
|
+
if (path.basename(projectPath).toLowerCase() !== agentInstructionFilename.toLowerCase()) return undefined;
|
|
284
|
+
|
|
285
|
+
return path.join(path.dirname(projectPath), claudeInstructionFilename);
|
|
286
|
+
}
|
|
287
|
+
|
|
246
288
|
function removeInstructionGitignoreEntries({
|
|
247
289
|
rootDir,
|
|
248
290
|
instructionDefinitions,
|
|
@@ -254,7 +296,7 @@ function removeInstructionGitignoreEntries({
|
|
|
254
296
|
if (!pathEntryExists(gitignoreFilepath)) return false;
|
|
255
297
|
|
|
256
298
|
const managedEntries = new Set(
|
|
257
|
-
instructionDefinitions.map((
|
|
299
|
+
getManagedInstructionProjectPaths(instructionDefinitions).map((projectPath) => normalizeGitignoreEntry(projectPath)),
|
|
258
300
|
);
|
|
259
301
|
const lines = fs.readFileSync(gitignoreFilepath, 'utf8').split(/\r?\n/);
|
|
260
302
|
const filteredLines: string[] = [];
|
|
@@ -341,12 +383,28 @@ function ensureInstructionFiles(
|
|
|
341
383
|
: upsertManagedInstructionSection(existingState.content, instructionContent);
|
|
342
384
|
if (nextContent === existingState.content) {
|
|
343
385
|
result.skipped.push(relativeProjectPath);
|
|
386
|
+
ensureClaudeCompanionIfNeeded({
|
|
387
|
+
dryRun,
|
|
388
|
+
instructionDefinition,
|
|
389
|
+
logPrefix,
|
|
390
|
+
overwriteBlockedPaths,
|
|
391
|
+
result,
|
|
392
|
+
rootDir,
|
|
393
|
+
});
|
|
344
394
|
continue;
|
|
345
395
|
}
|
|
346
396
|
|
|
347
397
|
if (!dryRun) fs.writeFileSync(projectFilepath, nextContent);
|
|
348
398
|
result.updated.push(relativeProjectPath);
|
|
349
399
|
logVerbose(`${logPrefix} Updated ${relativeProjectPath}`);
|
|
400
|
+
ensureClaudeCompanionIfNeeded({
|
|
401
|
+
dryRun,
|
|
402
|
+
instructionDefinition,
|
|
403
|
+
logPrefix,
|
|
404
|
+
overwriteBlockedPaths,
|
|
405
|
+
result,
|
|
406
|
+
rootDir,
|
|
407
|
+
});
|
|
350
408
|
continue;
|
|
351
409
|
}
|
|
352
410
|
|
|
@@ -357,6 +415,14 @@ function ensureInstructionFiles(
|
|
|
357
415
|
}
|
|
358
416
|
result.updated.push(relativeProjectPath);
|
|
359
417
|
logVerbose(`${logPrefix} Updated ${relativeProjectPath}`);
|
|
418
|
+
ensureClaudeCompanionIfNeeded({
|
|
419
|
+
dryRun,
|
|
420
|
+
instructionDefinition,
|
|
421
|
+
logPrefix,
|
|
422
|
+
overwriteBlockedPaths,
|
|
423
|
+
result,
|
|
424
|
+
rootDir,
|
|
425
|
+
});
|
|
360
426
|
continue;
|
|
361
427
|
}
|
|
362
428
|
|
|
@@ -373,12 +439,28 @@ function ensureInstructionFiles(
|
|
|
373
439
|
}
|
|
374
440
|
result.overwritten.push(relativeProjectPath);
|
|
375
441
|
logVerbose(`${logPrefix} Replaced ${relativeProjectPath}`);
|
|
442
|
+
ensureClaudeCompanionIfNeeded({
|
|
443
|
+
dryRun,
|
|
444
|
+
instructionDefinition,
|
|
445
|
+
logPrefix,
|
|
446
|
+
overwriteBlockedPaths,
|
|
447
|
+
result,
|
|
448
|
+
rootDir,
|
|
449
|
+
});
|
|
376
450
|
continue;
|
|
377
451
|
}
|
|
378
452
|
|
|
379
453
|
if (!dryRun) fs.writeFileSync(projectFilepath, instructionContent);
|
|
380
454
|
result.created.push(relativeProjectPath);
|
|
381
455
|
logVerbose(`${logPrefix} Created ${relativeProjectPath}`);
|
|
456
|
+
ensureClaudeCompanionIfNeeded({
|
|
457
|
+
dryRun,
|
|
458
|
+
instructionDefinition,
|
|
459
|
+
logPrefix,
|
|
460
|
+
overwriteBlockedPaths,
|
|
461
|
+
result,
|
|
462
|
+
rootDir,
|
|
463
|
+
});
|
|
382
464
|
}
|
|
383
465
|
|
|
384
466
|
return result;
|
|
@@ -422,6 +504,13 @@ function removeManagedInstructionFiles(
|
|
|
422
504
|
if (!dryRun) fs.removeSync(projectFilepath);
|
|
423
505
|
result.removed.push(relativeProjectPath);
|
|
424
506
|
logVerbose(`${logPrefix} Removed retired app-root ${relativeProjectPath}`);
|
|
507
|
+
removeClaudeCompanionIfNeeded({
|
|
508
|
+
dryRun,
|
|
509
|
+
instructionDefinition,
|
|
510
|
+
logPrefix,
|
|
511
|
+
result,
|
|
512
|
+
rootDir,
|
|
513
|
+
});
|
|
425
514
|
continue;
|
|
426
515
|
}
|
|
427
516
|
|
|
@@ -437,6 +526,13 @@ function removeManagedInstructionFiles(
|
|
|
437
526
|
if (!dryRun) fs.removeSync(projectFilepath);
|
|
438
527
|
result.removed.push(relativeProjectPath);
|
|
439
528
|
logVerbose(`${logPrefix} Removed retired app-root ${relativeProjectPath}`);
|
|
529
|
+
removeClaudeCompanionIfNeeded({
|
|
530
|
+
dryRun,
|
|
531
|
+
instructionDefinition,
|
|
532
|
+
logPrefix,
|
|
533
|
+
result,
|
|
534
|
+
rootDir,
|
|
535
|
+
});
|
|
440
536
|
continue;
|
|
441
537
|
}
|
|
442
538
|
|
|
@@ -452,6 +548,179 @@ function removeManagedInstructionFiles(
|
|
|
452
548
|
return result;
|
|
453
549
|
}
|
|
454
550
|
|
|
551
|
+
function ensureClaudeCompanionIfNeeded({
|
|
552
|
+
dryRun,
|
|
553
|
+
instructionDefinition,
|
|
554
|
+
logPrefix,
|
|
555
|
+
overwriteBlockedPaths,
|
|
556
|
+
result,
|
|
557
|
+
rootDir,
|
|
558
|
+
}: {
|
|
559
|
+
dryRun: boolean;
|
|
560
|
+
instructionDefinition: TAgentInstructionDefinition;
|
|
561
|
+
logPrefix: string;
|
|
562
|
+
overwriteBlockedPaths: Set<string>;
|
|
563
|
+
result: TEnsureInstructionFilesResult;
|
|
564
|
+
rootDir: string;
|
|
565
|
+
}) {
|
|
566
|
+
const claudeProjectPath = getClaudeCompanionProjectPath(instructionDefinition.projectPath);
|
|
567
|
+
if (!claudeProjectPath) return;
|
|
568
|
+
|
|
569
|
+
mergeEnsureInstructionResults(
|
|
570
|
+
result,
|
|
571
|
+
ensureClaudeInstructionSymlink({
|
|
572
|
+
claudeProjectPath,
|
|
573
|
+
dryRun,
|
|
574
|
+
logPrefix,
|
|
575
|
+
overwriteBlockedPaths,
|
|
576
|
+
rootDir,
|
|
577
|
+
}),
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function ensureClaudeInstructionSymlink({
|
|
582
|
+
claudeProjectPath,
|
|
583
|
+
dryRun,
|
|
584
|
+
logPrefix,
|
|
585
|
+
overwriteBlockedPaths,
|
|
586
|
+
rootDir,
|
|
587
|
+
}: {
|
|
588
|
+
claudeProjectPath: string;
|
|
589
|
+
dryRun: boolean;
|
|
590
|
+
logPrefix: string;
|
|
591
|
+
overwriteBlockedPaths: Set<string>;
|
|
592
|
+
rootDir: string;
|
|
593
|
+
}): TEnsureInstructionFilesResult {
|
|
594
|
+
const result = createEmptyInstructionResult();
|
|
595
|
+
const claudeFilepath = path.join(rootDir, claudeProjectPath);
|
|
596
|
+
const claudeParentDir = path.dirname(claudeFilepath);
|
|
597
|
+
const relativeClaudePath = path.relative(rootDir, claudeFilepath) || '.';
|
|
598
|
+
|
|
599
|
+
if (!fs.existsSync(claudeParentDir)) {
|
|
600
|
+
result.skipped.push(relativeClaudePath);
|
|
601
|
+
return result;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const existingState = inspectExistingClaudePath(claudeFilepath);
|
|
605
|
+
|
|
606
|
+
if (existingState.kind === 'correct') {
|
|
607
|
+
result.skipped.push(relativeClaudePath);
|
|
608
|
+
return result;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (existingState.kind === 'missing') {
|
|
612
|
+
if (!dryRun) fs.symlinkSync(agentInstructionFilename, claudeFilepath);
|
|
613
|
+
result.created.push(relativeClaudePath);
|
|
614
|
+
logVerbose(`${logPrefix} Created ${relativeClaudePath}`);
|
|
615
|
+
return result;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (existingState.kind === 'managed-different') {
|
|
619
|
+
if (!dryRun) {
|
|
620
|
+
fs.removeSync(claudeFilepath);
|
|
621
|
+
fs.symlinkSync(agentInstructionFilename, claudeFilepath);
|
|
622
|
+
}
|
|
623
|
+
result.updated.push(relativeClaudePath);
|
|
624
|
+
logVerbose(`${logPrefix} Updated ${relativeClaudePath}`);
|
|
625
|
+
return result;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const normalizedClaudeFilepath = normalizeAbsolutePath(claudeFilepath);
|
|
629
|
+
if (!overwriteBlockedPaths.has(normalizedClaudeFilepath)) {
|
|
630
|
+
result.blocked.push(relativeClaudePath);
|
|
631
|
+
return result;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (!dryRun) {
|
|
635
|
+
fs.removeSync(claudeFilepath);
|
|
636
|
+
fs.symlinkSync(agentInstructionFilename, claudeFilepath);
|
|
637
|
+
}
|
|
638
|
+
result.overwritten.push(relativeClaudePath);
|
|
639
|
+
logVerbose(`${logPrefix} Replaced ${relativeClaudePath}`);
|
|
640
|
+
|
|
641
|
+
return result;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function removeClaudeCompanionIfNeeded({
|
|
645
|
+
dryRun,
|
|
646
|
+
instructionDefinition,
|
|
647
|
+
logPrefix,
|
|
648
|
+
result,
|
|
649
|
+
rootDir,
|
|
650
|
+
}: {
|
|
651
|
+
dryRun: boolean;
|
|
652
|
+
instructionDefinition: TAgentInstructionDefinition;
|
|
653
|
+
logPrefix: string;
|
|
654
|
+
result: TEnsureInstructionFilesResult;
|
|
655
|
+
rootDir: string;
|
|
656
|
+
}) {
|
|
657
|
+
const claudeProjectPath = getClaudeCompanionProjectPath(instructionDefinition.projectPath);
|
|
658
|
+
if (!claudeProjectPath) return;
|
|
659
|
+
|
|
660
|
+
mergeEnsureInstructionResults(
|
|
661
|
+
result,
|
|
662
|
+
removeClaudeInstructionSymlink({
|
|
663
|
+
claudeProjectPath,
|
|
664
|
+
dryRun,
|
|
665
|
+
logPrefix,
|
|
666
|
+
rootDir,
|
|
667
|
+
}),
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function removeClaudeInstructionSymlink({
|
|
672
|
+
claudeProjectPath,
|
|
673
|
+
dryRun,
|
|
674
|
+
logPrefix,
|
|
675
|
+
rootDir,
|
|
676
|
+
}: {
|
|
677
|
+
claudeProjectPath: string;
|
|
678
|
+
dryRun: boolean;
|
|
679
|
+
logPrefix: string;
|
|
680
|
+
rootDir: string;
|
|
681
|
+
}): TEnsureInstructionFilesResult {
|
|
682
|
+
const result = createEmptyInstructionResult();
|
|
683
|
+
const claudeFilepath = path.join(rootDir, claudeProjectPath);
|
|
684
|
+
const relativeClaudePath = path.relative(rootDir, claudeFilepath) || '.';
|
|
685
|
+
|
|
686
|
+
if (!pathEntryExists(claudeFilepath)) return result;
|
|
687
|
+
|
|
688
|
+
const existingState = inspectExistingClaudePath(claudeFilepath);
|
|
689
|
+
if (existingState.kind === 'correct' || existingState.kind === 'managed-different') {
|
|
690
|
+
if (!dryRun) fs.removeSync(claudeFilepath);
|
|
691
|
+
result.removed.push(relativeClaudePath);
|
|
692
|
+
logVerbose(`${logPrefix} Removed retired app-root ${relativeClaudePath}`);
|
|
693
|
+
return result;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (existingState.kind === 'blocked') result.skipped.push(relativeClaudePath);
|
|
697
|
+
|
|
698
|
+
return result;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function inspectExistingClaudePath(claudeFilepath: string) {
|
|
702
|
+
if (!pathEntryExists(claudeFilepath)) return { kind: 'missing' as const };
|
|
703
|
+
|
|
704
|
+
const stats = fs.lstatSync(claudeFilepath);
|
|
705
|
+
const claudeParentDir = path.dirname(claudeFilepath);
|
|
706
|
+
const agentFilepath = path.join(claudeParentDir, agentInstructionFilename);
|
|
707
|
+
|
|
708
|
+
if (stats.isSymbolicLink()) {
|
|
709
|
+
const rawTarget = fs.readlinkSync(claudeFilepath);
|
|
710
|
+
const resolvedTarget = path.resolve(claudeParentDir, rawTarget);
|
|
711
|
+
if (normalizeAbsolutePath(resolvedTarget) !== normalizeAbsolutePath(agentFilepath)) return { kind: 'blocked' as const };
|
|
712
|
+
|
|
713
|
+
return rawTarget === agentInstructionFilename ? { kind: 'correct' as const } : { kind: 'managed-different' as const };
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (!stats.isFile()) return { kind: 'blocked' as const };
|
|
717
|
+
|
|
718
|
+
const content = fs.readFileSync(claudeFilepath, 'utf8');
|
|
719
|
+
if (content.trim() === claudeInstructionPointerContent) return { kind: 'managed-different' as const };
|
|
720
|
+
|
|
721
|
+
return { kind: 'blocked' as const };
|
|
722
|
+
}
|
|
723
|
+
|
|
455
724
|
function inspectExistingPath({
|
|
456
725
|
managedSourceRoot,
|
|
457
726
|
projectFilepath,
|
|
@@ -643,7 +912,7 @@ function renderEmbeddedProjectInstructions({ appRoot, coreRoot, includeMonorepoR
|
|
|
643
912
|
'- App roots default-export `defineApplication({ services, router, models, commands })`; `server/index.ts` is the canonical type root for the project app, services, router plugins, request context, and models.',
|
|
644
913
|
'- Client page files default-export `definePageRoute({ path, options, data, render })` or `defineErrorRoute({ code, options, render })`; route paths come from the definition object, not from `Router.page(...)` or the file path.',
|
|
645
914
|
'- Manual HTTP route files default-export `defineServerRoute({ method, path, options, handler })` or `defineServerRoutes(...)`; use `expressHandler(...)` only when raw Express `req`, `res`, or `next` is required.',
|
|
646
|
-
'- Controllers default-export `defineController({ path, actions })`; actions use `defineAction({ input, handler })`, and parsed input is read from the handler context.',
|
|
915
|
+
'- Controllers default-export `defineController({ path, actions })` from `@generated/server/controller`; actions use `defineAction({ input, handler })`, and parsed input is read from the app-typed handler context.',
|
|
647
916
|
'- Never import `@app` in page, route, or controller files. Never call top-level `Router.page(...)`, `Router.error(...)`, `Router.get(...)`, `Router.post(...)`, `Router.put(...)`, `Router.patch(...)`, `Router.delete(...)`, or `Router.express(...)` in app source.',
|
|
648
917
|
'- Runtime app, service, request, response, router, auth, and custom router-plugin access belongs only in typed callback parameters such as `data`, `render`, route `handler`, controller action `handler`, `defineServerRoutes((app) => ...)`, or typed service `this.app`/`this.services`.',
|
|
649
918
|
'',
|