proteum 2.4.4 → 2.5.1

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 (79) hide show
  1. package/README.md +81 -52
  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 +5 -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/server/services/AGENTS.md +4 -0
  12. package/agents/project/tests/AGENTS.md +1 -1
  13. package/cli/commands/doctor.ts +54 -3
  14. package/cli/commands/runtime.ts +6 -0
  15. package/cli/commands/worktree.ts +116 -0
  16. package/cli/compiler/artifacts/controllers.ts +16 -15
  17. package/cli/compiler/artifacts/discovery.ts +129 -17
  18. package/cli/compiler/artifacts/routing.ts +0 -5
  19. package/cli/compiler/artifacts/services.ts +253 -76
  20. package/cli/compiler/common/controllers.ts +159 -57
  21. package/cli/compiler/common/generatedRouteModules.ts +457 -363
  22. package/cli/mcp/router.ts +47 -3
  23. package/cli/presentation/commands.ts +25 -15
  24. package/cli/runtime/commands.ts +39 -12
  25. package/cli/runtime/worktreeBootstrap.ts +608 -0
  26. package/cli/scaffold/index.ts +28 -18
  27. package/cli/scaffold/templates.ts +44 -33
  28. package/cli/utils/agents.ts +14 -1
  29. package/client/app/index.ts +22 -5
  30. package/client/services/router/index.tsx +23 -3
  31. package/client/services/router/request/api.ts +16 -6
  32. package/common/dev/contractsDoctor.ts +1 -1
  33. package/common/dev/mcpPayloads.ts +8 -1
  34. package/common/env/proteumEnv.ts +14 -2
  35. package/common/router/contracts.ts +1 -1
  36. package/common/router/definitions.ts +177 -0
  37. package/common/router/index.ts +23 -12
  38. package/common/router/pageData.ts +5 -5
  39. package/common/router/register.ts +2 -2
  40. package/common/router/request/api.ts +12 -2
  41. package/docs/agent-routing.md +5 -2
  42. package/docs/diagnostics.md +2 -0
  43. package/docs/mcp.md +6 -3
  44. package/docs/migration-2.5.md +226 -0
  45. package/eslint.js +89 -42
  46. package/package.json +1 -1
  47. package/server/app/commands.ts +5 -1
  48. package/server/app/container/console/index.ts +1 -1
  49. package/server/app/controller/index.ts +98 -40
  50. package/server/app/index.ts +120 -3
  51. package/server/app/service/index.ts +5 -1
  52. package/server/index.ts +6 -2
  53. package/server/services/router/index.ts +50 -41
  54. package/server/services/router/response/index.ts +2 -2
  55. package/tests/agents-utils.test.cjs +14 -1
  56. package/tests/cli-mcp-command.test.cjs +84 -0
  57. package/tests/client-app-error-handling.test.cjs +100 -0
  58. package/tests/definition-contracts.test.cjs +453 -0
  59. package/tests/dev-transpile-watch.test.cjs +37 -31
  60. package/tests/eslint-rules.test.cjs +185 -8
  61. package/tests/mcp.test.cjs +90 -0
  62. package/tests/scaffold-templates.test.cjs +18 -0
  63. package/tests/server-app-report-error.test.cjs +135 -0
  64. package/tests/worktree-bootstrap.test.cjs +206 -0
  65. package/types/aliases.d.ts +0 -5
  66. package/types/controller-input.test.ts +23 -17
  67. package/types/controller-request-context.test.ts +10 -11
  68. package/cli/commands/migrate.ts +0 -51
  69. package/cli/migrate/pageContract.ts +0 -516
  70. package/docs/migrate-from-2.1.3.md +0 -396
  71. package/scripts/cleanup-generated-controllers.ts +0 -62
  72. package/scripts/fix-reference-app-typing.ts +0 -490
  73. package/scripts/format-router-registrations.ts +0 -119
  74. package/scripts/migrate-explicit-controllers-and-request.ts +0 -423
  75. package/scripts/refactor-client-app-imports.ts +0 -244
  76. package/scripts/refactor-client-pages.ts +0 -587
  77. package/scripts/refactor-server-controllers.ts +0 -471
  78. package/scripts/refactor-server-runtime-aliases.ts +0 -360
  79. package/scripts/restore-client-app-import-files.ts +0 -41
@@ -5,8 +5,6 @@
5
5
  // Npm
