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.
- package/AGENTS.md +44 -98
- package/README.md +121 -7
- package/agents/framework/AGENTS.md +133 -886
- package/agents/project/AGENTS.md +70 -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/deploy/web.ts +1 -2
- package/cli/commands/dev.ts +96 -1
- package/cli/commands/doctor.ts +8 -74
- package/cli/commands/explain.ts +8 -186
- 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/paths.ts +16 -1
- package/cli/presentation/commands.ts +59 -5
- package/cli/presentation/devSession.ts +5 -0
- package/cli/runtime/commands.ts +60 -1
- 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 +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 +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 +91 -0
- package/common/dev/proteumManifest.ts +135 -0
- package/common/dev/requestTrace.ts +109 -0
- package/common/env/proteumEnv.ts +284 -0
- package/common/router/index.ts +4 -22
- package/docs/dev-commands.md +86 -0
- package/docs/request-tracing.md +122 -0
- package/package.json +1 -2
- 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 +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 +66 -4
- package/server/services/router/http/index.ts +151 -0
- 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
package/cli/commands/doctor.ts
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
|
|
3
1
|
import cli from '..';
|
|
4
2
|
import Compiler from '../compiler';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
type TProteumManifest,
|
|
8
|
-
type TProteumManifestDiagnostic,
|
|
9
|
-
} from '../compiler/common/proteumManifest';
|
|
3
|
+
import { readProteumManifest } from '../compiler/common/proteumManifest';
|
|
4
|
+
import { buildDoctorResponse, renderDoctorHuman } from '@common/dev/diagnostics';
|
|
10
5
|
|
|
11
6
|
const allowedDoctorArgs = new Set(['json', 'strict']);
|
|
12
7
|
|
|
@@ -24,43 +19,6 @@ const validateDoctorArgs = () => {
|
|
|
24
19
|
}
|
|
25
20
|
};
|
|
26
21
|
|
|
27
|
-
const normalizePath = (value: string) => value.replace(/\\/g, '/');
|
|
28
|
-
|
|
29
|
-
const formatFilepath = (manifest: TProteumManifest, filepath: string) => {
|
|
30
|
-
const normalizedFilepath = normalizePath(filepath);
|
|
31
|
-
const normalizedAppRoot = normalizePath(manifest.app.root);
|
|
32
|
-
const normalizedCoreRoot = normalizePath(manifest.app.coreRoot);
|
|
33
|
-
|
|
34
|
-
if (normalizedFilepath === normalizedAppRoot) return '.';
|
|
35
|
-
if (normalizedFilepath.startsWith(normalizedAppRoot + '/'))
|
|
36
|
-
return normalizePath(path.relative(normalizedAppRoot, normalizedFilepath)) || '.';
|
|
37
|
-
|
|
38
|
-
if (normalizedFilepath === normalizedCoreRoot) return 'node_modules/proteum';
|
|
39
|
-
if (normalizedFilepath.startsWith(normalizedCoreRoot + '/'))
|
|
40
|
-
return normalizePath(path.join('node_modules/proteum', path.relative(normalizedCoreRoot, normalizedFilepath)));
|
|
41
|
-
|
|
42
|
-
return normalizedFilepath;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const formatLocation = (diagnostic: TProteumManifestDiagnostic) =>
|
|
46
|
-
diagnostic.sourceLocation ? `:${diagnostic.sourceLocation.line}:${diagnostic.sourceLocation.column}` : '';
|
|
47
|
-
|
|
48
|
-
const renderGroup = (manifest: TProteumManifest, diagnostics: TProteumManifestDiagnostic[], title: string) => {
|
|
49
|
-
if (diagnostics.length === 0) return `${title}\n- none`;
|
|
50
|
-
|
|
51
|
-
return [
|
|
52
|
-
title,
|
|
53
|
-
...diagnostics.map((diagnostic) => {
|
|
54
|
-
const related =
|
|
55
|
-
diagnostic.relatedFilepaths && diagnostic.relatedFilepaths.length > 0
|
|
56
|
-
? ` related=${diagnostic.relatedFilepaths.map((filepath) => formatFilepath(manifest, filepath)).join(',')}`
|
|
57
|
-
: '';
|
|
58
|
-
|
|
59
|
-
return `- ${diagnostic.code} ${diagnostic.message} source=${formatFilepath(manifest, diagnostic.filepath)}${formatLocation(diagnostic)}${related}`;
|
|
60
|
-
}),
|
|
61
|
-
].join('\n');
|
|
62
|
-
};
|
|
63
|
-
|
|
64
22
|
export const run = async (): Promise<void> => {
|
|
65
23
|
validateDoctorArgs();
|
|
66
24
|
|
|
@@ -68,41 +26,17 @@ export const run = async (): Promise<void> => {
|
|
|
68
26
|
await compiler.refreshGeneratedTypings();
|
|
69
27
|
|
|
70
28
|
const manifest = readProteumManifest(cli.paths.appRoot);
|
|
71
|
-
const
|
|
72
|
-
const warnings = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'warning');
|
|
29
|
+
const response = buildDoctorResponse(manifest, cli.args.strict === true);
|
|
73
30
|
|
|
74
31
|
if (cli.args.json === true) {
|
|
75
|
-
console.log(
|
|
76
|
-
JSON.stringify(
|
|
77
|
-
{
|
|
78
|
-
summary: {
|
|
79
|
-
errors: errors.length,
|
|
80
|
-
warnings: warnings.length,
|
|
81
|
-
strictFailed: cli.args.strict === true && manifest.diagnostics.length > 0,
|
|
82
|
-
},
|
|
83
|
-
diagnostics: manifest.diagnostics,
|
|
84
|
-
},
|
|
85
|
-
null,
|
|
86
|
-
2,
|
|
87
|
-
),
|
|
88
|
-
);
|
|
89
|
-
} else if (manifest.diagnostics.length === 0) {
|
|
90
|
-
console.log('Proteum doctor\n- No manifest diagnostics were found.');
|
|
32
|
+
console.log(JSON.stringify(response, null, 2));
|
|
91
33
|
} else {
|
|
92
|
-
console.log(
|
|
93
|
-
[
|
|
94
|
-
'Proteum doctor',
|
|
95
|
-
`- ${errors.length} errors`,
|
|
96
|
-
`- ${warnings.length} warnings`,
|
|
97
|
-
'',
|
|
98
|
-
renderGroup(manifest, errors, 'Errors'),
|
|
99
|
-
'',
|
|
100
|
-
renderGroup(manifest, warnings, 'Warnings'),
|
|
101
|
-
].join('\n'),
|
|
102
|
-
);
|
|
34
|
+
console.log(renderDoctorHuman(manifest, cli.args.strict === true));
|
|
103
35
|
}
|
|
104
36
|
|
|
105
37
|
if (cli.args.strict === true && manifest.diagnostics.length > 0) {
|
|
106
|
-
throw new Error(
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Proteum doctor failed in strict mode with ${response.summary.errors} errors and ${response.summary.warnings} warnings.`,
|
|
40
|
+
);
|
|
107
41
|
}
|
|
108
42
|
};
|
package/cli/commands/explain.ts
CHANGED
|
@@ -1,22 +1,15 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
|
|
3
1
|
import cli from '..';
|
|
4
2
|
import Compiler from '../compiler';
|
|
3
|
+
import { readProteumManifest } from '../compiler/common/proteumManifest';
|
|
5
4
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
type
|
|
10
|
-
|
|
11
|
-
type TProteumManifestRoute,
|
|
12
|
-
type TProteumManifestService,
|
|
13
|
-
} from '../compiler/common/proteumManifest';
|
|
5
|
+
explainSectionNames,
|
|
6
|
+
pickExplainManifestSections,
|
|
7
|
+
renderExplainHuman,
|
|
8
|
+
type TExplainSectionName,
|
|
9
|
+
} from '@common/dev/diagnostics';
|
|
14
10
|
|
|
15
|
-
const explainSectionNames = ['app', 'conventions', 'env', 'services', 'controllers', 'routes', 'layouts', 'diagnostics'] as const;
|
|
16
11
|
const allowedExplainArgs = new Set(['json', 'all', ...explainSectionNames]);
|
|
17
12
|
|
|
18
|
-
type TExplainSectionName = (typeof explainSectionNames)[number];
|
|
19
|
-
|
|
20
13
|
const validateExplainArgs = () => {
|
|
21
14
|
const enabledArgs = Object.entries(cli.args)
|
|
22
15
|
.filter(([name, value]) => name !== 'workdir' && name !== 'verbose' && value === true)
|
|
@@ -37,177 +30,6 @@ const getSelectedSections = (): TExplainSectionName[] => {
|
|
|
37
30
|
return explainSectionNames.filter((sectionName) => cli.args[sectionName] === true);
|
|
38
31
|
};
|
|
39
32
|
|
|
40
|
-
const pickManifestSections = (manifest: TProteumManifest, sectionNames: TExplainSectionName[]) => {
|
|
41
|
-
if (sectionNames.length === 0) return manifest;
|
|
42
|
-
|
|
43
|
-
const selected: Record<string, unknown> = {};
|
|
44
|
-
|
|
45
|
-
for (const sectionName of sectionNames) {
|
|
46
|
-
selected[sectionName] = manifest[sectionName];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return selected;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const normalizePath = (value: string) => value.replace(/\\/g, '/');
|
|
53
|
-
const formatLocation = (line?: number, column?: number) =>
|
|
54
|
-
line && column ? `:${line}:${column}` : line ? `:${line}` : '';
|
|
55
|
-
|
|
56
|
-
const formatFilepath = (manifest: TProteumManifest, filepath: string) => {
|
|
57
|
-
const normalizedFilepath = normalizePath(filepath);
|
|
58
|
-
const normalizedAppRoot = normalizePath(manifest.app.root);
|
|
59
|
-
const normalizedCoreRoot = normalizePath(manifest.app.coreRoot);
|
|
60
|
-
|
|
61
|
-
if (normalizedFilepath === normalizedAppRoot) return '.';
|
|
62
|
-
if (normalizedFilepath.startsWith(normalizedAppRoot + '/'))
|
|
63
|
-
return normalizePath(path.relative(normalizedAppRoot, normalizedFilepath)) || '.';
|
|
64
|
-
|
|
65
|
-
if (normalizedFilepath === normalizedCoreRoot) return 'node_modules/proteum';
|
|
66
|
-
if (normalizedFilepath.startsWith(normalizedCoreRoot + '/'))
|
|
67
|
-
return normalizePath(path.join('node_modules/proteum', path.relative(normalizedCoreRoot, normalizedFilepath)));
|
|
68
|
-
|
|
69
|
-
return normalizedFilepath;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const formatService = (manifest: TProteumManifest, service: TProteumManifestService) => {
|
|
73
|
-
if (service.kind === 'ref') {
|
|
74
|
-
return `- ${service.registeredName} -> ref ${service.refTo} [${service.parent}]`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const source = service.metasFilepath ? formatFilepath(manifest, service.metasFilepath) : 'unknown';
|
|
78
|
-
return `- ${service.registeredName} -> ${service.id} (${service.metaName}) [${service.scope}] priority=${service.priority} source=${source}`;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const formatController = (manifest: TProteumManifest, controller: TProteumManifestController) =>
|
|
82
|
-
`- ${controller.clientAccessor} -> POST ${controller.httpPath} [${controller.scope}] input=${controller.hasInput ? 'yes' : 'no'} source=${formatFilepath(manifest, controller.filepath)}${formatLocation(controller.sourceLocation.line, controller.sourceLocation.column)}#${controller.methodName}`;
|
|
83
|
-
|
|
84
|
-
const formatRouteTarget = (route: TProteumManifestRoute) => {
|
|
85
|
-
if (route.kind === 'client-error') return route.code !== undefined ? String(route.code) : route.codeRaw || '?';
|
|
86
|
-
return route.path || route.pathRaw || '?';
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const formatRoute = (manifest: TProteumManifest, route: TProteumManifestRoute) => {
|
|
90
|
-
const chunk = route.chunkId ? ` chunk=${route.chunkId}` : '';
|
|
91
|
-
const setup = route.hasSetup ? ' setup=yes' : ' setup=no';
|
|
92
|
-
const options = route.normalizedOptionKeys.length > 0 ? ` options=${route.normalizedOptionKeys.join(',')}` : '';
|
|
93
|
-
const resolution = route.targetResolution !== 'literal' ? ` resolution=${route.targetResolution}` : '';
|
|
94
|
-
|
|
95
|
-
return `- ${route.kind} ${route.methodName} ${formatRouteTarget(route)} [${route.scope}]${chunk}${setup}${options}${resolution} source=${formatFilepath(manifest, route.filepath)}${formatLocation(route.sourceLocation.line, route.sourceLocation.column)}`;
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const formatLayout = (manifest: TProteumManifest, layout: TProteumManifestLayout) =>
|
|
99
|
-
`- ${layout.chunkId || 'root'} depth=${layout.depth} [${layout.scope}] source=${formatFilepath(manifest, layout.filepath)}`;
|
|
100
|
-
|
|
101
|
-
const formatDiagnostic = (manifest: TProteumManifest, diagnostic: TProteumManifestDiagnostic) => {
|
|
102
|
-
const related =
|
|
103
|
-
diagnostic.relatedFilepaths && diagnostic.relatedFilepaths.length > 0
|
|
104
|
-
? ` related=${diagnostic.relatedFilepaths.map((filepath) => formatFilepath(manifest, filepath)).join(',')}`
|
|
105
|
-
: '';
|
|
106
|
-
|
|
107
|
-
return `- [${diagnostic.level}] ${diagnostic.code} ${diagnostic.message} source=${formatFilepath(manifest, diagnostic.filepath)}${formatLocation(diagnostic.sourceLocation?.line, diagnostic.sourceLocation?.column)}${related}`;
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const printSection = (title: string, lines: string[]) => {
|
|
111
|
-
if (lines.length === 0) return `${title}\n- none`;
|
|
112
|
-
return `${title}\n${lines.join('\n')}`;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
const renderSummary = (manifest: TProteumManifest) => {
|
|
116
|
-
const errorsCount = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'error').length;
|
|
117
|
-
const warningsCount = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'warning').length;
|
|
118
|
-
const lines = [
|
|
119
|
-
`Proteum manifest: ${formatFilepath(manifest, path.join(manifest.app.root, '.proteum', 'manifest.json'))}`,
|
|
120
|
-
`App: ${manifest.app.identity.name} (${manifest.app.identity.identifier})`,
|
|
121
|
-
`Env keys: ${manifest.env.loadedTopLevelKeys.join(', ') || 'none'}`,
|
|
122
|
-
`Services: ${manifest.services.app.length} app, ${manifest.services.routerPlugins.length} router plugins`,
|
|
123
|
-
`Controllers: ${manifest.controllers.length}`,
|
|
124
|
-
`Routes: ${manifest.routes.client.length} client, ${manifest.routes.server.length} server`,
|
|
125
|
-
`Layouts: ${manifest.layouts.length}`,
|
|
126
|
-
`Diagnostics: ${errorsCount} errors, ${warningsCount} warnings`,
|
|
127
|
-
'Use `proteum explain --json` for the full machine-readable manifest or pass section flags like `routes` and `services`.',
|
|
128
|
-
];
|
|
129
|
-
|
|
130
|
-
return lines.join('\n');
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const renderHuman = (manifest: TProteumManifest, sectionNames: TExplainSectionName[]) => {
|
|
134
|
-
if (sectionNames.length === 0) return renderSummary(manifest);
|
|
135
|
-
|
|
136
|
-
const sections: string[] = [];
|
|
137
|
-
|
|
138
|
-
for (const sectionName of sectionNames) {
|
|
139
|
-
if (sectionName === 'app') {
|
|
140
|
-
sections.push(
|
|
141
|
-
printSection('App', [
|
|
142
|
-
`- root=${formatFilepath(manifest, manifest.app.root)}`,
|
|
143
|
-
`- coreRoot=${formatFilepath(manifest, manifest.app.coreRoot)}`,
|
|
144
|
-
`- identity=${formatFilepath(manifest, manifest.app.identityFilepath)}`,
|
|
145
|
-
`- name=${manifest.app.identity.name}`,
|
|
146
|
-
`- identifier=${manifest.app.identity.identifier}`,
|
|
147
|
-
`- title=${manifest.app.identity.fullTitle || manifest.app.identity.title || manifest.app.identity.name}`,
|
|
148
|
-
]),
|
|
149
|
-
);
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (sectionName === 'conventions') {
|
|
154
|
-
sections.push(
|
|
155
|
-
printSection('Conventions', [
|
|
156
|
-
`- routeSetupOptionKeys=${manifest.conventions.routeSetupOptionKeys.join(', ')}`,
|
|
157
|
-
`- reservedRouteSetupKeys=${manifest.conventions.reservedRouteSetupKeys.join(', ')}`,
|
|
158
|
-
]),
|
|
159
|
-
);
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (sectionName === 'env') {
|
|
164
|
-
sections.push(
|
|
165
|
-
printSection('Env', [
|
|
166
|
-
`- source=${formatFilepath(manifest, manifest.env.sourceFilepath)}`,
|
|
167
|
-
`- loadedTopLevelKeys=${manifest.env.loadedTopLevelKeys.join(', ') || 'none'}`,
|
|
168
|
-
`- requiredTopLevelKeys=${manifest.env.requiredTopLevelKeys.join(', ')}`,
|
|
169
|
-
]),
|
|
170
|
-
);
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (sectionName === 'services') {
|
|
175
|
-
sections.push(
|
|
176
|
-
printSection('App Services', manifest.services.app.map((service) => formatService(manifest, service))),
|
|
177
|
-
printSection(
|
|
178
|
-
'Router Plugins',
|
|
179
|
-
manifest.services.routerPlugins.map((service) => formatService(manifest, service)),
|
|
180
|
-
),
|
|
181
|
-
);
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (sectionName === 'controllers') {
|
|
186
|
-
sections.push(printSection('Controllers', manifest.controllers.map((controller) => formatController(manifest, controller))));
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (sectionName === 'routes') {
|
|
191
|
-
sections.push(
|
|
192
|
-
printSection('Client Routes', manifest.routes.client.map((route) => formatRoute(manifest, route))),
|
|
193
|
-
printSection('Server Routes', manifest.routes.server.map((route) => formatRoute(manifest, route))),
|
|
194
|
-
);
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (sectionName === 'layouts') {
|
|
199
|
-
sections.push(printSection('Layouts', manifest.layouts.map((layout) => formatLayout(manifest, layout))));
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (sectionName === 'diagnostics') {
|
|
204
|
-
sections.push(printSection('Diagnostics', manifest.diagnostics.map((diagnostic) => formatDiagnostic(manifest, diagnostic))));
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return sections.join('\n\n');
|
|
209
|
-
};
|
|
210
|
-
|
|
211
33
|
export const run = async (): Promise<void> => {
|
|
212
34
|
validateExplainArgs();
|
|
213
35
|
|
|
@@ -218,9 +40,9 @@ export const run = async (): Promise<void> => {
|
|
|
218
40
|
const selectedSections = getSelectedSections();
|
|
219
41
|
|
|
220
42
|
if (cli.args.json === true) {
|
|
221
|
-
console.log(JSON.stringify(
|
|
43
|
+
console.log(JSON.stringify(pickExplainManifestSections(manifest, selectedSections), null, 2));
|
|
222
44
|
return;
|
|
223
45
|
}
|
|
224
46
|
|
|
225
|
-
console.log(
|
|
47
|
+
console.log(renderExplainHuman(manifest, selectedSections));
|
|
226
48
|
};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import got from 'got';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { UsageError } from 'clipanion';
|
|
5
|
+
|
|
6
|
+
import cli from '..';
|
|
7
|
+
import type {
|
|
8
|
+
TRequestTrace,
|
|
9
|
+
TRequestTraceArmResponse,
|
|
10
|
+
TRequestTraceErrorResponse,
|
|
11
|
+
TRequestTraceListItem,
|
|
12
|
+
TRequestTraceListResponse,
|
|
13
|
+
TRequestTraceResponse,
|
|
14
|
+
} from '../../common/dev/requestTrace';
|
|
15
|
+
|
|
16
|
+
type TTraceAction = 'latest' | 'show' | 'requests' | 'arm' | 'export';
|
|
17
|
+
|
|
18
|
+
const allowedActions = new Set<TTraceAction>(['latest', 'show', 'requests', 'arm', 'export']);
|
|
19
|
+
|
|
20
|
+
class TraceResponseError extends UsageError {}
|
|
21
|
+
|
|
22
|
+
const getAction = () => {
|
|
23
|
+
const action = typeof cli.args.action === 'string' && cli.args.action ? cli.args.action : 'latest';
|
|
24
|
+
if (!allowedActions.has(action as TTraceAction)) {
|
|
25
|
+
throw new UsageError(`Unsupported trace action "${action}". Expected one of: ${[...allowedActions].join(', ')}.`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return action as TTraceAction;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const normalizeBaseUrl = (value: string) => value.replace(/\/+$/, '');
|
|
32
|
+
|
|
33
|
+
const getRouterPortFromManifest = () => {
|
|
34
|
+
const manifestFilepath = path.join(cli.args.workdir as string, '.proteum', 'manifest.json');
|
|
35
|
+
if (!fs.existsSync(manifestFilepath)) return undefined;
|
|
36
|
+
|
|
37
|
+
const manifest = fs.readJsonSync(manifestFilepath, { throws: false }) as
|
|
38
|
+
| { env?: { resolved?: { routerPort?: number } } }
|
|
39
|
+
| undefined;
|
|
40
|
+
const port = manifest?.env?.resolved?.routerPort;
|
|
41
|
+
|
|
42
|
+
if (typeof port !== 'number' || port <= 0) return undefined;
|
|
43
|
+
|
|
44
|
+
return String(port);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const getRouterPort = () => {
|
|
48
|
+
const overridePort = typeof cli.args.port === 'string' && cli.args.port ? cli.args.port : '';
|
|
49
|
+
if (overridePort) return overridePort;
|
|
50
|
+
|
|
51
|
+
const envPort = process.env.PORT?.trim();
|
|
52
|
+
if (envPort) return envPort;
|
|
53
|
+
|
|
54
|
+
const manifestPort = getRouterPortFromManifest();
|
|
55
|
+
if (manifestPort) return manifestPort;
|
|
56
|
+
|
|
57
|
+
throw new UsageError(
|
|
58
|
+
`Could not determine the router port from PORT or .proteum/manifest.json in ${cli.args.workdir as string}. Pass --port or --url explicitly.`,
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const getRouterBaseUrls = () => {
|
|
63
|
+
const explicitUrl = typeof cli.args.url === 'string' && cli.args.url ? cli.args.url.trim() : '';
|
|
64
|
+
if (explicitUrl) return [normalizeBaseUrl(explicitUrl)];
|
|
65
|
+
|
|
66
|
+
const port = getRouterPort();
|
|
67
|
+
return [...new Set([`http://127.0.0.1:${port}`, `http://localhost:${port}`, `http://[::1]:${port}`])];
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const getTraceErrorMessage = (body: TRequestTraceErrorResponse | object | string | undefined, statusCode: number) => {
|
|
71
|
+
if (typeof body === 'object' && body !== null && 'error' in body && typeof body.error === 'string') {
|
|
72
|
+
return body.error;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return `Trace request failed with status ${statusCode}.`;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const requestJson = async <TResponse>(pathname: string, options?: { method?: 'GET' | 'POST'; json?: object }) => {
|
|
79
|
+
const attempts: string[] = [];
|
|
80
|
+
|
|
81
|
+
for (const baseUrl of getRouterBaseUrls()) {
|
|
82
|
+
try {
|
|
83
|
+
const response = await got(`${baseUrl}${pathname}`, {
|
|
84
|
+
method: options?.method || 'GET',
|
|
85
|
+
json: options?.json,
|
|
86
|
+
responseType: 'json',
|
|
87
|
+
throwHttpErrors: false,
|
|
88
|
+
retry: { limit: 0 },
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (response.statusCode >= 400) {
|
|
92
|
+
throw new TraceResponseError(
|
|
93
|
+
getTraceErrorMessage(response.body as TRequestTraceErrorResponse | object | string | undefined, response.statusCode),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return response.body as TResponse;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error instanceof TraceResponseError) throw error;
|
|
100
|
+
|
|
101
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
102
|
+
attempts.push(`${baseUrl}${pathname}: ${message}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw new UsageError(
|
|
107
|
+
[
|
|
108
|
+
'Could not reach the Proteum trace server.',
|
|
109
|
+
...attempts.map((attempt) => `- ${attempt}`),
|
|
110
|
+
'Make sure the app is running with `proteum dev`, or pass `--url http://host:port` if it is bound elsewhere.',
|
|
111
|
+
].join('\n'),
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const renderTraceSummary = (request: TRequestTraceListItem) =>
|
|
116
|
+
[
|
|
117
|
+
`${request.id} ${request.method} ${request.path}`,
|
|
118
|
+
`status=${request.statusCode ?? 'pending'}`,
|
|
119
|
+
`capture=${request.capture}`,
|
|
120
|
+
`events=${request.eventCount}`,
|
|
121
|
+
`calls=${request.callCount}`,
|
|
122
|
+
request.user ? `user=${request.user}` : '',
|
|
123
|
+
request.errorMessage ? `error=${request.errorMessage}` : '',
|
|
124
|
+
]
|
|
125
|
+
.filter(Boolean)
|
|
126
|
+
.join(' | ');
|
|
127
|
+
|
|
128
|
+
const renderTrace = (request: TRequestTrace) =>
|
|
129
|
+
[
|
|
130
|
+
`Request ${request.id}`,
|
|
131
|
+
`- ${request.method} ${request.path} status=${request.statusCode ?? 'pending'} capture=${request.capture}`,
|
|
132
|
+
`- started=${request.startedAt} durationMs=${request.durationMs ?? 'pending'} events=${request.events.length} dropped=${request.droppedEvents}`,
|
|
133
|
+
...(request.user ? [`- user=${request.user}`] : []),
|
|
134
|
+
...(request.persistedFilepath ? [`- persisted=${request.persistedFilepath}`] : []),
|
|
135
|
+
'Calls',
|
|
136
|
+
...(request.calls.length === 0
|
|
137
|
+
? ['- none']
|
|
138
|
+
: request.calls.map(
|
|
139
|
+
(call) =>
|
|
140
|
+
`- ${call.origin} ${call.label} ${call.method} ${call.path} status=${call.statusCode ?? 'pending'} durationMs=${call.durationMs ?? 'pending'} req=${call.requestDataKeys.join(',')} res=${call.resultKeys.join(',')}`,
|
|
141
|
+
)),
|
|
142
|
+
'Events',
|
|
143
|
+
...request.events.map(
|
|
144
|
+
(event) =>
|
|
145
|
+
`- [${event.elapsedMs}ms] ${event.type} ${Object.entries(event.details)
|
|
146
|
+
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
|
|
147
|
+
.join(' ')}`,
|
|
148
|
+
),
|
|
149
|
+
].join('\n');
|
|
150
|
+
|
|
151
|
+
const printJson = (value: object) => {
|
|
152
|
+
console.log(JSON.stringify(value, null, 2));
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export const run = async () => {
|
|
156
|
+
const action = getAction();
|
|
157
|
+
const requestId = typeof cli.args.id === 'string' ? cli.args.id : '';
|
|
158
|
+
const shouldPrintJson = cli.args.json === true;
|
|
159
|
+
|
|
160
|
+
if (action === 'requests') {
|
|
161
|
+
const response = await requestJson<TRequestTraceListResponse>('/__proteum/trace/requests');
|
|
162
|
+
if (shouldPrintJson) {
|
|
163
|
+
printJson(response);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log(['Proteum trace', ...response.requests.map(renderTraceSummary)].join('\n'));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (action === 'arm') {
|
|
172
|
+
const capture = typeof cli.args.capture === 'string' && cli.args.capture ? cli.args.capture : 'deep';
|
|
173
|
+
const response = await requestJson<TRequestTraceArmResponse>('/__proteum/trace/arm', {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
json: { capture },
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (shouldPrintJson) {
|
|
179
|
+
printJson(response);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log(`Armed next request trace with capture=${response.capture}.`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (action === 'latest') {
|
|
188
|
+
const response = await requestJson<TRequestTraceResponse>('/__proteum/trace/latest');
|
|
189
|
+
if (shouldPrintJson) {
|
|
190
|
+
printJson(response);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
console.log(renderTrace(response.request));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!requestId) {
|
|
199
|
+
throw new UsageError(`Trace action "${action}" requires a request id.`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const response = await requestJson<TRequestTraceResponse>(`/__proteum/trace/requests/${requestId}`);
|
|
203
|
+
|
|
204
|
+
if (action === 'show') {
|
|
205
|
+
if (shouldPrintJson) {
|
|
206
|
+
printJson(response);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
console.log(renderTrace(response.request));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const output =
|
|
215
|
+
typeof cli.args.output === 'string' && cli.args.output
|
|
216
|
+
? cli.args.output
|
|
217
|
+
: path.join(cli.args.workdir as string, 'var', 'traces', 'exports', `${response.request.id}.json`);
|
|
218
|
+
|
|
219
|
+
fs.ensureDirSync(path.dirname(output));
|
|
220
|
+
fs.writeJSONSync(output, response.request, { spaces: 2 });
|
|
221
|
+
|
|
222
|
+
if (shouldPrintJson) {
|
|
223
|
+
printJson({ output, request: response.request });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log(`Exported trace ${response.request.id} to ${output}`);
|
|
228
|
+
};
|