proteum 2.1.0 → 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 (83) hide show
  1. package/AGENTS.md +44 -98
  2. package/README.md +121 -7
  3. package/agents/framework/AGENTS.md +133 -886
  4. package/agents/project/AGENTS.md +70 -127
  5. package/agents/project/client/AGENTS.md +22 -93
  6. package/agents/project/client/pages/AGENTS.md +24 -26
  7. package/agents/project/server/routes/AGENTS.md +10 -8
  8. package/agents/project/server/services/AGENTS.md +22 -159
  9. package/agents/project/tests/AGENTS.md +11 -8
  10. package/cli/app/config.ts +7 -20
  11. package/cli/bin.js +8 -0
  12. package/cli/commands/command.ts +243 -0
  13. package/cli/commands/commandLocalRunner.js +198 -0
  14. package/cli/commands/deploy/web.ts +1 -2
  15. package/cli/commands/dev.ts +96 -1
  16. package/cli/commands/doctor.ts +8 -74
  17. package/cli/commands/explain.ts +8 -186
  18. package/cli/commands/trace.ts +228 -0
  19. package/cli/compiler/artifacts/commands.ts +217 -0
  20. package/cli/compiler/artifacts/manifest.ts +35 -21
  21. package/cli/compiler/artifacts/services.ts +300 -1
  22. package/cli/compiler/client/index.ts +43 -8
  23. package/cli/compiler/common/commands.ts +175 -0
  24. package/cli/compiler/common/index.ts +1 -1
  25. package/cli/compiler/common/proteumManifest.ts +15 -114
  26. package/cli/compiler/index.ts +25 -2
  27. package/cli/compiler/server/index.ts +31 -6
  28. package/cli/paths.ts +16 -1
  29. package/cli/presentation/commands.ts +59 -5
  30. package/cli/presentation/devSession.ts +5 -0
  31. package/cli/runtime/commands.ts +60 -1
  32. package/cli/tsconfig.json +4 -1
  33. package/cli/utils/check.ts +1 -1
  34. package/client/app/component.tsx +13 -9
  35. package/client/dev/profiler/index.tsx +1511 -0
  36. package/client/dev/profiler/noop.tsx +5 -0
  37. package/client/dev/profiler/runtime.noop.ts +116 -0
  38. package/client/dev/profiler/runtime.ts +840 -0
  39. package/client/services/router/components/router.tsx +30 -2
  40. package/client/services/router/index.tsx +27 -3
  41. package/client/services/router/request/api.ts +133 -17
  42. package/commands/proteum/diagnostics.ts +11 -0
  43. package/common/dev/commands.ts +50 -0
  44. package/common/dev/diagnostics.ts +298 -0
  45. package/common/dev/profiler.ts +91 -0
  46. package/common/dev/proteumManifest.ts +135 -0
  47. package/common/dev/requestTrace.ts +109 -0
  48. package/common/env/proteumEnv.ts +284 -0
  49. package/common/router/index.ts +4 -22
  50. package/docs/dev-commands.md +86 -0
  51. package/docs/request-tracing.md +122 -0
  52. package/package.json +1 -2
  53. package/server/app/commands.ts +35 -370
  54. package/server/app/commandsManager.ts +393 -0
  55. package/server/app/container/config.ts +11 -49
  56. package/server/app/container/console/index.ts +2 -3
  57. package/server/app/container/index.ts +5 -2
  58. package/server/app/container/trace/index.ts +364 -0
  59. package/server/app/devCommands.ts +192 -0
  60. package/server/app/devDiagnostics.ts +53 -0
  61. package/server/app/index.ts +27 -4
  62. package/server/services/cron/CronTask.ts +73 -5
  63. package/server/services/cron/index.ts +34 -11
  64. package/server/services/fetch/index.ts +3 -10
  65. package/server/services/prisma/index.ts +66 -4
  66. package/server/services/router/http/index.ts +151 -0
  67. package/server/services/router/index.ts +200 -12
  68. package/server/services/router/request/api.ts +30 -1
  69. package/server/services/router/response/index.ts +83 -10
  70. package/server/services/router/response/page/document.tsx +16 -0
  71. package/server/services/router/response/page/index.tsx +27 -1
  72. package/skills/clean-project-code/SKILL.md +7 -2
  73. package/test-results/.last-run.json +4 -0
  74. package/types/aliases.d.ts +6 -0
  75. package/types/global/utils.d.ts +7 -14
  76. package/Rte.zip +0 -0
  77. package/agents/project/agents.md.zip +0 -0
  78. package/doc/TODO.md +0 -71
  79. package/doc/front/router.md +0 -27
  80. package/doc/workspace/workspace.png +0 -0
  81. package/doc/workspace/workspace2.png +0 -0
  82. package/doc/workspace/workspace_26.01.22.png +0 -0
  83. package/server/services/router/http/session.ts.old +0 -40
