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
package/cli/commands/dev.ts
CHANGED
|
@@ -28,19 +28,24 @@ import { clearInteractiveConsole } from '../presentation/welcome';
|
|
|
28
28
|
import { renderWarning } from '../presentation/ink';
|
|
29
29
|
import {
|
|
30
30
|
createDevSessionRecord,
|
|
31
|
-
inspectDevSessionFile,
|
|
32
31
|
listDevSessionInspections,
|
|
32
|
+
prepareDevSessionStart,
|
|
33
33
|
removeDevSessionRecord,
|
|
34
34
|
removeDevSessionRecordSync,
|
|
35
35
|
resolveDevSessionFilePath,
|
|
36
36
|
stopDevSessionFile,
|
|
37
37
|
updateDevSessionRecord,
|
|
38
|
+
writeDevSessionRecord,
|
|
39
|
+
writeMachineDevSessionRecord,
|
|
38
40
|
type TDevSessionInspection,
|
|
39
41
|
type TStopDevSessionResult,
|
|
40
42
|
} from '../runtime/devSessions';
|
|
41
43
|
import { resolveFrameworkInstallInfo } from '../paths';
|
|
42
44
|
import { logVerbose } from '../runtime/verbose';
|
|
45
|
+
import { ensureMachineMcpDaemonProcess } from '../runtime/mcpDaemon';
|
|
46
|
+
import { inspectDevPort, type TDevPortInspection } from '../runtime/ports';
|
|
43
47
|
import { configureProjectAgentInstructions, resolveProjectAgentMonorepoRoot } from '../utils/agents';
|
|
48
|
+
import { quoteCommandArgument } from '../utils/agentOutput';
|
|
44
49
|
|
|
45
50
|
// Core
|
|
46
51
|
import { app, App } from '../app';
|
|
@@ -335,6 +340,40 @@ const describeStopResult = (result: TStopDevSessionResult) => {
|
|
|
335
340
|
.join(' | ');
|
|
336
341
|
};
|
|
337
342
|
|
|
343
|
+
const describeBlockingDevSession = (inspection: TDevSessionInspection) => {
|
|
344
|
+
if (!inspection.record) {
|
|
345
|
+
return [
|
|
346
|
+
'- invalid session',
|
|
347
|
+
inspection.sessionFilePath,
|
|
348
|
+
inspection.parseError || 'Unreadable session file.',
|
|
349
|
+
]
|
|
350
|
+
.filter(Boolean)
|
|
351
|
+
.join(' | ');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const publicUrl = inspection.record.publicUrl || `http://localhost:${inspection.record.routerPort}`;
|
|
355
|
+
|
|
356
|
+
return [
|
|
357
|
+
`- pid ${inspection.record.pid}`,
|
|
358
|
+
`port ${inspection.record.routerPort}`,
|
|
359
|
+
publicUrl,
|
|
360
|
+
`session ${inspection.sessionFilePath}`,
|
|
361
|
+
].join(' | ');
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const createBlockingDevSessionMessage = (blocking: TDevSessionInspection[]) => {
|
|
365
|
+
const firstSessionFilePath = blocking[0]?.sessionFilePath || '<session-file>';
|
|
366
|
+
|
|
367
|
+
return [
|
|
368
|
+
`A Proteum dev session is already running for ${app.paths.root}.`,
|
|
369
|
+
'Stop the existing session before starting another server in the same worktree:',
|
|
370
|
+
...blocking.map(describeBlockingDevSession),
|
|
371
|
+
'',
|
|
372
|
+
`Run: npx proteum dev stop --session-file ${quoteCommandArgument(firstSessionFilePath)}`,
|
|
373
|
+
'Then start dev again with the intended session file and port.',
|
|
374
|
+
].join('\n');
|
|
375
|
+
};
|
|
376
|
+
|
|
338
377
|
const printJson = (payload: unknown) => {
|
|
339
378
|
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
340
379
|
};
|
|
@@ -400,38 +439,120 @@ const runStopCommand = async () => {
|
|
|
400
439
|
|
|
401
440
|
const ensureDevSessionSlot = async () => {
|
|
402
441
|
const sessionFilePath = getResolvedDevSessionFilePath();
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
`A Proteum dev session is already registered at ${sessionFilePath} (pid ${existingInspection.record.pid}, port ${existingInspection.record.routerPort}). ` +
|
|
409
|
-
'Use `proteum dev stop` or restart with `proteum dev --replace-existing`.',
|
|
410
|
-
);
|
|
411
|
-
}
|
|
442
|
+
const startPreparation = await prepareDevSessionStart({
|
|
443
|
+
appRoot: app.paths.root,
|
|
444
|
+
replaceExisting: cli.args.replaceExisting === true,
|
|
445
|
+
sessionFilePath,
|
|
446
|
+
});
|
|
412
447
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
throw new Error(`Could not stop the existing Proteum dev session registered at ${sessionFilePath}.`);
|
|
416
|
-
}
|
|
417
|
-
} else if (existingInspection) {
|
|
418
|
-
await stopDevSessionFile(sessionFilePath);
|
|
448
|
+
if (startPreparation.blocking.length > 0) {
|
|
449
|
+
throw new Error(createBlockingDevSessionMessage(startPreparation.blocking));
|
|
419
450
|
}
|
|
420
451
|
|
|
421
452
|
currentDevSessionFilePath = sessionFilePath;
|
|
422
453
|
registerDevSessionExitCleanup();
|
|
423
|
-
|
|
424
|
-
|
|
454
|
+
const sessionRecord = createDevSessionRecord({
|
|
455
|
+
appRoot: app.paths.root,
|
|
456
|
+
port: app.env.router.port,
|
|
425
457
|
sessionFilePath,
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}),
|
|
431
|
-
{ spaces: 2 },
|
|
432
|
-
);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
await writeDevSessionRecord(sessionRecord);
|
|
461
|
+
await writeMachineDevSessionRecord(sessionRecord);
|
|
433
462
|
|
|
434
463
|
logVerbose(`Registered Proteum dev session at ${sessionFilePath}.`);
|
|
464
|
+
if (startPreparation.cleaned.length > 0) {
|
|
465
|
+
logVerbose(
|
|
466
|
+
`Cleaned ${startPreparation.cleaned.length} stale Proteum dev session file${startPreparation.cleaned.length === 1 ? '' : 's'}.`,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
if (startPreparation.replaced) {
|
|
470
|
+
logVerbose(`Replaced Proteum dev session at ${startPreparation.replaced.sessionFilePath}.`);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const ensureMachineMcpDaemonForDev = async () => {
|
|
475
|
+
try {
|
|
476
|
+
const result = await ensureMachineMcpDaemonProcess({ coreRoot: cli.paths.core.root });
|
|
477
|
+
const record = result.inspection.record;
|
|
478
|
+
if (!record) return;
|
|
479
|
+
|
|
480
|
+
logVerbose(
|
|
481
|
+
result.started
|
|
482
|
+
? `Started Proteum machine MCP daemon at ${record.mcpUrl}.`
|
|
483
|
+
: `Proteum machine MCP daemon already running at ${record.mcpUrl}.`,
|
|
484
|
+
);
|
|
485
|
+
} catch (error) {
|
|
486
|
+
console.warn(
|
|
487
|
+
`Warning: Proteum could not ensure the machine MCP daemon. ${
|
|
488
|
+
error instanceof Error ? error.message : String(error)
|
|
489
|
+
}`,
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const describeDevPortBlocker = (inspection: TDevPortInspection) => {
|
|
495
|
+
const lines = [
|
|
496
|
+
`Proteum cannot start this dev server on port ${inspection.router.port}.`,
|
|
497
|
+
`Router port ${inspection.router.port}: ${inspection.router.available ? 'free' : 'occupied'}.`,
|
|
498
|
+
`HMR port ${inspection.hmr.port}: ${inspection.hmr.available ? 'free' : 'occupied'}.`,
|
|
499
|
+
];
|
|
500
|
+
|
|
501
|
+
if (!inspection.router.available && inspection.router.proteum && inspection.router.matchesApp) {
|
|
502
|
+
lines.push(
|
|
503
|
+
`The router port is already serving this Proteum app${inspection.router.app?.appRoot ? ` from ${inspection.router.app.appRoot}` : ''}.`,
|
|
504
|
+
);
|
|
505
|
+
lines.push('Next action: run `npx proteum runtime status`, use or stop the existing runtime, then start one tracked dev session.');
|
|
506
|
+
lines.push('Do not start a second dev server for the same worktree.');
|
|
507
|
+
} else if (!inspection.router.available && inspection.router.proteum) {
|
|
508
|
+
lines.push(
|
|
509
|
+
`The router port is already serving ${inspection.router.app?.identifier || inspection.router.app?.name || 'another Proteum app'}${inspection.router.app?.appRoot ? ` from ${inspection.router.app.appRoot}` : ''}.`,
|
|
510
|
+
);
|
|
511
|
+
lines.push(
|
|
512
|
+
inspection.recommendedPort
|
|
513
|
+
? `Next action: npx proteum dev --session-file var/run/proteum/dev/agents/<task>.json --port ${inspection.recommendedPort}`
|
|
514
|
+
: 'Next action: choose a free router/HMR port pair, then rerun proteum dev with --port <free-port>.',
|
|
515
|
+
);
|
|
516
|
+
} else if (!inspection.router.available) {
|
|
517
|
+
lines.push('The router port is occupied by a non-Proteum or unrecognized process.');
|
|
518
|
+
lines.push(
|
|
519
|
+
inspection.recommendedPort
|
|
520
|
+
? `Next action: npx proteum dev --session-file var/run/proteum/dev/agents/<task>.json --port ${inspection.recommendedPort}`
|
|
521
|
+
: 'Next action: choose a free router/HMR port pair, then rerun proteum dev with --port <free-port>.',
|
|
522
|
+
);
|
|
523
|
+
} else if (!inspection.hmr.available) {
|
|
524
|
+
lines.push('The HMR port is occupied.');
|
|
525
|
+
lines.push(
|
|
526
|
+
inspection.recommendedPort
|
|
527
|
+
? `Next action: npx proteum dev --session-file var/run/proteum/dev/agents/<task>.json --port ${inspection.recommendedPort}`
|
|
528
|
+
: 'Next action: choose a free router/HMR port pair, then rerun proteum dev with --port <free-port>.',
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
lines.push('Do not inspect page bodies to identify the owner; use `npx proteum runtime status` for compact port/runtime state.');
|
|
533
|
+
|
|
534
|
+
return lines.join('\n');
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const ensureConfiguredDevPortsAvailable = async () => {
|
|
538
|
+
const inspection = await inspectDevPort({
|
|
539
|
+
appRoot: app.paths.root,
|
|
540
|
+
port: app.env.router.port,
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
if (inspection.canStartOnConfiguredPort) return;
|
|
544
|
+
|
|
545
|
+
if (cli.args.replaceExisting === true) {
|
|
546
|
+
const requestedSessionFilePath = getResolvedDevSessionFilePath();
|
|
547
|
+
const [requestedSession] = await listDevSessionInspections({
|
|
548
|
+
appRoot: app.paths.root,
|
|
549
|
+
sessionFilePath: requestedSessionFilePath,
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
if (requestedSession?.record && requestedSession.live) return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
throw new Error(describeDevPortBlocker(inspection));
|
|
435
556
|
};
|
|
436
557
|
|
|
437
558
|
async function startApp(app: App) {
|
|
@@ -757,8 +878,10 @@ const createIndexedSourceWatching = ({
|
|
|
757
878
|
|
|
758
879
|
const runDevLoop = async () => {
|
|
759
880
|
devSessionStopping = false;
|
|
881
|
+
await ensureConfiguredDevPortsAvailable();
|
|
760
882
|
clearInteractiveConsole();
|
|
761
883
|
await ensureDevSessionSlot();
|
|
884
|
+
await ensureMachineMcpDaemonForDev();
|
|
762
885
|
const proteumInstall = resolveFrameworkInstallInfo({
|
|
763
886
|
appRoot: app.paths.root,
|
|
764
887
|
framework: cli.paths.framework,
|
package/cli/commands/diagnose.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { TRequestTraceErrorResponse, TRequestTraceArmResponse } from '../..
|
|
|
12
12
|
import type { TDevSessionErrorResponse, TDevSessionStartResponse } from '../../common/dev/session';
|
|
13
13
|
import { summarizeTraceForDiagnose } from '@common/dev/inspection';
|
|
14
14
|
import { readProteumManifest } from '../compiler/common/proteumManifest';
|
|
15
|
+
import { compactList, printAgentResponse, printJson, quoteCommandArgument, truncateForAgent } from '../utils/agentOutput';
|
|
15
16
|
|
|
16
17
|
const normalizeBaseUrl = (value: string) => value.replace(/\/+$/, '');
|
|
17
18
|
const truncate = (value: string, max = 160) => (value.length <= max ? value : `${value.slice(0, max)}...`);
|
|
@@ -186,6 +187,7 @@ const renderOrientation = (response: TDiagnoseResponse) =>
|
|
|
186
187
|
: [
|
|
187
188
|
'Orientation',
|
|
188
189
|
`- agents=${response.orientation.guidance.agents}`,
|
|
190
|
+
`- documentation=${response.orientation.guidance.documentation}`,
|
|
189
191
|
`- diagnostics=${response.orientation.guidance.diagnostics}`,
|
|
190
192
|
`- optimizations=${response.orientation.guidance.optimizations}`,
|
|
191
193
|
`- codingStyle=${response.orientation.guidance.codingStyle}`,
|
|
@@ -237,6 +239,133 @@ const renderHuman = (manifest: ReturnType<typeof readProteumManifest>, response:
|
|
|
237
239
|
renderLogs(response.serverLogs),
|
|
238
240
|
].join('\n');
|
|
239
241
|
|
|
242
|
+
const compactOwnerMatch = (match: TDiagnoseResponse['owner']['matches'][number]) => ({
|
|
243
|
+
kind: match.kind,
|
|
244
|
+
label: match.label,
|
|
245
|
+
score: match.score,
|
|
246
|
+
scope: match.scopeLabel,
|
|
247
|
+
origin: match.originHint,
|
|
248
|
+
source: match.source,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const compactDiagnostic = (diagnostic: TDiagnoseResponse['doctor']['diagnostics'][number]) => ({
|
|
252
|
+
level: diagnostic.level,
|
|
253
|
+
code: diagnostic.code,
|
|
254
|
+
message: truncateForAgent(diagnostic.message),
|
|
255
|
+
filepath: diagnostic.filepath,
|
|
256
|
+
sourceLocation: diagnostic.sourceLocation,
|
|
257
|
+
fixHint: diagnostic.fixHint ? truncateForAgent(diagnostic.fixHint) : undefined,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const compactRequest = (request: TDiagnoseResponse['request']) =>
|
|
261
|
+
request
|
|
262
|
+
? {
|
|
263
|
+
id: request.id,
|
|
264
|
+
method: request.method,
|
|
265
|
+
path: request.path,
|
|
266
|
+
statusCode: request.statusCode,
|
|
267
|
+
durationMs: request.durationMs,
|
|
268
|
+
capture: request.capture,
|
|
269
|
+
user: request.user,
|
|
270
|
+
errorMessage: request.errorMessage,
|
|
271
|
+
counts: {
|
|
272
|
+
calls: request.calls.length,
|
|
273
|
+
events: request.events.length,
|
|
274
|
+
sqlQueries: request.sqlQueries.length,
|
|
275
|
+
droppedEvents: request.droppedEvents,
|
|
276
|
+
},
|
|
277
|
+
}
|
|
278
|
+
: undefined;
|
|
279
|
+
|
|
280
|
+
const buildDiagnoseFullDetailCommand = ({
|
|
281
|
+
hitPath,
|
|
282
|
+
query,
|
|
283
|
+
}: {
|
|
284
|
+
hitPath: string;
|
|
285
|
+
query: string;
|
|
286
|
+
}) =>
|
|
287
|
+
[
|
|
288
|
+
'proteum diagnose',
|
|
289
|
+
quoteCommandArgument(query),
|
|
290
|
+
hitPath ? `--hit ${quoteCommandArgument(hitPath)}` : '',
|
|
291
|
+
typeof cli.args.port === 'string' && cli.args.port ? `--port ${cli.args.port}` : '',
|
|
292
|
+
typeof cli.args.url === 'string' && cli.args.url ? `--url ${quoteCommandArgument(cli.args.url)}` : '',
|
|
293
|
+
'--full',
|
|
294
|
+
]
|
|
295
|
+
.filter(Boolean)
|
|
296
|
+
.join(' ');
|
|
297
|
+
|
|
298
|
+
const printCompactDiagnose = ({
|
|
299
|
+
hitPath,
|
|
300
|
+
query,
|
|
301
|
+
response,
|
|
302
|
+
}: {
|
|
303
|
+
hitPath: string;
|
|
304
|
+
query: string;
|
|
305
|
+
response: TDiagnoseResponse;
|
|
306
|
+
}) => {
|
|
307
|
+
const request = compactRequest(response.request);
|
|
308
|
+
const traceSummary = summarizeTraceForDiagnose(response.request);
|
|
309
|
+
const doctorSummary = `${response.doctor.summary.errors} doctor errors/${response.doctor.summary.warnings} warnings`;
|
|
310
|
+
const contractsSummary = `${response.contracts.summary.errors} contract errors/${response.contracts.summary.warnings} warnings`;
|
|
311
|
+
const summary = `${response.query || query || 'request'}: ${traceSummary}; ${doctorSummary}; ${contractsSummary}`;
|
|
312
|
+
const nextActions = response.orientation?.nextSteps || [];
|
|
313
|
+
const requestId = response.request?.id;
|
|
314
|
+
|
|
315
|
+
printAgentResponse({
|
|
316
|
+
summary,
|
|
317
|
+
data: {
|
|
318
|
+
query: response.query,
|
|
319
|
+
request,
|
|
320
|
+
owner: {
|
|
321
|
+
top: response.owner.matches[0] ? compactOwnerMatch(response.owner.matches[0]) : undefined,
|
|
322
|
+
matches: compactList(response.owner.matches, 4).map(compactOwnerMatch),
|
|
323
|
+
totalReturned: response.owner.matches.length,
|
|
324
|
+
},
|
|
325
|
+
suspects: compactList(response.suspects, 6),
|
|
326
|
+
chain: compactList(response.chain || [], 8),
|
|
327
|
+
diagnostics: {
|
|
328
|
+
doctor: {
|
|
329
|
+
summary: response.doctor.summary,
|
|
330
|
+
top: compactList(response.doctor.diagnostics, 6).map(compactDiagnostic),
|
|
331
|
+
total: response.doctor.diagnostics.length,
|
|
332
|
+
},
|
|
333
|
+
contracts: {
|
|
334
|
+
summary: response.contracts.summary,
|
|
335
|
+
top: compactList(response.contracts.diagnostics, 6).map(compactDiagnostic),
|
|
336
|
+
total: response.contracts.diagnostics.length,
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
logs: compactList(response.serverLogs.logs, 10).map((entry) => ({
|
|
340
|
+
level: entry.level,
|
|
341
|
+
time: entry.time,
|
|
342
|
+
text: truncateForAgent(entry.text),
|
|
343
|
+
})),
|
|
344
|
+
instructions: response.orientation
|
|
345
|
+
? {
|
|
346
|
+
mustRead: [...new Set([response.orientation.guidance.agents, ...response.orientation.guidance.areaAgents])],
|
|
347
|
+
documentation: response.orientation.guidance.documentation,
|
|
348
|
+
diagnostics: response.orientation.guidance.diagnostics,
|
|
349
|
+
codingStyle: response.orientation.guidance.codingStyle,
|
|
350
|
+
optimizations: response.orientation.guidance.optimizations,
|
|
351
|
+
}
|
|
352
|
+
: undefined,
|
|
353
|
+
},
|
|
354
|
+
nextActions,
|
|
355
|
+
fullDetailCommand: buildDiagnoseFullDetailCommand({ hitPath, query }),
|
|
356
|
+
omitted: [
|
|
357
|
+
...(requestId
|
|
358
|
+
? [
|
|
359
|
+
{
|
|
360
|
+
reason: 'Full request events, API call payloads, and SQL text are omitted from the default diagnose response.',
|
|
361
|
+
command: `proteum trace show ${quoteCommandArgument(requestId)} --events`,
|
|
362
|
+
},
|
|
363
|
+
]
|
|
364
|
+
: []),
|
|
365
|
+
],
|
|
366
|
+
});
|
|
367
|
+
};
|
|
368
|
+
|
|
240
369
|
const resolveManifest = async () => {
|
|
241
370
|
try {
|
|
242
371
|
return readProteumManifest(cli.paths.appRoot);
|
|
@@ -258,7 +387,6 @@ export const run = async () => {
|
|
|
258
387
|
const method = typeof cli.args.method === 'string' && cli.args.method ? cli.args.method.trim().toUpperCase() : 'GET';
|
|
259
388
|
const logsLevel = typeof cli.args.logsLevel === 'string' && cli.args.logsLevel ? cli.args.logsLevel.trim() : 'warn';
|
|
260
389
|
const logsLimit = typeof cli.args.logsLimit === 'string' && cli.args.logsLimit ? cli.args.logsLimit.trim() : '40';
|
|
261
|
-
const shouldPrintJson = cli.args.json === true;
|
|
262
390
|
const hitPath = hit || (target.startsWith('/') ? target : '');
|
|
263
391
|
const query = target || hitPath;
|
|
264
392
|
let parsedDataJson: unknown;
|
|
@@ -308,11 +436,16 @@ export const run = async () => {
|
|
|
308
436
|
const diagnose = await requestJson<TDiagnoseResponse>(
|
|
309
437
|
`/__proteum/diagnose?${new URLSearchParams(diagnoseRequest).toString()}`,
|
|
310
438
|
);
|
|
311
|
-
if (
|
|
312
|
-
|
|
439
|
+
if (cli.args.full === true) {
|
|
440
|
+
printJson(diagnose.body);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (cli.args.human === true) {
|
|
445
|
+
const manifest = await resolveManifest();
|
|
446
|
+
console.log(renderHuman(manifest, diagnose.body));
|
|
313
447
|
return;
|
|
314
448
|
}
|
|
315
449
|
|
|
316
|
-
|
|
317
|
-
console.log(renderHuman(manifest, diagnose.body));
|
|
450
|
+
printCompactDiagnose({ hitPath, query, response: diagnose.body });
|
|
318
451
|
};
|
package/cli/commands/doctor.ts
CHANGED
|
@@ -3,8 +3,9 @@ import Compiler from '../compiler';
|
|
|
3
3
|
import { readProteumManifest } from '../compiler/common/proteumManifest';
|
|
4
4
|
import { buildContractsDoctorResponse } from '@common/dev/contractsDoctor';
|
|
5
5
|
import { buildDoctorResponse, renderDoctorHuman, renderDoctorResponseHuman } from '@common/dev/diagnostics';
|
|
6
|
+
import { compactList, printAgentResponse, printJson, truncateForAgent } from '../utils/agentOutput';
|
|
6
7
|
|
|
7
|
-
const allowedDoctorArgs = new Set(['contracts', 'json', 'strict']);
|
|
8
|
+
const allowedDoctorArgs = new Set(['contracts', 'full', 'human', 'json', 'strict']);
|
|
8
9
|
|
|
9
10
|
const validateDoctorArgs = () => {
|
|
10
11
|
const enabledArgs = Object.entries(cli.args)
|
|
@@ -20,6 +21,15 @@ const validateDoctorArgs = () => {
|
|
|
20
21
|
}
|
|
21
22
|
};
|
|
22
23
|
|
|
24
|
+
const compactDiagnostic = (diagnostic: ReturnType<typeof buildDoctorResponse>['diagnostics'][number]) => ({
|
|
25
|
+
level: diagnostic.level,
|
|
26
|
+
code: diagnostic.code,
|
|
27
|
+
message: truncateForAgent(diagnostic.message),
|
|
28
|
+
filepath: diagnostic.filepath,
|
|
29
|
+
sourceLocation: diagnostic.sourceLocation,
|
|
30
|
+
fixHint: diagnostic.fixHint ? truncateForAgent(diagnostic.fixHint) : undefined,
|
|
31
|
+
});
|
|
32
|
+
|
|
23
33
|
export const run = async (): Promise<void> => {
|
|
24
34
|
validateDoctorArgs();
|
|
25
35
|
|
|
@@ -32,9 +42,9 @@ export const run = async (): Promise<void> => {
|
|
|
32
42
|
? buildContractsDoctorResponse(manifest, cli.args.strict === true)
|
|
33
43
|
: buildDoctorResponse(manifest, cli.args.strict === true);
|
|
34
44
|
|
|
35
|
-
if (cli.args.
|
|
36
|
-
|
|
37
|
-
} else {
|
|
45
|
+
if (cli.args.full === true) {
|
|
46
|
+
printJson(response);
|
|
47
|
+
} else if (cli.args.human === true) {
|
|
38
48
|
console.log(
|
|
39
49
|
cli.args.contracts === true
|
|
40
50
|
? renderDoctorResponseHuman({
|
|
@@ -45,6 +55,16 @@ export const run = async (): Promise<void> => {
|
|
|
45
55
|
})
|
|
46
56
|
: renderDoctorHuman(manifest, cli.args.strict === true),
|
|
47
57
|
);
|
|
58
|
+
} else {
|
|
59
|
+
printAgentResponse({
|
|
60
|
+
summary: `${cli.args.contracts === true ? 'Doctor contracts' : 'Doctor'}: ${response.summary.errors} errors, ${response.summary.warnings} warnings`,
|
|
61
|
+
data: {
|
|
62
|
+
summary: response.summary,
|
|
63
|
+
diagnostics: compactList(response.diagnostics, 10).map(compactDiagnostic),
|
|
64
|
+
totalDiagnostics: response.diagnostics.length,
|
|
65
|
+
},
|
|
66
|
+
fullDetailCommand: `proteum doctor${cli.args.contracts === true ? ' --contracts' : ''} --full`,
|
|
67
|
+
});
|
|
48
68
|
}
|
|
49
69
|
|
|
50
70
|
if (cli.args.strict === true && response.diagnostics.length > 0) {
|
package/cli/commands/explain.ts
CHANGED
|
@@ -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,122 @@ 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 = () => cli.args.full === true || cli.args.manifest === true;
|
|
46
|
+
|
|
47
|
+
const buildSectionFlagCommand = (selectedSections: TExplainSectionName[]) =>
|
|
48
|
+
selectedSections.length === explainSectionNames.length
|
|
49
|
+
? 'proteum explain --all --full'
|
|
50
|
+
: `proteum explain ${selectedSections.map((sectionName) => `--${sectionName}`).join(' ')} --full`;
|
|
51
|
+
|
|
52
|
+
const summarizeSelectedSection = (manifest: ReturnType<typeof readProteumManifest>, sectionName: TExplainSectionName) => {
|
|
53
|
+
if (sectionName === 'app') return { section: sectionName, count: 1 };
|
|
54
|
+
if (sectionName === 'conventions')
|
|
55
|
+
return {
|
|
56
|
+
section: sectionName,
|
|
57
|
+
count: manifest.conventions.routeOptionKeys.length + manifest.conventions.reservedRouteOptionKeys.length,
|
|
58
|
+
};
|
|
59
|
+
if (sectionName === 'env') return { section: sectionName, count: manifest.env.requiredVariables.length };
|
|
60
|
+
if (sectionName === 'connected') return { section: sectionName, count: manifest.connectedProjects.length };
|
|
61
|
+
if (sectionName === 'services')
|
|
62
|
+
return { section: sectionName, count: manifest.services.app.length + manifest.services.routerPlugins.length };
|
|
63
|
+
if (sectionName === 'controllers') return { section: sectionName, count: manifest.controllers.length };
|
|
64
|
+
if (sectionName === 'commands') return { section: sectionName, count: manifest.commands.length };
|
|
65
|
+
if (sectionName === 'routes') return { section: sectionName, count: manifest.routes.client.length + manifest.routes.server.length };
|
|
66
|
+
if (sectionName === 'layouts') return { section: sectionName, count: manifest.layouts.length };
|
|
67
|
+
return { section: sectionName, count: manifest.diagnostics.length };
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const printCompactOwner = (ownerQuery: string, response: ReturnType<typeof explainOwner>) => {
|
|
71
|
+
const topOwner = response.matches[0];
|
|
72
|
+
|
|
73
|
+
printAgentResponse({
|
|
74
|
+
summary: topOwner
|
|
75
|
+
? `${ownerQuery} -> ${topOwner.kind} ${topOwner.label} (${topOwner.scopeLabel})`
|
|
76
|
+
: `${ownerQuery} -> no manifest owner matched`,
|
|
77
|
+
data: {
|
|
78
|
+
query: ownerQuery,
|
|
79
|
+
normalizedQuery: response.normalizedQuery,
|
|
80
|
+
top: topOwner ? compactOwnerMatch(topOwner) : undefined,
|
|
81
|
+
matches: compactList(response.matches, 6).map(compactOwnerMatch),
|
|
82
|
+
totalReturned: response.matches.length,
|
|
83
|
+
},
|
|
84
|
+
nextActions: [
|
|
85
|
+
{
|
|
86
|
+
label: 'Orient',
|
|
87
|
+
command: `proteum orient ${quoteCommandArgument(ownerQuery)}`,
|
|
88
|
+
reason: 'Resolve the owner together with the relevant instruction files and next command.',
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
fullDetailCommand: `proteum explain owner ${quoteCommandArgument(ownerQuery)} --full`,
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const printCompactExplain = (manifest: ReturnType<typeof readProteumManifest>, selectedSections: TExplainSectionName[] = []) => {
|
|
96
|
+
const errors = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'error').length;
|
|
97
|
+
const warnings = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'warning').length;
|
|
98
|
+
const requiredEnvProvided = manifest.env.requiredVariables.filter((variable) => variable.provided).length;
|
|
99
|
+
const hasSelectedSections = selectedSections.length > 0;
|
|
100
|
+
const fullDetailCommand = hasSelectedSections ? buildSectionFlagCommand(selectedSections) : 'proteum explain --manifest';
|
|
101
|
+
|
|
102
|
+
printAgentResponse({
|
|
103
|
+
summary: hasSelectedSections
|
|
104
|
+
? `${manifest.app.identity.identifier}: summarized ${selectedSections.join(', ')} sections; use --full for raw section arrays`
|
|
105
|
+
: `${manifest.app.identity.identifier}: ${manifest.controllers.length} controllers, ${manifest.routes.client.length + manifest.routes.server.length} routes, ${errors} errors, ${warnings} warnings`,
|
|
106
|
+
data: {
|
|
107
|
+
app: {
|
|
108
|
+
root: manifest.app.root,
|
|
109
|
+
coreRoot: manifest.app.coreRoot,
|
|
110
|
+
identifier: manifest.app.identity.identifier,
|
|
111
|
+
name: manifest.app.identity.name,
|
|
112
|
+
},
|
|
113
|
+
counts: {
|
|
114
|
+
commands: manifest.commands.length,
|
|
115
|
+
connectedProjects: manifest.connectedProjects.length,
|
|
116
|
+
controllers: manifest.controllers.length,
|
|
117
|
+
diagnostics: manifest.diagnostics.length,
|
|
118
|
+
layouts: manifest.layouts.length,
|
|
119
|
+
routesClient: manifest.routes.client.length,
|
|
120
|
+
routesServer: manifest.routes.server.length,
|
|
121
|
+
servicesApp: manifest.services.app.length,
|
|
122
|
+
servicesRouterPlugins: manifest.services.routerPlugins.length,
|
|
123
|
+
},
|
|
124
|
+
diagnostics: { errors, warnings },
|
|
125
|
+
selectedSections: hasSelectedSections ? selectedSections.map((sectionName) => summarizeSelectedSection(manifest, sectionName)) : undefined,
|
|
126
|
+
env: {
|
|
127
|
+
requiredProvided: requiredEnvProvided,
|
|
128
|
+
requiredTotal: manifest.env.requiredVariables.length,
|
|
129
|
+
routerPort: manifest.env.resolved.routerPort,
|
|
130
|
+
},
|
|
131
|
+
summaryItems: buildExplainSummaryItems(manifest),
|
|
132
|
+
},
|
|
133
|
+
nextActions: [
|
|
134
|
+
{
|
|
135
|
+
label: 'Orient Target',
|
|
136
|
+
command: 'proteum orient <route|file|controller|error>',
|
|
137
|
+
reason: 'Use orient for task-specific owner, instruction, and next-command routing.',
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
fullDetailCommand,
|
|
141
|
+
omitted: [
|
|
142
|
+
{
|
|
143
|
+
reason: hasSelectedSections
|
|
144
|
+
? 'Selected manifest sections are summarized by default to avoid large route/controller dumps.'
|
|
145
|
+
: 'Full manifest sections are omitted from the default agent summary.',
|
|
146
|
+
command: fullDetailCommand,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
|
|
34
152
|
export const run = async (): Promise<void> => {
|
|
35
153
|
validateExplainArgs();
|
|
36
154
|
|
|
@@ -42,8 +160,13 @@ export const run = async (): Promise<void> => {
|
|
|
42
160
|
|
|
43
161
|
if (ownerQuery) {
|
|
44
162
|
const response = explainOwner(manifest, ownerQuery);
|
|
45
|
-
if (cli.args.
|
|
46
|
-
|
|
163
|
+
if (cli.args.full === true || cli.args.manifest === true) {
|
|
164
|
+
printJson(response);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (cli.args.human !== true) {
|
|
169
|
+
printCompactOwner(ownerQuery, response);
|
|
47
170
|
return;
|
|
48
171
|
}
|
|
49
172
|
|
|
@@ -64,10 +187,15 @@ export const run = async (): Promise<void> => {
|
|
|
64
187
|
|
|
65
188
|
const selectedSections = getSelectedSections();
|
|
66
189
|
|
|
67
|
-
if (
|
|
68
|
-
|
|
190
|
+
if (hasExplicitDetailSelection()) {
|
|
191
|
+
printJson(pickExplainManifestSections(manifest, cli.args.manifest === true ? [...explainSectionNames] : selectedSections));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (cli.args.human === true) {
|
|
196
|
+
console.log(renderExplainHuman(manifest, selectedSections));
|
|
69
197
|
return;
|
|
70
198
|
}
|
|
71
199
|
|
|
72
|
-
|
|
200
|
+
printCompactExplain(manifest, selectedSections);
|
|
73
201
|
};
|