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.
- package/AGENTS.md +37 -49
- package/README.md +52 -1
- package/agents/framework/AGENTS.md +104 -236
- package/agents/project/AGENTS.md +36 -70
- package/cli/commands/command.ts +243 -0
- package/cli/commands/commandLocalRunner.js +198 -0
- package/cli/commands/dev.ts +95 -1
- package/cli/commands/doctor.ts +8 -74
- package/cli/commands/explain.ts +8 -194
- package/cli/commands/trace.ts +8 -0
- package/cli/compiler/artifacts/commands.ts +217 -0
- package/cli/compiler/artifacts/manifest.ts +17 -2
- package/cli/compiler/artifacts/services.ts +291 -0
- package/cli/compiler/client/index.ts +13 -0
- package/cli/compiler/common/commands.ts +175 -0
- package/cli/compiler/common/proteumManifest.ts +15 -124
- package/cli/compiler/index.ts +25 -2
- package/cli/compiler/server/index.ts +3 -0
- package/cli/presentation/commands.ts +37 -5
- package/cli/runtime/commands.ts +29 -1
- package/cli/tsconfig.json +4 -1
- package/cli/utils/check.ts +1 -1
- package/client/app/component.tsx +11 -0
- package/client/dev/profiler/index.tsx +1511 -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 +25 -0
- 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 +91 -0
- package/common/dev/proteumManifest.ts +135 -0
- package/common/dev/requestTrace.ts +28 -1
- package/docs/dev-commands.md +86 -0
- package/docs/request-tracing.md +2 -0
- package/package.json +1 -2
- package/server/app/commands.ts +35 -370
- package/server/app/commandsManager.ts +393 -0
- package/server/app/container/console/index.ts +0 -2
- package/server/app/container/trace/index.ts +88 -8
- package/server/app/devCommands.ts +192 -0
- package/server/app/devDiagnostics.ts +53 -0
- package/server/app/index.ts +27 -4
- 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 +1 -1
- package/server/services/router/http/index.ts +132 -21
- package/server/services/router/index.ts +40 -4
- package/server/services/router/request/api.ts +30 -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
|
@@ -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.
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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/
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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.
|
|
254
|
-
const
|
|
255
|
-
if (!
|
|
256
|
-
res.status(404).json({ error:
|
|
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
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
package/types/aliases.d.ts
CHANGED
|
@@ -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>;
|