proteum 2.1.0 → 2.1.2

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 (95) hide show
  1. package/AGENTS.md +44 -98
  2. package/README.md +143 -10
  3. package/agents/framework/AGENTS.md +146 -886
  4. package/agents/project/AGENTS.md +73 -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/create.ts +5 -0
  15. package/cli/commands/deploy/web.ts +1 -2
  16. package/cli/commands/dev.ts +98 -2
  17. package/cli/commands/doctor.ts +8 -74
  18. package/cli/commands/explain.ts +8 -186
  19. package/cli/commands/init.ts +2 -94
  20. package/cli/commands/trace.ts +228 -0
  21. package/cli/compiler/artifacts/commands.ts +217 -0
  22. package/cli/compiler/artifacts/manifest.ts +35 -21
  23. package/cli/compiler/artifacts/services.ts +300 -1
  24. package/cli/compiler/client/index.ts +43 -8
  25. package/cli/compiler/common/commands.ts +175 -0
  26. package/cli/compiler/common/index.ts +1 -1
  27. package/cli/compiler/common/proteumManifest.ts +15 -114
  28. package/cli/compiler/index.ts +25 -2
  29. package/cli/compiler/server/index.ts +31 -6
  30. package/cli/index.ts +1 -4
  31. package/cli/paths.ts +16 -1
  32. package/cli/presentation/commands.ts +104 -14
  33. package/cli/presentation/devSession.ts +22 -3
  34. package/cli/presentation/proteum_logo_400x400_square_icon.txt +400 -0
  35. package/cli/runtime/commands.ts +121 -4
  36. package/cli/scaffold/index.ts +720 -0
  37. package/cli/scaffold/templates.ts +344 -0
  38. package/cli/scaffold/types.ts +26 -0
  39. package/cli/tsconfig.json +4 -1
  40. package/cli/utils/check.ts +1 -1
  41. package/client/app/component.tsx +13 -9
  42. package/client/dev/profiler/index.tsx +2511 -0
  43. package/client/dev/profiler/noop.tsx +5 -0
  44. package/client/dev/profiler/runtime.noop.ts +116 -0
  45. package/client/dev/profiler/runtime.ts +840 -0
  46. package/client/services/router/components/router.tsx +30 -2
  47. package/client/services/router/index.tsx +27 -3
  48. package/client/services/router/request/api.ts +133 -17
  49. package/commands/proteum/diagnostics.ts +11 -0
  50. package/common/dev/commands.ts +50 -0
  51. package/common/dev/diagnostics.ts +298 -0
  52. package/common/dev/profiler.ts +92 -0
  53. package/common/dev/proteumManifest.ts +135 -0
  54. package/common/dev/requestTrace.ts +115 -0
  55. package/common/env/proteumEnv.ts +284 -0
  56. package/common/router/index.ts +4 -22
  57. package/docs/dev-commands.md +93 -0
  58. package/docs/diagnostics.md +88 -0
  59. package/docs/request-tracing.md +132 -0
  60. package/eslint.js +11 -6
  61. package/package.json +3 -3
  62. package/server/app/commands.ts +35 -370
  63. package/server/app/commandsManager.ts +393 -0
  64. package/server/app/container/config.ts +11 -49
  65. package/server/app/container/console/index.ts +2 -3
  66. package/server/app/container/index.ts +5 -2
  67. package/server/app/container/trace/index.ts +364 -0
  68. package/server/app/devCommands.ts +192 -0
  69. package/server/app/devDiagnostics.ts +53 -0
  70. package/server/app/index.ts +29 -6
  71. package/server/index.ts +0 -1
  72. package/server/services/auth/index.ts +525 -61
  73. package/server/services/auth/router/index.ts +106 -7
  74. package/server/services/cron/CronTask.ts +73 -5
  75. package/server/services/cron/index.ts +34 -11
  76. package/server/services/fetch/index.ts +3 -10
  77. package/server/services/prisma/index.ts +66 -4
  78. package/server/services/router/http/index.ts +173 -6
  79. package/server/services/router/index.ts +200 -12
  80. package/server/services/router/request/api.ts +30 -1
  81. package/server/services/router/response/index.ts +83 -10
  82. package/server/services/router/response/page/document.tsx +16 -0
  83. package/server/services/router/response/page/index.tsx +27 -1
  84. package/skills/clean-project-code/SKILL.md +7 -2
  85. package/test-results/.last-run.json +4 -0
  86. package/types/aliases.d.ts +6 -0
  87. package/types/global/utils.d.ts +7 -14
  88. package/Rte.zip +0 -0
  89. package/agents/project/agents.md.zip +0 -0
  90. package/doc/TODO.md +0 -71
  91. package/doc/front/router.md +0 -27
  92. package/doc/workspace/workspace.png +0 -0
  93. package/doc/workspace/workspace2.png +0 -0
  94. package/doc/workspace/workspace_26.01.22.png +0 -0
  95. package/server/services/router/http/session.ts.old +0 -40
