proteum 2.4.3 → 2.5.0

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.
Files changed (74) hide show
  1. package/README.md +60 -55
  2. package/agents/project/AGENTS.md +112 -31
  3. package/agents/project/CODING_STYLE.md +2 -2
  4. package/agents/project/app-root/AGENTS.md +1 -3
  5. package/agents/project/client/AGENTS.md +1 -1
  6. package/agents/project/client/pages/AGENTS.md +21 -9
  7. package/agents/project/diagnostics.md +2 -2
  8. package/agents/project/optimizations.md +1 -1
  9. package/agents/project/root/AGENTS.md +105 -22
  10. package/agents/project/server/routes/AGENTS.md +30 -1
  11. package/agents/project/tests/AGENTS.md +1 -1
  12. package/cli/commands/doctor.ts +54 -3
  13. package/cli/commands/runtime.ts +6 -0
  14. package/cli/commands/worktree.ts +116 -0
  15. package/cli/compiler/artifacts/controllers.ts +16 -15
  16. package/cli/compiler/artifacts/discovery.ts +129 -17
  17. package/cli/compiler/artifacts/routing.ts +0 -5
  18. package/cli/compiler/artifacts/services.ts +253 -76
  19. package/cli/compiler/common/controllers.ts +159 -57
  20. package/cli/compiler/common/generatedRouteModules.ts +457 -363
  21. package/cli/mcp/router.ts +47 -3
  22. package/cli/presentation/commands.ts +25 -15
  23. package/cli/runtime/commands.ts +39 -12
  24. package/cli/runtime/worktreeBootstrap.ts +608 -0
  25. package/cli/scaffold/index.ts +28 -18
  26. package/cli/scaffold/templates.ts +44 -33
  27. package/cli/utils/agents.ts +14 -1
  28. package/client/services/router/index.tsx +23 -3
  29. package/client/services/router/request/api.ts +14 -4
  30. package/common/dev/contractsDoctor.ts +1 -1
  31. package/common/dev/mcpPayloads.ts +8 -1
  32. package/common/env/proteumEnv.ts +14 -2
  33. package/common/router/contracts.ts +1 -1
  34. package/common/router/definitions.ts +177 -0
  35. package/common/router/index.ts +23 -12
  36. package/common/router/pageData.ts +5 -5
  37. package/common/router/register.ts +2 -2
  38. package/common/router/request/api.ts +12 -2
  39. package/docs/agent-routing.md +5 -2
  40. package/docs/diagnostics.md +2 -0
  41. package/docs/mcp.md +6 -3
  42. package/eslint.js +36 -1
  43. package/package.json +1 -1
  44. package/server/app/commands.ts +5 -1
  45. package/server/app/container/console/http-client-error-context.test.cjs +10 -1
  46. package/server/app/container/console/index.ts +2 -1
  47. package/server/app/controller/index.ts +98 -40
  48. package/server/app/index.ts +92 -1
  49. package/server/app/service/index.ts +5 -1
  50. package/server/index.ts +6 -2
  51. package/server/services/router/index.ts +47 -38
  52. package/server/services/router/response/index.ts +2 -2
  53. package/tests/agents-utils.test.cjs +14 -1
  54. package/tests/cli-mcp-command.test.cjs +84 -0
  55. package/tests/definition-contracts.test.cjs +453 -0
  56. package/tests/dev-transpile-watch.test.cjs +37 -28
  57. package/tests/eslint-rules.test.cjs +39 -1
  58. package/tests/mcp.test.cjs +90 -0
  59. package/tests/worktree-bootstrap.test.cjs +206 -0
  60. package/types/aliases.d.ts +0 -5
  61. package/types/controller-input.test.ts +23 -17
  62. package/types/controller-request-context.test.ts +10 -11
  63. package/cli/commands/migrate.ts +0 -51
  64. package/cli/migrate/pageContract.ts +0 -516
  65. package/docs/migrate-from-2.1.3.md +0 -396
  66. package/scripts/cleanup-generated-controllers.ts +0 -62
  67. package/scripts/fix-reference-app-typing.ts +0 -490
  68. package/scripts/format-router-registrations.ts +0 -119
  69. package/scripts/migrate-explicit-controllers-and-request.ts +0 -423
  70. package/scripts/refactor-client-app-imports.ts +0 -244
  71. package/scripts/refactor-client-pages.ts +0 -587
  72. package/scripts/refactor-server-controllers.ts +0 -471
  73. package/scripts/refactor-server-runtime-aliases.ts +0 -360
  74. package/scripts/restore-client-app-import-files.ts +0 -41
