proteum 2.1.0-5 → 2.1.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 (56) hide show
  1. package/AGENTS.md +37 -49
  2. package/README.md +52 -1
  3. package/agents/framework/AGENTS.md +104 -236
  4. package/agents/project/AGENTS.md +36 -70
  5. package/cli/commands/command.ts +243 -0
  6. package/cli/commands/commandLocalRunner.js +198 -0
  7. package/cli/commands/dev.ts +95 -1
  8. package/cli/commands/doctor.ts +8 -74
  9. package/cli/commands/explain.ts +8 -194
  10. package/cli/commands/trace.ts +8 -0
  11. package/cli/compiler/artifacts/commands.ts +217 -0
  12. package/cli/compiler/artifacts/manifest.ts +17 -2
  13. package/cli/compiler/artifacts/services.ts +291 -0
  14. package/cli/compiler/client/index.ts +13 -0
  15. package/cli/compiler/common/commands.ts +175 -0
  16. package/cli/compiler/common/proteumManifest.ts +15 -124
  17. package/cli/compiler/index.ts +25 -2
  18. package/cli/compiler/server/index.ts +3 -0
  19. package/cli/presentation/commands.ts +37 -5
  20. package/cli/runtime/commands.ts +29 -1
  21. package/cli/tsconfig.json +4 -1
  22. package/cli/utils/check.ts +1 -1
  23. package/client/app/component.tsx +11 -0
  24. package/client/dev/profiler/index.tsx +1511 -0
  25. package/client/dev/profiler/noop.tsx +5 -0
  26. package/client/dev/profiler/runtime.noop.ts +116 -0
  27. package/client/dev/profiler/runtime.ts +840 -0
  28. package/client/services/router/components/router.tsx +30 -2
  29. package/client/services/router/index.tsx +25 -0
  30. package/client/services/router/request/api.ts +133 -17
  31. package/commands/proteum/diagnostics.ts +11 -0
  32. package/common/dev/commands.ts +50 -0
  33. package/common/dev/diagnostics.ts +298 -0
  34. package/common/dev/profiler.ts +91 -0
  35. package/common/dev/proteumManifest.ts +135 -0
  36. package/common/dev/requestTrace.ts +28 -1
  37. package/docs/dev-commands.md +86 -0
  38. package/docs/request-tracing.md +2 -0
  39. package/package.json +1 -2
  40. package/server/app/commands.ts +35 -370
  41. package/server/app/commandsManager.ts +393 -0
  42. package/server/app/container/console/index.ts +0 -2
  43. package/server/app/container/trace/index.ts +88 -8
  44. package/server/app/devCommands.ts +192 -0
  45. package/server/app/devDiagnostics.ts +53 -0
  46. package/server/app/index.ts +27 -4
  47. package/server/services/cron/CronTask.ts +73 -5
  48. package/server/services/cron/index.ts +34 -11
  49. package/server/services/fetch/index.ts +3 -10
  50. package/server/services/prisma/index.ts +1 -1
  51. package/server/services/router/http/index.ts +132 -21
  52. package/server/services/router/index.ts +40 -4
  53. package/server/services/router/request/api.ts +30 -1
  54. package/skills/clean-project-code/SKILL.md +7 -2
  55. package/test-results/.last-run.json +4 -0
  56. package/types/aliases.d.ts +6 -0
@@ -21,8 +21,11 @@ import * as csp from 'express-csp-header';
21
21
 
22
22
  // Core
23
23
  import Container from '@server/app/container';
24
+ import type CronManager from '@server/services/cron';
25
+ import type CronTask from '@server/services/cron/CronTask';
24
26
  import type { TServerRouter } from '..';
25
27
  import { serverHotReloadMessageType } from '@common/dev/serverHotReload';
28
+ import { explainSectionNames } from '@common/dev/diagnostics';
26
29
 
27
30
  // Middlewaees (core)
28
31
  import { isMutipart, MiddlewareFormData } from './multipart';
@@ -230,41 +233,149 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
230
233
  }
231
234
 
