proteum 2.1.9 → 2.2.0

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 (79) hide show
  1. package/.codex/environments/environment.toml +11 -0
  2. package/AGENTS.md +25 -11
  3. package/README.md +19 -9
  4. package/agents/project/AGENTS.md +165 -120
  5. package/agents/project/CODING_STYLE.md +1 -1
  6. package/agents/project/app-root/AGENTS.md +16 -0
  7. package/agents/project/client/AGENTS.md +5 -5
  8. package/agents/project/client/pages/AGENTS.md +13 -13
  9. package/agents/project/diagnostics.md +19 -10
  10. package/agents/project/optimizations.md +5 -6
  11. package/agents/project/root/AGENTS.md +295 -0
  12. package/agents/project/server/routes/AGENTS.md +2 -2
  13. package/agents/project/server/services/AGENTS.md +4 -2
  14. package/agents/project/tests/AGENTS.md +2 -2
  15. package/cli/app/index.ts +31 -7
  16. package/cli/commands/configure.ts +226 -0
  17. package/cli/commands/dev.ts +0 -2
  18. package/cli/commands/diagnose.ts +33 -1
  19. package/cli/commands/explain.ts +1 -1
  20. package/cli/commands/migrate.ts +51 -0
  21. package/cli/commands/orient.ts +169 -0
  22. package/cli/commands/perf.ts +8 -1
  23. package/cli/commands/verify.ts +1003 -49
  24. package/cli/compiler/artifacts/manifest.ts +4 -4
  25. package/cli/compiler/artifacts/routing.ts +2 -2
  26. package/cli/compiler/artifacts/services.ts +12 -3
  27. package/cli/compiler/client/index.ts +65 -19
  28. package/cli/compiler/common/files/style.ts +47 -2
  29. package/cli/compiler/common/generatedRouteModules.ts +31 -38
  30. package/cli/compiler/common/index.ts +10 -0
  31. package/cli/compiler/common/proteumManifest.ts +1 -0
  32. package/cli/compiler/server/index.ts +34 -9
  33. package/cli/context.ts +6 -1
  34. package/cli/index.ts +7 -8
  35. package/cli/migrate/pageContract.ts +516 -0
  36. package/cli/paths.ts +47 -6
  37. package/cli/presentation/commands.ts +100 -10
  38. package/cli/presentation/devSession.ts +4 -6
  39. package/cli/presentation/help.ts +2 -2
  40. package/cli/presentation/ink.ts +10 -5
  41. package/cli/presentation/welcome.ts +2 -4
  42. package/cli/runtime/commands.ts +94 -1
  43. package/cli/scaffold/index.ts +2 -2
  44. package/cli/scaffold/templates.ts +4 -2
  45. package/cli/utils/agents.ts +273 -58
  46. package/client/dev/profiler/index.tsx +3 -2
  47. package/client/router.ts +10 -2
  48. package/client/services/router/index.tsx +6 -22
  49. package/common/dev/connect.ts +20 -4
  50. package/common/dev/console.ts +7 -0
  51. package/common/dev/contractsDoctor.ts +354 -0
  52. package/common/dev/diagnostics.ts +10 -7
  53. package/common/dev/inspection.ts +830 -38
  54. package/common/dev/performance.ts +19 -5
  55. package/common/dev/profiler.ts +1 -0
  56. package/common/dev/proteumManifest.ts +5 -4
  57. package/common/dev/requestTrace.ts +12 -1
  58. package/common/router/contracts.ts +8 -11
  59. package/common/router/index.ts +2 -2
  60. package/common/router/pageData.ts +72 -0
  61. package/common/router/register.ts +10 -46
  62. package/common/router/response/page.ts +28 -16
  63. package/docs/dev-sessions.md +8 -4
  64. package/docs/diagnostics.md +77 -11
  65. package/docs/migrate-from-2.1.3.md +388 -0
  66. package/docs/request-tracing.md +25 -6
  67. package/package.json +6 -1
  68. package/scripts/update-codex-agents.ts +2 -2
  69. package/server/app/container/console/index.ts +11 -1
  70. package/server/app/container/trace/index.ts +117 -0
  71. package/server/app/devDiagnostics.ts +1 -1
  72. package/server/app/index.ts +5 -1
  73. package/server/services/auth/index.ts +9 -0
  74. package/server/services/router/index.ts +64 -14
  75. package/server/services/router/request/api.ts +7 -1
  76. package/server/services/router/response/index.ts +8 -28
  77. package/types/global/vendors.d.ts +12 -0
  78. package/types/vendors.d.ts +12 -0
  79. package/common/router/pageSetup.ts +0 -51
