proteum 2.2.9 → 2.4.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 +10 -4
- package/README.md +58 -15
- package/agents/project/AGENTS.md +53 -10
- package/agents/project/DOCUMENTATION.md +1326 -0
- package/agents/project/app-root/AGENTS.md +2 -2
- package/agents/project/diagnostics.md +12 -7
- package/agents/project/optimizations.md +1 -0
- package/agents/project/root/AGENTS.md +24 -9
- package/agents/project/tests/AGENTS.md +7 -0
- package/agents/project/tests/e2e/AGENTS.md +13 -0
- package/agents/project/tests/e2e/REAL_WORLD_JOURNEY_TESTS.md +192 -0
- package/cli/commands/connect.ts +40 -4
- package/cli/commands/dev.ts +148 -25
- package/cli/commands/diagnose.ts +138 -5
- package/cli/commands/doctor.ts +24 -4
- package/cli/commands/explain.ts +134 -6
- package/cli/commands/mcp.ts +133 -0
- package/cli/commands/orient.ts +93 -3
- package/cli/commands/perf.ts +118 -13
- package/cli/commands/runtime.ts +234 -0
- package/cli/commands/trace.ts +116 -21
- package/cli/mcp/router.ts +1010 -0
- package/cli/presentation/commands.ts +93 -26
- package/cli/presentation/devSession.ts +2 -0
- package/cli/presentation/help.ts +1 -1
- package/cli/runtime/commands.ts +215 -24
- package/cli/runtime/devSessions.ts +328 -2
- package/cli/runtime/mcpDaemon.ts +288 -0
- package/cli/runtime/ports.ts +151 -0
- package/cli/utils/agentOutput.ts +46 -0
- package/cli/utils/agents.ts +194 -51
- package/cli/utils/appRoots.ts +232 -0
- package/common/dev/diagnostics.ts +1 -1
- package/common/dev/inspection.ts +22 -7
- package/common/dev/mcpPayloads.ts +1150 -0
- package/common/dev/mcpServer.ts +287 -0
- package/docs/agent-routing.md +137 -0
- package/docs/dev-commands.md +2 -0
- package/docs/dev-sessions.md +4 -1
- package/docs/diagnostics.md +70 -24
- package/docs/mcp.md +206 -0
- package/docs/migrate-from-2.1.3.md +14 -6
- package/docs/request-tracing.md +12 -6
- package/package.json +11 -3
- package/server/app/devMcp.ts +204 -0
- package/server/services/router/http/cache.ts +116 -0
- package/server/services/router/http/index.ts +94 -35
- package/server/services/router/index.ts +8 -11
- package/server/services/router/request/ip.test.cjs +0 -1
- package/tests/agents-utils.test.cjs +92 -14
- package/tests/cli-mcp-command.test.cjs +262 -0
- package/tests/codex-mcp-usage.test.cjs +307 -0
- package/tests/dev-sessions.test.cjs +113 -0
- package/tests/dev-transpile-watch.test.cjs +117 -9
- package/tests/eslint-rules.test.cjs +0 -1
- package/tests/inspection.test.cjs +66 -0
- package/tests/mcp.test.cjs +873 -0
- package/tests/router-cache-config.test.cjs +73 -0
- package/vitest.config.mjs +9 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import cli from '..';
|
|
2
|
+
import { startProteumMachineMcpRouter, startProteumMachineMcpRouterHttp } from '../mcp/router';
|
|
3
|
+
import {
|
|
4
|
+
ensureMachineMcpDaemonProcess,
|
|
5
|
+
inspectMachineMcpDaemonRecord,
|
|
6
|
+
resolveMachineMcpDaemonPort,
|
|
7
|
+
stopMachineMcpDaemonProcess,
|
|
8
|
+
} from '../runtime/mcpDaemon';
|
|
9
|
+
|
|
10
|
+
const printJson = (payload: unknown) => {
|
|
11
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const printStatus = async () => {
|
|
15
|
+
const inspection = await inspectMachineMcpDaemonRecord({ cleanStale: true });
|
|
16
|
+
|
|
17
|
+
if (cli.args.json === true) {
|
|
18
|
+
printJson({
|
|
19
|
+
daemon: inspection
|
|
20
|
+
? {
|
|
21
|
+
live: inspection.live,
|
|
22
|
+
stale: inspection.stale,
|
|
23
|
+
invalid: inspection.invalid,
|
|
24
|
+
parseError: inspection.parseError,
|
|
25
|
+
record: inspection.record,
|
|
26
|
+
}
|
|
27
|
+
: null,
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!inspection?.record || !inspection.live) {
|
|
33
|
+
console.info('No live Proteum machine MCP daemon found.');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.info(
|
|
38
|
+
[
|
|
39
|
+
`Proteum machine MCP daemon is running.`,
|
|
40
|
+
`pid ${inspection.record.pid}`,
|
|
41
|
+
`mcp ${inspection.record.mcpUrl}`,
|
|
42
|
+
`health ${inspection.record.healthUrl}`,
|
|
43
|
+
].join('\n'),
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const runDaemon = async () => {
|
|
48
|
+
const existing = await inspectMachineMcpDaemonRecord({ cleanStale: true });
|
|
49
|
+
|
|
50
|
+
if (existing?.record && existing.live && existing.record.pid !== process.pid) {
|
|
51
|
+
if (cli.args.json === true) {
|
|
52
|
+
printJson({ started: false, daemon: existing.record });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.info(`Proteum machine MCP daemon is already running at ${existing.record.mcpUrl} (pid ${existing.record.pid}).`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const port = resolveMachineMcpDaemonPort(typeof cli.args.port === 'string' ? cli.args.port : undefined);
|
|
61
|
+
|
|
62
|
+
await startProteumMachineMcpRouterHttp({
|
|
63
|
+
port,
|
|
64
|
+
version: String(cli.packageJson.version || ''),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (cli.args.json === true) {
|
|
68
|
+
printJson({
|
|
69
|
+
started: true,
|
|
70
|
+
daemon: {
|
|
71
|
+
pid: process.pid,
|
|
72
|
+
mcpUrl: `http://127.0.0.1:${port}/mcp`,
|
|
73
|
+
healthUrl: `http://127.0.0.1:${port}/health`,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
} else {
|
|
77
|
+
console.info(`Proteum machine MCP daemon started at http://127.0.0.1:${port}/mcp.`);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const ensureDaemon = async () => {
|
|
82
|
+
const result = await ensureMachineMcpDaemonProcess({
|
|
83
|
+
coreRoot: cli.paths.core.root,
|
|
84
|
+
port: typeof cli.args.port === 'string' ? cli.args.port : undefined,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (cli.args.json === true) {
|
|
88
|
+
printJson({ started: result.started, daemon: result.inspection.record });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (result.inspection.record) {
|
|
93
|
+
console.info(
|
|
94
|
+
result.started
|
|
95
|
+
? `Proteum machine MCP daemon started at ${result.inspection.record.mcpUrl}.`
|
|
96
|
+
: `Proteum machine MCP daemon is already running at ${result.inspection.record.mcpUrl}.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const run = async () => {
|
|
102
|
+
if (cli.args.action === 'status') {
|
|
103
|
+
await printStatus();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (cli.args.action === 'stop') {
|
|
108
|
+
const result = await stopMachineMcpDaemonProcess();
|
|
109
|
+
if (cli.args.json === true) {
|
|
110
|
+
printJson({ stopped: result.stopped, daemon: result.inspection?.record || null });
|
|
111
|
+
} else if (result.stopped) {
|
|
112
|
+
console.info('Proteum machine MCP daemon stopped.');
|
|
113
|
+
} else if (result.inspection?.record) {
|
|
114
|
+
console.info(`Could not stop Proteum machine MCP daemon pid ${result.inspection.record.pid}.`);
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (cli.args.daemon === true) {
|
|
121
|
+
await runDaemon();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (cli.args.stdio !== true && (process.stdout.isTTY || cli.args.json === true)) {
|
|
126
|
+
await ensureDaemon();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await startProteumMachineMcpRouter({
|
|
131
|
+
version: String(cli.packageJson.version || ''),
|
|
132
|
+
});
|
|
133
|
+
};
|
package/cli/commands/orient.ts
CHANGED
|
@@ -7,7 +7,9 @@ import cli from '..';
|
|
|
7
7
|
import Compiler from '../compiler';
|
|
8
8
|
import { readProteumManifest } from '../compiler/common/proteumManifest';
|
|
9
9
|
import { buildOrientationResponse, type TOrientResponse } from '@common/dev/inspection';
|
|
10
|
+
import { resolveTriggeredInstructionReads } from '@common/dev/mcpPayloads';
|
|
10
11
|
import type { TProteumManifest } from '@common/dev/proteumManifest';
|
|
12
|
+
import { compactList, printAgentResponse, printJson, quoteCommandArgument } from '../utils/agentOutput';
|
|
11
13
|
|
|
12
14
|
const normalizeBaseUrl = (value: string) => value.replace(/\/+$/, '');
|
|
13
15
|
const dedupe = <TValue>(values: TValue[]) => [...new Set(values)];
|
|
@@ -124,6 +126,7 @@ const renderHuman = (response: TOrientResponse) =>
|
|
|
124
126
|
...(response.app.routerPort ? [`- routerPort=${response.app.routerPort}`] : []),
|
|
125
127
|
'Guidance',
|
|
126
128
|
`- agents=${response.guidance.agents}`,
|
|
129
|
+
`- documentation=${response.guidance.documentation}`,
|
|
127
130
|
`- diagnostics=${response.guidance.diagnostics}`,
|
|
128
131
|
`- optimizations=${response.guidance.optimizations}`,
|
|
129
132
|
`- codingStyle=${response.guidance.codingStyle}`,
|
|
@@ -153,6 +156,88 @@ const renderHuman = (response: TOrientResponse) =>
|
|
|
153
156
|
...(response.warnings.length === 0 ? ['- none'] : response.warnings.map((warning) => `- ${warning}`)),
|
|
154
157
|
].join('\n');
|
|
155
158
|
|
|
159
|
+
const compactOwnerMatch = (match: TOrientResponse['owner']['matches'][number]) => ({
|
|
160
|
+
kind: match.kind,
|
|
161
|
+
label: match.label,
|
|
162
|
+
score: match.score,
|
|
163
|
+
scope: match.scopeLabel,
|
|
164
|
+
origin: match.originHint,
|
|
165
|
+
source: match.source,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const buildInstructionPlan = (response: TOrientResponse) => {
|
|
169
|
+
const triggered = resolveTriggeredInstructionReads({
|
|
170
|
+
codingStyle: response.guidance.codingStyle,
|
|
171
|
+
diagnostics: response.guidance.diagnostics,
|
|
172
|
+
documentation: response.guidance.documentation,
|
|
173
|
+
optimizations: response.guidance.optimizations,
|
|
174
|
+
query: response.normalizedQuery || response.query,
|
|
175
|
+
rootAgentsFile:
|
|
176
|
+
response.app.repoRoot !== response.app.appRoot
|
|
177
|
+
? path.join(response.app.repoRoot, 'AGENTS.md')
|
|
178
|
+
: response.guidance.agents,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
mustRead: [
|
|
183
|
+
...new Set([
|
|
184
|
+
response.guidance.agents,
|
|
185
|
+
...response.guidance.areaAgents,
|
|
186
|
+
...triggered.map((entry) => entry.file),
|
|
187
|
+
]),
|
|
188
|
+
],
|
|
189
|
+
triggered,
|
|
190
|
+
readWhen: [
|
|
191
|
+
{
|
|
192
|
+
file: response.guidance.documentation,
|
|
193
|
+
when: 'Read before non-trivial coding tasks to choose the smallest `/docs` pack and update docs after changes.',
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
file: response.guidance.diagnostics,
|
|
197
|
+
when: 'Read only for raw errors, failing requests, traces, perf regressions, or reproduction work.',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
file: response.guidance.codingStyle,
|
|
201
|
+
when: 'Read before editing implementation files.',
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
file: response.guidance.optimizations,
|
|
205
|
+
when: 'Read after client-side implementation or when the task explicitly concerns packages, build, runtime, or performance.',
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const printCompactOrient = (response: TOrientResponse) => {
|
|
212
|
+
const topOwner = response.owner.matches[0];
|
|
213
|
+
const summary = topOwner
|
|
214
|
+
? `${response.query} -> ${topOwner.kind} ${topOwner.label} (${topOwner.scopeLabel})`
|
|
215
|
+
: `${response.query} -> no manifest owner matched`;
|
|
216
|
+
|
|
217
|
+
printAgentResponse({
|
|
218
|
+
summary,
|
|
219
|
+
data: {
|
|
220
|
+
query: response.query,
|
|
221
|
+
app: response.app,
|
|
222
|
+
owner: {
|
|
223
|
+
top: topOwner ? compactOwnerMatch(topOwner) : undefined,
|
|
224
|
+
matches: compactList(response.owner.matches, 4).map(compactOwnerMatch),
|
|
225
|
+
totalReturned: response.owner.matches.length,
|
|
226
|
+
},
|
|
227
|
+
instructions: buildInstructionPlan(response),
|
|
228
|
+
connected: {
|
|
229
|
+
imports: compactList(response.connected.imports, 4),
|
|
230
|
+
producers: compactList(response.connected.producers, 3),
|
|
231
|
+
totalImports: response.connected.imports.length,
|
|
232
|
+
totalProducers: response.connected.producers.length,
|
|
233
|
+
},
|
|
234
|
+
warnings: response.warnings,
|
|
235
|
+
},
|
|
236
|
+
nextActions: response.nextSteps,
|
|
237
|
+
fullDetailCommand: `proteum orient ${quoteCommandArgument(response.query)} --full`,
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
|
|
156
241
|
export const run = async () => {
|
|
157
242
|
const query = typeof cli.args.query === 'string' ? cli.args.query.trim() : '';
|
|
158
243
|
if (!query) throw new UsageError('A query is required. Example: proteum orient /api/Auth/CurrentUser');
|
|
@@ -160,10 +245,15 @@ export const run = async () => {
|
|
|
160
245
|
const manifest = await resolveManifest();
|
|
161
246
|
const response = buildOrientationResponse(manifest, query);
|
|
162
247
|
|
|
163
|
-
if (cli.args.
|
|
164
|
-
|
|
248
|
+
if (cli.args.full === true) {
|
|
249
|
+
printJson(response);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (cli.args.human === true) {
|
|
254
|
+
console.log(renderHuman(response));
|
|
165
255
|
return;
|
|
166
256
|
}
|
|
167
257
|
|
|
168
|
-
|
|
258
|
+
printCompactOrient(response);
|
|
169
259
|
};
|
package/cli/commands/perf.ts
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'path';
|
|
|
4
4
|
import { UsageError } from 'clipanion';
|
|
5
5
|
|
|
6
6
|
import cli from '..';
|
|
7
|
+
import { compactList, printAgentResponse, printJson, quoteCommandArgument, truncateForAgent } from '../utils/agentOutput';
|
|
7
8
|
import type {
|
|
8
9
|
TPerfCompareResponse,
|
|
9
10
|
TPerfMemoryResponse,
|
|
@@ -102,10 +103,6 @@ const requestJson = async <TResponse>(pathname: string) => {
|
|
|
102
103
|
);
|
|
103
104
|
};
|
|
104
105
|
|
|
105
|
-
const printJson = (value: object) => {
|
|
106
|
-
console.log(JSON.stringify(value, null, 2));
|
|
107
|
-
};
|
|
108
|
-
|
|
109
106
|
const renderWindow = (label: string, value: { startedAt: string; finishedAt: string; requestCount: number; availableRequestCount: number }) =>
|
|
110
107
|
`${label}=${value.requestCount}/${value.availableRequestCount} traces ${value.startedAt}..${value.finishedAt}`;
|
|
111
108
|
|
|
@@ -180,9 +177,113 @@ const renderRequest = (response: TPerfRequestResponse) =>
|
|
|
180
177
|
)),
|
|
181
178
|
].join('\n');
|
|
182
179
|
|
|
180
|
+
const compactTopLikeRow = (row: TPerfTopResponse['rows'][number]) => ({
|
|
181
|
+
label: row.label,
|
|
182
|
+
requestCount: row.requestCount,
|
|
183
|
+
avgDurationMs: row.avgDurationMs,
|
|
184
|
+
p95DurationMs: row.p95DurationMs,
|
|
185
|
+
maxDurationMs: row.maxDurationMs,
|
|
186
|
+
avgCpuMs: row.avgCpuMs,
|
|
187
|
+
avgSqlDurationMs: row.avgSqlDurationMs,
|
|
188
|
+
avgRenderDurationMs: row.avgRenderDurationMs,
|
|
189
|
+
avgHeapDeltaBytes: row.avgHeapDeltaBytes,
|
|
190
|
+
slowestRequestId: row.slowestRequestId,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const compactSql = (query: TPerfRequestResponse['request']['hottestSqlQueries'][number]) => ({
|
|
194
|
+
callerLabel: query.callerLabel,
|
|
195
|
+
operation: query.operation,
|
|
196
|
+
model: query.model,
|
|
197
|
+
fingerprint: query.fingerprint,
|
|
198
|
+
durationMs: query.durationMs,
|
|
199
|
+
query: truncateForAgent(query.query, 140),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const printCompactTop = (response: TPerfTopResponse) => {
|
|
203
|
+
printAgentResponse({
|
|
204
|
+
summary: `Perf top ${response.groupBy}: ${response.summary.requestCount} requests, ${response.summary.errorCount} errors, p95=${formatDuration(response.summary.p95DurationMs)}`,
|
|
205
|
+
data: {
|
|
206
|
+
groupBy: response.groupBy,
|
|
207
|
+
window: response.window,
|
|
208
|
+
summary: response.summary,
|
|
209
|
+
rows: compactList(response.rows, 8).map(compactTopLikeRow),
|
|
210
|
+
totalRows: response.rows.length,
|
|
211
|
+
},
|
|
212
|
+
fullDetailCommand: 'proteum perf top --full',
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const printCompactCompare = (response: TPerfCompareResponse) => {
|
|
217
|
+
printAgentResponse({
|
|
218
|
+
summary: `Perf compare ${response.groupBy}: ${response.rows.length} rows`,
|
|
219
|
+
data: {
|
|
220
|
+
groupBy: response.groupBy,
|
|
221
|
+
baseline: response.baseline,
|
|
222
|
+
target: response.target,
|
|
223
|
+
rows: compactList(response.rows, 8),
|
|
224
|
+
totalRows: response.rows.length,
|
|
225
|
+
},
|
|
226
|
+
fullDetailCommand: 'proteum perf compare --full',
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const printCompactMemory = (response: TPerfMemoryResponse) => {
|
|
231
|
+
printAgentResponse({
|
|
232
|
+
summary: `Perf memory ${response.groupBy}: ${response.rows.length} rows`,
|
|
233
|
+
data: {
|
|
234
|
+
groupBy: response.groupBy,
|
|
235
|
+
window: response.window,
|
|
236
|
+
rows: compactList(response.rows, 8),
|
|
237
|
+
totalRows: response.rows.length,
|
|
238
|
+
},
|
|
239
|
+
fullDetailCommand: 'proteum perf memory --full',
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const printCompactRequest = (response: TPerfRequestResponse) => {
|
|
244
|
+
printAgentResponse({
|
|
245
|
+
summary: `${response.request.requestId}: ${response.request.method} ${response.request.path} total=${formatDuration(response.request.totalDurationMs)} cpu=${formatDuration(response.request.cpuTotalMs)} sql=${formatDuration(response.request.sqlDurationMs)}`,
|
|
246
|
+
data: {
|
|
247
|
+
request: {
|
|
248
|
+
requestId: response.request.requestId,
|
|
249
|
+
method: response.request.method,
|
|
250
|
+
path: response.request.path,
|
|
251
|
+
statusCode: response.request.statusCode,
|
|
252
|
+
routeLabel: response.request.routeLabel,
|
|
253
|
+
controllerLabel: response.request.controllerLabel,
|
|
254
|
+
totalDurationMs: response.request.totalDurationMs,
|
|
255
|
+
cpuTotalMs: response.request.cpuTotalMs,
|
|
256
|
+
sqlDurationMs: response.request.sqlDurationMs,
|
|
257
|
+
callDurationMs: response.request.callDurationMs,
|
|
258
|
+
renderDurationMs: response.request.renderDurationMs,
|
|
259
|
+
selfDurationMs: response.request.selfDurationMs,
|
|
260
|
+
heapDeltaBytes: response.request.heapDeltaBytes,
|
|
261
|
+
},
|
|
262
|
+
stages: compactList(response.request.stages, 8),
|
|
263
|
+
hotCalls: compactList(response.request.hottestCalls, 6),
|
|
264
|
+
chain: compactList(response.request.chain || [], 8),
|
|
265
|
+
hotSql: compactList(response.request.hottestSqlQueries, 6).map(compactSql),
|
|
266
|
+
},
|
|
267
|
+
nextActions: [
|
|
268
|
+
{
|
|
269
|
+
label: 'Diagnose Request',
|
|
270
|
+
command: `proteum diagnose ${quoteCommandArgument(response.request.path)}`,
|
|
271
|
+
reason: 'Combine this request with owner, diagnostics, suspects, and logs.',
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
label: 'Trace Events',
|
|
275
|
+
command: `proteum trace show ${quoteCommandArgument(response.request.requestId)} --events`,
|
|
276
|
+
reason: 'Open raw event detail only if the compact waterfall is insufficient.',
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
fullDetailCommand: `proteum perf request ${quoteCommandArgument(response.request.requestId)} --full`,
|
|
280
|
+
});
|
|
281
|
+
};
|
|
282
|
+
|
|
183
283
|
export const run = async () => {
|
|
184
284
|
const action = getAction();
|
|
185
|
-
const
|
|
285
|
+
const shouldPrintFull = cli.args.full === true;
|
|
286
|
+
const shouldPrintHuman = cli.args.human === true;
|
|
186
287
|
const groupBy = typeof cli.args.groupBy === 'string' && cli.args.groupBy ? cli.args.groupBy : 'path';
|
|
187
288
|
const limit =
|
|
188
289
|
typeof cli.args.limit === 'string' && cli.args.limit ? Math.max(1, Number.parseInt(cli.args.limit, 10) || 12) : 12;
|
|
@@ -192,12 +293,13 @@ export const run = async () => {
|
|
|
192
293
|
const response = await requestJson<TPerfTopResponse>(
|
|
193
294
|
`/__proteum/perf/top?${new URLSearchParams({ groupBy, limit: String(limit), since }).toString()}`,
|
|
194
295
|
);
|
|
195
|
-
if (
|
|
296
|
+
if (shouldPrintFull) {
|
|
196
297
|
printJson(response);
|
|
197
298
|
return;
|
|
198
299
|
}
|
|
199
300
|
|
|
200
|
-
console.log(renderTop(response));
|
|
301
|
+
if (shouldPrintHuman) console.log(renderTop(response));
|
|
302
|
+
else printCompactTop(response);
|
|
201
303
|
return;
|
|
202
304
|
}
|
|
203
305
|
|
|
@@ -212,12 +314,13 @@ export const run = async () => {
|
|
|
212
314
|
target: targetWindow,
|
|
213
315
|
}).toString()}`,
|
|
214
316
|
);
|
|
215
|
-
if (
|
|
317
|
+
if (shouldPrintFull) {
|
|
216
318
|
printJson(response);
|
|
217
319
|
return;
|
|
218
320
|
}
|
|
219
321
|
|
|
220
|
-
console.log(renderCompare(response));
|
|
322
|
+
if (shouldPrintHuman) console.log(renderCompare(response));
|
|
323
|
+
else printCompactCompare(response);
|
|
221
324
|
return;
|
|
222
325
|
}
|
|
223
326
|
|
|
@@ -226,12 +329,13 @@ export const run = async () => {
|
|
|
226
329
|
const response = await requestJson<TPerfMemoryResponse>(
|
|
227
330
|
`/__proteum/perf/memory?${new URLSearchParams({ groupBy, limit: String(limit), since }).toString()}`,
|
|
228
331
|
);
|
|
229
|
-
if (
|
|
332
|
+
if (shouldPrintFull) {
|
|
230
333
|
printJson(response);
|
|
231
334
|
return;
|
|
232
335
|
}
|
|
233
336
|
|
|
234
|
-
console.log(renderMemory(response));
|
|
337
|
+
if (shouldPrintHuman) console.log(renderMemory(response));
|
|
338
|
+
else printCompactMemory(response);
|
|
235
339
|
return;
|
|
236
340
|
}
|
|
237
341
|
|
|
@@ -241,10 +345,11 @@ export const run = async () => {
|
|
|
241
345
|
const response = await requestJson<TPerfRequestResponse>(
|
|
242
346
|
`/__proteum/perf/request?${new URLSearchParams({ query: requestTarget }).toString()}`,
|
|
243
347
|
);
|
|
244
|
-
if (
|
|
348
|
+
if (shouldPrintFull) {
|
|
245
349
|
printJson(response);
|
|
246
350
|
return;
|
|
247
351
|
}
|
|
248
352
|
|
|
249
|
-
console.log(renderRequest(response));
|
|
353
|
+
if (shouldPrintHuman) console.log(renderRequest(response));
|
|
354
|
+
else printCompactRequest(response);
|
|
250
355
|
};
|
|
@@ -0,0 +1,234 @@
|
|
|
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 { readProteumManifest } from '../compiler/common/proteumManifest';
|
|
8
|
+
import { listDevSessionInspections, writeMachineDevSessionRecord, type TDevSessionInspection } from '../runtime/devSessions';
|
|
9
|
+
import { inspectDevPort, type TDevPortInspection } from '../runtime/ports';
|
|
10
|
+
import { printAgentResponse, printJson, quoteCommandArgument } from '../utils/agentOutput';
|
|
11
|
+
import type { TDoctorResponse } from '@common/dev/diagnostics';
|
|
12
|
+
import type { TProteumManifest } from '@common/dev/proteumManifest';
|
|
13
|
+
|
|
14
|
+
type TRuntimeAction = 'status';
|
|
15
|
+
|
|
16
|
+
const allowedActions = new Set<TRuntimeAction>(['status']);
|
|
17
|
+
|
|
18
|
+
const getAction = () => {
|
|
19
|
+
const action = typeof cli.args.action === 'string' && cli.args.action ? cli.args.action : 'status';
|
|
20
|
+
if (!allowedActions.has(action as TRuntimeAction)) {
|
|
21
|
+
throw new UsageError(`Unsupported runtime action "${action}". Expected one of: ${[...allowedActions].join(', ')}.`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return action as TRuntimeAction;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const readManifestIfAvailable = (): TProteumManifest | undefined => {
|
|
28
|
+
const manifestFilepath = path.join(cli.paths.appRoot, '.proteum', 'manifest.json');
|
|
29
|
+
if (!fs.existsSync(manifestFilepath)) return undefined;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
return readProteumManifest(cli.paths.appRoot);
|
|
33
|
+
} catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const getSessionUrl = (inspection: TDevSessionInspection) => {
|
|
39
|
+
if (!inspection.record) return '';
|
|
40
|
+
if (inspection.record.publicUrl) return inspection.record.publicUrl.replace(/\/+$/, '');
|
|
41
|
+
return `http://localhost:${inspection.record.routerPort}`;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const getSessionMcpUrl = (inspection: TDevSessionInspection) => {
|
|
45
|
+
const sessionUrl = getSessionUrl(inspection);
|
|
46
|
+
return sessionUrl ? `${sessionUrl}/__proteum/mcp` : '';
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const probeDoctor = async (baseUrl: string) => {
|
|
50
|
+
if (!baseUrl) return { reachable: false, error: 'No dev URL is registered.' };
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const response = await got(`${baseUrl}/__proteum/doctor`, {
|
|
54
|
+
responseType: 'json',
|
|
55
|
+
retry: { limit: 0 },
|
|
56
|
+
throwHttpErrors: false,
|
|
57
|
+
timeout: { request: 1200 },
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (response.statusCode >= 400) {
|
|
61
|
+
return { reachable: false, statusCode: response.statusCode, error: `Doctor returned HTTP ${response.statusCode}.` };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const doctor = response.body as TDoctorResponse;
|
|
65
|
+
return {
|
|
66
|
+
reachable: true,
|
|
67
|
+
statusCode: response.statusCode,
|
|
68
|
+
doctor: doctor.summary,
|
|
69
|
+
};
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return {
|
|
72
|
+
reachable: false,
|
|
73
|
+
error: error instanceof Error ? error.message : String(error),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const compactSession = (inspection: TDevSessionInspection) => ({
|
|
79
|
+
sessionFilePath: inspection.sessionFilePath,
|
|
80
|
+
live: inspection.live,
|
|
81
|
+
stale: inspection.stale,
|
|
82
|
+
invalid: inspection.invalid,
|
|
83
|
+
parseError: inspection.parseError,
|
|
84
|
+
pid: inspection.record?.pid,
|
|
85
|
+
routerPort: inspection.record?.routerPort,
|
|
86
|
+
publicUrl: inspection.record?.publicUrl,
|
|
87
|
+
mcpUrl: inspection.record ? getSessionMcpUrl(inspection) : undefined,
|
|
88
|
+
state: inspection.record?.state,
|
|
89
|
+
startedAt: inspection.record?.startedAt,
|
|
90
|
+
updatedAt: inspection.record?.updatedAt,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const createStartDevCommand = (port?: number) =>
|
|
94
|
+
`proteum dev --session-file var/run/proteum/dev/agents/<task>.json --port ${port || '<free-port>'}`;
|
|
95
|
+
|
|
96
|
+
const describePortOwner = (portInspection?: TDevPortInspection) => {
|
|
97
|
+
if (!portInspection || portInspection.router.available) return '';
|
|
98
|
+
if (portInspection.router.proteum) {
|
|
99
|
+
const appLabel =
|
|
100
|
+
portInspection.router.app?.identifier ||
|
|
101
|
+
portInspection.router.app?.name ||
|
|
102
|
+
portInspection.router.app?.appRoot ||
|
|
103
|
+
'another Proteum app';
|
|
104
|
+
return `Configured router port ${portInspection.router.port} is already occupied by ${appLabel}.`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return `Configured router port ${portInspection.router.port} is already occupied by a non-Proteum or unrecognized process.`;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const getNextActions = ({
|
|
111
|
+
health,
|
|
112
|
+
portInspection,
|
|
113
|
+
selectedSession,
|
|
114
|
+
}: {
|
|
115
|
+
health: { reachable: boolean };
|
|
116
|
+
portInspection?: TDevPortInspection;
|
|
117
|
+
selectedSession: TDevSessionInspection | undefined;
|
|
118
|
+
}) => {
|
|
119
|
+
if (!selectedSession?.record || !selectedSession.live) {
|
|
120
|
+
const portOwner = describePortOwner(portInspection);
|
|
121
|
+
|
|
122
|
+
if (portInspection?.router.proteum && portInspection.router.matchesApp) {
|
|
123
|
+
return [
|
|
124
|
+
{
|
|
125
|
+
label: 'Use Existing Runtime',
|
|
126
|
+
command: `proteum diagnose ${quoteCommandArgument('/')} --port ${portInspection.router.port}`,
|
|
127
|
+
reason:
|
|
128
|
+
'A Proteum runtime for this app already responds on the configured router port, but no tracked session file is live. Do not start a second dev server; use this port for CLI evidence or stop the owning process before starting a tracked session.',
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const startPort =
|
|
134
|
+
portInspection && !portInspection.canStartOnConfiguredPort ? portInspection.recommendedPort : portInspection?.router.port;
|
|
135
|
+
|
|
136
|
+
return [
|
|
137
|
+
{
|
|
138
|
+
label: 'Start Dev',
|
|
139
|
+
command: createStartDevCommand(startPort),
|
|
140
|
+
reason: portOwner
|
|
141
|
+
? `${portOwner} Use an alternate free router/HMR port pair; do not probe page bodies to identify port owners.`
|
|
142
|
+
: 'Create a tracked dev session before request-time diagnostics.',
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!health.reachable) {
|
|
148
|
+
return [
|
|
149
|
+
{
|
|
150
|
+
label: 'Stop Unreachable Dev',
|
|
151
|
+
command: `proteum dev stop --session-file ${quoteCommandArgument(selectedSession.sessionFilePath)}`,
|
|
152
|
+
reason: 'A tracked session exists but the runtime and MCP endpoint are unreachable.',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
label: 'Start Dev',
|
|
156
|
+
command: createStartDevCommand(portInspection?.recommendedPort),
|
|
157
|
+
reason: 'Start a fresh tracked session after stopping the unreachable one.',
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return [
|
|
163
|
+
{
|
|
164
|
+
label: 'Diagnose Root',
|
|
165
|
+
command: `proteum diagnose ${quoteCommandArgument('/')} --port ${selectedSession.record.routerPort}`,
|
|
166
|
+
reason: 'Use the selected runtime for the smallest request-level diagnostic pass.',
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export const run = async () => {
|
|
172
|
+
const action = getAction();
|
|
173
|
+
if (action !== 'status') return;
|
|
174
|
+
|
|
175
|
+
const manifest = readManifestIfAvailable();
|
|
176
|
+
const sessions = await listDevSessionInspections({
|
|
177
|
+
appRoot: cli.paths.appRoot,
|
|
178
|
+
sessionFilePath: typeof cli.args.sessionFile === 'string' && cli.args.sessionFile ? cli.args.sessionFile : undefined,
|
|
179
|
+
});
|
|
180
|
+
const liveSessions = sessions.filter((inspection) => inspection.live && inspection.record);
|
|
181
|
+
await Promise.allSettled(
|
|
182
|
+
liveSessions.map((inspection) =>
|
|
183
|
+
inspection.record ? writeMachineDevSessionRecord(inspection.record) : Promise.resolve(undefined),
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
const selectedSession =
|
|
187
|
+
liveSessions.find((inspection) => inspection.record?.state === 'ready') || liveSessions[0] || sessions.find((inspection) => inspection.record);
|
|
188
|
+
const selectedBaseUrl = selectedSession ? getSessionUrl(selectedSession) : '';
|
|
189
|
+
const health = selectedSession && selectedSession.live ? await probeDoctor(selectedBaseUrl) : { reachable: false, error: 'No live tracked dev session.' };
|
|
190
|
+
const configuredDevPort = manifest
|
|
191
|
+
? await inspectDevPort({
|
|
192
|
+
appRoot: cli.paths.appRoot,
|
|
193
|
+
port: manifest.env.resolved.routerPort,
|
|
194
|
+
})
|
|
195
|
+
: undefined;
|
|
196
|
+
|
|
197
|
+
const payload = {
|
|
198
|
+
appRoot: cli.paths.appRoot,
|
|
199
|
+
manifest: manifest
|
|
200
|
+
? {
|
|
201
|
+
identifier: manifest.app.identity.identifier,
|
|
202
|
+
name: manifest.app.identity.name,
|
|
203
|
+
routerPort: manifest.env.resolved.routerPort,
|
|
204
|
+
diagnostics: {
|
|
205
|
+
errors: manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'error').length,
|
|
206
|
+
warnings: manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'warning').length,
|
|
207
|
+
},
|
|
208
|
+
counts: {
|
|
209
|
+
connectedProjects: manifest.connectedProjects.length,
|
|
210
|
+
controllers: manifest.controllers.length,
|
|
211
|
+
routes: manifest.routes.client.length + manifest.routes.server.length,
|
|
212
|
+
},
|
|
213
|
+
}
|
|
214
|
+
: undefined,
|
|
215
|
+
selected: selectedSession ? compactSession(selectedSession) : undefined,
|
|
216
|
+
sessions: sessions.map(compactSession),
|
|
217
|
+
health,
|
|
218
|
+
configuredDevPort,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
if (cli.args.full === true) {
|
|
222
|
+
printJson(payload);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
printAgentResponse({
|
|
227
|
+
summary: selectedSession
|
|
228
|
+
? `${selectedSession.live ? 'live' : 'stale'} dev session on ${selectedSession.record?.routerPort || 'unknown port'}; health=${health.reachable ? 'reachable' : 'unreachable'}`
|
|
229
|
+
: describePortOwner(configuredDevPort) || 'No tracked Proteum dev session found.',
|
|
230
|
+
data: payload,
|
|
231
|
+
nextActions: getNextActions({ health, portInspection: configuredDevPort, selectedSession }),
|
|
232
|
+
fullDetailCommand: 'proteum runtime status --full',
|
|
233
|
+
});
|
|
234
|
+
};
|