232
235
  private registerDevTraceRoutes(routes: express.Express) {
233
- if (!this.app.container.Trace.isEnabled()) return;
236
+ if (!__DEV__ || this.app.env.profile !== 'dev') return;
237
+
238
+ if (this.app.container.Trace.isEnabled()) {
239
+ routes.get('/__proteum/trace/requests', (req, res) => {
240
+ const rawLimit = Array.isArray(req.query.limit) ? req.query.limit[0] : req.query.limit;
241
+ const parsedLimit = typeof rawLimit === 'string' ? Number.parseInt(rawLimit, 10) : NaN;
242
+ const limit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : 20;
243
+
244
+ res.json({ requests: this.app.container.Trace.listRequests(limit) });
245
+ });
246
+
247
+ routes.get('/__proteum/trace/latest', (_req, res) => {
248
+ const request = this.app.container.Trace.getLatestRequest();
249
+ if (!request) {
250
+ res.status(404).json({ error: 'No request trace is available yet.' });
251
+ return;
252
+ }
253
+
254
+ res.json({ request });
255
+ });
256
+
257
+ routes.get('/__proteum/trace/requests/:id', (req, res) => {
258
+ const request = this.app.container.Trace.getRequest(req.params.id);
259
+ if (!request) {
260
+ res.status(404).json({ error: `Trace ${req.params.id} was not found.` });
261
+ return;
262
+ }
263
+
264
+ res.json({ request });
265
+ });
266
+
267
+ routes.post('/__proteum/trace/arm', (req, res) => {
268
+ const rawCapture = typeof req.body.capture === 'string' ? req.body.capture : 'deep';
269
+ const capture = this.app.container.Trace.armNextRequest(rawCapture);
270
+
271
+ res.json({ armed: true, capture });
272
+ });
273
+ }
274
+
275
+ routes.get('/__proteum/explain', (req, res) => {
276
+ const rawSections = [
277
+ ...(Array.isArray(req.query.section) ? req.query.section : req.query.section ? [req.query.section] : []),
278
+ ...(Array.isArray(req.query.sections)
279
+ ? req.query.sections.flatMap((value) => (typeof value === 'string' ? value.split(',') : []))
280
+ : typeof req.query.sections === 'string'
281
+ ? req.query.sections.split(',')
282
+ : []),
283
+ ]
284
+ .map((value) => (typeof value === 'string' ? value.trim() : ''))
285
+ .filter(Boolean);
286
+
287
+ try {
288
+ const diagnostics = this.app.getDevDiagnostics();
289
+ const sections = diagnostics.normalizeExplainSections(rawSections);
290
+ res.json(diagnostics.explain(sections));
291
+ } catch (error) {
292
+ const message = error instanceof Error ? error.message : String(error);
293
+ const isBadRequest = explainSectionNames.some((sectionName) => message.includes(sectionName)) || message.includes('Unknown explain section');
294
+ res.status(isBadRequest ? 400 : 500).json({ error: message });
295
+ }
296
+ });
297
+
298
+ routes.get('/__proteum/doctor', (req, res) => {
299
+ const rawStrict = Array.isArray(req.query.strict) ? req.query.strict[0] : req.query.strict;
300
+ const strict = rawStrict === '1' || rawStrict === 'true';
234
301
 
235
- routes.get('/__proteum/trace/requests', (req, res) => {
236
- const rawLimit = Array.isArray(req.query.limit) ? req.query.limit[0] : req.query.limit;
237
- const parsedLimit = typeof rawLimit === 'string' ? Number.parseInt(rawLimit, 10) : NaN;
238
- const limit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : 20;
302
+ try {
303
+ res.json(this.app.getDevDiagnostics().doctor(strict));
304
+ } catch (error) {
305
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
306
+ }
307
+ });
239
308
 
240
- res.json({ requests: this.app.container.Trace.listRequests(limit) });
309
+ routes.get('/__proteum/cron/tasks', (_req, res) => {
310
+ const cron = this.getCronManager();
311
+ res.json({
312
+ automaticExecution: cron?.isAutomaticExecutionEnabled() ?? false,
313
+ tasks: cron?.listTasks() ?? [],
314
+ });
241
315
  });
242
316
 
243
- routes.get('/__proteum/trace/latest', (_req, res) => {
244
- const request = this.app.container.Trace.getLatestRequest();
245
- if (!request) {
246
- res.status(404).json({ error: 'No request trace is available yet.' });
317
+ routes.get('/__proteum/commands', (_req, res) => {
318
+ res.json({ commands: this.app.getDevCommands().list() });
319
+ });
320
+
321
+ routes.post('/__proteum/commands/run', async (req, res) => {
322
+ const commandPath = typeof req.body?.path === 'string' ? req.body.path.trim() : '';
323
+ if (!commandPath) {
324
+ res.status(400).json({ error: 'Command path is required.' });
247
325
  return;
248
326
  }
249
327
 
250
- res.json({ request });
328
+ try {
329
+ const execution = await this.app.getDevCommands().run(commandPath);
330
+ res.json({ execution });
331
+ } catch (error) {
332
+ const execution =
333
+ error instanceof Error && 'execution' in error && typeof error.execution === 'object'
334
+ ? error.execution
335
+ : undefined;
336
+ const statusCode = error instanceof Error && error.name === 'NotFound' ? 404 : 500;
337
+
338
+ res.status(statusCode).json({
339
+ error: error instanceof Error ? error.message : String(error),
340
+ execution,
341
+ });
342
+ }
251
343
  });
252
344
 
253
- routes.get('/__proteum/trace/requests/:id', (req, res) => {
254
- const request = this.app.container.Trace.getRequest(req.params.id);
255
- if (!request) {
256
- res.status(404).json({ error: `Trace ${req.params.id} was not found.` });
345
+ routes.post('/__proteum/cron/tasks/run', async (req, res) => {
346
+ const cron = this.getCronManager();
347
+ if (!cron) {
348
+ res.status(404).json({ error: 'Cron service is not registered for this app.' });
257
349
  return;
258
350
  }
259
351
 
260
- res.json({ request });
261
- });
352
+ const name = typeof req.body?.name === 'string' ? req.body.name.trim() : '';
353
+ if (!name) {
354
+ res.status(400).json({ error: 'Cron task name is required.' });
355
+ return;
356
+ }
262
357
 
263
- routes.post('/__proteum/trace/arm', (req, res) => {
264
- const rawCapture = typeof req.body.capture === 'string' ? req.body.capture : 'deep';
265
- const capture = this.app.container.Trace.armNextRequest(rawCapture);
358
+ let task: CronTask;
359
+ try {
360
+ task = cron.get(name);
361
+ } catch (error) {
362
+ res.status(404).json({ error: error instanceof Error ? error.message : String(error) });
363
+ return;
364
+ }
266
365
 
267
- res.json({ armed: true, capture });
366
+ try {
367
+ await cron.exec(name);
368
+ res.json({ task: task.toProfilerTask() });
369
+ } catch (error) {
370
+ res.status(500).json({
371
+ error: error instanceof Error ? error.message : String(error),
372
+ task: task.toProfilerTask(),
373
+ });
374
+ }
268
375
  });
269
376
  }
377
+
378
+ private getCronManager() {
379
+ return (this.app as typeof this.app & { Cron?: CronManager }).Cron;
380
+ }
270
381
  }
@@ -36,6 +36,12 @@ import BaseRouter, {
36
36
  import type { TSsrUnresolvedRoute, TRegisterPageArgs } from '@common/router/contracts';
37
37
  import { buildRegex, getRegisterPageArgs } from '@common/router/register';
38
38
  import { layoutsList, getLayout } from '@common/router/layouts';
39
+ import {
40
+ profilerOriginHeader,
41
+ profilerParentRequestIdHeader,
42
+ profilerSessionIdHeader,
43
+ profilerTraceRequestIdHeader,
44
+ } from '@common/dev/profiler';
39
45
  import { TFetcherList } from '@common/router/request/api';
40
46
  import type { TFrontRenderer } from '@common/router/response/page';
41
47
 
@@ -587,7 +593,11 @@ export default class ServerRouter<
587
593
  url: request.url,
588
594
  headers: request.headers,
589
595
  data: request.data,
596
+ profilerSessionId: request.headers[profilerSessionIdHeader] || undefined,
597
+ profilerOrigin: request.headers[profilerOriginHeader] || undefined,
598
+ profilerParentRequestId: request.headers[profilerParentRequestIdHeader] || undefined,
590
599
  });
600
+ if (this.app.container.Trace.isEnabled()) res.setHeader(profilerTraceRequestIdHeader, request.id);
591
601
 
592
602
  let response: ServerResponse<this>;
593
603
  try {
@@ -899,10 +909,36 @@ export default class ServerRouter<
899
909
  if (!fetcher || !('method' in fetcher)) continue;
900
910
 
901
911
  const { method, path, data } = fetcher;
912
+ const callId = this.app.container.Trace.startCall(request.id, {
913
+ origin: 'api-batch-fetcher',
914
+ label: id,
915
+ method,
916
+ path,
917
+ fetcherId: id,
918
+ requestDataKeys: data && typeof data === 'object' ? Object.keys(data) : [],
919
+ requestData: data,
920
+ });
902
921
 
903
- const response = await this.resolve(request.children(method, path, data));
904
-
905
- responseData[id] = response.data;
922
+ try {
923
+ const response = await this.resolve(request.children(method, path, data));
924
+ responseData[id] = response.data;
925
+ this.app.container.Trace.finishCall(request.id, callId, {
926
+ statusCode: response.statusCode,
927
+ resultKeys:
928
+ response.data && typeof response.data === 'object' && !Array.isArray(response.data)
929
+ ? Object.keys(response.data as Record<string, unknown>)
930
+ : [],
931
+ result: response.data,
932
+ });
933
+ } catch (error) {
934
+ const typedError = error instanceof Error ? error : new Error(typeof error === 'string' ? error : 'Unknown error');
935
+ const statusCode = 'http' in typedError ? Number((typedError as Error & { http?: number }).http) : undefined;
936
+ this.app.container.Trace.finishCall(request.id, callId, {
937
+ statusCode: Number.isFinite(statusCode) ? statusCode : undefined,
938
+ errorMessage: typedError.message,
939
+ });
940
+ throw error;
941
+ }
906
942
 
907
943
  // TODO: merge response.headers ?
908
944
  }
@@ -929,7 +965,7 @@ export default class ServerRouter<
929
965
  'error',
930
966
  {
931
967
  code,
932
- error: error instanceof Error ? error : new Error(error.message),
968
+ error,
933
969
  },
934
970
  'summary',
935
971
  );
@@ -80,7 +80,36 @@ export default class ApiClientRequest extends RequestService implements ApiClien
80
80
 
81
81
  // Create a children request to resolve the api data
82
82
  const request = this.request.children(method, path, data);
83
- fetchedData[id] = await request.router.resolve(request).then((res) => res.data);
83
+ const callId = this.request.router.app.container.Trace.startCall(this.request.id, {
84
+ origin: 'ssr-fetcher',
85
+ label: id,
86
+ method,
87
+ path,
88
+ fetcherId: id,
89
+ requestDataKeys: data && typeof data === 'object' ? Object.keys(data) : [],
90
+ requestData: data,
91
+ });
92
+
93
+ try {
94
+ const response = await request.router.resolve(request);
95
+ fetchedData[id] = response.data;
96
+ this.request.router.app.container.Trace.finishCall(this.request.id, callId, {
97
+ statusCode: response.statusCode,
98
+ resultKeys:
99
+ response.data && typeof response.data === 'object' && !Array.isArray(response.data)
100
+ ? Object.keys(response.data as Record<string, unknown>)
101
+ : [],
102
+ result: response.data,
103
+ });
104
+ } catch (error) {
105
+ const typedError = error instanceof Error ? error : new Error(typeof error === 'string' ? error : 'Unknown error');
106
+ const statusCode = 'http' in typedError ? Number((typedError as Error & { http?: number }).http) : undefined;
107
+ this.request.router.app.container.Trace.finishCall(this.request.id, callId, {
108
+ statusCode: Number.isFinite(statusCode) ? statusCode : undefined,
109
+ errorMessage: typedError.message,
110
+ });
111
+ throw error;
112
+ }
84
113
  }
85
114
 
86
115
  return fetchedData;
@@ -32,10 +32,13 @@ Use fast search tools such as `rg` to verify references. Check exports, dynamic
32
32
  5. Keep uncertain cases.
33
33
  If code looks unused but safety is not clear, keep it and report it separately. Favor false negatives over unsafe deletions.
34
34
 
35
- 6. Apply style only on touched files.
35
+ 6. Audit redundancy candidates across the project.
36
+ List catalogs, constants, and functions that look redundant and could be centralized, unified, or merged. Treat this as a reporting task by default unless the user explicitly asked for structural refactoring. Give each candidate an impact score from 1 to 5, where 5 is the highest expected payoff for maintainability, consistency, or simplification.
37
+
38
+ 7. Apply style only on touched files.
36
39
  Follow the repository coding style for files you change. Run Prettier with the repo config, preferably on touched files only unless the user explicitly asks for repo-wide formatting.
37
40
 
38
- 7. Verify.
41
+ 8. Verify.
39
42
  Run the smallest relevant verification available after edits. Prefer project-native checks. If no suitable automated verification exists, say so explicitly.
40
43
 
41
44
  ## Deletion Rules
@@ -50,6 +53,7 @@ Run the smallest relevant verification available after edits. Prefer project-nat
50
53
  Always report:
51
54
  - what was removed
52
55
  - what was intentionally left unchanged because it was uncertain
56
+ - which catalogs, constants, and functions appear redundant across the project, with a proposed centralization or merge direction and an impact score from 1 to 5
53
57
  - what verification was run
54
58
  - what could not be verified automatically
55
59
 
@@ -59,5 +63,6 @@ When the user provides a cleanup brief, turn it into an execution checklist befo
59
63
  - instructions read
60
64
  - safe deletion targets identified
61
65
  - risky candidates deferred
66
+ - redundancy candidates and impact scoring captured
62
67
  - formatting scope confirmed
63
68
  - verification plan chosen
@@ -0,0 +1,4 @@
1
+ {
2
+ "status": "failed",
3
+ "failedTests": []
4
+ }
@@ -18,6 +18,12 @@ declare module '@/server' {
18
18
  export = InstanceType<ServerApplicationClass>;
19
19
  }
20
20
 
21
+ declare module '@/server/index' {
22
+ import ServerApplicationBase from '../server/app';
23
+
24
+ export default class ServerApplication extends ServerApplicationBase {}
25
+ }
26
+
21
27
  declare module '@/client' {
22
28
  const ClientApplicationClass: import('../client/app').default;
23
29
  export = InstanceType<ClientApplicationClass>;