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
@@ -1,12 +1,7 @@
1
- import path from 'path';
2
-
3
1
  import cli from '..';
4
2
  import Compiler from '../compiler';
5
- import {
6
- readProteumManifest,
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 errors = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'error');
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(`Proteum doctor failed in strict mode with ${errors.length} errors and ${warnings.length} warnings.`);
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
  };
@@ -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
- readProteumManifest,
7
- type TProteumManifestDiagnostic,
8
- type TProteumManifest,
9
- type TProteumManifestController,
10
- type TProteumManifestLayout,
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(pickManifestSections(manifest, selectedSections), null, 2));
43
+ console.log(JSON.stringify(pickExplainManifestSections(manifest, selectedSections), null, 2));
222
44
  return;
223
45
  }
224
46
 
225
- console.log(renderHuman(manifest, selectedSections));
47
+ console.log(renderExplainHuman(manifest, selectedSections));
226
48
  };
@@ -1,97 +1,5 @@
1
- /*----------------------------------
2
- - DEPENDANCES
3
- ----------------------------------*/
1
+ import { runInitScaffold } from '../scaffold';
4
2
 
5
- // Npm
6
- import fs from 'fs-extra';
7
- import path from 'path';
8
- import prompts from 'prompts';
9
- import cmd from 'node-cmd';
10
- import replaceOnce from 'replace-once';
11
- import { UsageError } from 'clipanion';
12
-
13
- // Cor elibs
14
- import cli from '..';
15
- import { ensureProjectAgentSymlinks } from '../utils/agents';
16
-
17
- // Configs
18
- const filesToConfig = ['package.json', 'identity.yaml'];
19
-
20
- /*----------------------------------
21
- - COMMANDE
22
- ----------------------------------*/
23
3
  export const run = async (): Promise<void> => {
24
- const skeletonPath = path.join(cli.paths.core.cli, 'skeleton');
25
-
26
- if (!fs.existsSync(skeletonPath)) {
27
- throw new UsageError(
28
- 'Proteum init is unavailable in this checkout because `cli/skeleton` is missing. Restore the scaffold assets or use an installed Proteum package that includes them.',
29
- );
30
- }
31
-
32
- const config = await prompts([
33
- {
34
- type: 'text',
35
- name: 'name',
36
- message: 'Project name ?',
37
- initial: 'MyProject',
38
- validate: (value) => /[a-z0-9\-\.]/i.test(value) || 'Must only include alphanumeric characters, and - . ',
39
- },
40
- {
41
- type: 'text',
42
- name: 'dirname',
43
- message: 'Folder name ?',
44
- initial: (value) => value.toLowerCase(),
45
- validate: (value) =>
46
- /[a-z0-9\-\.]/.test(value) || 'Must only include lowercase alphanumeric characters, and - . ',
47
- },
48
- {
49
- type: 'text',
50
- name: 'description',
51
- message: 'Briefly describe your project to your mom:',
52
- initial: 'It will revolutionnize the world',
53
- validate: (value) => /[a-z0-9\-\. ]/i.test(value) || 'Must only include alphanumeric characters, and - . ',
54
- },
55
- { type: 'toggle', name: 'microservice', message: 'Separate API from the UI servers ?' },
56
- ]);
57
-
58
- const placeholders = {
59
- PROJECT_NAME: config.name,
60
- PACKAGE_NAME: config.name.toLowerCase(),
61
- PROJECT_DESCRIPTION: config.description,
62
- };
63
-
64
- const paths = {
65
- skeleton: skeletonPath,
66
- project: path.join(process.cwd(), config.dirname),
67
- };
68
-
69
- // Copy skeleton to cwd/<project-name>
70
- console.info('Creating project skeleton ...');
71
- fs.copySync(paths.skeleton, paths.project);
72
-
73
- // Sync framework-owned Codex assets into the new project.
74
- ensureProjectAgentSymlinks({ appRoot: paths.project, coreRoot: cli.paths.core.root });
75
-
76
- // Replace placeholders
77
- console.info('Configuring project ...');
78
- for (const file of filesToConfig) {
79
- console.log('- ' + file);
80
-
81
- const filepath = path.join(paths.project, file);
82
- const content = fs.readFileSync(filepath, 'utf-8');
83
-
84
- const placeholders_keys = Object.keys(placeholders).map((k) => '{{ ' + k + ' }}');
85
- const values = Object.values(placeholders);
86
-
87
- fs.writeFileSync(filepath, replaceOnce(content, placeholders_keys, values));
88
- }
89
-
90
- // Npm install
91
- console.info('Installing packages ...');
92
- cmd.runSync(`cd "${paths.project}" && npm i`);
93
-
94
- // Run demo app
95
- /*console.info("Run demo ...");
96
- await cli.shell('5htp dev');*/
4
+ await runInitScaffold();
97
5
  };
@@ -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
+ };