proteum 2.2.8 → 2.3.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 (51) hide show
  1. package/AGENTS.md +5 -3
  2. package/README.md +50 -12
  3. package/agents/project/AGENTS.md +47 -10
  4. package/agents/project/CODING_STYLE.md +5 -1
  5. package/agents/project/client/AGENTS.md +2 -0
  6. package/agents/project/diagnostics.md +8 -5
  7. package/agents/project/optimizations.md +1 -0
  8. package/agents/project/root/AGENTS.md +18 -10
  9. package/agents/project/tests/AGENTS.md +6 -1
  10. package/agents/project/tests/e2e/AGENTS.md +13 -0
  11. package/agents/project/tests/e2e/REAL_WORLD_JOURNEY_TESTS.md +192 -0
  12. package/cli/commands/check.ts +21 -3
  13. package/cli/commands/configure.ts +1 -0
  14. package/cli/commands/connect.ts +40 -4
  15. package/cli/commands/diagnose.ts +136 -5
  16. package/cli/commands/doctor.ts +24 -4
  17. package/cli/commands/explain.ts +105 -6
  18. package/cli/commands/mcp.ts +16 -0
  19. package/cli/commands/orient.ts +66 -3
  20. package/cli/commands/perf.ts +118 -13
  21. package/cli/commands/runtime.ts +151 -0
  22. package/cli/commands/trace.ts +116 -21
  23. package/cli/mcp/provider.ts +365 -0
  24. package/cli/mcp/stdio.ts +16 -0
  25. package/cli/presentation/commands.ts +79 -22
  26. package/cli/presentation/devSession.ts +2 -0
  27. package/cli/runtime/commands.ts +95 -12
  28. package/cli/utils/agentOutput.ts +46 -0
  29. package/cli/utils/agents.ts +225 -48
  30. package/common/dev/inspection.ts +30 -9
  31. package/common/dev/mcpPayloads.ts +736 -0
  32. package/common/dev/mcpServer.ts +254 -0
  33. package/docs/agent-routing.md +126 -0
  34. package/docs/dev-commands.md +2 -0
  35. package/docs/dev-sessions.md +2 -1
  36. package/docs/diagnostics.md +68 -23
  37. package/docs/mcp.md +149 -0
  38. package/docs/migrate-from-2.1.3.md +15 -5
  39. package/docs/request-tracing.md +12 -6
  40. package/eslint.js +220 -0
  41. package/package.json +2 -1
  42. package/server/app/devMcp.ts +159 -0
  43. package/server/services/router/http/cache.ts +116 -0
  44. package/server/services/router/http/index.ts +94 -35
  45. package/server/services/router/index.ts +8 -11
  46. package/tests/agents-utils.test.cjs +89 -11
  47. package/tests/dev-transpile-watch.test.cjs +117 -8
  48. package/tests/eslint-rules.test.cjs +110 -0
  49. package/tests/inspection.test.cjs +67 -0
  50. package/tests/mcp.test.cjs +127 -0
  51. package/tests/router-cache-config.test.cjs +74 -0
@@ -2,14 +2,16 @@ import cli from '..';
2
2
  import Compiler from '../compiler';
3
3
  import { readProteumManifest } from '../compiler/common/proteumManifest';
4
4
  import {
5
+ buildExplainSummaryItems,
5
6
  explainSectionNames,
6
7
  pickExplainManifestSections,
7
8
  renderExplainHuman,
8
9
  type TExplainSectionName,
9
10
  } from '@common/dev/diagnostics';
10
11
  import { explainOwner } from '@common/dev/inspection';
12
+ import { compactList, printAgentResponse, printJson, quoteCommandArgument } from '../utils/agentOutput';
11
13
 
12
- const allowedExplainArgs = new Set(['json', 'all', ...explainSectionNames]);
14
+ const allowedExplainArgs = new Set(['json', 'all', 'full', 'human', 'manifest', ...explainSectionNames]);
13
15
 
