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
@@ -98,8 +98,8 @@ const getNodeLocation = (sourceFile: ts.SourceFile, node: ts.Node): TControllerS
98
98
  return { line: line + 1, column: character + 1 };
99
99
  };
100
100
 
101
- const hasModifier = (node: ts.Node, kind: ts.SyntaxKind) =>
102
- !!node.modifiers?.some((modifier) => modifier.kind === kind);
101
+ const hasModifier = (node: ts.Node & { modifiers?: ts.NodeArray<ts.ModifierLike> }, kind: ts.SyntaxKind) =>
102
+ !!node.modifiers?.some((modifier: ts.ModifierLike) => modifier.kind === kind);
103
103
 
104
104
  const getDefaultExportClass = (sourceFile: ts.SourceFile) => {
105
105
  const classes = new Map<string, ts.ClassDeclaration>();
@@ -125,42 +125,165 @@ const getDefaultExportClass = (sourceFile: ts.SourceFile) => {
125
125
  return undefined;
126
126
  };
127
127
 
128
- const getExportedString = (sourceFile: ts.SourceFile, exportName: string) => {
128
+ const getPropertyKey = (name: ts.PropertyName) => {
129
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text;
130
+ return undefined;
131
+ };
132
+
133
+ const getPropertyAssignment = (node: ts.ObjectLiteralExpression, propertyName: string) => {
134
+ for (const property of node.properties) {
135
+ if (!ts.isPropertyAssignment(property)) continue;
136
+
137
+ const key = getPropertyKey(property.name);
138
+ if (key === propertyName) return property.initializer;
139
+ }
140
+
141
+ return undefined;
142
+ };
143
+
144
+ const collectConstInitializers = (sourceFile: ts.SourceFile) => {
145
+ const initializers = new Map<string, ts.Expression>();
146
+
129
147
  for (const statement of sourceFile.statements) {
130
148
  if (!ts.isVariableStatement(statement)) continue;
131
- if (!hasModifier(statement, ts.SyntaxKind.ExportKeyword)) continue;
149
+ if (!(statement.declarationList.flags & ts.NodeFlags.Const)) continue;
132
150
 
133
151
  for (const declaration of statement.declarationList.declarations) {
134
- if (!ts.isIdentifier(declaration.name)) continue;
135
- if (declaration.name.text !== exportName) continue;
136
- if (!declaration.initializer || !ts.isStringLiteral(declaration.initializer)) continue;
152
+ if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue;
137
153
 
138
- return declaration.initializer.text;
154
+ initializers.set(declaration.name.text, declaration.initializer);
139
155
  }
140
156
  }
141
157
 
158
+ return initializers;
159
+ };
160
+
161
+ const tryEvaluateStaticString = (
162
+ sourceFile: ts.SourceFile,
163
+ expression: ts.Expression,
164
+ activeNames = new Set<string>(),
165
+ ): string | undefined => {
166
+ if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) return expression.text;
167
+
168
+ if (ts.isIdentifier(expression)) {
169
+ if (activeNames.has(expression.text)) return undefined;
170
+
171
+ const initializer = collectConstInitializers(sourceFile).get(expression.text);
172
+ if (!initializer) return undefined;
173
+
174
+ activeNames.add(expression.text);
175
+ const value = tryEvaluateStaticString(sourceFile, initializer, activeNames);
176
+ activeNames.delete(expression.text);
177
+
178
+ return value;
179
+ }
180
+
181
+ return undefined;
182
+ };
183
+
184
+ const resolveIdentifierExpression = (sourceFile: ts.SourceFile, expression: ts.Expression): ts.Expression => {
185
+ if (!ts.isIdentifier(expression)) return expression;
186
+
187
+ return collectConstInitializers(sourceFile).get(expression.text) || expression;
188
+ };
189
+
190
+ const getDefaultControllerExpression = (sourceFile: ts.SourceFile) => {
191
+ for (const statement of sourceFile.statements) {
192
+ if (!ts.isExportAssignment(statement) || statement.isExportEquals) continue;
193
+
194
+ return resolveIdentifierExpression(sourceFile, statement.expression);
195
+ }
196
+
197
+ return undefined;
198
+ };
199
+
200
+ const getCallExpressionName = (node: ts.Expression) => {
201
+ if (ts.isIdentifier(node)) return node.text;
202
+ if (ts.isPropertyAccessExpression(node)) return node.name.text;
142
203
  return undefined;
143
204
  };
144
205
 
145
- const countInputCalls = (method: ts.MethodDeclaration) => {
146
- let inputCallsCount = 0;
147
-
148
- const visit = (node: ts.Node) => {
149
- if (
150
- ts.isCallExpression(node) &&
151
- ts.isPropertyAccessExpression(node.expression) &&
152
- node.expression.expression.kind === ts.SyntaxKind.ThisKeyword &&
153
- node.expression.name.text === 'input'
154
- ) {
155
- inputCallsCount++;
206
+ const assertNoControllerMagicImports = (sourceFile: ts.SourceFile) => {
207
+ for (const statement of sourceFile.statements) {
208
+ if (!ts.isImportDeclaration(statement)) continue;
209
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
210
+ if (statement.moduleSpecifier.text !== '@app') continue;
211
+
212
+ const location = getNodeLocation(sourceFile, statement);
213
+ throw new Error(
214
+ `${sourceFile.fileName}:${location.line}:${location.column} imports @app. Controller modules must export defineController(...) and receive services through typed action context.`,
215
+ );
216
+ }
217
+ };
218
+
219
+ const readExplicitController = (
220
+ sourceFile: ts.SourceFile,
221
+ filepath: string,
222
+ root: string,
223
+ ): TControllerFileMeta | undefined => {
224
+ const expression = getDefaultControllerExpression(sourceFile);
225
+ if (!expression) return undefined;
226
+
227
+ assertNoControllerMagicImports(sourceFile);
228
+
229
+ if (!ts.isCallExpression(expression) || getCallExpressionName(expression.expression) !== 'defineController') {
230
+ const location = getNodeLocation(sourceFile, expression);
231
+ throw new Error(
232
+ `${filepath}:${location.line}:${location.column} must default-export defineController({ path, actions }). Legacy controller classes are no longer supported.`,
233
+ );
234
+ }
235
+
236
+ const [definitionArg] = [...expression.arguments];
237
+ if (!definitionArg || !ts.isObjectLiteralExpression(definitionArg)) {
238
+ throw new Error(`defineController(...) in ${filepath} must receive an object literal.`);
239
+ }
240
+
241
+ const pathExpression = getPropertyAssignment(definitionArg, 'path');
242
+ const routeBasePath =
243
+ pathExpression ? tryEvaluateStaticString(sourceFile, pathExpression) || getControllerBasePathFromFilepath(filepath, root) : getControllerBasePathFromFilepath(filepath, root);
244
+ const actionsExpression = getPropertyAssignment(definitionArg, 'actions');
245
+
246
+ if (!actionsExpression || !ts.isObjectLiteralExpression(actionsExpression)) {
247
+ throw new Error(`defineController(...) in ${filepath} must declare an actions object literal.`);
248
+ }
249
+
250
+ const methods: TControllerMethodMeta[] = [];
251
+
252
+ for (const property of actionsExpression.properties) {
253
+ if (!ts.isPropertyAssignment(property)) continue;
254
+
255
+ const methodName = getPropertyKey(property.name);
256
+ if (!methodName) continue;
257
+
258
+ const actionExpression = resolveIdentifierExpression(sourceFile, property.initializer);
259
+ if (!ts.isCallExpression(actionExpression) || getCallExpressionName(actionExpression.expression) !== 'defineAction') {
260
+ const location = getNodeLocation(sourceFile, property);
261
+ throw new Error(`${filepath}:${location.line}:${location.column} controller actions must use defineAction(...).`);
156
262
  }
157
263
 
158
- ts.forEachChild(node, visit);
159
- };
264
+ const [actionArg] = [...actionExpression.arguments];
265
+ const hasInput =
266
+ !!actionArg &&
267
+ ts.isObjectLiteralExpression(actionArg) &&
268
+ getPropertyAssignment(actionArg, 'input') !== undefined;
269
+
270
+ methods.push({
271
+ name: methodName,
272
+ inputCallsCount: hasInput ? 1 : 0,
273
+ routePath: [routeBasePath, methodName].filter(Boolean).join('/'),
274
+ sourceLocation: getNodeLocation(sourceFile, property.name),
275
+ });
276
+ }
160
277
 
161
- if (method.body) ts.forEachChild(method.body, visit);
278
+ if (!methods.length) return undefined;
162
279
 
163
- return inputCallsCount;
280
+ return {
281
+ filepath,
282
+ importPath: '',
283
+ className: path.basename(filepath, '.ts').replace(/[^A-Za-z0-9_$]+/g, '_') || 'Controller',
284
+ routeBasePath,
285
+ methods,
286
+ };
164
287
  };
165
288
 
166
289
  /*----------------------------------
@@ -176,45 +299,24 @@ export const indexControllers = (searchDirs: TControllerSearchDir[]) => {
176
299
  for (const filepath of controllerFiles.sort((a, b) => a.localeCompare(b))) {
177
300
  const code = fs.readFileSync(filepath, 'utf8');
178
301
  const sourceFile = parseSourceFile(filepath, code);
302
+ const explicitController = readExplicitController(sourceFile, filepath, searchDir.root);
179
303
 
180
- const controllerPathOverride = getExportedString(sourceFile, 'controllerPath');
181
- const defaultClass = getDefaultExportClass(sourceFile);
182
-
183
- if (!defaultClass) continue;
184
-
185
- const className = defaultClass.name?.text || getGeneratedClassName(filepath);
186
- const routeBasePath = controllerPathOverride || getControllerBasePathFromFilepath(filepath, searchDir.root);
187
- const methods: TControllerMethodMeta[] = [];
188
-
189
- for (const member of defaultClass.members) {
190
- if (!ts.isMethodDeclaration(member)) continue;
191
- if (!member.body) continue;
192
- if (!member.name || !ts.isIdentifier(member.name)) continue;
193
-
194
- const methodName = member.name.text;
195
- const inputCallsCount = countInputCalls(member);
196
-
197
- if (inputCallsCount > 1) {
198
- throw new Error(`${filepath}#${methodName} uses this.input() more than once.`);
199
- }
200
-
201
- methods.push({
202
- name: methodName,
203
- inputCallsCount,
204
- routePath: [routeBasePath, methodName].filter(Boolean).join('/'),
205
- sourceLocation: getNodeLocation(sourceFile, member.name),
304
+ if (explicitController) {
305
+ controllers.push({
306
+ ...explicitController,
307
+ importPath: buildImportPath(searchDir, filepath),
206
308
  });
309
+ continue;
207
310
  }
208
311
 
209
- if (!methods.length) continue;
312
+ const defaultClass = getDefaultExportClass(sourceFile);
210
313
 
211
- controllers.push({
212
- filepath,
213
- importPath: buildImportPath(searchDir, filepath),
214
- className,
215
- routeBasePath,
216
- methods,
217
- });
314
+ if (!defaultClass) continue;
315
+
316
+ const location = getNodeLocation(sourceFile, defaultClass);
317
+ throw new Error(
318
+ `${filepath}:${location.line}:${location.column} uses a legacy controller class. Export defineController({ path, actions }) instead.`,
319
+ );
218
320
  }
219
321
  }
220
322