@@ -0,0 +1,364 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ import type ApplicationContainer from '..';
5
+ import {
6
+ traceCaptureModes,
7
+ type TTraceCaptureMode,
8
+ type TTraceCall,
9
+ type TTraceCallOrigin,
10
+ type TTraceEvent,
11
+ type TTraceEventType,
12
+ type TTraceSummaryValue,
13
+ type TRequestTrace,
14
+ type TRequestTraceListItem,
15
+ } from '@common/dev/requestTrace';
16
+
17
+ export type Config = {
18
+ enable: boolean;
19
+ requestsLimit: number;
20
+ eventsLimit: number;
21
+ capture: TTraceCaptureMode;
22
+ persistOnError: boolean;
23
+ };
24
+
25
+ type TTraceInspectable = object | PrimitiveValue | bigint | symbol | null | undefined | (() => void);
26
+ type TTraceDetails = { [key: string]: TTraceInspectable };
27
+
28
+ const capturePriority: Record<TTraceCaptureMode, number> = { summary: 0, resolve: 1, deep: 2 };
29
+ const sensitiveKeyPattern =
30
+ /(^|\.)(authorization|cookie|set-cookie|password|pass|pwd|secret|token|refreshToken|accessToken|apiKey|apiSecret|secretAccessKey|accessKeyId|privateKey|session|jwt|rawBody)$/i;
31
+ const maxStringLength = 240;
32
+
33
+ const isTraceCaptureMode = (value: string): value is TTraceCaptureMode =>
34
+ traceCaptureModes.includes(value as TTraceCaptureMode);
35
+
36
+ const isSensitiveKeyPath = (keyPath: string[]) => sensitiveKeyPattern.test(keyPath.join('.'));
37
+
38
+ const summarizeString = (value: string) =>
39
+ value.length <= maxStringLength ? value : `${value.slice(0, maxStringLength)}…`;
40
+
41
+ const summarizeError = (error: Error): TTraceSummaryValue => ({
42
+ kind: 'error',
43
+ name: error.name,
44
+ message: error.message,
45
+ stack: error.stack?.split('\n').slice(0, 5).join('\n'),
46
+ });
47
+
48
+ const summarizeValue = (
49
+ value: TTraceInspectable,
50
+ depth: number,
51
+ seen: WeakSet<object>,
52
+ keyPath: string[],
53
+ ): TTraceSummaryValue => {
54
+ if (isSensitiveKeyPath(keyPath)) return { kind: 'redacted', reason: `Sensitive key ${keyPath[keyPath.length - 1] || 'value'}` };
55
+ if (value === undefined) return { kind: 'undefined' };
56
+ if (value === null) return null;
57
+
58
+ if (typeof value === 'string') return summarizeString(value);
59
+ if (typeof value === 'number' || typeof value === 'boolean') return value;
60
+ if (typeof value === 'bigint') return { kind: 'bigint', value: value.toString() };
61
+ if (typeof value === 'symbol') return { kind: 'symbol', value: value.toString() };
62
+ if (typeof value === 'function') return { kind: 'function', name: value.name || 'anonymous' };
63
+
64
+ if (value instanceof Date) return { kind: 'date', value: value.toISOString() };
65
+ if (value instanceof Error) return summarizeError(value);
66
+ if (Buffer.isBuffer(value)) return { kind: 'buffer', byteLength: value.byteLength };
67
+ if (value instanceof Map) return { kind: 'map', size: value.size };
68
+ if (value instanceof Set) return { kind: 'set', size: value.size };
69
+
70
+ if (seen.has(value)) {
71
+ return {
72
+ kind: 'object',
73
+ constructorName: value.constructor?.name || 'Object',
74
+ keys: [],
75
+ entries: {},
76
+ truncated: true,
77
+ };
78
+ }
79
+
80
+ seen.add(value);
81
+
82
+ if (Array.isArray(value)) {
83
+ if (depth <= 0) return { kind: 'array', length: value.length, items: [], truncated: value.length > 0 };
84
+
85
+ const items = value
86
+ .slice(0, 10)
87
+ .map((item, index) => summarizeValue(item as TTraceInspectable, depth - 1, seen, [...keyPath, `[${index}]`]));
88
+ return { kind: 'array', length: value.length, items, truncated: value.length > items.length };
89
+ }
90
+
91
+ const constructorName = value.constructor?.name || 'Object';
92
+ const keys = Object.keys(value);
93
+ if (depth <= 0) {
94
+ return { kind: 'object', constructorName, keys, entries: {}, truncated: keys.length > 0 };
95
+ }
96
+
97
+ const entries: { [key: string]: TTraceSummaryValue } = {};
98
+ for (const key of keys.slice(0, 20)) {
99
+ const record = value as Record<string, TTraceInspectable>;
100
+ entries[key] = summarizeValue(record[key], depth - 1, seen, [...keyPath, key]);
101
+ }
102
+
103
+ return { kind: 'object', constructorName, keys, entries, truncated: keys.length > Object.keys(entries).length };
104
+ };
105
+
106
+ const summarizeDetails = (details: TTraceDetails, capture: TTraceCaptureMode) => {
107
+ const depth = capture === 'deep' ? 3 : 1;
108
+ const summarized: { [key: string]: TTraceSummaryValue } = {};
109
+
110
+ for (const key of Object.keys(details)) {
111
+ summarized[key] = summarizeValue(details[key], depth, new WeakSet<object>(), [key]);
112
+ }
113
+
114
+ return summarized;
115
+ };
116
+
117
+ const summarizeCaptureValue = (value: TTraceInspectable, capture: TTraceCaptureMode, key: string) =>
118
+ summarizeValue(value, capture === 'deep' ? 3 : 1, new WeakSet<object>(), [key]);
119
+
120
+ const nowIso = () => new Date().toISOString();
121
+
122
+ export default class Trace {
123
+ private requests = new Map<string, TRequestTrace>();
124
+ private order: string[] = [];
125
+ private armedCapture?: TTraceCaptureMode;
126
+
127
+ public constructor(
128
+ private container: typeof ApplicationContainer,
129
+ private config: Config,
130
+ ) {}
131
+
132
+ public isEnabled() {
133
+ return __DEV__ && this.config.enable && this.container.Environment.profile === 'dev';
134
+ }
135
+
136
+ public armNextRequest(capture: string) {
137
+ if (!isTraceCaptureMode(capture)) {
138
+ throw new Error(`Unsupported trace capture mode "${capture}". Expected one of: ${traceCaptureModes.join(', ')}.`);
139
+ }
140
+
141
+ this.armedCapture = capture;
142
+ return capture;
143
+ }
144
+
145
+ public startRequest(input: {
146
+ id: string;
147
+ method: string;
148
+ path: string;
149
+ url: string;
150
+ headers: object;
151
+ data: object;
152
+ profilerSessionId?: string;
153
+ profilerOrigin?: string;
154
+ profilerParentRequestId?: string;
155
+ }) {
156
+ if (!this.isEnabled()) return;
157
+
158
+ const capture = this.armedCapture ?? this.config.capture;
159
+ this.armedCapture = undefined;
160
+
161
+ const trace: TRequestTrace = {
162
+ id: input.id,
163
+ method: input.method,
164
+ path: input.path,
165
+ url: input.url,
166
+ capture,
167
+ profilerSessionId: input.profilerSessionId,
168
+ profilerOrigin: input.profilerOrigin,
169
+ profilerParentRequestId: input.profilerParentRequestId,
170
+ startedAt: nowIso(),
171
+ droppedEvents: 0,
172
+ calls: [],
173
+ events: [],
174
+ };
175
+
176
+ this.requests.set(trace.id, trace);
177
+ this.order.push(trace.id);
178
+ this.trimRequestBuffer();
179
+
180
+ this.record(trace.id, 'request.start', { method: input.method, path: input.path, url: input.url, headers: input.headers, data: input.data });
181
+ }
182
+
183
+ public setRequestUser(requestId: string, user?: string) {
184
+ const trace = this.requests.get(requestId);
185
+ if (!trace) return;
186
+
187
+ trace.user = user;
188
+ if (user) this.record(requestId, 'request.user', { user });
189
+ }
190
+
191
+ public getCapture(requestId: string) {
192
+ return this.requests.get(requestId)?.capture;
193
+ }
194
+
195
+ public shouldCapture(requestId: string, minimumCapture: TTraceCaptureMode) {
196
+ const capture = this.getCapture(requestId);
197
+ if (!capture) return false;
198
+
199
+ return capturePriority[capture] >= capturePriority[minimumCapture];
200
+ }
201
+
202
+ public record(requestId: string, type: TTraceEventType, details: TTraceDetails, minimumCapture: TTraceCaptureMode = 'summary') {
203
+ const trace = this.requests.get(requestId);
204
+ if (!trace || !this.shouldCapture(requestId, minimumCapture)) return;
205
+
206
+ if (trace.events.length >= this.config.eventsLimit) {
207
+ trace.droppedEvents++;
208
+ return;
209
+ }
210
+
211
+ const event: TTraceEvent = {
212
+ index: trace.events.length,
213
+ at: nowIso(),
214
+ elapsedMs: Math.max(0, Date.now() - Date.parse(trace.startedAt)),
215
+ type,
216
+ details: summarizeDetails(details, trace.capture),
217
+ };
218
+
219
+ trace.events.push(event);
220
+ }
221
+
222
+ public finishRequest(requestId: string, output: { statusCode: number; user?: string; errorMessage?: string }) {
223
+ const trace = this.requests.get(requestId);
224
+ if (!trace) return;
225
+
226
+ if (output.user) trace.user = output.user;
227
+ trace.statusCode = output.statusCode;
228
+ trace.errorMessage = output.errorMessage;
229
+
230
+ this.record(
231
+ requestId,
232
+ 'request.finish',
233
+ { statusCode: output.statusCode, user: output.user || '', errorMessage: output.errorMessage || '' },
234
+ 'summary',
235
+ );
236
+
237
+ trace.finishedAt = nowIso();
238
+ trace.durationMs = Math.max(0, Date.parse(trace.finishedAt) - Date.parse(trace.startedAt));
239
+
240
+ if (this.config.persistOnError && trace.statusCode >= 500) {
241
+ trace.persistedFilepath = this.exportRequest(requestId);
242
+ }
243
+ }
244
+
245
+ public startCall(
246
+ requestId: string,
247
+ input: {
248
+ origin: TTraceCallOrigin;
249
+ label: string;
250
+ method?: string;
251
+ path?: string;
252
+ fetcherId?: string;
253
+ parentId?: string;
254
+ requestDataKeys?: string[];
255
+ requestData?: TTraceInspectable;
256
+ },
257
+ ) {
258
+ const trace = this.requests.get(requestId);
259
+ if (!trace) return undefined;
260
+
261
+ const call: TTraceCall = {
262
+ id: `${requestId}:call:${trace.calls.length}`,
263
+ parentId: input.parentId,
264
+ origin: input.origin,
265
+ label: input.label,
266
+ method: input.method || '',
267
+ path: input.path || '',
268
+ fetcherId: input.fetcherId,
269
+ startedAt: nowIso(),
270
+ requestDataKeys: input.requestDataKeys || [],
271
+ requestData: input.requestData !== undefined ? summarizeCaptureValue(input.requestData, trace.capture, 'requestData') : undefined,
272
+ resultKeys: [],
273
+ };
274
+
275
+ trace.calls.push(call);
276
+ return call.id;
277
+ }
278
+
279
+ public finishCall(
280
+ requestId: string,
281
+ callId: string | undefined,
282
+ output: {
283
+ statusCode?: number;
284
+ errorMessage?: string;
285
+ resultKeys?: string[];
286
+ result?: TTraceInspectable;
287
+ } = {},
288
+ ) {
289
+ if (!callId) return;
290
+
291
+ const trace = this.requests.get(requestId);
292
+ const call = trace?.calls.find((candidate) => candidate.id === callId);
293
+ if (!trace || !call) return;
294
+
295
+ call.finishedAt = nowIso();
296
+ call.durationMs = Math.max(0, Date.parse(call.finishedAt) - Date.parse(call.startedAt));
297
+ call.statusCode = output.statusCode;
298
+ call.errorMessage = output.errorMessage;
299
+ call.resultKeys = output.resultKeys || [];
300
+ call.result = output.result !== undefined ? summarizeCaptureValue(output.result, trace.capture, 'result') : undefined;
301
+ }
302
+
303
+ public listRequests(limit = 20): TRequestTraceListItem[] {
304
+ return [...this.order]
305
+ .reverse()
306
+ .slice(0, limit)
307
+ .map((requestId) => this.requests.get(requestId))
308
+ .filter((trace): trace is TRequestTrace => trace !== undefined)
309
+ .map((trace) => ({
310
+ id: trace.id,
311
+ method: trace.method,
312
+ path: trace.path,
313
+ url: trace.url,
314
+ capture: trace.capture,
315
+ startedAt: trace.startedAt,
316
+ finishedAt: trace.finishedAt,
317
+ durationMs: trace.durationMs,
318
+ statusCode: trace.statusCode,
319
+ user: trace.user,
320
+ droppedEvents: trace.droppedEvents,
321
+ persistedFilepath: trace.persistedFilepath,
322
+ errorMessage: trace.errorMessage,
323
+ profilerSessionId: trace.profilerSessionId,
324
+ profilerOrigin: trace.profilerOrigin,
325
+ profilerParentRequestId: trace.profilerParentRequestId,
326
+ eventCount: trace.events.length,
327
+ callCount: trace.calls.length,
328
+ }));
329
+ }
330
+
331
+ public getLatestRequest() {
332
+ const latestRequestId = this.order[this.order.length - 1];
333
+ return latestRequestId ? this.requests.get(latestRequestId) : undefined;
334
+ }
335
+
336
+ public getRequest(requestId: string) {
337
+ return this.requests.get(requestId);
338
+ }
339
+
340
+ public exportRequest(requestId: string, filepath?: string) {
341
+ const trace = this.requests.get(requestId);
342
+ if (!trace) throw new Error(`Trace ${requestId} was not found.`);
343
+
344
+ const outputFilepath =
345
+ filepath ||
346
+ path.join(this.container.path.var, 'traces', trace.startedAt.slice(0, 10), `${trace.id}.json`);
347
+
348
+ fs.ensureDirSync(path.dirname(outputFilepath));
349
+ fs.writeJSONSync(outputFilepath, trace, { spaces: 2 });
350
+
351
+ trace.persistedFilepath = outputFilepath;
352
+
353
+ return outputFilepath;
354
+ }
355
+
356
+ private trimRequestBuffer() {
357
+ const overflow = this.order.length - this.config.requestsLimit;
358
+ if (overflow <= 0) return;
359
+
360
+ for (const requestId of this.order.splice(0, overflow)) {
361
+ this.requests.delete(requestId);
362
+ }
363
+ }
364
+ }
@@ -0,0 +1,192 @@
1
+ import type { Application } from './index';
2
+ import type { Commands } from './commands';
3
+ import { normalizeDevCommandPath, type TDevCommandDefinition, type TDevCommandExecution } from '@common/dev/commands';
4
+ import type { TTraceSummaryValue } from '@common/dev/requestTrace';
5
+ import { NotFound } from '@common/errors';
6
+
7
+ export type TGeneratedCommandDefinition = TDevCommandDefinition & {
8
+ Command: new (app: Application) => Commands<any>;
9
+ };
10
+
11
+ type TSerializableValue = object | PrimitiveValue | bigint | symbol | null | undefined | (() => void);
12
+
13
+ const maxSummaryStringLength = 240;
14
+ const sensitiveKeyPattern =
15
+ /(^|\.)(authorization|cookie|set-cookie|password|pass|pwd|secret|token|refreshToken|accessToken|apiKey|apiSecret|secretAccessKey|accessKeyId|privateKey|session|jwt|rawBody)$/i;
16
+
17
+ const nowIso = () => new Date().toISOString();
18
+ const getDurationMs = (startedAt: string, finishedAt: string) => Math.max(0, Date.parse(finishedAt) - Date.parse(startedAt));
19
+ const isSensitiveKeyPath = (keyPath: string[]) => sensitiveKeyPattern.test(keyPath.join('.'));
20
+ const summarizeString = (value: string) =>
21
+ value.length <= maxSummaryStringLength ? value : `${value.slice(0, maxSummaryStringLength)}…`;
22
+
23
+ const summarizeError = (error: Error): TTraceSummaryValue => ({
24
+ kind: 'error',
25
+ name: error.name,
26
+ message: error.message,
27
+ stack: error.stack?.split('\n').slice(0, 5).join('\n'),
28
+ });
29
+
30
+ const summarizeValue = (
31
+ value: TSerializableValue,
32
+ depth: number,
33
+ seen: WeakSet<object>,
34
+ keyPath: string[],
35
+ ): TTraceSummaryValue => {
36
+ if (isSensitiveKeyPath(keyPath)) return { kind: 'redacted', reason: `Sensitive key ${keyPath[keyPath.length - 1] || 'value'}` };
37
+ if (value === undefined) return { kind: 'undefined' };
38
+ if (value === null) return null;
39
+
40
+ if (typeof value === 'string') return summarizeString(value);
41
+ if (typeof value === 'number' || typeof value === 'boolean') return value;
42
+ if (typeof value === 'bigint') return { kind: 'bigint', value: value.toString() };
43
+ if (typeof value === 'symbol') return { kind: 'symbol', value: value.toString() };
44
+ if (typeof value === 'function') return { kind: 'function', name: value.name || 'anonymous' };
45
+
46
+ if (value instanceof Date) return { kind: 'date', value: value.toISOString() };
47
+ if (value instanceof Error) return summarizeError(value);
48
+ if (Buffer.isBuffer(value)) return { kind: 'buffer', byteLength: value.byteLength };
49
+ if (value instanceof Map) return { kind: 'map', size: value.size };
50
+ if (value instanceof Set) return { kind: 'set', size: value.size };
51
+
52
+ if (seen.has(value)) {
53
+ return {
54
+ kind: 'object',
55
+ constructorName: value.constructor?.name || 'Object',
56
+ keys: [],
57
+ entries: {},
58
+ truncated: true,
59
+ };
60
+ }
61
+
62
+ seen.add(value);
63
+
64
+ if (Array.isArray(value)) {
65
+ if (depth <= 0) return { kind: 'array', length: value.length, items: [], truncated: value.length > 0 };
66
+
67
+ const items = value
68
+ .slice(0, 10)
69
+ .map((item, index) => summarizeValue(item as TSerializableValue, depth - 1, seen, [...keyPath, `[${index}]`]));
70
+
71
+ return { kind: 'array', length: value.length, items, truncated: value.length > items.length };
72
+ }
73
+
74
+ const constructorName = value.constructor?.name || 'Object';
75
+ const keys = Object.keys(value);
76
+ if (depth <= 0) {
77
+ return { kind: 'object', constructorName, keys, entries: {}, truncated: keys.length > 0 };
78
+ }
79
+
80
+ const entries: { [key: string]: TTraceSummaryValue } = {};
81
+ for (const key of keys.slice(0, 20)) {
82
+ const record = value as Record<string, TSerializableValue>;
83
+ entries[key] = summarizeValue(record[key], depth - 1, seen, [...keyPath, key]);
84
+ }
85
+
86
+ return { kind: 'object', constructorName, keys, entries, truncated: keys.length > Object.keys(entries).length };
87
+ };
88
+
89
+ const serializeJsonResult = (value: unknown) => {
90
+ if (value === undefined) return undefined;
91
+
92
+ try {
93
+ return JSON.parse(JSON.stringify(value)) as unknown;
94
+ } catch {
95
+ return undefined;
96
+ }
97
+ };
98
+
99
+ export class DevCommandExecutionError extends Error {
100
+ public constructor(
101
+ message: string,
102
+ public execution: TDevCommandExecution,
103
+ public cause?: unknown,
104
+ ) {
105
+ super(message);
106
+ this.name = 'DevCommandExecutionError';
107
+ }
108
+ }
109
+
110
+ const loadGeneratedCommandDefinitions = () =>
111
+ (((require('@generated/server/commands') as { default?: TGeneratedCommandDefinition[] }).default || []) as TGeneratedCommandDefinition[]).sort(
112
+ (a, b) => a.path.localeCompare(b.path),
113
+ );
114
+
115
+ export default class DevCommandsRegistry<TApplication extends Application = Application> {
116
+ private definitions = loadGeneratedCommandDefinitions();
117
+
118
+ public constructor(private app: TApplication) {}
119
+
120
+ public list() {
121
+ return this.definitions.map((definition) => ({
122
+ path: definition.path,
123
+ className: definition.className,
124
+ methodName: definition.methodName,
125
+ importPath: definition.importPath,
126
+ filepath: definition.filepath,
127
+ sourceLocation: definition.sourceLocation,
128
+ scope: definition.scope,
129
+ }));
130
+ }
131
+
132
+ private getDefinition(commandPath: string) {
133
+ const normalizedPath = normalizeDevCommandPath(commandPath);
134
+ const matchingDefinitions = this.definitions.filter((definition) => definition.path === normalizedPath);
135
+
136
+ if (matchingDefinitions.length === 0) {
137
+ throw new NotFound(`Command "${normalizedPath}" was not found.`);
138
+ }
139
+
140
+ if (matchingDefinitions.length > 1) {
141
+ throw new Error(`Command "${normalizedPath}" is ambiguous because it is registered more than once.`);
142
+ }
143
+
144
+ return matchingDefinitions[0];
145
+ }
146
+
147
+ public async run(commandPath: string): Promise<TDevCommandExecution> {
148
+ const definition = this.getDefinition(commandPath);
149
+ const startedAt = nowIso();
150
+
151
+ try {
152
+ const instance = new definition.Command(this.app);
153
+ const method = (instance as Record<string, unknown>)[definition.methodName];
154
+
155
+ if (typeof method !== 'function') {
156
+ throw new Error(
157
+ `Command "${definition.path}" could not be executed because ${definition.className}.${definition.methodName} is not callable.`,
158
+ );
159
+ }
160
+
161
+ const value = await method.call(instance);
162
+ const finishedAt = nowIso();
163
+
164
+ return {
165
+ command: this.list().find((command) => command.path === definition.path) || definition,
166
+ startedAt,
167
+ finishedAt,
168
+ durationMs: getDurationMs(startedAt, finishedAt),
169
+ status: 'completed',
170
+ result:
171
+ value === undefined
172
+ ? undefined
173
+ : {
174
+ json: serializeJsonResult(value),
175
+ summary: summarizeValue(value as TSerializableValue, 3, new WeakSet<object>(), ['result']),
176
+ },
177
+ };
178
+ } catch (error) {
179
+ const finishedAt = nowIso();
180
+ const execution: TDevCommandExecution = {
181
+ command: this.list().find((command) => command.path === definition.path) || definition,
182
+ startedAt,
183
+ finishedAt,
184
+ durationMs: getDurationMs(startedAt, finishedAt),
185
+ status: 'error',
186
+ errorMessage: error instanceof Error ? error.message : String(error),
187
+ };
188
+
189
+ throw new DevCommandExecutionError(execution.errorMessage || `Command "${definition.path}" failed.`, execution, error);
190
+ }
191
+ }
192
+ }
@@ -0,0 +1,53 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ import type { Application } from './index';
5
+ import {
6
+ buildDoctorResponse,
7
+ explainSectionNames,
8
+ pickExplainManifestSections,
9
+ type TDoctorResponse,
10
+ type TExplainSectionName,
11
+ } from '@common/dev/diagnostics';
12
+ import type { TProteumManifest } from '@common/dev/proteumManifest';
13
+
14
+ const isExplainSectionName = (value: string): value is TExplainSectionName =>
15
+ explainSectionNames.includes(value as TExplainSectionName);
16
+
17
+ export default class DevDiagnosticsRegistry<TApplication extends Application = Application> {
18
+ public constructor(private app: TApplication) {}
19
+
20
+ private getManifestFilepath() {
21
+ return path.join(this.app.container.path.root, '.proteum', 'manifest.json');
22
+ }
23
+
24
+ public readManifest(): TProteumManifest {
25
+ const filepath = this.getManifestFilepath();
26
+ if (!fs.existsSync(filepath)) {
27
+ throw new Error(`Proteum manifest not found at ${filepath}. Run a Proteum command that refreshes generated artifacts first.`);
28
+ }
29
+
30
+ return fs.readJsonSync(filepath) as TProteumManifest;
31
+ }
32
+
33
+ public normalizeExplainSections(rawSections: string[]) {
34
+ const sections = [...new Set(rawSections.map((section) => section.trim()).filter(Boolean))];
35
+ const invalidSections = sections.filter((section) => !isExplainSectionName(section));
36
+
37
+ if (invalidSections.length > 0) {
38
+ throw new Error(
39
+ `Unknown explain section(s): ${invalidSections.join(', ')}. Allowed values: ${explainSectionNames.join(', ')}.`,
40
+ );
41
+ }
42
+
43
+ return sections as TExplainSectionName[];
44
+ }
45
+
46
+ public explain(sectionNames: TExplainSectionName[] = []) {
47
+ return pickExplainManifestSections(this.readManifest(), sectionNames);
48
+ }
49
+
50
+ public doctor(strict = false): TDoctorResponse {
51
+ return buildDoctorResponse(this.readManifest(), strict);
52
+ }
53
+ }
@@ -2,10 +2,14 @@
2
2
  - DEPENDANCES