@@ -10,16 +10,33 @@ import cronParser, { CronExpression } from 'cron-parser';
10
10
  ----------------------------------*/
11
11
 
12
12
  import type CronManager from '.';
13
+ import type {
14
+ TProfilerCronTask,
15
+ TProfilerCronTaskFrequency,
16
+ TProfilerCronTaskRunStatus,
17
+ TProfilerCronTaskTrigger,
18
+ } from '@common/dev/profiler';
13
19
 
14
20
  export type TFrequence = string | Date;
15
21
  export type TRunner = () => Promise<any>;
22
+ const nowIso = () => new Date().toISOString();
16
23
 
17
24
  /*----------------------------------
18
25
  - CLASS
19
26
  ----------------------------------*/
20
27
  export default class CronTask {
21
28
  public cron?: CronExpression;
29
+ public frequency!: TProfilerCronTaskFrequency;
22
30
  public nextInvocation?: Date;
31
+ public registeredAt = nowIso();
32
+ public running = false;
33
+ public lastTrigger?: TProfilerCronTaskTrigger;
34
+ public lastRunStartedAt?: string;
35
+ public lastRunFinishedAt?: string;
36
+ public lastRunDurationMs?: number;
37
+ public lastRunStatus?: TProfilerCronTaskRunStatus;
38
+ public lastErrorMessage?: string;
39
+ public runCount = 0;
23
40
 
24
41
  public constructor(
25
42
  private manager: CronManager,
@@ -35,6 +52,10 @@ export default class CronTask {
35
52
 
36
53
  public schedule(next: TFrequence) {
37
54
  this.cron = undefined;
55
+ this.frequency =
56
+ typeof next === 'string'
57
+ ? { kind: 'cron', value: next }
58
+ : { kind: 'date', value: next.toISOString() };
38
59
 
39
60
  // Cron expression
40
61
  if (typeof next === 'string') {
@@ -58,16 +79,63 @@ export default class CronTask {
58
79
  else this.nextInvocation = undefined;
59
80
  }
60
81
 
61
- public run(now: boolean = false) {
82
+ public toProfilerTask(): TProfilerCronTask {
83
+ return {
84
+ name: this.nom,
85
+ registeredAt: this.registeredAt,
86
+ frequency: this.frequency,
87
+ autoexec: Boolean(this.autoexec),
88
+ automaticExecution: this.manager.isAutomaticExecutionEnabled(),
89
+ nextInvocation: this.nextInvocation?.toISOString(),
90
+ running: this.running,
91
+ lastTrigger: this.lastTrigger,
92
+ lastRunStartedAt: this.lastRunStartedAt,
93
+ lastRunFinishedAt: this.lastRunFinishedAt,
94
+ lastRunDurationMs: this.lastRunDurationMs,
95
+ lastRunStatus: this.lastRunStatus,
96
+ lastErrorMessage: this.lastErrorMessage,
97
+ runCount: this.runCount,
98
+ };
99
+ }
100
+
101
+ public async run(now: boolean = false, trigger: TProfilerCronTaskTrigger = 'scheduler') {
62
102
  // Update invocation date
63
103
  const maintenant = new Date();
64
104
  const runnable = this.nextInvocation !== undefined && this.nextInvocation.valueOf() <= maintenant.valueOf();
65
105
  if (runnable) this.scheduleNext();
66
- else if (now === false) return;
106
+ else if (now === false) return false;
107
+
108
+ if (this.running) return false;
109
+
110
+ this.running = true;
111
+ this.lastTrigger = trigger;
112
+ const startedAt = nowIso();
113
+ this.lastRunStartedAt = startedAt;
114
+ this.lastRunFinishedAt = undefined;
115
+ this.lastRunDurationMs = undefined;
116
+ this.lastRunStatus = undefined;
117
+ this.lastErrorMessage = undefined;
67
118
 
68
119
  // Execution
69
- this.runner().then(() => {
70
- this.manager.config.debug && console.info(`Task runned.`);
71
- });
120
+ try {
121
+ await this.runner();
122
+ this.runCount += 1;
123
+ const finishedAt = nowIso();
124
+ this.lastRunFinishedAt = finishedAt;
125
+ this.lastRunDurationMs = Math.max(0, Date.parse(finishedAt) - Date.parse(startedAt));
126
+ this.lastRunStatus = 'completed';
127
+ this.manager.config.debug && console.info(`[cron][${this.nom}] Task completed.`);
128
+ return true;
129
+ } catch (error) {
130
+ this.runCount += 1;
131
+ const finishedAt = nowIso();
132
+ this.lastRunFinishedAt = finishedAt;
133
+ this.lastRunDurationMs = Math.max(0, Date.parse(finishedAt) - Date.parse(startedAt));
134
+ this.lastRunStatus = 'error';
135
+ this.lastErrorMessage = error instanceof Error ? error.message : String(error);
136
+ throw error;
137
+ } finally {
138
+ this.running = false;
139
+ }
72
140
  }
73
141
  }
@@ -6,6 +6,7 @@
6
6
  import type { Application } from '@server/app/index';
7
7
  import Service from '@server/app/service';
8
8
  import { NotFound } from '@common/errors';
9
+ import type { TProfilerCronTaskTrigger } from '@common/dev/profiler';
9
10
  import context from '@server/context';
10
11
 
11
12
  /*----------------------------------
@@ -32,7 +33,7 @@ export type Services = {};
32
33
 
33
34
  export default class CronManager extends Service<Config, Hooks, Application, Application> {
34
35
  public static taches: { [nom: string]: CronTask } = {};
35
- public static timer: NodeJS.Timeout;
36
+ public static timer?: NodeJS.Timeout;
36
37
 
37
38
  /*----------------------------------
38
39
  - LIFECICLE
@@ -40,8 +41,17 @@ export default class CronManager extends Service<Config, Hooks, Application, App
40
41
 
41
42
  public async ready() {
42
43
  clearInterval(CronManager.timer);
44
+ if (!this.isAutomaticExecutionEnabled()) {
45
+ this.config.debug && console.info('[cron] Automatic execution disabled in dev mode.');
46
+ return;
47
+ }
48
+
43
49
  CronManager.timer = setInterval(() => {
44
- for (const id in CronManager.taches) CronManager.taches[id].run();
50
+ for (const id in CronManager.taches) {
51
+ void this.runTask(id, false, 'scheduler').catch((error) => {
52
+ console.error(`[cron][${id}] Task failed.`, error);
53
+ });
54
+ }
45
55
  }, 10000);
46
56
  }
47
57
 
@@ -59,16 +69,12 @@ export default class CronManager extends Service<Config, Hooks, Application, App
59
69
  * @param autoexec true to execute the task immediatly
60
70
  * @returns The CronTask that just have been created
61
71
  */
62
- public task(nom: string, frequence: TFrequence, run: TRunner, autoexec?: boolean) {
63
- return new Promise<CronTask>((resolve, reject) => {
64
- context.run({ channelType: 'cron', channelId: nom }, async () => {
65
- CronManager.taches[nom] = new CronTask(this, nom, frequence, run, autoexec);
72
+ public async task(nom: string, frequence: TFrequence, run: TRunner, autoexec?: boolean) {
73
+ CronManager.taches[nom] = new CronTask(this, nom, frequence, run, autoexec);
66
74
 
67
- if (autoexec) await CronManager.taches[nom].run(true);
75
+ if (autoexec && this.isAutomaticExecutionEnabled()) await this.runTask(nom, true, 'autoexec');
68
76
 
69
- resolve(CronManager.taches[nom]);
70
- });
71
- });
77
+ return CronManager.taches[nom];
72
78
  }
73
79
 
74
80
  public async exec(nom: string) {
@@ -76,7 +82,8 @@ export default class CronManager extends Service<Config, Hooks, Application, App
76
82
 
77
83
  if (tache === undefined) throw new NotFound('Tâche NotFound: ' + nom);
78
84
 
79
- await tache.run(true);
85
+ await this.runTask(nom, true, 'manual');
86
+ return tache;
80
87
  }
81
88
  public get(): typeof CronManager.taches;
82
89
  public get(name: string): CronTask;
@@ -87,4 +94,20 @@ export default class CronManager extends Service<Config, Hooks, Application, App
87
94
  if (cron === undefined) throw new Error(`L'instance de la tâche cron ${name} n'a pas été trouvée`);
88
95
  return cron;
89
96
  }
97
+
98
+ public isAutomaticExecutionEnabled() {
99
+ return !__DEV__;
100
+ }
101
+
102
+ public listTasks() {
103
+ return Object.values(CronManager.taches).map((task) => task.toProfilerTask());
104
+ }
105
+
106
+ private async runTask(name: string, now: boolean, trigger: TProfilerCronTaskTrigger) {
107
+ const task = this.get(name);
108
+
109
+ return context.run({ channelType: 'cron', channelId: name }, async () => {
110
+ return task.run(now, trigger);
111
+ });
112
+ }
90
113
  }
@@ -7,9 +7,6 @@ import type { default as sharp, Sharp } from 'sharp';
7
7
  import fs from 'fs-extra';
8
8
  import got, { Method, Options, Response as GotResponse } from 'got';
9
9
 
10
- // Node
11
- import request from 'request';
12
-
13
10
  // Core: general
14
11
  import type { Application } from '@server/app/index';
15
12
  import Service from '@server/app/service';
@@ -109,14 +106,10 @@ export default class FetchService extends Service<Config, Hooks, Application, Ap
109
106
  ----------------------------------*/
110
107
 
111
108
  public toBuffer(uri: string): Promise<Buffer> {
112
- return new Promise<Buffer>((resolve, reject) => {
113
- request(uri, { encoding: null }, (err, res, body) => {
114
- if (err) return reject(err);
115
-
116
- if (!body) return reject(`Body is empty for ${uri}.`);
109
+ return got(uri, { responseType: 'buffer', throwHttpErrors: false }).then((response) => {
110
+ if (!response.body || response.body.length === 0) throw new Error(`Body is empty for ${uri}.`);
117
111
 
118
- resolve(body);
119
- });
112
+ return response.body;
120
113
  });
121
114
  }
122
115
 
@@ -23,6 +23,62 @@ import { NotFound } from '@common/errors';
23
23
 
24
24
  export type SqlQuery = ReturnType<ModelsManager['SQL']>;
25
25
 
26
+ type DecimalLike = {
27
+ constructor?: { name?: string };
28
+ equals: (value: number) => boolean;
29
+ toNumber: () => number;
30
+ toString: () => string;
31
+ };
32
+
33
+ /*----------------------------------
34
+ - HELPERS
35
+ ----------------------------------*/
36
+
37
+ const isDecimalLike = (value: object): value is DecimalLike =>
38
+ 'constructor' in value &&
39
+ 'equals' in value &&
40
+ 'toNumber' in value &&
41
+ 'toString' in value &&
42
+ typeof value.constructor === 'function' &&
43
+ value.constructor.name === 'Decimal' &&
44
+ typeof value.equals === 'function' &&
45
+ typeof value.toNumber === 'function' &&
46
+ typeof value.toString === 'function';
47
+
48
+ const isPlainObject = (value: object) => {
49
+ const prototype = Object.getPrototypeOf(value);
50
+ return prototype === Object.prototype || prototype === null;
51
+ };
52
+
53
+ const normalizeBigInt = (value: bigint) => {
54
+ const number = Number(value);
55
+ return Number.isSafeInteger(number) ? number : value.toString();
56
+ };
57
+
58
+ const normalizeDecimal = (value: DecimalLike) => {
59
+ const number = value.toNumber();
60
+ return Number.isFinite(number) && value.equals(number) ? number : value.toString();
61
+ };
62
+
63
+ const normalizeSqlScalar = (value: bigint | DecimalLike) =>
64
+ typeof value === 'bigint' ? normalizeBigInt(value) : normalizeDecimal(value);
65
+
66
+ const normalizeSqlResult = <T>(value: T): T => {
67
+ if (typeof value === 'bigint') return normalizeSqlScalar(value) as T;
68
+
69
+ if (Array.isArray(value)) return value.map((item) => normalizeSqlResult(item)) as T;
70
+
71
+ if (value === null || value === undefined || typeof value !== 'object' || value instanceof Date) return value;
72
+
73
+ if (isDecimalLike(value)) return normalizeSqlScalar(value) as T;
74
+
75
+ if (!isPlainObject(value)) return value;
76
+
77
+ return Object.fromEntries(
78
+ Object.entries(value).map(([key, nestedValue]) => [key, normalizeSqlResult(nestedValue)]),
79
+ ) as T;
80
+ };
81
+
26
82
  /*----------------------------------
27
83
  - SERVICE CONFIG
28
84
  ----------------------------------*/
@@ -33,6 +89,11 @@ export type Hooks = {};
33
89
 
34
90
  export type Services = {};
35
91
 
92
+ // Fix: Do not know how to serialize a BigInt
93
+ BigInt.prototype.toJSON = function () {
94
+ return normalizeBigInt(this.valueOf());
95
+ };
96
+
36
97
  /*----------------------------------
37
98
  - CLASSE
38
99
  ----------------------------------*/
@@ -43,7 +104,7 @@ export default class ModelsManager extends Service<Config, Hooks, Application, A
43
104
  public constructor(...args: TServiceArgs<ModelsManager>) {
44
105
  super(...args);
45
106
 
46
- dotenv.config();
107
+ dotenv.config({ quiet: true });
47
108
 
48
109
  const databaseUrl = process.env.DATABASE_URL;
49
110
  if (!databaseUrl)
@@ -74,9 +135,10 @@ export default class ModelsManager extends Service<Config, Hooks, Application, A
74
135
  public SQL<TRowData extends {} | number | string>(strings: TemplateStringsArray, ...data: any[]) {
75
136
  const string = this.string(strings, ...data);
76
137
 
77
- const query = () => {
78
- return this.client.$queryRawUnsafe(string) as Promise<TRowData[]>;
79
- };
138
+ const query = () =>
139
+ this.client.$queryRawUnsafe(string).then((resultatRequetes) => normalizeSqlResult(resultatRequetes)) as Promise<
140
+ TRowData[]
141
+ >;
80
142
 
81
143
  query.all = query;
82
144
  query.value = <TValue extends any = number>() =>
@@ -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';
@@ -206,6 +209,7 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
206
209
  }),
207
210
  );
208
211
 
212
+ this.registerDevTraceRoutes(routes);
209
213
  routes.use(routeRequest);
210
214
 
211
215
  /*----------------------------------
@@ -227,4 +231,151 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
227
231
  public async cleanup() {
228
232
  this.http.close();
229
233
  }
234
+
235
+ private registerDevTraceRoutes(routes: express.Express) {
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';
301
+
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
+ });
308
+
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
+ });
315
+ });
316
+
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.' });
325
+ return;
326
+ }
327
+
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
+ }
343
+ });
344
+
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.' });
349
+ return;
350
+ }
351
+
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
+ }
357
+
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
+ }
365
+
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
+ }
375
+ });
376
+ }
377
+
378
+ private getCronManager() {
379
+ return (this.app as typeof this.app & { Cron?: CronManager }).Cron;
380
+ }
230
381
  }