proteum 2.3.0 → 2.4.2
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 +8 -3
- package/README.md +20 -15
- package/agents/project/AGENTS.md +16 -10
- package/agents/project/DOCUMENTATION.md +1326 -0
- package/agents/project/app-root/AGENTS.md +2 -2
- package/agents/project/diagnostics.md +10 -9
- package/agents/project/optimizations.md +1 -1
- package/agents/project/root/AGENTS.md +15 -8
- package/agents/project/server/services/AGENTS.md +1 -0
- package/agents/project/tests/AGENTS.md +1 -0
- package/cli/commands/db.ts +160 -0
- package/cli/commands/dev.ts +148 -25
- package/cli/commands/diagnose.ts +2 -0
- package/cli/commands/explain.ts +38 -9
- package/cli/commands/mcp.ts +126 -9
- package/cli/commands/orient.ts +44 -17
- package/cli/commands/runtime.ts +100 -17
- package/cli/mcp/router.ts +1028 -0
- package/cli/presentation/commands.ts +56 -25
- package/cli/presentation/help.ts +1 -1
- package/cli/runtime/commands.ts +163 -21
- package/cli/runtime/devSessions.ts +328 -2
- package/cli/runtime/mcpDaemon.ts +288 -0
- package/cli/runtime/ports.ts +151 -0
- package/cli/utils/agents.ts +94 -17
- package/cli/utils/appRoots.ts +232 -0
- package/common/dev/database.ts +226 -0
- package/common/dev/diagnostics.ts +1 -1
- package/common/dev/inspection.ts +8 -1
- package/common/dev/mcpPayloads.ts +456 -17
- package/common/dev/mcpServer.ts +51 -0
- package/docs/agent-routing.md +32 -21
- package/docs/dev-commands.md +1 -1
- package/docs/dev-sessions.md +3 -1
- package/docs/diagnostics.md +21 -20
- package/docs/mcp.md +114 -50
- package/docs/migrate-from-2.1.3.md +3 -5
- package/docs/request-tracing.md +3 -3
- package/package.json +10 -3
- package/server/app/devDiagnostics.ts +92 -0
- package/server/app/devMcp.ts +55 -0
- package/server/services/prisma/mariadb.ts +7 -3
- package/server/services/router/http/index.ts +25 -0
- package/server/services/router/request/ip.test.cjs +0 -1
- package/tests/agents-utils.test.cjs +58 -3
- package/tests/cli-mcp-command.test.cjs +327 -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 +0 -1
- package/tests/eslint-rules.test.cjs +0 -1
- package/tests/inspection.test.cjs +0 -1
- package/tests/mcp.test.cjs +769 -2
- package/tests/router-cache-config.test.cjs +0 -1
- package/vitest.config.mjs +9 -0
- package/cli/mcp/provider.ts +0 -365
- package/cli/mcp/stdio.ts +0 -16
package/cli/commands/explain.ts
CHANGED
|
@@ -42,8 +42,30 @@ const compactOwnerMatch = (match: ReturnType<typeof explainOwner>['matches'][num
|
|
|
42
42
|
source: match.source,
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
const hasExplicitDetailSelection = (
|
|
46
|
-
|
|
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
|
+
};
|
|
47
69
|
|
|
48
70
|
const printCompactOwner = (ownerQuery: string, response: ReturnType<typeof explainOwner>) => {
|
|
49
71
|
const topOwner = response.matches[0];
|
|
@@ -70,13 +92,17 @@ const printCompactOwner = (ownerQuery: string, response: ReturnType<typeof expla
|
|
|
70
92
|
});
|
|
71
93
|
};
|
|
72
94
|
|
|
73
|
-
const printCompactExplain = (manifest: ReturnType<typeof readProteumManifest
|
|
95
|
+
const printCompactExplain = (manifest: ReturnType<typeof readProteumManifest>, selectedSections: TExplainSectionName[] = []) => {
|
|
74
96
|
const errors = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'error').length;
|
|
75
97
|
const warnings = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'warning').length;
|
|
76
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';
|
|
77
101
|
|
|
78
102
|
printAgentResponse({
|
|
79
|
-
summary:
|
|
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`,
|
|
80
106
|
data: {
|
|
81
107
|
app: {
|
|
82
108
|
root: manifest.app.root,
|
|
@@ -96,6 +122,7 @@ const printCompactExplain = (manifest: ReturnType<typeof readProteumManifest>) =
|
|
|
96
122
|
servicesRouterPlugins: manifest.services.routerPlugins.length,
|
|
97
123
|
},
|
|
98
124
|
diagnostics: { errors, warnings },
|
|
125
|
+
selectedSections: hasSelectedSections ? selectedSections.map((sectionName) => summarizeSelectedSection(manifest, sectionName)) : undefined,
|
|
99
126
|
env: {
|
|
100
127
|
requiredProvided: requiredEnvProvided,
|
|
101
128
|
requiredTotal: manifest.env.requiredVariables.length,
|
|
@@ -110,11 +137,13 @@ const printCompactExplain = (manifest: ReturnType<typeof readProteumManifest>) =
|
|
|
110
137
|
reason: 'Use orient for task-specific owner, instruction, and next-command routing.',
|
|
111
138
|
},
|
|
112
139
|
],
|
|
113
|
-
fullDetailCommand
|
|
140
|
+
fullDetailCommand,
|
|
114
141
|
omitted: [
|
|
115
142
|
{
|
|
116
|
-
reason:
|
|
117
|
-
|
|
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,
|
|
118
147
|
},
|
|
119
148
|
],
|
|
120
149
|
});
|
|
@@ -158,7 +187,7 @@ export const run = async (): Promise<void> => {
|
|
|
158
187
|
|
|
159
188
|
const selectedSections = getSelectedSections();
|
|
160
189
|
|
|
161
|
-
if (hasExplicitDetailSelection(
|
|
190
|
+
if (hasExplicitDetailSelection()) {
|
|
162
191
|
printJson(pickExplainManifestSections(manifest, cli.args.manifest === true ? [...explainSectionNames] : selectedSections));
|
|
163
192
|
return;
|
|
164
193
|
}
|
|
@@ -168,5 +197,5 @@ export const run = async (): Promise<void> => {
|
|
|
168
197
|
return;
|
|
169
198
|
}
|
|
170
199
|
|
|
171
|
-
printCompactExplain(manifest);
|
|
200
|
+
printCompactExplain(manifest, selectedSections);
|
|
172
201
|
};
|
package/cli/commands/mcp.ts
CHANGED
|
@@ -1,16 +1,133 @@
|
|
|
1
1
|
import cli from '..';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { startProteumMachineMcpRouter, startProteumMachineMcpRouterHttp } from '../mcp/router';
|
|
3
|
+
import {
|
|
4
|
+
ensureMachineMcpDaemonProcess,
|
|
5
|
+
inspectMachineMcpDaemonRecord,
|
|
6
|
+
resolveMachineMcpDaemonPort,
|
|
7
|
+
stopMachineMcpDaemonProcess,
|
|
8
|
+
} from '../runtime/mcpDaemon';
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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 || ''),
|
|
10
65
|
});
|
|
11
66
|
|
|
12
|
-
|
|
13
|
-
|
|
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({
|
|
14
131
|
version: String(cli.packageJson.version || ''),
|
|
15
132
|
});
|
|
16
133
|
};
|
package/cli/commands/orient.ts
CHANGED
|
@@ -7,6 +7,7 @@ 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';
|
|
11
12
|
import { compactList, printAgentResponse, printJson, quoteCommandArgument } from '../utils/agentOutput';
|
|
12
13
|
|
|
@@ -125,6 +126,7 @@ const renderHuman = (response: TOrientResponse) =>
|
|
|
125
126
|
...(response.app.routerPort ? [`- routerPort=${response.app.routerPort}`] : []),
|
|
126
127
|
'Guidance',
|
|
127
128
|
`- agents=${response.guidance.agents}`,
|
|
129
|
+
`- documentation=${response.guidance.documentation}`,
|
|
128
130
|
`- diagnostics=${response.guidance.diagnostics}`,
|
|
129
131
|
`- optimizations=${response.guidance.optimizations}`,
|
|
130
132
|
`- codingStyle=${response.guidance.codingStyle}`,
|
|
@@ -163,23 +165,48 @@ const compactOwnerMatch = (match: TOrientResponse['owner']['matches'][number]) =
|
|
|
163
165
|
source: match.source,
|
|
164
166
|
});
|
|
165
167
|
|
|
166
|
-
const buildInstructionPlan = (response: TOrientResponse) =>
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
+
};
|
|
183
210
|
|
|
184
211
|
const printCompactOrient = (response: TOrientResponse) => {
|
|
185
212
|
const topOwner = response.owner.matches[0];
|
package/cli/commands/runtime.ts
CHANGED
|
@@ -5,7 +5,8 @@ import { UsageError } from 'clipanion';
|
|
|
5
5
|
|
|
6
6
|
import cli from '..';
|
|
7
7
|
import { readProteumManifest } from '../compiler/common/proteumManifest';
|
|
8
|
-
import { listDevSessionInspections, type TDevSessionInspection } from '../runtime/devSessions';
|
|
8
|
+
import { listDevSessionInspections, writeMachineDevSessionRecord, type TDevSessionInspection } from '../runtime/devSessions';
|
|
9
|
+
import { inspectDevPort, type TDevPortInspection } from '../runtime/ports';
|
|
9
10
|
import { printAgentResponse, printJson, quoteCommandArgument } from '../utils/agentOutput';
|
|
10
11
|
import type { TDoctorResponse } from '@common/dev/diagnostics';
|
|
11
12
|
import type { TProteumManifest } from '@common/dev/proteumManifest';
|
|
@@ -40,6 +41,11 @@ const getSessionUrl = (inspection: TDevSessionInspection) => {
|
|
|
40
41
|
return `http://localhost:${inspection.record.routerPort}`;
|
|
41
42
|
};
|
|
42
43
|
|
|
44
|
+
const getSessionMcpUrl = (inspection: TDevSessionInspection) => {
|
|
45
|
+
const sessionUrl = getSessionUrl(inspection);
|
|
46
|
+
return sessionUrl ? `${sessionUrl}/__proteum/mcp` : '';
|
|
47
|
+
};
|
|
48
|
+
|
|
43
49
|
const probeDoctor = async (baseUrl: string) => {
|
|
44
50
|
if (!baseUrl) return { reachable: false, error: 'No dev URL is registered.' };
|
|
45
51
|
|
|
@@ -78,11 +84,90 @@ const compactSession = (inspection: TDevSessionInspection) => ({
|
|
|
78
84
|
pid: inspection.record?.pid,
|
|
79
85
|
routerPort: inspection.record?.routerPort,
|
|
80
86
|
publicUrl: inspection.record?.publicUrl,
|
|
87
|
+
mcpUrl: inspection.record ? getSessionMcpUrl(inspection) : undefined,
|
|
81
88
|
state: inspection.record?.state,
|
|
82
89
|
startedAt: inspection.record?.startedAt,
|
|
83
90
|
updatedAt: inspection.record?.updatedAt,
|
|
84
91
|
});
|
|
85
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
|
+
|
|
86
171
|
export const run = async () => {
|
|
87
172
|
const action = getAction();
|
|
88
173
|
if (action !== 'status') return;
|
|
@@ -93,10 +178,21 @@ export const run = async () => {
|
|
|
93
178
|
sessionFilePath: typeof cli.args.sessionFile === 'string' && cli.args.sessionFile ? cli.args.sessionFile : undefined,
|
|
94
179
|
});
|
|
95
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
|
+
);
|
|
96
186
|
const selectedSession =
|
|
97
187
|
liveSessions.find((inspection) => inspection.record?.state === 'ready') || liveSessions[0] || sessions.find((inspection) => inspection.record);
|
|
98
188
|
const selectedBaseUrl = selectedSession ? getSessionUrl(selectedSession) : '';
|
|
99
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;
|
|
100
196
|
|
|
101
197
|
const payload = {
|
|
102
198
|
appRoot: cli.paths.appRoot,
|
|
@@ -119,6 +215,7 @@ export const run = async () => {
|
|
|
119
215
|
selected: selectedSession ? compactSession(selectedSession) : undefined,
|
|
120
216
|
sessions: sessions.map(compactSession),
|
|
121
217
|
health,
|
|
218
|
+
configuredDevPort,
|
|
122
219
|
};
|
|
123
220
|
|
|
124
221
|
if (cli.args.full === true) {
|
|
@@ -129,23 +226,9 @@ export const run = async () => {
|
|
|
129
226
|
printAgentResponse({
|
|
130
227
|
summary: selectedSession
|
|
131
228
|
? `${selectedSession.live ? 'live' : 'stale'} dev session on ${selectedSession.record?.routerPort || 'unknown port'}; health=${health.reachable ? 'reachable' : 'unreachable'}`
|
|
132
|
-
: 'No tracked Proteum dev session found.',
|
|
229
|
+
: describePortOwner(configuredDevPort) || 'No tracked Proteum dev session found.',
|
|
133
230
|
data: payload,
|
|
134
|
-
nextActions: selectedSession
|
|
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
|
-
],
|
|
231
|
+
nextActions: getNextActions({ health, portInspection: configuredDevPort, selectedSession }),
|
|
149
232
|
fullDetailCommand: 'proteum runtime status --full',
|
|
150
233
|
});
|
|
151
234
|
};
|