3
3
  ----------------------------------*/
4
4
 
5
+ process.env.DOTENV_CONFIG_QUIET ??= 'true';
6
+
5
7
  // Core
6
8
  import AppContainer from './container';
7
9
  import ApplicationService, { AnyService } from './service';
8
- import CommandsManager from './commands';
10
+ import CommandsManager from './commandsManager';
11
+ import DevCommandsRegistry from './devCommands';
12
+ import DevDiagnosticsRegistry from './devDiagnostics';
9
13
  import ServicesContainer, { ServicesContainer as ServicesContainerClass, TServiceMetas } from './service/container';
10
14
 
11
15
  // Built-in
@@ -29,6 +33,10 @@ type Hooks = {
29
33
  error: { args: [error: Error, request?: ServerRequest<TServerRouter>] };
30
34
  };
31
35
 
36
+ export type TApplicationStartOptions = {
37
+ skipRootServices?: string[];
38
+ };
39
+
32
40
  export const Service = ServicesContainer;
33
41
 
34
42
  // Without prettify, we don't get a clear list of the class properties
@@ -116,21 +124,31 @@ export abstract class Application<
116
124
  ----------------------------------*/
117
125
 
118
126
  private commandsManager = new CommandsManager(this, { debug: true }, this);
127
+ private devCommandsRegistry?: DevCommandsRegistry<this>;
128
+ private devDiagnosticsRegistry?: DevDiagnosticsRegistry<this>;
119
129
 