6
6
  import zod from 'zod';
7
7
 
8
- // Core
9
- import context from '@server/context';
10
8
  import {
11
9
  toValidationSchema,
12
10
  type TValidationSchema,
@@ -63,11 +61,6 @@ export type TControllerRequestContext<
63
61
  } & (TRouter extends TAnyRouter ? TRouterContextServices<TControllerRouter<TRouter>> : {}) &
64
62
  TRequestServices;
65
63
 
66
- type TControllerBaseContext<TApplication extends object> = {
67
- app: TApplication;
68
- request: { data: TObjetDonnees };
69
- };
70
-
71
64
  type TControllerDefaultContext<TApplication extends object, TRequestServices extends object> = {
72
65
  app: TApplication;
73
66
  context: object;
@@ -80,43 +73,108 @@ type TControllerDefaultContext<TApplication extends object, TRequestServices ext
80
73
  } & TRouterContextServices<TControllerApplicationRouter<TApplication>> &
81
74
  TRequestServices;
82
75
 
83
- /*----------------------------------
84
- - CLASS
85
- ----------------------------------*/
86
-
87
- export default abstract class Controller<
76
+ export type TControllerActionContext<
77
+ TInput = undefined,
88
78
  TApplication extends object = object,
89
- TRouter extends object = object,
90
79
  TRequestServices extends object = {},