@@ -14,12 +14,12 @@ const { v4: uuid } = require('uuid') as { v4: () => string };
14
14
  import got from 'got';
15
15
  import hInterval from 'human-interval';
16
16
  import type express from 'express';
17
- import type { Request, Response, NextFunction } from 'express';
18
17
  import zod, { ZodError } from 'zod';
19
18
  export { default as schema } from 'zod';
20
19
 
21
20
  // Core
22
21
  import type { Application } from '@server/app/index';
22
+ import { runControllerAction, type TControllerActionDefinition } from '@server/app/controller';
23
23
  import Service, { TServiceArgs } from '@server/app/service';
24
24
  import context from '@server/context';
25
25
  import type DisksManager from '@server/services/disks';
@@ -30,9 +30,12 @@ import BaseRouter, {
30
30
  TErrorRoute,
31
31
  TRouteModule,
32
32
  TRouteOptions,
33
+ type TRouteDefinition,
34
+ type TRouteMetadata,
33
35
  defaultOptions,
34
36
  matchRoute,
35
37
  buildUrl,
38
+ withRouteMetadata,
36
39
  } from '@common/router';
37
40
  import type { TSsrUnresolvedRoute, TRegisterPageArgs } from '@common/router/contracts';
38
41
  import { buildRegex, getRegisterPageArgs } from '@common/router/register';
@@ -76,7 +79,7 @@ type TGeneratedControllerDefinition = {
76
79
  path: string;
77
80
  filepath: string;
78
81
  sourceLocation: { line: number; column: number };
79
- Controller: new (request: TRouterContext<TServerRouter>) => { [method: string]: () => any };
82
+ action: TControllerActionDefinition<any, any, any, any>;
80
83
  method: string;
81
84
  };
82
85
 
@@ -385,7 +388,7 @@ export default class ServerRouter<
385
388
  }
386
389
  }
387
390
 
388
- this.afterRegister();
391
+ this.refreshRouteRegistration();
389
392
  }
390
393
 