120
130
  public command(...args: Parameters<CommandsManager['command']>) {
121
131
  return this.commandsManager.command(...args);
122
132
  }
123
133
 
134
+ public getDevCommands() {
135
+ this.devCommandsRegistry ??= new DevCommandsRegistry(this);
136
+ return this.devCommandsRegistry;
137
+ }
138
+
139
+ public getDevDiagnostics() {
140
+ this.devDiagnosticsRegistry ??= new DevDiagnosticsRegistry(this);
141
+ return this.devDiagnosticsRegistry;
142
+ }
143
+
124
144
  /*----------------------------------
125
145
  - LAUNCH
126
146
  ----------------------------------*/
127
147
 
128
- public async start() {
129
- console.log('Build date', BUILD_DATE);
130
- console.log('Core version', CORE_VERSION);
148
+ public async start(options: TApplicationStartOptions = {}) {
131
149
  const startTime = Date.now();
132
150
 
133
- const startingServices = await this.ready();
151
+ const startingServices = await this.ready(options);
134
152
  await Promise.all(startingServices);
135
153
  await this.runHook('ready');
136
154
 
@@ -181,8 +199,9 @@ export abstract class Application<
181
199
  return (service as AnyService & { ready: () => Promise<any> }).ready();
182
200
  }
183
201
 
184
- public async ready() {
202
+ public async ready(options: TApplicationStartOptions = {}) {
185
203
  const startingServices: Promise<any>[] = [];
204
+ const skippedRootServices = new Set(options.skipRootServices || []);
186
205
 
187
206
  const processService = async (_propKey: string, service: AnyService) => {
188
207
  if (service.status !== 'starting') return;
@@ -208,6 +227,10 @@ export abstract class Application<
208
227
 
209
228
  for (const [serviceName, service] of this.listRootServices()) {
210
229
  const rootService = service as AnyService;
230
+ if (skippedRootServices.has(serviceName)) {
231
+ rootService.status = 'stopped';
232
+ continue;
233
+ }
211
234
 
212
235
  // TODO: move to router
213
236
  // Application.on('service.ready')