91
- TContext extends TControllerBaseContext<TApplication> = TControllerDefaultContext<TApplication, TRequestServices>,
92
- > {
93
- public constructor(public request: TContext) {}
94
-
95
- public get app(): TApplication {
96
- return this.request.app as TApplication;
97
- }
98
-
99
- public get services(): TApplication {
100
- return this.app;
101
- }
102
-
103
- public get models(): TControllerModelsClient<TApplication> {
104
- const app = this.app as {
105
- models?: { client?: TControllerModelsClient<TApplication> };
106
- Models?: { client?: TControllerModelsClient<TApplication> };
107
- };
108
- return (app.models?.client ?? app.Models?.client) as TControllerModelsClient<TApplication>;
109
- }
80
+ > = TControllerDefaultContext<TApplication, TRequestServices> & {
81
+ input: TInput;
82
+ models: TControllerModelsClient<TApplication>;
83
+ services: TApplication;
84
+ };
110
85
 
111
- public input<TSchema extends TValidationSchema>(schema: TSchema): zod.output<TSchema>;
112
- public input<TShape extends TValidationShape>(schema: TShape): zod.output<zod.ZodObject<TShape>>;
113
- public input(schema: TValidationSchema | TValidationShape) {
114
- const store = context.getStore() as { inputSchemaUsed?: boolean } | undefined;
86
+ export type TControllerActionDefinition<
87
+ TInput = undefined,
88
+ TResult = unknown,
89
+ TApplication extends object = object,
90
+ TRequestServices extends object = {},
91
+ > = {
92
+ input?: TValidationSchema | TValidationShape;
93
+ handler: (context: TControllerActionContext<TInput, TApplication, TRequestServices>) => TResult;
94
+ };
115
95
 
116
- if (store?.inputSchemaUsed) throw new Error('Controller.input() can only be called once per request handler.');
96
+ export type TControllerDefinition<
97
+ TActions extends Record<string, TControllerActionDefinition<any, any, any, any>>,
98
+ > = {
99
+ kind: 'controller';
100
+ path?: string;
101
+ actions: TActions;
102
+ };
117
103
 
118
- if (store) store.inputSchemaUsed = true;
104
+ export type TControllerActionInput<TController, TMethod extends keyof any> = TController extends { actions: infer TActions }
105
+ ? TMethod extends keyof TActions
106
+ ? TActions[TMethod] extends TControllerActionDefinition<infer TInput, any, any, any>
107
+ ? TInput
108
+ : never
109
+ : never
110
+ : never;
119
111
 
120
- return toValidationSchema(schema).parse(this.request.request.data);
121
- }
112
+ export type TControllerActionResult<TController, TMethod extends keyof any> = TController extends {
113
+ actions: infer TActions;
114
+ }
115
+ ? TMethod extends keyof TActions
116
+ ? TActions[TMethod] extends TControllerActionDefinition<any, infer TResult, any, any>
117
+ ? Awaited<TResult>
118
+ : never
119
+ : never
120
+ : never;
121
+
122
+ export function defineAction<TSchema extends TValidationSchema, TResult>(
123
+ definition: {
124
+ input: TSchema;
125
+ handler: (context: TControllerActionContext<zod.output<TSchema>>) => TResult;
126
+ },
127
+ ): TControllerActionDefinition<zod.output<TSchema>, TResult>;
128
+ export function defineAction<TShape extends TValidationShape, TResult>(
129
+ definition: {
130
+ input: TShape;
131
+ handler: (context: TControllerActionContext<zod.output<zod.ZodObject<TShape>>>) => TResult;
132
+ },
133
+ ): TControllerActionDefinition<zod.output<zod.ZodObject<TShape>>, TResult>;
134
+ export function defineAction<TResult>(
135
+ definition: {
136
+ handler: (context: TControllerActionContext<undefined>) => TResult;
137
+ },
138
+ ): TControllerActionDefinition<undefined, TResult>;
139
+ export function defineAction(definition: {
140
+ input?: TValidationSchema | TValidationShape;
141
+ handler: (context: TControllerActionContext<any>) => unknown;
142
+ }) {
143
+ return definition;
122
144
  }
145
+
146
+ export const defineController = <
147
+ TActions extends Record<string, TControllerActionDefinition<any, any, any, any>>,
148
+ >({
149
+ path,
150
+ actions,
151
+ }: {
152
+ path?: string;
153
+ actions: TActions;
154
+ }): TControllerDefinition<TActions> => ({
155
+ kind: 'controller',
156
+ path,
157
+ actions,
158
+ });
159
+
160
+ export const runControllerAction = (
161
+ action: TControllerActionDefinition<any, any, any, any>,
162
+ requestContext: TControllerDefaultContext<any, any>,
163
+ ) => {
164
+ const input = action.input === undefined ? undefined : toValidationSchema(action.input).parse(requestContext.request.data);
165
+ const app = requestContext.app as {
166
+ models?: { client?: object };
167
+ Models?: { client?: object };
168
+ };
169
+ const appRecord = app as Record<string, unknown>;
170
+ const explicitModels = Object.prototype.hasOwnProperty.call(appRecord, 'models')
171
+ ? (appRecord.models as { client?: object } | undefined)
172
+ : undefined;
173
+
174
+ return action.handler({
175
+ ...requestContext,
176
+ input,
177
+ models: explicitModels?.client ?? app.Models?.client ?? {},
178
+ services: requestContext.app,
179
+ });
180
+ };
@@ -40,6 +40,33 @@ export type TApplicationStartOptions = {
40
40
 
41
41
  export const Service = ServicesContainer;
42
42
 
43
+ type TApplicationDefinitionValue<TValue, TApplication extends object> =
44
+ | TValue
45
+ | ((app: TApplication) => TValue);
46
+
47
+ type TDefinedApplicationContext<
48
+ TServices extends Record<string, unknown>,
49
+ TRouter,
50
+ TModels = unknown,
51
+ > = Application &
52
+ TServices & {
53
+ Router: TRouter;
54
+ models?: TModels;
55
+ Models?: TModels;
56
+ };
57
+
58
+ export type TApplicationDefinition<
59
+ TServices extends Record<string, unknown> = {},
60
+ TRouter = unknown,
61
+ TModels = unknown,
62
+ TCommands = unknown,
63
+ > = {
64
+ services?: TApplicationDefinitionValue<TServices, TDefinedApplicationContext<TServices, TRouter>>;
65
+ router?: TApplicationDefinitionValue<TRouter, TDefinedApplicationContext<TServices, TRouter>>;
66
+ models?: TApplicationDefinitionValue<TModels, TDefinedApplicationContext<TServices, TRouter>>;
67
+ commands?: TApplicationDefinitionValue<TCommands, TDefinedApplicationContext<TServices, TRouter, TModels>>;
68
+ };
69
+
43
70
  // Without prettify, we don't get a clear list of the class properties
44
71
  type Prettify<T> = { [K in keyof T]: T[K] } & {};
45
72
 
@@ -65,6 +92,24 @@ const createCommandsManager = (app: Application) => new CommandsManager(app, { d
65
92
  const createDevCommandsRegistry = (app: Application) => new DevCommandsRegistry(app);
66
93
  const createDevDiagnosticsRegistry = (app: Application) => new DevDiagnosticsRegistry(app);
67
94
 
95
+ const normalizeReportedError = (rejection: unknown, fallbackMessage: string): Error => {
96
+ if (rejection instanceof Error) return rejection;
97
+ if (typeof rejection === 'string') return new Error(rejection);
98
+
99
+ return new Error(fallbackMessage);
100
+ };
101
+
102
+ const getNumericErrorProperty = (error: Error, property: 'http' | 'status' | 'statusCode') => {
103
+ const value = (error as Error & Partial<Record<typeof property, unknown>>)[property];
104
+ return typeof value === 'number' ? value : undefined;
105
+ };
106
+
107
+ const getErrorHttpCode = (error: Error) =>
108
+ getNumericErrorProperty(error, 'http') ??
109
+ getNumericErrorProperty(error, 'status') ??
110
+ getNumericErrorProperty(error, 'statusCode') ??
111
+ 500;
112
+
68
113
  /*----------------------------------
69
114
  - FUNCTIONS
70
115
  ----------------------------------*/
@@ -78,7 +123,7 @@ export abstract class Application<
78
123
  public app!: this;
79
124
  public servicesContainer!: TServicesContainer;
80
125
  public userType!: TUser;
81
- public declare Router: object;
126
+ public declare Router: unknown;
82
127
 
83
128
  /*----------------------------------
84
129
  - PROPERTIES
@@ -111,12 +156,12 @@ export abstract class Application<
111
156
  // Handle unhandled crash
112
157
  this.on('error', (e, request) => this.container.handleBug(e, 'An error occured in the application', request));
113
158
 
114
- process.on('unhandledRejection', (error: any, _promise: any) => {
159
+ process.on('unhandledRejection', (error: unknown) => {
115
160
  // Log so we know it's coming from unhandledRejection
116
161
  console.error('unhandledRejection', error);
117
162
 
118
163
  // We don't log the error here because it's the role of the app to decidehiw to log errors
119
- this.runHook('error', error);
164
+ void this.reportError(error);
120
165
  });
121
166
 
122
167
  // We can't pass this in super so we assign here
@@ -128,6 +173,14 @@ export abstract class Application<
128
173
  return this.container.Console.createBugReport(new Anomaly(...anomalyArgs));
129
174
  }
130
175
 
176
+ public async reportError(rejection: unknown, request?: ServerRequest<TServerRouter>) {
177
+ const error = normalizeReportedError(rejection, 'Unknown application error');
178
+ const code = getErrorHttpCode(error);
179
+
180
+ if (code === 500) await this.runHook('error', error, request);
181
+ else await this.runHook('error.' + code, error, request);
182
+ }
183
+
131
184
  /*----------------------------------
132
185
  - COMMANDS
133
186
  ----------------------------------*/
@@ -268,4 +321,68 @@ export abstract class Application<
268
321
  }
269
322
  }
270
323
 
324
+ const resolveApplicationDefinitionValue = <TValue, TApplication extends object>(
325
+ value: TApplicationDefinitionValue<TValue, TApplication> | undefined,
326
+ app: TApplication,
327
+ ): TValue | undefined => (typeof value === 'function' ? (value as (app: TApplication) => TValue)(app) : value);
328
+
329
+ const assignApplicationRecord = (target: object, value: unknown) => {
330
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return;
331
+
332
+ Object.assign(target, value);
333
+ };
334
+
335
+ export const defineApplication = <
336
+ TServices extends Record<string, unknown> = {},
337
+ TRouter = unknown,
338
+ TModels = unknown,
339
+ TCommands = unknown,
340
+ >(
341
+ definition: TApplicationDefinition<TServices, TRouter, TModels, TCommands>,
342
+ ) => {
343
+ type TDefinedApplication = Application &
344
+ TServices & {
345
+ Router: TRouter;
346
+ models?: TModels;
347
+ Models?: TModels;
348
+ commands?: TCommands;
349
+ };
350
+
351
+ class DefinedApplication extends Application {
352
+ public constructor() {
353
+ super();
354
+
355
+ const self = this as unknown as TDefinedApplication & Record<string, unknown>;
356
+ const services = resolveApplicationDefinitionValue(
357
+ definition.services,
358
+ self as TDefinedApplicationContext<TServices, TRouter>,
359
+ );
360
+ assignApplicationRecord(self, services);
361
+
362
+ const router = resolveApplicationDefinitionValue(
363
+ definition.router,
364
+ self as TDefinedApplicationContext<TServices, TRouter>,
365
+ );
366
+ if (router !== undefined) (self as Record<string, unknown>).Router = router;
367
+
368
+ const models = resolveApplicationDefinitionValue(
369
+ definition.models,
370
+ self as TDefinedApplicationContext<TServices, TRouter>,
371
+ );
372
+ if (models !== undefined) {
373
+ (self as Record<string, unknown>).models = models;
374
+ (self as Record<string, unknown>).Models = models;
375
+ }
376
+
377
+ const commands = resolveApplicationDefinitionValue(
378
+ definition.commands,
379
+ self as TDefinedApplicationContext<TServices, TRouter, TModels>,
380
+ );
381
+ if (commands !== undefined) (self as Record<string, unknown>).commands = commands;
382
+ }
383
+ }
384
+
385
+ return DefinedApplication as unknown as new () => TDefinedApplication;
386
+ };
387
+
271
388
  export default Application;
@@ -145,7 +145,11 @@ export default abstract class Service<
145
145
  models?: { client?: TServiceModelsClient<TApplication> };
146
146
  Models?: { client?: TServiceModelsClient<TApplication> };
147
147
  };
148
- const models = app.models?.client ?? app.Models?.client;
148
+ const appRecord = app as Record<string, unknown>;
149
+ const explicitModels = Object.prototype.hasOwnProperty.call(appRecord, 'models')
150
+ ? (appRecord.models as { client?: TServiceModelsClient<TApplication> } | undefined)
151
+ : undefined;
152
+ const models = explicitModels?.client ?? app.Models?.client;
149
153
 
150
154
  if (!models)
151
155
  throw new Error(`${this.constructor.name} tried to access models but no Models service is registered.`);
package/server/index.ts CHANGED
@@ -3,6 +3,10 @@ import Application from '@/server/index';
3
3
  import { isServerHotReloadRequest, serverHotReloadMessageType } from '@common/dev/serverHotReload';
4
4
 
5
5
  const application = AppContainer.start(Application);
6
+ const router = application.Router as {
7
+ started?: Promise<unknown>;
8
+ reloadGeneratedDefinitions?: (changedFiles: string[]) => Promise<unknown>;
9
+ };
6
10
  let shutdownPromise: Promise<void> | undefined;
7
11
 
8
12
  const shutdownApplication = async (reason: string) => {
@@ -36,8 +40,8 @@ if (__DEV__ && typeof process.send === 'function') {
36
40
 
37
41
  void (async () => {
38
42
  try {
39
- await application.Router?.started;
40
- await application.Router.reloadGeneratedDefinitions(message.changedFiles);
43
+ await router.started;
44
+ await router.reloadGeneratedDefinitions?.(message.changedFiles);
41
45
 
42
46
  process.send?.({ type: serverHotReloadMessageType.succeeded, changedFiles: message.changedFiles });
43
47
  } catch (error) {
@@ -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;
@@ -628,7 +637,7 @@ export default class ServerRouter<
628
637
  error instanceof Error ? error : new Error(typeof error === 'string' ? error : 'Unknown request.finished hook error');
629
638
 
630
639
  try {
631
- await this.app.runHook('error', typedError, request);
640
+ await this.app.reportError(typedError, request);
632
641
  } catch (hookError) {
633
642
  console.error('request.finished hook error', typedError, 'Error hook failure', hookError);
634
643
  }
@@ -1102,7 +1111,7 @@ export default class ServerRouter<
1102
1111
  console.log(LogPrefix, 'Error catched from the router:', error);
1103
1112
 
1104
1113
  // Report error
1105
- await this.app.runHook('error', error, request);
1114
+ await this.app.reportError(error, request);
1106
1115
 
1107
1116
  // Don't exose technical errors to users
1108
1117
  if (this.app.env.profile === 'prod')
@@ -1114,7 +1123,7 @@ export default class ServerRouter<
1114
1123
  /*if (this.app.env.profile === "dev")
1115
1124
  console.warn(e);*/
1116
1125
 
1117
- await this.app.runHook('error.' + code, error, request);
1126
+ await this.app.reportError(error, request);
1118
1127
  }
1119
1128
 
1120
1129
  // Return error based on the request format
@@ -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/);