14
16
  const validateExplainArgs = () => {
15
17
  const enabledArgs = Object.entries(cli.args)
@@ -31,6 +33,93 @@ const getSelectedSections = (): TExplainSectionName[] => {
31
33
  return explainSectionNames.filter((sectionName) => cli.args[sectionName] === true);
32
34
  };
33
35
 
36
+ const compactOwnerMatch = (match: ReturnType<typeof explainOwner>['matches'][number]) => ({
37
+ kind: match.kind,
38
+ label: match.label,
39
+ score: match.score,
40
+ scope: match.scopeLabel,
41
+ origin: match.originHint,
42
+ source: match.source,
43
+ });
44
+
45
+ const hasExplicitDetailSelection = (selectedSections: TExplainSectionName[]) =>
46
+ cli.args.full === true || cli.args.manifest === true || cli.args.all === true || selectedSections.length > 0;
47
+
48
+ const printCompactOwner = (ownerQuery: string, response: ReturnType<typeof explainOwner>) => {
49
+ const topOwner = response.matches[0];
50
+
51
+ printAgentResponse({
52
+ summary: topOwner
53
+ ? `${ownerQuery} -> ${topOwner.kind} ${topOwner.label} (${topOwner.scopeLabel})`
54
+ : `${ownerQuery} -> no manifest owner matched`,
55
+ data: {
56
+ query: ownerQuery,
57
+ normalizedQuery: response.normalizedQuery,
58
+ top: topOwner ? compactOwnerMatch(topOwner) : undefined,
59
+ matches: compactList(response.matches, 6).map(compactOwnerMatch),
60
+ totalReturned: response.matches.length,
61
+ },
62
+ nextActions: [
63
+ {
64
+ label: 'Orient',
65
+ command: `proteum orient ${quoteCommandArgument(ownerQuery)}`,
66
+ reason: 'Resolve the owner together with the relevant instruction files and next command.',
67
+ },
68
+ ],
69
+ fullDetailCommand: `proteum explain owner ${quoteCommandArgument(ownerQuery)} --full`,
70
+ });
71
+ };
72
+
73
+ const printCompactExplain = (manifest: ReturnType<typeof readProteumManifest>) => {
74
+ const errors = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'error').length;
75
+ const warnings = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'warning').length;
76
+ const requiredEnvProvided = manifest.env.requiredVariables.filter((variable) => variable.provided).length;
77
+
78
+ printAgentResponse({
79
+ summary: `${manifest.app.identity.identifier}: ${manifest.controllers.length} controllers, ${manifest.routes.client.length + manifest.routes.server.length} routes, ${errors} errors, ${warnings} warnings`,
80
+ data: {
81
+ app: {
82
+ root: manifest.app.root,
83
+ coreRoot: manifest.app.coreRoot,
84
+ identifier: manifest.app.identity.identifier,
85
+ name: manifest.app.identity.name,
86
+ },
87
+ counts: {
88
+ commands: manifest.commands.length,
89
+ connectedProjects: manifest.connectedProjects.length,
90
+ controllers: manifest.controllers.length,
91
+ diagnostics: manifest.diagnostics.length,
92
+ layouts: manifest.layouts.length,
93
+ routesClient: manifest.routes.client.length,
94
+ routesServer: manifest.routes.server.length,
95
+ servicesApp: manifest.services.app.length,
96
+ servicesRouterPlugins: manifest.services.routerPlugins.length,
97
+ },
98
+ diagnostics: { errors, warnings },
99
+ env: {
100
+ requiredProvided: requiredEnvProvided,
101
+ requiredTotal: manifest.env.requiredVariables.length,
102
+ routerPort: manifest.env.resolved.routerPort,
103
+ },
104
+ summaryItems: buildExplainSummaryItems(manifest),
105
+ },
106
+ nextActions: [
107
+ {
108
+ label: 'Orient Target',
109
+ command: 'proteum orient <route|file|controller|error>',
110
+ reason: 'Use orient for task-specific owner, instruction, and next-command routing.',
111
+ },
112
+ ],
113
+ fullDetailCommand: 'proteum explain --manifest',
114
+ omitted: [
115
+ {
116
+ reason: 'Full manifest sections are omitted from the default agent summary.',
117
+ command: 'proteum explain --manifest',
118
+ },
119
+ ],
120
+ });
121
+ };
122
+
34
123
  export const run = async (): Promise<void> => {
35
124
  validateExplainArgs();
36
125
 
@@ -42,8 +131,13 @@ export const run = async (): Promise<void> => {
42
131
 
43
132
  if (ownerQuery) {
44
133
  const response = explainOwner(manifest, ownerQuery);
45
- if (cli.args.json === true) {
46
- console.log(JSON.stringify(response, null, 2));
134
+ if (cli.args.full === true || cli.args.manifest === true) {
135
+ printJson(response);
136
+ return;
137
+ }
138
+
139
+ if (cli.args.human !== true) {
140
+ printCompactOwner(ownerQuery, response);
47
141
  return;
48
142
  }
49
143
 
@@ -64,10 +158,15 @@ export const run = async (): Promise<void> => {
64
158
 
65
159
  const selectedSections = getSelectedSections();
66
160
 
67
- if (cli.args.json === true) {
68
- console.log(JSON.stringify(pickExplainManifestSections(manifest, selectedSections), null, 2));
161
+ if (hasExplicitDetailSelection(selectedSections)) {
162
+ printJson(pickExplainManifestSections(manifest, cli.args.manifest === true ? [...explainSectionNames] : selectedSections));
163
+ return;
164
+ }
165
+
166
+ if (cli.args.human === true) {
167
+ console.log(renderExplainHuman(manifest, selectedSections));
69
168
  return;
70
169
  }
71
170
 
72
- console.log(renderExplainHuman(manifest, selectedSections));
171
+ printCompactExplain(manifest);
73
172
  };
@@ -0,0 +1,16 @@
1
+ import cli from '..';
2
+ import { CliProteumMcpProvider } from '../mcp/provider';
3
+ import { startProteumMcpStdioServer } from '../mcp/stdio';
4
+
5
+ export const run = async () => {
6
+ const provider = new CliProteumMcpProvider({
7
+ appRoot: cli.paths.appRoot,
8
+ sessionFilePath: typeof cli.args.sessionFile === 'string' && cli.args.sessionFile ? cli.args.sessionFile : undefined,
9
+ url: typeof cli.args.url === 'string' && cli.args.url ? cli.args.url : undefined,
10
+ });
11
+
12
+ await startProteumMcpStdioServer({
13
+ provider,
14
+ version: String(cli.packageJson.version || ''),
15
+ });
16
+ };
@@ -8,6 +8,7 @@ import Compiler from '../compiler';
8
8
  import { readProteumManifest } from '../compiler/common/proteumManifest';
9
9
  import { buildOrientationResponse, type TOrientResponse } from '@common/dev/inspection';
10
10
  import type { TProteumManifest } from '@common/dev/proteumManifest';
11
+ import { compactList, printAgentResponse, printJson, quoteCommandArgument } from '../utils/agentOutput';
11
12
 
12
13
  const normalizeBaseUrl = (value: string) => value.replace(/\/+$/, '');
13
14
  const dedupe = <TValue>(values: TValue[]) => [...new Set(values)];
@@ -153,6 +154,63 @@ const renderHuman = (response: TOrientResponse) =>
153
154
  ...(response.warnings.length === 0 ? ['- none'] : response.warnings.map((warning) => `- ${warning}`)),
154
155
  ].join('\n');
155
156
 
157
+ const compactOwnerMatch = (match: TOrientResponse['owner']['matches'][number]) => ({
158
+ kind: match.kind,
159
+ label: match.label,
160
+ score: match.score,
161
+ scope: match.scopeLabel,
162
+ origin: match.originHint,
163
+ source: match.source,
164
+ });
165
+
166
+ const buildInstructionPlan = (response: TOrientResponse) => ({
167
+ mustRead: [...new Set([response.guidance.agents, ...response.guidance.areaAgents])],
168
+ readWhen: [
169
+ {
170
+ file: response.guidance.diagnostics,
171
+ when: 'Read only for raw errors, failing requests, traces, perf regressions, or reproduction work.',
172
+ },
173
+ {
174
+ file: response.guidance.codingStyle,
175
+ when: 'Read before editing implementation files.',
176
+ },
177
+ {
178
+ file: response.guidance.optimizations,
179
+ when: 'Read after client-side implementation or when the task explicitly concerns packages, build, runtime, or performance.',
180
+ },
181
+ ],
182
+ });
183
+
184
+ const printCompactOrient = (response: TOrientResponse) => {
185
+ const topOwner = response.owner.matches[0];
186
+ const summary = topOwner
187
+ ? `${response.query} -> ${topOwner.kind} ${topOwner.label} (${topOwner.scopeLabel})`
188
+ : `${response.query} -> no manifest owner matched`;
189
+
190
+ printAgentResponse({
191
+ summary,
192
+ data: {
193
+ query: response.query,
194
+ app: response.app,
195
+ owner: {
196
+ top: topOwner ? compactOwnerMatch(topOwner) : undefined,
197
+ matches: compactList(response.owner.matches, 4).map(compactOwnerMatch),
198
+ totalReturned: response.owner.matches.length,
199
+ },
200
+ instructions: buildInstructionPlan(response),
201
+ connected: {
202
+ imports: compactList(response.connected.imports, 4),
203
+ producers: compactList(response.connected.producers, 3),
204
+ totalImports: response.connected.imports.length,
205
+ totalProducers: response.connected.producers.length,
206
+ },
207
+ warnings: response.warnings,
208
+ },
209
+ nextActions: response.nextSteps,
210
+ fullDetailCommand: `proteum orient ${quoteCommandArgument(response.query)} --full`,
211
+ });
212
+ };
213
+
156
214
  export const run = async () => {
157
215
  const query = typeof cli.args.query === 'string' ? cli.args.query.trim() : '';
158
216
  if (!query) throw new UsageError('A query is required. Example: proteum orient /api/Auth/CurrentUser');
@@ -160,10 +218,15 @@ export const run = async () => {
160
218
  const manifest = await resolveManifest();
161
219
  const response = buildOrientationResponse(manifest, query);
162
220
 
163
- if (cli.args.json === true) {
164
- console.log(JSON.stringify(response, null, 2));
221
+ if (cli.args.full === true) {
222
+ printJson(response);
223
+ return;
224
+ }
225
+
226
+ if (cli.args.human === true) {
227
+ console.log(renderHuman(response));
165
228
  return;
166
229
  }
167
230
 
168
- console.log(renderHuman(response));
231
+ printCompactOrient(response);
169
232
  };
@@ -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 shouldPrintJson = cli.args.json === true;
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 (shouldPrintJson) {
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 (shouldPrintJson) {
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 (shouldPrintJson) {
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 (shouldPrintJson) {
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,151 @@
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, type TDevSessionInspection } from '../runtime/devSessions';
9
+ import { printAgentResponse, printJson, quoteCommandArgument } from '../utils/agentOutput';
10
+ import type { TDoctorResponse } from '@common/dev/diagnostics';
11
+ import type { TProteumManifest } from '@common/dev/proteumManifest';
12
+
13
+ type TRuntimeAction = 'status';
14
+
15
+ const allowedActions = new Set<TRuntimeAction>(['status']);
16
+
17
+ const getAction = () => {
18
+ const action = typeof cli.args.action === 'string' && cli.args.action ? cli.args.action : 'status';
19
+ if (!allowedActions.has(action as TRuntimeAction)) {
20
+ throw new UsageError(`Unsupported runtime action "${action}". Expected one of: ${[...allowedActions].join(', ')}.`);
21
+ }
22
+
23
+ return action as TRuntimeAction;
24
+ };
25
+
26
+ const readManifestIfAvailable = (): TProteumManifest | undefined => {
27
+ const manifestFilepath = path.join(cli.paths.appRoot, '.proteum', 'manifest.json');
28
+ if (!fs.existsSync(manifestFilepath)) return undefined;
29
+
30
+ try {
31
+ return readProteumManifest(cli.paths.appRoot);
32
+ } catch {
33
+ return undefined;
34
+ }
35
+ };
36
+
37
+ const getSessionUrl = (inspection: TDevSessionInspection) => {
38
+ if (!inspection.record) return '';
39
+ if (inspection.record.publicUrl) return inspection.record.publicUrl.replace(/\/+$/, '');
40
+ return `http://localhost:${inspection.record.routerPort}`;
41
+ };
42
+
43
+ const probeDoctor = async (baseUrl: string) => {
44
+ if (!baseUrl) return { reachable: false, error: 'No dev URL is registered.' };
45
+
46
+ try {
47
+ const response = await got(`${baseUrl}/__proteum/doctor`, {
48
+ responseType: 'json',
49
+ retry: { limit: 0 },
50
+ throwHttpErrors: false,
51
+ timeout: { request: 1200 },
52
+ });
53
+
54
+ if (response.statusCode >= 400) {
55
+ return { reachable: false, statusCode: response.statusCode, error: `Doctor returned HTTP ${response.statusCode}.` };
56
+ }
57
+
58
+ const doctor = response.body as TDoctorResponse;
59
+ return {
60
+ reachable: true,
61
+ statusCode: response.statusCode,
62
+ doctor: doctor.summary,
63
+ };
64
+ } catch (error) {
65
+ return {
66
+ reachable: false,
67
+ error: error instanceof Error ? error.message : String(error),
68
+ };
69
+ }
70
+ };
71
+
72
+ const compactSession = (inspection: TDevSessionInspection) => ({
73
+ sessionFilePath: inspection.sessionFilePath,
74
+ live: inspection.live,
75
+ stale: inspection.stale,
76
+ invalid: inspection.invalid,
77
+ parseError: inspection.parseError,
78
+ pid: inspection.record?.pid,
79
+ routerPort: inspection.record?.routerPort,
80
+ publicUrl: inspection.record?.publicUrl,
81
+ state: inspection.record?.state,
82
+ startedAt: inspection.record?.startedAt,
83
+ updatedAt: inspection.record?.updatedAt,
84
+ });
85
+
86
+ export const run = async () => {
87
+ const action = getAction();
88
+ if (action !== 'status') return;
89
+
90
+ const manifest = readManifestIfAvailable();
91
+ const sessions = await listDevSessionInspections({
92
+ appRoot: cli.paths.appRoot,
93
+ sessionFilePath: typeof cli.args.sessionFile === 'string' && cli.args.sessionFile ? cli.args.sessionFile : undefined,
94
+ });
95
+ const liveSessions = sessions.filter((inspection) => inspection.live && inspection.record);
96
+ const selectedSession =
97
+ liveSessions.find((inspection) => inspection.record?.state === 'ready') || liveSessions[0] || sessions.find((inspection) => inspection.record);
98
+ const selectedBaseUrl = selectedSession ? getSessionUrl(selectedSession) : '';
99
+ const health = selectedSession && selectedSession.live ? await probeDoctor(selectedBaseUrl) : { reachable: false, error: 'No live tracked dev session.' };
100
+
101
+ const payload = {
102
+ appRoot: cli.paths.appRoot,
103
+ manifest: manifest
104
+ ? {
105
+ identifier: manifest.app.identity.identifier,
106
+ name: manifest.app.identity.name,
107
+ routerPort: manifest.env.resolved.routerPort,
108
+ diagnostics: {
109
+ errors: manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'error').length,
110
+ warnings: manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'warning').length,
111
+ },
112
+ counts: {
113
+ connectedProjects: manifest.connectedProjects.length,
114
+ controllers: manifest.controllers.length,
115
+ routes: manifest.routes.client.length + manifest.routes.server.length,
116
+ },
117
+ }
118
+ : undefined,
119
+ selected: selectedSession ? compactSession(selectedSession) : undefined,
120
+ sessions: sessions.map(compactSession),
121
+ health,
122
+ };
123
+
124
+ if (cli.args.full === true) {
125
+ printJson(payload);
126
+ return;
127
+ }
128
+
129
+ printAgentResponse({
130
+ summary: selectedSession
131
+ ? `${selectedSession.live ? 'live' : 'stale'} dev session on ${selectedSession.record?.routerPort || 'unknown port'}; health=${health.reachable ? 'reachable' : 'unreachable'}`
132
+ : 'No tracked Proteum dev session found.',
133
+ data: payload,
134
+ nextActions: selectedSession?.record
135
+ ? [
136
+ {
137
+ label: 'Diagnose Root',
138
+ command: `proteum diagnose ${quoteCommandArgument('/')} --port ${selectedSession.record.routerPort}`,
139
+ reason: 'Use the selected runtime for the smallest request-level diagnostic pass.',
140
+ },
141
+ ]
142
+ : [
143
+ {
144
+ label: 'Start Dev',
145
+ command: 'proteum dev --session-file var/run/proteum/dev/agents/<task>.json --port <free-port>',
146
+ reason: 'Create a tracked dev session before request-time diagnostics.',
147
+ },
148
+ ],
149
+ fullDetailCommand: 'proteum runtime status --full',
150
+ });
151
+ };