proteum 2.1.9 → 2.2.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/.codex/environments/environment.toml +11 -0
- package/AGENTS.md +27 -11
- package/README.md +30 -11
- package/agents/project/AGENTS.md +172 -123
- package/agents/project/CODING_STYLE.md +1 -1
- package/agents/project/app-root/AGENTS.md +16 -0
- package/agents/project/client/AGENTS.md +5 -5
- package/agents/project/client/pages/AGENTS.md +13 -13
- package/agents/project/diagnostics.md +19 -10
- package/agents/project/optimizations.md +5 -6
- package/agents/project/root/AGENTS.md +297 -0
- package/agents/project/server/routes/AGENTS.md +2 -2
- package/agents/project/server/services/AGENTS.md +4 -2
- package/agents/project/tests/AGENTS.md +9 -2
- package/cli/app/index.ts +31 -7
- package/cli/commands/configure.ts +226 -0
- package/cli/commands/dev.ts +0 -2
- package/cli/commands/diagnose.ts +33 -1
- package/cli/commands/explain.ts +1 -1
- package/cli/commands/migrate.ts +51 -0
- package/cli/commands/orient.ts +169 -0
- package/cli/commands/perf.ts +8 -1
- package/cli/commands/verify.ts +1003 -49
- package/cli/compiler/artifacts/manifest.ts +4 -4
- package/cli/compiler/artifacts/routing.ts +2 -2
- package/cli/compiler/artifacts/services.ts +12 -3
- package/cli/compiler/client/index.ts +65 -19
- package/cli/compiler/common/files/style.ts +47 -2
- package/cli/compiler/common/generatedRouteModules.ts +31 -38
- package/cli/compiler/common/index.ts +10 -0
- package/cli/compiler/common/proteumManifest.ts +1 -0
- package/cli/compiler/server/index.ts +34 -9
- package/cli/context.ts +6 -1
- package/cli/index.ts +7 -8
- package/cli/migrate/pageContract.ts +516 -0
- package/cli/paths.ts +47 -6
- package/cli/presentation/commands.ts +100 -10
- package/cli/presentation/devSession.ts +4 -6
- package/cli/presentation/help.ts +2 -2
- package/cli/presentation/ink.ts +10 -5
- package/cli/presentation/welcome.ts +2 -4
- package/cli/runtime/commands.ts +94 -1
- package/cli/scaffold/index.ts +2 -2
- package/cli/scaffold/templates.ts +4 -2
- package/cli/utils/agents.ts +273 -58
- package/client/dev/profiler/index.tsx +3 -2
- package/client/router.ts +10 -2
- package/client/services/router/index.tsx +6 -22
- package/common/dev/connect.ts +20 -4
- package/common/dev/console.ts +7 -0
- package/common/dev/contractsDoctor.ts +354 -0
- package/common/dev/diagnostics.ts +10 -7
- package/common/dev/inspection.ts +830 -38
- package/common/dev/performance.ts +19 -5
- package/common/dev/profiler.ts +1 -0
- package/common/dev/proteumManifest.ts +5 -4
- package/common/dev/requestTrace.ts +78 -1
- package/common/env/proteumEnv.ts +10 -3
- package/common/router/contracts.ts +8 -11
- package/common/router/index.ts +2 -2
- package/common/router/pageData.ts +72 -0
- package/common/router/register.ts +10 -46
- package/common/router/response/page.ts +28 -16
- package/docs/assets/unique-domains-chip.png +0 -0
- package/docs/dev-sessions.md +8 -4
- package/docs/diagnostics.md +77 -11
- package/docs/migrate-from-2.1.3.md +388 -0
- package/docs/request-tracing.md +42 -9
- package/package.json +6 -1
- package/scripts/update-codex-agents.ts +2 -2
- package/server/app/container/console/index.ts +11 -1
- package/server/app/container/trace/index.ts +370 -72
- package/server/app/devDiagnostics.ts +1 -1
- package/server/app/index.ts +5 -1
- package/server/services/auth/index.ts +9 -0
- package/server/services/prisma/index.ts +15 -12
- package/server/services/router/http/index.ts +1 -1
- package/server/services/router/index.ts +105 -23
- package/server/services/router/request/api.ts +7 -1
- package/server/services/router/request/index.ts +2 -1
- package/server/services/router/response/index.ts +8 -28
- package/types/global/vendors.d.ts +12 -0
- package/types/vendors.d.ts +12 -0
- package/common/router/pageSetup.ts +0 -51
package/cli/commands/dev.ts
CHANGED
|
@@ -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,
|
package/cli/commands/diagnose.ts
CHANGED
|
@@ -163,7 +163,35 @@ const renderOwners = (matches: TExplainOwnerMatch[]) =>
|
|
|
163
163
|
? ['Owner matches', '- none'].join('\n')
|
|
164
164
|
: [
|
|
165
165
|
'Owner matches',
|
|
166
|
-
...matches.map(
|
|
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,
|
package/cli/commands/explain.ts
CHANGED
|
@@ -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
|
+
};
|
package/cli/commands/perf.ts
CHANGED
|
@@ -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
|
|