391
394
  private registerControllers(definitions: TGeneratedControllerDefinition[]) {
@@ -393,10 +396,8 @@ export default class ServerRouter<
393
396
  const route: TRoute<TRouterContext<this>> = {
394
397
  method: 'POST',
395
398
  path: definition.path,
396
- controller: (requestContext: TRouterContext<this>) => {
397
- const controller = new definition.Controller(requestContext);
398
- return controller[definition.method]();
399
- },
399
+ controller: (requestContext: TRouterContext<this>) =>
400
+ runControllerAction(definition.action, requestContext as any),
400
401
  options: { ...defaultOptions, filepath: definition.filepath, sourceLocation: definition.sourceLocation },
401
402
  };
402
403
 
@@ -419,7 +420,31 @@ export default class ServerRouter<
419
420
  - REGISTER
420
421
  ----------------------------------*/
421
422
 
422
- public page(...args: TRegisterPageArgs<any, TRouteOptions>) {
423
+ public registerRouteDefinition(definition: TRouteDefinition, metadata: TRouteMetadata = {}) {
424
+ if (definition.kind === 'page') {
425
+ return this.page(
426
+ definition.path,
427
+ withRouteMetadata(definition.options, metadata),
428
+ definition.data,
429
+ definition.render,
430
+ );
431
+ }
432
+
433
+ if (definition.kind === 'error') {
434
+ return this.error(definition.code, withRouteMetadata(definition.options, metadata), definition.render);
435
+ }
436
+
437
+ const method = definition.method === '*' ? '*' : (definition.method.toUpperCase() as TRouteHttpMethod);
438
+
439
+ return this.registerApi(
440
+ method,
441
+ definition.path,
442
+ withRouteMetadata(definition.options, metadata),
443
+ definition.handler as TServerController<this>,
444
+ );
445
+ }
446
+
447
+ protected page(...args: TRegisterPageArgs<any, TRouteOptions>) {
423
448
  const { path, options, data, renderer, layout } = getRegisterPageArgs(...args);
424
449
 
425
450
  const { regex, keys } = buildRegex(path);
@@ -442,7 +467,7 @@ export default class ServerRouter<
442
467
  return this;
443
468
  }
444
469
 
445
- public error(
470
+ protected error(
446
471
  code: number,
447
472
  options: Partial<TRouteOptions>,
448
473
  renderer: TFrontRenderer<{}, { message: string }>,
@@ -461,34 +486,13 @@ export default class ServerRouter<
461
486
  this.errors[code] = route;
462
487
  }
463
488
 
464
- public all = (...args: TApiRegisterArgs<this>) => this.registerApi('*', ...args);
465
- public options = (...args: TApiRegisterArgs<this>) => this.registerApi('OPTIONS', ...args);
466
- public get = (...args: TApiRegisterArgs<this>) => this.registerApi('GET', ...args);
467
- public post = (...args: TApiRegisterArgs<this>) => this.registerApi('POST', ...args);
468
- public put = (...args: TApiRegisterArgs<this>) => this.registerApi('PUT', ...args);
469
- public patch = (...args: TApiRegisterArgs<this>) => this.registerApi('PATCH', ...args);
470
- public delete = (...args: TApiRegisterArgs<this>) => this.registerApi('DELETE', ...args);
471
-
472
- public express(
473
- middleware: (req: Request, res: Response, next: NextFunction, requestContext: TRouterContext<this>) => void,
474
- ) {
475
- return (context: TRouterContext<this>) =>
476
- new Promise((resolve) => {
477
- context.request.res.on('finish', function () {
478
- //console.log('the response has been sent', request.res.statusCode);
479
- resolve(true);
480
- });
481
-
482
- middleware(
483
- context.request.req,
484
- context.request.res,
485
- () => {
486
- resolve(true);
487
- },
488
- context,
489
- );
490
- });
491
- }
489
+ protected all = (...args: TApiRegisterArgs<this>) => this.registerApi('*', ...args);
490
+ protected options = (...args: TApiRegisterArgs<this>) => this.registerApi('OPTIONS', ...args);
491
+ protected get = (...args: TApiRegisterArgs<this>) => this.registerApi('GET', ...args);
492
+ protected post = (...args: TApiRegisterArgs<this>) => this.registerApi('POST', ...args);
493
+ protected put = (...args: TApiRegisterArgs<this>) => this.registerApi('PUT', ...args);
494
+ protected patch = (...args: TApiRegisterArgs<this>) => this.registerApi('PATCH', ...args);
495
+ protected delete = (...args: TApiRegisterArgs<this>) => this.registerApi('DELETE', ...args);
492
496
 
493
497
  protected registerApi(method: TRouteHttpMethod, ...args: TApiRegisterArgs<this>): this {
494
498
  let path: string;
@@ -565,7 +569,11 @@ export default class ServerRouter<
565
569
  );
566
570
  }
567
571
 
568
- private async afterRegister() {
572
+ public refreshRouteRegistration() {
573
+ this.afterRegister();
574
+ }
575
+
576
+ private afterRegister() {
569
577
  // Ordonne par ordre de priorité
570
578
  this.config.debug && console.info('Loading routes ...');
571
579
  this.routes.sort((r1, r2) => {
@@ -579,6 +587,7 @@ export default class ServerRouter<
579
587
  return 0;
580
588
  });
581
589
  // - Génère les définitions de route pour le client
590
+ this.ssrRoutes = [];
582
591
  this.config.debug && console.info(`Registered routes:`);
583
592
  for (const route of this.routes) {
584
593
  const chunkId = route.options.id;
@@ -60,6 +60,7 @@ export type TRouterContext<TRouter extends TServerRouter> =
60
60
  // Request context
61
61
  {
62
62
  app: TServerRouterApplication<TRouter>;
63
+ services: TServerRouterApplication<TRouter>;
63
64
  context: TRouterContext<TRouter>; // = this
64
65
  request: ServerRequest<TRouter>;
65
66
  api: ServerRequest<TRouter>['api'];
@@ -155,14 +156,12 @@ export default class ServerResponse<
155
156
  const contextStore = context.getStore() as
156
157
  | {
157
158
  requestContext?: TRouterContext<TAnyRouter>;
158
- inputSchemaUsed?: boolean;
159
159
  ownerLabel?: string;
160
160
  ownerFilepath?: string;
161
161
  }
162
162
  | undefined;
163
163
  if (contextStore) {
164
164
  contextStore.requestContext = requestContext;
165
- contextStore.inputSchemaUsed = false;
166
165
  contextStore.ownerLabel = getRouteTraceTarget(route as TAnyRoute<TRouterContext<TServerRouter>>);
167
166
  contextStore.ownerFilepath = route.options.filepath || undefined;
168
167
  }
@@ -229,6 +228,7 @@ export default class ServerResponse<
229
228
  const requestContext: TRouterContext<TRouter> = {
230
229
  // Router context
231
230
  app: this.app,
231
+ services: this.app,
232
232
  context: undefined!,
233
233
  request: this.request,
234
234
  response: this,
@@ -104,11 +104,23 @@ test('standalone configure creates tracked instruction files with routing contra
104
104
  assert.match(agentsContent, /\/__proteum\/mcp/);
105
105
  assert.match(agentsContent, /central MCP ready banner/);
106
106
  assert.match(agentsContent, /proteum-mcp-v1/);
107
+ assert.match(agentsContent, /## Explicit App-Building Contract/);
108
+ assert.match(agentsContent, /defineApplication\(\{ services, router, models, commands \}\)/);
109
+ assert.match(agentsContent, /definePageRoute\(\{ path, options, data, render \}\)/);
110
+ assert.match(agentsContent, /defineController\(\{ path, actions \}\)/);
111
+ assert.match(agentsContent, /Never import `@app` in page, route, or controller files/);
112
+ assert.match(agentsContent, /Never call top-level `Router\.page\(\.\.\.\)`/);
107
113
  assert.match(agentsContent, /## Triggered Instruction Reads/);
114
+ assert.match(agentsContent, /Worktree Preflight/);
115
+ assert.match(agentsContent, /npx proteum worktree init --source <source-app-root>/);
116
+ assert.match(agentsContent, /--skip-deps --reason/);
108
117
  assert.match(agentsContent, /Git lifecycle/);
109
118
  assert.match(agentsContent, /read Root contract fallback before any git write/);
119
+ assert.match(agentsContent, /Before git writes after a bug fix, behavior change, decision change, or docs-relevant production change/);
110
120
  assert.match(agentsContent, /add or update focused unit tests/);
111
- assert.match(agentsContent, /read Root contract fallback, `CODING_STYLE\.md`, `tests\/AGENTS\.md`/);
121
+ assert.match(agentsContent, /read Root contract fallback, `DOCUMENTATION\.md`, `CODING_STYLE\.md`, `tests\/AGENTS\.md`/);
122
+ assert.match(agentsContent, /Bug fixes, regressions, incidents, broken public routes, auth\/OAuth failures/);
123
+ assert.match(agentsContent, /docs\/fixes\/YYYY-MM-DD-short-bug-name\.md/);
112
124
  assert.match(agentsContent, /GEO\/SEO\/crawler\/structured-data\/AI-source changes/);
113
125
  assert.match(agentsContent, /MCP-selected previews are enough/);
114
126
  assert.doesNotMatch(agentsContent, /Conventional Commits/);
@@ -234,6 +246,7 @@ test('monorepo configure writes root and app instruction files', () => {
234
246
  assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /## Known Proteum Apps/);
235
247
  assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /apps\/product/);
236
248
  assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /Do not start `npx proteum dev` from this root/);
249
+ assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /Worktree Preflight/);
237
250
  assert.match(fs.readFileSync(path.join(monorepoRoot, 'CODING_STYLE.md'), 'utf8'), /## Source: CODING_STYLE\.md/);
238
251
  assert.match(fs.readFileSync(path.join(monorepoRoot, 'DOCUMENTATION.md'), 'utf8'), /## Source: DOCUMENTATION\.md/);
239
252
  assert.match(fs.readFileSync(path.join(monorepoRoot, 'diagnostics.md'), 'utf8'), /## Source: diagnostics\.md/);
@@ -1,4 +1,5 @@
1
1
  const assert = require('node:assert/strict');
2
+ const crypto = require('node:crypto');
2
3
  const { spawn, spawnSync } = require('node:child_process');
3
4
  const fs = require('node:fs');
4
5
  const http = require('node:http');
@@ -55,6 +56,42 @@ const createProteumApp = (appRoot, { routerPort = 3020 } = {}) => {
55
56
  );
56
57
  };
57
58
 
59
+ const hashFile = (filepath) => crypto.createHash('sha256').update(fs.readFileSync(filepath)).digest('hex');
60
+
61
+ const writeFreshWorktreeBootstrapMarker = (appRoot) => {
62
+ const timestamp = new Date().toISOString();
63
+ fs.mkdirSync(path.join(appRoot, 'node_modules'), { recursive: true });
64
+ writeFile(path.join(appRoot, '.env'), 'PORT=3020\n');
65
+ writeFile(path.join(appRoot, 'package-lock.json'), '{"lockfileVersion":3}\n');
66
+ writeFile(path.join(appRoot, 'AGENTS.md'), '# Agents\n');
67
+
68
+ writeFile(
69
+ path.join(appRoot, '.proteum', 'worktree-bootstrap.json'),
70
+ JSON.stringify(
71
+ {
72
+ version: 1,
73
+ createdAt: timestamp,
74
+ updatedAt: timestamp,
75
+ proteumVersion: require('../package.json').version,
76
+ packageLockHash: hashFile(path.join(appRoot, 'package-lock.json')),
77
+ proteumConfigHash: hashFile(path.join(appRoot, 'proteum.config.ts')),
78
+ agentsHash: hashFile(path.join(appRoot, 'AGENTS.md')),
79
+ env: { present: true, copied: false },
80
+ refresh: { status: 'ok', ranAt: timestamp },
81
+ dependencies: {
82
+ status: 'up-to-date',
83
+ ranAt: timestamp,
84
+ nodeModulesPresent: true,
85
+ packageLockHash: hashFile(path.join(appRoot, 'package-lock.json')),
86
+ },
87
+ runtimeStatus: { status: 'ok', checkedAt: timestamp, summary: 'runtime ok' },
88
+ },
89
+ null,
90
+ 2,
91
+ ),
92
+ );
93
+ };
94
+
58
95
  const listen = async (server, port = 0) =>
59
96
  await new Promise((resolve, reject) => {
60
97
  server.once('error', reject);
@@ -150,6 +187,7 @@ test('top-level help lists the machine-scope mcp router', () => {
150
187
 
151
188
  assert.equal(result.status, 0);
152
189
  assert.match(result.stdout, /proteum mcp\b/);
190
+ assert.match(result.stdout, /proteum worktree\b/);
153
191
  assert.match(result.stdout, /machine-scope MCP router/);
154
192
  });
155
193
 
@@ -287,6 +325,52 @@ test('runtime status manifest guard points to explain manifest', () => {
287
325
  assert.match(payload.nextActions[0].command, /proteum explain --manifest/);
288
326
  });
289
327
 
328
+ test('runtime status blocks unbootstrapped Codex worktrees', () => {
329
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-cli-worktree-block-'));
330
+ const appRoot = path.join(root, '.codex', 'worktrees', 'product');
331
+ createProteumApp(appRoot);
332
+
333
+ const result = spawnSync(process.execPath, [cliBin, 'runtime', 'status'], {
334
+ cwd: appRoot,
335
+ encoding: 'utf8',
336
+ });
337
+ const payload = JSON.parse(result.stdout);
338
+
339
+ assert.equal(result.status, 1);
340
+ assert.equal(payload.ok, false);
341
+ assert.match(payload.summary, /has not completed Proteum worktree bootstrap/);
342
+ assert.match(payload.nextActions[0].command, /proteum worktree init --source <source-app-root>/);
343
+ });
344
+
345
+ test('runtime status allows fresh Codex worktree bootstrap markers and reports stale refresh action', () => {
346
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-cli-worktree-fresh-'));
347
+ const appRoot = path.join(root, '.codex', 'worktrees', 'product');
348
+ createProteumApp(appRoot);
349
+ writeFreshWorktreeBootstrapMarker(appRoot);
350
+
351
+ const fresh = spawnSync(process.execPath, [cliBin, 'runtime', 'status'], {
352
+ cwd: appRoot,
353
+ encoding: 'utf8',
354
+ });
355
+ const freshPayload = JSON.parse(fresh.stdout);
356
+
357
+ assert.equal(fresh.status, 0);
358
+ assert.equal(freshPayload.ok, true);
359
+ assert.equal(freshPayload.data.worktreeBootstrap.state, 'fresh');
360
+
361
+ writeFile(path.join(appRoot, 'package-lock.json'), '{"lockfileVersion":3,"changed":true}\n');
362
+
363
+ const stale = spawnSync(process.execPath, [cliBin, 'runtime', 'status'], {
364
+ cwd: appRoot,
365
+ encoding: 'utf8',
366
+ });
367
+ const stalePayload = JSON.parse(stale.stdout);
368
+
369
+ assert.equal(stale.status, 1);
370
+ assert.equal(stalePayload.ok, false);
371
+ assert.match(stalePayload.nextActions[0].command, /--refresh/);
372
+ });
373
+
290
374
  test('runtime status reports occupied configured port without probing page bodies', async () => {
291
375
  const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-cli-port-'));
292
376
  const otherRoot = path.join(repoRoot, 'apps', 'other');