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.
- package/AGENTS.md +44 -98
- package/README.md +143 -10
- package/agents/framework/AGENTS.md +146 -886
- package/agents/project/AGENTS.md +73 -127
- package/agents/project/client/AGENTS.md +22 -93
- package/agents/project/client/pages/AGENTS.md +24 -26
- package/agents/project/server/routes/AGENTS.md +10 -8
- package/agents/project/server/services/AGENTS.md +22 -159
- package/agents/project/tests/AGENTS.md +11 -8
- package/cli/app/config.ts +7 -20
- package/cli/bin.js +8 -0
- package/cli/commands/command.ts +243 -0
- package/cli/commands/commandLocalRunner.js +198 -0
- package/cli/commands/create.ts +5 -0
- package/cli/commands/deploy/web.ts +1 -2
- package/cli/commands/dev.ts +98 -2
- package/cli/commands/doctor.ts +8 -74
- package/cli/commands/explain.ts +8 -186
- package/cli/commands/init.ts +2 -94
- package/cli/commands/trace.ts +228 -0
- package/cli/compiler/artifacts/commands.ts +217 -0
- package/cli/compiler/artifacts/manifest.ts +35 -21
- package/cli/compiler/artifacts/services.ts +300 -1
- package/cli/compiler/client/index.ts +43 -8
- package/cli/compiler/common/commands.ts +175 -0
- package/cli/compiler/common/index.ts +1 -1
- package/cli/compiler/common/proteumManifest.ts +15 -114
- package/cli/compiler/index.ts +25 -2
- package/cli/compiler/server/index.ts +31 -6
- package/cli/index.ts +1 -4
- package/cli/paths.ts +16 -1
- package/cli/presentation/commands.ts +104 -14
- package/cli/presentation/devSession.ts +22 -3
- package/cli/presentation/proteum_logo_400x400_square_icon.txt +400 -0
- package/cli/runtime/commands.ts +121 -4
- package/cli/scaffold/index.ts +720 -0
- package/cli/scaffold/templates.ts +344 -0
- package/cli/scaffold/types.ts +26 -0
- package/cli/tsconfig.json +4 -1
- package/cli/utils/check.ts +1 -1
- package/client/app/component.tsx +13 -9
- package/client/dev/profiler/index.tsx +2511 -0
- package/client/dev/profiler/noop.tsx +5 -0
- package/client/dev/profiler/runtime.noop.ts +116 -0
- package/client/dev/profiler/runtime.ts +840 -0
- package/client/services/router/components/router.tsx +30 -2
- package/client/services/router/index.tsx +27 -3
- package/client/services/router/request/api.ts +133 -17
- package/commands/proteum/diagnostics.ts +11 -0
- package/common/dev/commands.ts +50 -0
- package/common/dev/diagnostics.ts +298 -0
- package/common/dev/profiler.ts +92 -0
- package/common/dev/proteumManifest.ts +135 -0
- package/common/dev/requestTrace.ts +115 -0
- package/common/env/proteumEnv.ts +284 -0
- package/common/router/index.ts +4 -22
- package/docs/dev-commands.md +93 -0
- package/docs/diagnostics.md +88 -0
- package/docs/request-tracing.md +132 -0
- package/eslint.js +11 -6
- package/package.json +3 -3
- package/server/app/commands.ts +35 -370
- package/server/app/commandsManager.ts +393 -0
- package/server/app/container/config.ts +11 -49
- package/server/app/container/console/index.ts +2 -3
- package/server/app/container/index.ts +5 -2
- package/server/app/container/trace/index.ts +364 -0
- package/server/app/devCommands.ts +192 -0
- package/server/app/devDiagnostics.ts +53 -0
- package/server/app/index.ts +29 -6
- package/server/index.ts +0 -1
- package/server/services/auth/index.ts +525 -61
- package/server/services/auth/router/index.ts +106 -7
- package/server/services/cron/CronTask.ts +73 -5
- package/server/services/cron/index.ts +34 -11
- package/server/services/fetch/index.ts +3 -10
- package/server/services/prisma/index.ts +66 -4
- package/server/services/router/http/index.ts +173 -6
- package/server/services/router/index.ts +200 -12
- package/server/services/router/request/api.ts +30 -1
- package/server/services/router/response/index.ts +83 -10
- package/server/services/router/response/page/document.tsx +16 -0
- package/server/services/router/response/page/index.tsx +27 -1
- package/skills/clean-project-code/SKILL.md +7 -2
- package/test-results/.last-run.json +4 -0
- package/types/aliases.d.ts +6 -0
- package/types/global/utils.d.ts +7 -14
- package/Rte.zip +0 -0
- package/agents/project/agents.md.zip +0 -0
- package/doc/TODO.md +0 -71
- package/doc/front/router.md +0 -27
- package/doc/workspace/workspace.png +0 -0
- package/doc/workspace/workspace2.png +0 -0
- package/doc/workspace/workspace_26.01.22.png +0 -0
- 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
|
+
}
|
package/server/app/index.ts
CHANGED
|
@@ -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 './
|
|
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')
|