@@ -0,0 +1,226 @@
1
+ /*----------------------------------
2
+ - DEPENDANCES
3
+ ----------------------------------*/
4
+
5
+ // Npm
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import prompts from 'prompts';
9
+ import { UsageError } from 'clipanion';
10
+
11
+ // Configs
12
+ import cli from '..';
13
+ import { renderRows } from '../presentation/layout';
14
+ import { isLikelyProteumAppRoot } from '../presentation/commands';
15
+ import { renderStep, renderSuccess, renderTitle, renderWarning } from '../presentation/ink';
16
+ import { configureProjectAgentSymlinks, type TConfigureProjectAgentSymlinksResult } from '../utils/agents';
17
+
18
+ /*----------------------------------
19
+ - HELPERS
20
+ ----------------------------------*/
21
+
22
+ const findLikelyRepoRoot = (startPath: string) => {
23
+ let currentPath = path.resolve(startPath);
24
+
25
+ while (true) {
26
+ if (fs.existsSync(path.join(currentPath, '.git'))) return currentPath;
27
+
28
+ const parentPath = path.dirname(currentPath);
29
+ if (parentPath === currentPath) return undefined;
30
+ currentPath = parentPath;
31
+ }
32
+ };
33
+
34
+ const resolveCanonicalPath = (inputPath: string) => {
35
+ const resolvedPath = path.resolve(inputPath);
36
+
37
+ try {
38
+ return fs.realpathSync(resolvedPath);
39
+ } catch {
40
+ return resolvedPath;
41
+ }
42
+ };
43
+
44
+ const isInsideDirectory = ({ child, parent }: { child: string; parent: string }) => {
45
+ const relativePath = path.relative(parent, child);
46
+ return relativePath !== '' && !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
47
+ };
48
+
49
+ const assertProteumAppRoot = (appRoot: string) => {
50
+ if (isLikelyProteumAppRoot(appRoot)) return;
51
+
52
+ throw new UsageError(
53
+ `This command expects a Proteum app root. Missing one or more required entries in ${appRoot}.`,
54
+ );
55
+ };
56
+
57
+ const promptMonorepoRoot = async ({
58
+ appRoot,
59
+ defaultRoot,
60
+ }: {
61
+ appRoot: string;
62
+ defaultRoot?: string;
63
+ }) => {
64
+ const response = await prompts(
65
+ {
66
+ type: 'text',
67
+ name: 'value',
68
+ message: 'Monorepo root path',
69
+ initial: defaultRoot,
70
+ validate: (input) => {
71
+ const resolvedRoot = resolveCanonicalPath(String(input || defaultRoot || ''));
72
+
73
+ if (!input && !defaultRoot) return 'A monorepo root path is required.';
74
+ if (!fs.existsSync(resolvedRoot) || !fs.statSync(resolvedRoot).isDirectory())
75
+ return `Directory not found: ${resolvedRoot}`;
76
+ if (!isInsideDirectory({ child: appRoot, parent: resolvedRoot }))
77
+ return `The Proteum app root must be inside the monorepo root: ${resolvedRoot}`;
78
+
79
+ return true;
80
+ },
81
+ },
82
+ {
83
+ onCancel: () => {
84
+ throw new UsageError('Cancelled `proteum configure agents`.');
85
+ },
86
+ },
87
+ );
88
+
89
+ return resolveCanonicalPath(String(response.value || defaultRoot || ''));
90
+ };
91
+
92
+ const promptBlockedOverwritePaths = async (blockedPaths: string[]) => {
93
+ if (blockedPaths.length === 0) return [];
94
+
95
+ console.info(await renderWarning('Proteum found existing non-managed instruction paths.'));
96
+ console.info(['Choose whether to overwrite each path with a Proteum-managed symlink:', ...blockedPaths.map((entry) => `- ${entry}`)].join('\n'));
97
+
98
+ const overwriteBlockedPaths: string[] = [];
99
+
100
+ for (const blockedPath of blockedPaths) {
101
+ const response = await prompts(
102
+ {
103
+ type: 'confirm',
104
+ name: 'value',
105
+ message: `Overwrite ${blockedPath}?`,
106
+ initial: false,
107
+ },
108
+ {
109
+ onCancel: () => {
110
+ throw new UsageError('Cancelled `proteum configure agents`.');
111
+ },
112
+ },
113
+ );
114
+
115
+ if (response.value === true) overwriteBlockedPaths.push(blockedPath);
116
+ }
117
+
118
+ return overwriteBlockedPaths;
119
+ };
120
+
121
+ const renderConfigureResultSections = (result: TConfigureProjectAgentSymlinksResult) => {
122
+ const sections: string[] = [];
123
+
124
+ sections.push(
125
+ renderRows(
126
+ [
127
+ { label: 'mode', value: result.mode },
128
+ ...(result.monorepoRoot ? [{ label: 'monorepo root', value: result.monorepoRoot }] : []),
129
+ ],
130
+ { minLabelWidth: 16, maxLabelWidth: 16 },
131
+ ),
132
+ );
133
+
134
+ if (result.created.length > 0) sections.push(['Created:', ...result.created.map((entry) => `- ${entry}`)].join('\n'));
135
+ if (result.updated.length > 0) sections.push(['Updated:', ...result.updated.map((entry) => `- ${entry}`)].join('\n'));
136
+ if (result.overwritten.length > 0)
137
+ sections.push(['Overwritten:', ...result.overwritten.map((entry) => `- ${entry}`)].join('\n'));
138
+ if (result.updatedGitignores.length > 0)
139
+ sections.push(['Updated .gitignore:', ...result.updatedGitignores.map((entry) => `- ${entry}`)].join('\n'));
140
+ if (result.blocked.length > 0)
141
+ sections.push(
142
+ [
143
+ 'Skipped existing non-managed paths:',
144
+ ...result.blocked.map((entry) => `- ${entry}`),
145
+ ].join('\n'),
146
+ );
147
+
148
+ return sections;
149
+ };
150
+
151
+ /*----------------------------------
152
+ - COMMAND
153
+ ----------------------------------*/
154
+
155
+ export const run = async (): Promise<void> => {
156
+ if (cli.args.action !== 'agents') throw new UsageError('Usage: `proteum configure agents`');
157
+
158
+ const appRoot = resolveCanonicalPath(cli.paths.appRoot);
159
+ assertProteumAppRoot(appRoot);
160
+
161
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
162
+ throw new UsageError('`proteum configure agents` is interactive and requires a TTY.');
163
+ }
164
+
165
+ const likelyRepoRoot = findLikelyRepoRoot(appRoot);
166
+ const defaultMonorepoRoot =
167
+ likelyRepoRoot && likelyRepoRoot !== appRoot && isInsideDirectory({ child: appRoot, parent: likelyRepoRoot })
168
+ ? likelyRepoRoot
169
+ : undefined;
170
+ console.info(
171
+ [
172
+ await renderTitle('PROTEUM CONFIGURE AGENTS', 'Configure Proteum-managed instruction symlinks.'),
173
+ renderRows([{ label: 'app', value: appRoot === process.cwd() ? '.' : appRoot }]),
174
+ ].join('\n\n'),
175
+ );
176
+
177
+ const monorepoResponse = await prompts(
178
+ {
179
+ type: 'confirm',
180
+ name: 'value',
181
+ message: 'Is this Proteum app part of a monorepo?',
182
+ initial: defaultMonorepoRoot !== undefined,
183
+ },
184
+ {
185
+ onCancel: () => {
186
+ throw new UsageError('Cancelled `proteum configure agents`.');
187
+ },
188
+ },
189
+ );
190
+ const isMonorepo = monorepoResponse.value === true;
191
+ const monorepoRoot = isMonorepo
192
+ ? await promptMonorepoRoot({
193
+ appRoot,
194
+ defaultRoot: defaultMonorepoRoot,
195
+ })
196
+ : undefined;
197
+
198
+ const preview = configureProjectAgentSymlinks({
199
+ appRoot,
200
+ coreRoot: cli.paths.core.root,
201
+ dryRun: true,
202
+ monorepoRoot,
203
+ });
204
+ const overwriteBlockedPaths = await promptBlockedOverwritePaths(preview.blocked);
205
+
206
+ console.info(
207
+ await renderStep(
208
+ '[1/1]',
209
+ isMonorepo
210
+ ? `Writing monorepo-aware instruction symlinks using ${monorepoRoot}.`
211
+ : 'Writing standalone instruction symlinks.',
212
+ ),
213
+ );
214
+
215
+ const result = configureProjectAgentSymlinks({
216
+ appRoot,
217
+ coreRoot: cli.paths.core.root,
218
+ monorepoRoot,
219
+ overwriteBlockedPaths,
220
+ });
221
+ const sections = renderConfigureResultSections(result);
222
+
223
+ console.info(await renderSuccess('Proteum-managed instruction symlinks are configured.'));
224
+
225
+ if (sections.length > 0) console.info(`\n${sections.join('\n\n')}`);
226
+ };
@@ -21,7 +21,6 @@ import {
21
21
  // Configs
22
22
  import Compiler from '../compiler';
23
23
  import { createDevEventServer } from './devEvents';
24
- import { ensureProjectAgentSymlinks } from '../utils/agents';
25
24
  import { renderDevSession, renderServerReadyBanner, renderDevShutdownBanner } from '../presentation/devSession';
26
25
  import { clearInteractiveConsole } from '../presentation/welcome';
27
26
  import {
@@ -557,7 +556,6 @@ const createIndexedSourceWatching = ({
557
556
  const runDevLoop = async () => {
558
557
  devSessionStopping = false;
559
558
  clearInteractiveConsole();
560
- ensureProjectAgentSymlinks({ appRoot: app.paths.root, coreRoot: cli.paths.core.root });
561
559
  await ensureDevSessionSlot();
562
560
  const proteumInstall = resolveFrameworkInstallInfo({
563
561
  appRoot: app.paths.root,
@@ -163,7 +163,35 @@ const renderOwners = (matches: TExplainOwnerMatch[]) =>
163
163
  ? ['Owner matches', '- none'].join('\n')
164
164
  : [
165
165
  'Owner matches',
166
- ...matches.map((match) => `- [${match.kind}] ${match.label} score=${match.score} source=${formatSource(match)}`),
166
+ ...matches.map(
167
+ (match) =>
168
+ `- [${match.kind}] ${match.label} score=${match.score} scope=${match.scopeLabel} origin=${match.originHint} source=${formatSource(match)}`,
169
+ ),
170
+ ].join('\n');
171
+
172
+ const renderChain = (response: TDiagnoseResponse) =>
173
+ !response.chain || response.chain.length === 0
174
+ ? ['Chain', '- none'].join('\n')
175
+ : [
176
+ 'Chain',
177
+ ...response.chain.map(
178
+ (item) =>
179
+ `- [${item.kind}] ${item.label}${item.source?.filepath ? ` source=${item.source.filepath}${item.source.line ? `:${item.source.line}` : ''}${item.source.column ? `:${item.source.column}` : ''}` : ''}${item.details.length > 0 ? ` details=${item.details.join(', ')}` : ''}`,
180
+ ),
181
+ ].join('\n');
182
+
183
+ const renderOrientation = (response: TDiagnoseResponse) =>
184
+ !response.orientation
185
+ ? ['Orientation', '- none'].join('\n')
186
+ : [
187
+ 'Orientation',
188
+ `- agents=${response.orientation.guidance.agents}`,
189
+ `- diagnostics=${response.orientation.guidance.diagnostics}`,
190
+ `- optimizations=${response.orientation.guidance.optimizations}`,
191
+ `- codingStyle=${response.orientation.guidance.codingStyle}`,
192
+ `- areaAgents=${response.orientation.guidance.areaAgents.join(', ') || 'none'}`,
193
+ `- connected imports=${response.orientation.connected.imports.length} producers=${response.orientation.connected.producers.length}`,
194
+ ...response.orientation.nextSteps.map((step) => `- next=${step.command} (${step.reason})`),
167
195
  ].join('\n');
168
196
 
169
197
  const renderSuspects = (response: TDiagnoseResponse) =>
@@ -188,6 +216,10 @@ const renderHuman = (manifest: ReturnType<typeof readProteumManifest>, response:
188
216
  '',
189
217
  renderOwners(response.owner.matches.slice(0, 6)),
190
218
  '',
219
+ renderChain(response),
220
+ '',
221
+ renderOrientation(response),
222
+ '',
191
223
  renderDoctorResponseHuman({
192
224
  emptyMessage: 'No manifest diagnostics were found.',
193
225
  manifest,
@@ -55,7 +55,7 @@ export const run = async (): Promise<void> => {
55
55
  ? ['- No matching manifest owners were found.']
56
56
  : response.matches.map(
57
57
  (match) =>
58
- `- [${match.kind}] ${match.label} score=${match.score} source=${match.source.filepath}${match.source.line ? `:${match.source.line}` : ''}${match.source.column ? `:${match.source.column}` : ''}`,
58
+ `- [${match.kind}] ${match.label} score=${match.score} scope=${match.scopeLabel} origin=${match.originHint} source=${match.source.filepath}${match.source.line ? `:${match.source.line}` : ''}${match.source.column ? `:${match.source.column}` : ''}`,
59
59
  )),
60
60
  ].join('\n'),
61
61
  );
@@ -0,0 +1,51 @@
1
+ import path from 'path';
2
+
3
+ import cli from '..';
4
+ import { runPageContractMigration } from '../migrate/pageContract';
5
+
6
+ const printHuman = (summary: ReturnType<typeof runPageContractMigration>) => {
7
+ console.log(`Proteum migrate page-contract`);
8
+ console.log(`App root: ${summary.appRoot}`);
9
+ console.log(`Scanned files: ${summary.scannedFiles}`);
10
+ console.log(`Changed files: ${summary.changedFiles.length}${summary.dryRun ? ' (dry run)' : ''}`);
11
+
12
+ if (summary.changedFiles.length > 0) {
13
+ console.log('');
14
+ console.log('Rewritten files:');
15
+ for (const filepath of summary.changedFiles) {
16
+ console.log(`- ${path.relative(summary.appRoot, filepath)}`);
17
+ }
18
+ }
19
+
20
+ if (summary.manualFixes.length > 0) {
21
+ console.log('');
22
+ console.log('Manual fixes required:');
23
+ for (const fix of summary.manualFixes) {
24
+ console.log(
25
+ `- ${path.relative(summary.appRoot, fix.filepath)}:${fix.line}:${fix.column} ${fix.routeLabel} :: ${fix.reason}`,
26
+ );
27
+ }
28
+ }
29
+ };
30
+
31
+ export const run = async (): Promise<void> => {
32
+ const action = String(cli.args.action || '').trim();
33
+ if (action !== 'page-contract') {
34
+ throw new Error(`Unsupported migrate action "${action}". Expected "page-contract".`);
35
+ }
36
+
37
+ const summary = runPageContractMigration({
38
+ appRoot: String(cli.args.workdir || process.cwd()),
39
+ dryRun: cli.args.dryRun === true,
40
+ });
41
+
42
+ if (cli.args.json === true) {
43
+ console.log(JSON.stringify(summary, null, 2));
44
+ } else {
45
+ printHuman(summary);
46
+ }
47
+
48
+ if (summary.manualFixes.length > 0) {
49
+ throw new Error(`Page-contract migration requires manual fixes in ${summary.manualFixes.length} location(s).`);
50
+ }
51
+ };
@@ -0,0 +1,169 @@
1
+ import got from 'got';
2
+ import path from 'path';
3
+ import fs from 'fs-extra';
4
+ import { UsageError } from 'clipanion';
5
+
6
+ import cli from '..';
7
+ import Compiler from '../compiler';
8
+ import { readProteumManifest } from '../compiler/common/proteumManifest';
9
+ import { buildOrientationResponse, type TOrientResponse } from '@common/dev/inspection';
10
+ import type { TProteumManifest } from '@common/dev/proteumManifest';
11
+
12
+ const normalizeBaseUrl = (value: string) => value.replace(/\/+$/, '');
13
+ const dedupe = <TValue>(values: TValue[]) => [...new Set(values)];
14
+
15
+ const buildBaseUrlCandidates = (value: string) => {
16
+ const normalized = normalizeBaseUrl(value);
17
+
18
+ try {
19
+ const parsed = new URL(normalized);
20
+ const port = parsed.port;
21
+ const pathname = parsed.pathname === '/' ? '' : parsed.pathname;
22
+ const search = parsed.search;
23
+ const hash = parsed.hash;
24
+ const buildUrl = (hostname: string) => `${parsed.protocol}//${hostname}${port ? `:${port}` : ''}${pathname}${search}${hash}`;
25
+
26
+ if (parsed.hostname === '127.0.0.1') return dedupe([normalized, buildUrl('localhost'), buildUrl('[::1]')]);
27
+ if (parsed.hostname === 'localhost') return dedupe([normalized, buildUrl('127.0.0.1'), buildUrl('[::1]')]);
28
+ if (parsed.hostname === '[::1]' || parsed.hostname === '::1') return dedupe([normalized, buildUrl('localhost'), buildUrl('127.0.0.1')]);
29
+ } catch (_error) {}
30
+
31
+ return [normalized];
32
+ };
33
+
34
+ const getRouterPortFromManifest = () => {
35
+ const manifestFilepath = path.join(cli.args.workdir as string, '.proteum', 'manifest.json');
36
+ if (!fs.existsSync(manifestFilepath)) return undefined;
37
+
38
+ const manifest = fs.readJsonSync(manifestFilepath, { throws: false }) as
39
+ | { env?: { resolved?: { routerPort?: number } } }
40
+ | undefined;
41
+ const port = manifest?.env?.resolved?.routerPort;
42
+
43
+ if (typeof port !== 'number' || port <= 0) return undefined;
44
+
45
+ return String(port);
46
+ };
47
+
48
+ const getRouterPort = () => {
49
+ const overridePort = typeof cli.args.port === 'string' && cli.args.port ? cli.args.port : '';
50
+ if (overridePort) return overridePort;
51
+
52
+ const envPort = process.env.PORT?.trim();
53
+ if (envPort) return envPort;
54
+
55
+ const manifestPort = getRouterPortFromManifest();
56
+ if (manifestPort) return manifestPort;
57
+
58
+ throw new UsageError(
59
+ `Could not determine the router port from PORT or .proteum/manifest.json in ${cli.args.workdir as string}. Pass --port or --url explicitly.`,
60
+ );
61
+ };
62
+
63
+ const getRouterBaseUrls = () => {
64
+ const explicitUrl = typeof cli.args.url === 'string' && cli.args.url ? cli.args.url.trim() : '';
65
+ if (explicitUrl) return buildBaseUrlCandidates(explicitUrl);
66
+
67
+ const port = getRouterPort();
68
+ return dedupe([`http://localhost:${port}`, `http://127.0.0.1:${port}`, `http://[::1]:${port}`]);
69
+ };
70
+
71
+ const requestJson = async <TResponse>(pathname: string) => {
72
+ const attempts: string[] = [];
73
+
74
+ for (const baseUrl of getRouterBaseUrls()) {
75
+ try {
76
+ const response = await got(`${baseUrl}${pathname}`, {
77
+ responseType: 'json',
78
+ retry: { limit: 0 },
79
+ throwHttpErrors: false,
80
+ });
81
+
82
+ if (response.statusCode >= 400) {
83
+ const body = response.body as { error?: string } | undefined;
84
+ throw new UsageError(body?.error || `Orient request failed with status ${response.statusCode}.`);
85
+ }
86
+
87
+ return response.body as TResponse;
88
+ } catch (error) {
89
+ if (error instanceof UsageError) throw error;
90
+ attempts.push(`${baseUrl}${pathname}: ${error instanceof Error ? error.message : String(error)}`);
91
+ }
92
+ }
93
+
94
+ throw new UsageError(
95
+ [
96
+ 'Could not reach the Proteum dev diagnostics server for orient.',
97
+ ...attempts.map((attempt) => `- ${attempt}`),
98
+ 'Make sure the app is running with `proteum dev`, or omit --port/--url to read the local manifest from disk.',
99
+ ].join('\n'),
100
+ );
101
+ };
102
+
103
+ const resolveManifest = async (): Promise<TProteumManifest> => {
104
+ const shouldUseRemoteServer =
105
+ (typeof cli.args.port === 'string' && cli.args.port.length > 0) ||
106
+ (typeof cli.args.url === 'string' && cli.args.url.length > 0);
107
+
108
+ if (shouldUseRemoteServer) {
109
+ return await requestJson<TProteumManifest>('/__proteum/explain');
110
+ }
111
+
112
+ const compiler = new Compiler('dev');
113
+ await compiler.refreshGeneratedTypings();
114
+ return readProteumManifest(cli.paths.appRoot);
115
+ };
116
+
117
+ const renderHuman = (response: TOrientResponse) =>
118
+ [
119
+ 'Proteum orient',
120
+ `- query=${response.query}`,
121
+ `- appRoot=${response.app.appRoot}`,
122
+ `- repoRoot=${response.app.repoRoot}`,
123
+ `- identifier=${response.app.identifier}`,
124
+ ...(response.app.routerPort ? [`- routerPort=${response.app.routerPort}`] : []),
125
+ 'Guidance',
126
+ `- agents=${response.guidance.agents}`,
127
+ `- diagnostics=${response.guidance.diagnostics}`,
128
+ `- optimizations=${response.guidance.optimizations}`,
129
+ `- codingStyle=${response.guidance.codingStyle}`,
130
+ `- areaAgents=${response.guidance.areaAgents.join(', ') || 'none'}`,
131
+ 'Owner',
132
+ ...(response.owner.matches.length === 0
133
+ ? ['- none']
134
+ : response.owner.matches.slice(0, 6).map(
135
+ (match) =>
136
+ `- [${match.kind}] ${match.label} score=${match.score} scope=${match.scopeLabel} origin=${match.originHint} source=${match.source.filepath}${match.source.line ? `:${match.source.line}` : ''}${match.source.column ? `:${match.source.column}` : ''}`,
137
+ )),
138
+ 'Connected',
139
+ ...(response.connected.imports.length === 0
140
+ ? ['- imports=none']
141
+ : response.connected.imports.map(
142
+ (entry) => `- import ${entry.namespace}.${entry.clientAccessor} -> ${entry.httpPath} source=${entry.filepath}`,
143
+ )),
144
+ ...(response.connected.producers.length === 0
145
+ ? ['- producers=none']
146
+ : response.connected.producers.map(
147
+ (project) =>
148
+ `- producer ${project.namespace} identifier=${project.identityIdentifier || project.identityName || 'unknown'} source=${project.sourceKind || 'missing'} internal=${project.urlInternal || 'missing'}`,
149
+ )),
150
+ 'Next',
151
+ ...response.nextSteps.map((step) => `- ${step.command} (${step.reason})`),
152
+ 'Warnings',
153
+ ...(response.warnings.length === 0 ? ['- none'] : response.warnings.map((warning) => `- ${warning}`)),
154
+ ].join('\n');
155
+
156
+ export const run = async () => {
157
+ const query = typeof cli.args.query === 'string' ? cli.args.query.trim() : '';
158
+ if (!query) throw new UsageError('A query is required. Example: proteum orient /api/Auth/CurrentUser');
159
+
160
+ const manifest = await resolveManifest();
161
+ const response = buildOrientationResponse(manifest, query);
162
+
163
+ if (cli.args.json === true) {
164
+ console.log(JSON.stringify(response, null, 2));
165
+ return;
166
+ }
167
+
168
+ console.log(renderHuman(response));
169
+ };
@@ -164,12 +164,19 @@ const renderRequest = (response: TPerfRequestResponse) =>
164
164
  (call) =>
165
165
  `- ${call.label} | duration=${formatDuration(call.durationMs)} status=${call.statusCode ?? 'pending'} origin=${call.origin}${call.errorMessage ? ` error=${truncate(call.errorMessage, 96)}` : ''}`,
166
166
  )),
167
+ 'Chain',
168
+ ...(!response.request.chain || response.request.chain.length === 0
169
+ ? ['- none']
170
+ : response.request.chain.map(
171
+ (item) =>
172
+ `- [${item.kind}] ${item.label}${item.source?.filepath ? ` | ${item.source.filepath}${item.source.line ? `:${item.source.line}` : ''}${item.source.column ? `:${item.source.column}` : ''}` : ''}${item.details.length > 0 ? ` | ${item.details.join(', ')}` : ''}`,
173
+ )),
167
174
  'Hot SQL',
168
175
  ...(response.request.hottestSqlQueries.length === 0
169
176
  ? ['- none']
170
177
  : response.request.hottestSqlQueries.map(
171
178
  (query) =>
172
- `- ${query.callerLabel} | ${query.operation}${query.model ? ` ${query.model}` : ''} | duration=${formatDuration(query.durationMs)} | ${truncate(query.query, 104)}`,
179
+ `- ${query.callerLabel} | ${query.operation}${query.model ? ` ${query.model}` : ''}${query.fingerprint ? ` | fp=${query.fingerprint}` : ''} | duration=${formatDuration(query.durationMs)} | ${truncate(query.query, 104)}`,
173
180
  )),
174
181
  ].join('\n');
175
182