proteum 2.3.0 → 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.
Files changed (49) hide show
  1. package/AGENTS.md +8 -3
  2. package/README.md +20 -15
  3. package/agents/project/AGENTS.md +15 -10
  4. package/agents/project/DOCUMENTATION.md +1326 -0
  5. package/agents/project/app-root/AGENTS.md +2 -2
  6. package/agents/project/diagnostics.md +9 -8
  7. package/agents/project/root/AGENTS.md +14 -8
  8. package/agents/project/tests/AGENTS.md +1 -0
  9. package/cli/commands/dev.ts +148 -25
  10. package/cli/commands/diagnose.ts +2 -0
  11. package/cli/commands/explain.ts +38 -9
  12. package/cli/commands/mcp.ts +126 -9
  13. package/cli/commands/orient.ts +44 -17
  14. package/cli/commands/runtime.ts +100 -17
  15. package/cli/mcp/router.ts +1010 -0
  16. package/cli/presentation/commands.ts +34 -24
  17. package/cli/presentation/help.ts +1 -1
  18. package/cli/runtime/commands.ts +129 -21
  19. package/cli/runtime/devSessions.ts +328 -2
  20. package/cli/runtime/mcpDaemon.ts +288 -0
  21. package/cli/runtime/ports.ts +151 -0
  22. package/cli/utils/agents.ts +93 -17
  23. package/cli/utils/appRoots.ts +232 -0
  24. package/common/dev/diagnostics.ts +1 -1
  25. package/common/dev/inspection.ts +8 -1
  26. package/common/dev/mcpPayloads.ts +431 -17
  27. package/common/dev/mcpServer.ts +33 -0
  28. package/docs/agent-routing.md +32 -21
  29. package/docs/dev-commands.md +1 -1
  30. package/docs/dev-sessions.md +3 -1
  31. package/docs/diagnostics.md +21 -20
  32. package/docs/mcp.md +109 -52
  33. package/docs/migrate-from-2.1.3.md +3 -5
  34. package/docs/request-tracing.md +3 -3
  35. package/package.json +10 -3
  36. package/server/app/devMcp.ts +45 -0
  37. package/server/services/router/request/ip.test.cjs +0 -1
  38. package/tests/agents-utils.test.cjs +58 -3
  39. package/tests/cli-mcp-command.test.cjs +262 -0
  40. package/tests/codex-mcp-usage.test.cjs +307 -0
  41. package/tests/dev-sessions.test.cjs +113 -0
  42. package/tests/dev-transpile-watch.test.cjs +0 -1
  43. package/tests/eslint-rules.test.cjs +0 -1
  44. package/tests/inspection.test.cjs +0 -1
  45. package/tests/mcp.test.cjs +748 -2
  46. package/tests/router-cache-config.test.cjs +0 -1
  47. package/vitest.config.mjs +9 -0
  48. package/cli/mcp/provider.ts +0 -365
  49. package/cli/mcp/stdio.ts +0 -16
@@ -10,7 +10,9 @@ import {
10
10
  compactOrientationResponse,
11
11
  compactPerfRequestResponse,
12
12
  compactPerfTopResponse,
13
+ compactRouteCandidatesResponse,
13
14
  compactTraceResponse,
15
+ compactWorkflowStartResponse,
14
16
  resolveInstructionRouting,
15
17
  } from '@common/dev/mcpPayloads';
16
18
  import type { TProteumMcpProvider } from '@common/dev/mcpServer';
@@ -64,6 +66,42 @@ export const createRuntimeProteumMcpProvider = ({
64
66
  },
65
67
  });
66
68
  },
69
+ async workflowStart({ file, query, route, task }) {
70
+ const manifest = readManifest();
71
+ const doctor = buildDoctorResponse(manifest);
72
+ const contracts = buildContractsDoctorResponse(manifest);
73
+ const ownerQuery = [route, file, query]
74
+ .map((value) => value?.trim())
75
+ .find((value): value is string => Boolean(value));
76
+
77
+ return compactWorkflowStartResponse({
78
+ contracts,
79
+ doctor,
80
+ file,
81
+ health: {
82
+ reachable: true,
83
+ doctor: doctor.summary,
84
+ contracts: contracts.summary,
85
+ },
86
+ manifest,
87
+ owner: ownerQuery ? explainOwner(manifest, ownerQuery) : undefined,
88
+ query,
89
+ route,
90
+ runtime: {
91
+ publicUrl,
92
+ routerPort,
93
+ source: 'proteum-dev-runtime',
94
+ mcpUrl: `${publicUrl}/__proteum/mcp`,
95
+ traceEnabled: app.container.Trace.isDevTraceEnabled(),
96
+ profilerEnabled: app.container.Trace.isProfilingEnabled(),
97
+ connectedProjects: Object.entries(app.connectedProjects || {}).map(([namespace, project]) => ({
98
+ namespace,
99
+ urlInternal: (project as { urlInternal?: string }).urlInternal,
100
+ })),
101
+ },
102
+ task,
103
+ });
104
+ },
67
105
  async orient({ query }) {
68
106
  return compactOrientationResponse(buildOrientationResponse(readManifest(), query));
69
107
  },
@@ -80,6 +118,13 @@ export const createRuntimeProteumMcpProvider = ({
80
118
  query: normalizedQuery,
81
119
  });
82
120
  },
121
+ async routeCandidates({ limit, query }) {
122
+ return compactRouteCandidatesResponse({
123
+ limit,
124
+ manifest: readManifest(),
125
+ query,
126
+ });
127
+ },
83
128
  async doctor({ contracts = true }) {
84
129
  const manifest = readManifest();
85
130
 
@@ -1,5 +1,4 @@
1
1
  const assert = require('node:assert/strict');
2
- const test = require('node:test');
3
2
 
4
3
  const { resolveRequestIp } = require('./ip.ts');
5
4
 
@@ -2,7 +2,6 @@ const assert = require('node:assert/strict');
2
2
  const fs = require('node:fs');
3
3
  const os = require('node:os');
4
4
  const path = require('node:path');
5
- const test = require('node:test');
6
5
 
7
6
  const coreRoot = path.resolve(__dirname, '..');
8
7
  process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
@@ -24,12 +23,14 @@ const createCoreFixture = () => {
24
23
 
25
24
  writeFile(path.join(agentsRoot, 'AGENTS.md'), '# Root Contract\n\n- Root rule\n');
26
25
  writeFile(path.join(agentsRoot, 'CODING_STYLE.md'), '# Coding Style\n\n- Style rule\n');
26
+ writeFile(path.join(agentsRoot, 'DOCUMENTATION.md'), '# Documentation\n\n- Documentation rule\n');
27
27
  writeFile(path.join(agentsRoot, 'diagnostics.md'), '# Diagnostics\n\n- Diagnostics rule\n');
28
28
  writeFile(path.join(agentsRoot, 'optimizations.md'), '# Optimizations\n\n- Optimization rule\n');
29
29
  writeFile(path.join(agentsRoot, 'client', 'AGENTS.md'), '# Client Rules\n\n- Client rule\n');
30
30
  writeFile(path.join(agentsRoot, 'client', 'pages', 'AGENTS.md'), '# Page Rules\n\n- Page rule\n');
31
31
  writeFile(path.join(agentsRoot, 'server', 'routes', 'AGENTS.md'), '# Route Rules\n\n- Route rule\n');
32
32
  writeFile(path.join(agentsRoot, 'server', 'services', 'AGENTS.md'), '# Service Rules\n\n- Service rule\n');
33
+ writeFile(path.join(agentsRoot, 'tests', 'AGENTS.md'), '# Test Rules\n\n- Test rule\n');
33
34
  writeFile(path.join(agentsRoot, 'tests', 'e2e', 'AGENTS.md'), '# E2E Rules\n\n- E2E rule\n');
34
35
  writeFile(
35
36
  path.join(agentsRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'),
@@ -53,6 +54,7 @@ const createAppFixture = () => {
53
54
  '# Proteum-managed instruction files',
54
55
  '/AGENTS.md',
55
56
  '/CODING_STYLE.md',
57
+ '/DOCUMENTATION.md',
56
58
  '# End Proteum-managed instruction files',
57
59
  '/.proteum',
58
60
  '',
@@ -62,23 +64,61 @@ const createAppFixture = () => {
62
64
  return appRoot;
63
65
  };
64
66
 
67
+ test('project instruction sources require unit tests for applicable production changes', () => {
68
+ const projectAgentsRoot = path.join(coreRoot, 'agents', 'project');
69
+
70
+ assert.match(
71
+ fs.readFileSync(path.join(projectAgentsRoot, 'AGENTS.md'), 'utf8'),
72
+ /production changes must always add or update focused unit tests/,
73
+ );
74
+ assert.match(
75
+ fs.readFileSync(path.join(projectAgentsRoot, 'root', 'AGENTS.md'), 'utf8'),
76
+ /always add or update focused unit tests/,
77
+ );
78
+ assert.match(
79
+ fs.readFileSync(path.join(projectAgentsRoot, 'tests', 'AGENTS.md'), 'utf8'),
80
+ /For every production change, add or update focused unit tests/,
81
+ );
82
+ });
83
+
65
84
  test('standalone configure creates tracked instruction files with routing contract and split docs', () => {
66
85
  const coreRoot = createCoreFixture();
67
86
  const appRoot = createAppFixture();
68
87
  const result = configureProjectAgentInstructions({ appRoot, coreRoot });
69
88
  const agentsContent = fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8');
70
89
  const codingStyleContent = fs.readFileSync(path.join(appRoot, 'CODING_STYLE.md'), 'utf8');
90
+ const documentationContent = fs.readFileSync(path.join(appRoot, 'DOCUMENTATION.md'), 'utf8');
71
91
  const gitignoreContent = fs.readFileSync(path.join(appRoot, '.gitignore'), 'utf8');
72
92
 
73
93
  assert.equal(result.blocked.length, 0);
74
94
  assert.match(agentsContent, /^# Proteum Instructions/m);
75
95
  assert.match(agentsContent, /<!-- proteum-instructions:start -->/);
76
96
  assert.match(agentsContent, /## Agent Routing Contract/);
77
- assert.match(agentsContent, /npx proteum orient <query>/);
97
+ assert.match(agentsContent, /npx proteum runtime status/);
98
+ assert.match(agentsContent, /MCP `workflow_start`/);
99
+ assert.match(agentsContent, /project_resolve \{ cwd \}/);
100
+ assert.match(agentsContent, /instructions_resolve \{ projectId \}/);
101
+ assert.match(agentsContent, /Do not run CLI equivalents after a successful MCP result/);
102
+ assert.match(agentsContent, /Read full files only before edits or git writes/);
103
+ assert.match(agentsContent, /explain_summary/);
104
+ assert.match(agentsContent, /\/__proteum\/mcp/);
105
+ assert.match(agentsContent, /proteum-mcp-v1/);
106
+ assert.match(agentsContent, /## Triggered Instruction Reads/);
107
+ assert.match(agentsContent, /Git lifecycle/);
108
+ assert.match(agentsContent, /read Root contract fallback before any git write/);
109
+ assert.match(agentsContent, /add or update focused unit tests/);
110
+ assert.match(agentsContent, /read Root contract fallback, `CODING_STYLE\.md`, `tests\/AGENTS\.md`/);
111
+ assert.match(agentsContent, /MCP-selected previews are enough/);
112
+ assert.doesNotMatch(agentsContent, /Conventional Commits/);
113
+ assert.match(agentsContent, /They are not deleted/);
78
114
  assert.doesNotMatch(agentsContent, /## Source: CODING_STYLE\.md/);
79
115
  assert.match(codingStyleContent, /## Source: CODING_STYLE\.md/);
80
116
  assert.match(codingStyleContent, /## Coding Style/);
81
117
  assert.doesNotMatch(codingStyleContent, /## Source: client\/AGENTS\.md/);
118
+ assert.match(documentationContent, /## Source: DOCUMENTATION\.md/);
119
+ assert.match(documentationContent, /## Documentation/);
120
+ assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'AGENTS.md')), true);
121
+ assert.match(fs.readFileSync(path.join(appRoot, 'tests', 'AGENTS.md'), 'utf8'), /Test rule/);
82
122
  assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'e2e', 'AGENTS.md')), true);
83
123
  assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md')), true);
84
124
  assert.match(fs.readFileSync(path.join(appRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'), 'utf8'), /Journey rule/);
@@ -86,6 +126,7 @@ test('standalone configure creates tracked instruction files with routing contra
86
126
  assert.doesNotMatch(agentsContent, /Before reading or applying instructions from this file/);
87
127
  assert.doesNotMatch(gitignoreContent, /Proteum-managed instruction files/);
88
128
  assert.doesNotMatch(gitignoreContent, /^\/AGENTS\.md$/m);
129
+ assert.doesNotMatch(gitignoreContent, /^\/DOCUMENTATION\.md$/m);
89
130
  });
90
131
 
91
132
  test('configure preserves project content outside the managed section', () => {
@@ -176,6 +217,10 @@ test('monorepo configure writes root and app instruction files', () => {
176
217
 
177
218
  fs.mkdirSync(path.join(monorepoRoot, '.git'));
178
219
  fs.mkdirSync(path.join(appRoot, 'client'), { recursive: true });
220
+ fs.mkdirSync(path.join(appRoot, 'server'), { recursive: true });
221
+ writeFile(path.join(appRoot, 'package.json'), '{"name":"product"}\n');
222
+ writeFile(path.join(appRoot, 'identity.config.ts'), 'export default {};\n');
223
+ writeFile(path.join(appRoot, 'proteum.config.ts'), 'export default {};\n');
179
224
 
180
225
  configureProjectAgentInstructions({ appRoot, coreRoot });
181
226
 
@@ -184,16 +229,26 @@ test('monorepo configure writes root and app instruction files', () => {
184
229
  assert.equal(result.mode, 'monorepo');
185
230
  assert.equal(resolveProjectAgentMonorepoRoot(appRoot), fs.realpathSync(monorepoRoot));
186
231
  assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /## Agent Routing Contract/);
232
+ assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /## Known Proteum Apps/);
233
+ assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /apps\/product/);
234
+ assert.match(fs.readFileSync(path.join(monorepoRoot, 'AGENTS.md'), 'utf8'), /Do not start `npx proteum dev` from this root/);
187
235
  assert.match(fs.readFileSync(path.join(monorepoRoot, 'CODING_STYLE.md'), 'utf8'), /## Source: CODING_STYLE\.md/);
236
+ assert.match(fs.readFileSync(path.join(monorepoRoot, 'DOCUMENTATION.md'), 'utf8'), /## Source: DOCUMENTATION\.md/);
188
237
  assert.match(fs.readFileSync(path.join(monorepoRoot, 'diagnostics.md'), 'utf8'), /## Source: diagnostics\.md/);
189
238
  assert.match(fs.readFileSync(path.join(monorepoRoot, 'optimizations.md'), 'utf8'), /## Source: optimizations\.md/);
239
+ assert.match(fs.readFileSync(path.join(monorepoRoot, 'tests', 'AGENTS.md'), 'utf8'), /## Source: tests\/AGENTS\.md/);
190
240
  assert.match(fs.readFileSync(path.join(monorepoRoot, 'tests', 'e2e', 'AGENTS.md'), 'utf8'), /## Source: tests\/e2e\/AGENTS\.md/);
191
241
  assert.match(fs.readFileSync(path.join(monorepoRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'), 'utf8'), /## Source: tests\/e2e\/REAL_WORLD_JOURNEY_TESTS\.md/);
192
242
  assert.doesNotMatch(fs.readFileSync(path.join(monorepoRoot, 'tests', 'e2e', 'REAL_WORLD_JOURNEY_TESTS.md'), 'utf8'), /## Source: CODING_STYLE\.md/);
243
+ assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'AGENTS.md')), false);
193
244
  assert.equal(fs.existsSync(path.join(appRoot, 'tests', 'e2e', 'AGENTS.md')), false);
194
- assert.match(fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8'), /## Agent Routing Contract/);
245
+ const appAgentsContent = fs.readFileSync(path.join(appRoot, 'AGENTS.md'), 'utf8');
246
+ assert.match(appAgentsContent, /## Agent Routing Contract/);
247
+ assert.doesNotMatch(appAgentsContent, /## Known Proteum Apps/);
248
+ assert.doesNotMatch(appAgentsContent, /Do not start `npx proteum dev` from this root/);
195
249
  assert.match(fs.readFileSync(path.join(appRoot, 'client', 'AGENTS.md'), 'utf8'), /## Source: client\/AGENTS\.md/);
196
250
  assert.equal(fs.existsSync(path.join(appRoot, 'CODING_STYLE.md')), false);
251
+ assert.equal(fs.existsSync(path.join(appRoot, 'DOCUMENTATION.md')), false);
197
252
  assert.equal(fs.existsSync(path.join(appRoot, 'diagnostics.md')), false);
198
253
  assert.equal(fs.existsSync(path.join(appRoot, 'optimizations.md')), false);
199
254
  assert.equal(result.removed.some((entry) => entry.endsWith('/apps/product/CODING_STYLE.md')), true);
@@ -0,0 +1,262 @@
1
+ const assert = require('node:assert/strict');
2
+ const { spawn, spawnSync } = require('node:child_process');
3
+ const fs = require('node:fs');
4
+ const http = require('node:http');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+
8
+ const coreRoot = path.resolve(__dirname, '..');
9
+ const cliBin = path.join(coreRoot, 'cli', 'bin.js');
10
+
11
+ const writeFile = (filepath, content) => {
12
+ fs.mkdirSync(path.dirname(filepath), { recursive: true });
13
+ fs.writeFileSync(filepath, content);
14
+ };
15
+
16
+ const createProteumApp = (appRoot, { routerPort = 3020 } = {}) => {
17
+ writeFile(path.join(appRoot, 'package.json'), '{"name":"fixture"}\n');
18
+ writeFile(path.join(appRoot, 'identity.config.ts'), 'export default {};\n');
19
+ writeFile(path.join(appRoot, 'proteum.config.ts'), 'export default {};\n');
20
+ fs.mkdirSync(path.join(appRoot, 'client'), { recursive: true });
21
+ fs.mkdirSync(path.join(appRoot, 'server'), { recursive: true });
22
+ writeFile(
23
+ path.join(appRoot, '.proteum', 'manifest.json'),
24
+ JSON.stringify({
25
+ version: 10,
26
+ app: {
27
+ root: appRoot,
28
+ coreRoot,
29
+ identityFilepath: path.join(appRoot, 'identity.config.ts'),
30
+ setupFilepath: path.join(appRoot, 'proteum.config.ts'),
31
+ identity: { name: 'Product', identifier: 'ProductApp', description: '' },
32
+ setup: {},
33
+ },
34
+ conventions: { routeOptionKeys: [], reservedRouteOptionKeys: [] },
35
+ env: {
36
+ source: 'test',
37
+ loadedVariableKeys: [],
38
+ requiredVariables: [],
39
+ resolved: {
40
+ name: 'test',
41
+ profile: 'dev',
42
+ routerPort,
43
+ routerCurrentDomain: 'localhost',
44
+ routerInternalUrl: `http://localhost:${routerPort}`,
45
+ },
46
+ },
47
+ connectedProjects: [],
48
+ services: { app: [], routerPlugins: [] },
49
+ controllers: [],
50
+ commands: [],
51
+ routes: { client: [], server: [] },
52
+ layouts: [],
53
+ diagnostics: [],
54
+ }),
55
+ );
56
+ };
57
+
58
+ const listen = async (server, port = 0) =>
59
+ await new Promise((resolve, reject) => {
60
+ server.once('error', reject);
61
+ server.listen(port, '127.0.0.1', () => resolve(server.address().port));
62
+ });
63
+
64
+ const closeServer = async (server) =>
65
+ await new Promise((resolve, reject) => {
66
+ server.close((error) => (error ? reject(error) : resolve()));
67
+ });
68
+
69
+ const runCli = async (args, { cwd }) =>
70
+ await new Promise((resolve, reject) => {
71
+ const child = spawn(process.execPath, [cliBin, ...args], {
72
+ cwd,
73
+ stdio: ['ignore', 'pipe', 'pipe'],
74
+ });
75
+ let stdout = '';
76
+ let stderr = '';
77
+
78
+ child.stdout.on('data', (chunk) => {
79
+ stdout += chunk.toString();
80
+ });
81
+ child.stderr.on('data', (chunk) => {
82
+ stderr += chunk.toString();
83
+ });
84
+ child.once('error', reject);
85
+ child.once('close', (status) => resolve({ status, stdout, stderr }));
86
+ });
87
+
88
+ test('top-level help lists the machine-scope mcp router', () => {
89
+ const result = spawnSync(process.execPath, [cliBin, '--help'], {
90
+ cwd: coreRoot,
91
+ encoding: 'utf8',
92
+ });
93
+
94
+ assert.equal(result.status, 0);
95
+ assert.match(result.stdout, /proteum mcp\b/);
96
+ assert.match(result.stdout, /machine-scope MCP router/);
97
+ });
98
+
99
+ test('mcp help describes projectId routing', () => {
100
+ const result = spawnSync(process.execPath, [cliBin, 'mcp', '--help'], {
101
+ cwd: coreRoot,
102
+ encoding: 'utf8',
103
+ });
104
+ const output = `${result.stdout}\n${result.stderr}`;
105
+
106
+ assert.equal(result.status, 0);
107
+ assert.match(output, /machine-scope MCP router/);
108
+ assert.match(output, /projectId/);
109
+ assert.match(output, /--daemon/);
110
+ assert.match(output, /--stdio/);
111
+ });
112
+
113
+ test('explain help describes compact section summaries', () => {
114
+ const result = spawnSync(process.execPath, [cliBin, 'explain', '--help'], {
115
+ cwd: coreRoot,
116
+ encoding: 'utf8',
117
+ });
118
+ const output = `${result.stdout}\n${result.stderr}`;
119
+
120
+ assert.equal(result.status, 0);
121
+ assert.match(output, /Summarize generated routes, controllers, and commands together/);
122
+ assert.match(output, /--routes --controllers --commands --full/);
123
+ assert.match(output, /Explicit section flags summarize those sections by default/);
124
+ });
125
+
126
+ test('runtime status from a monorepo wrapper returns app candidates instead of treating wrapper as app', () => {
127
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-cli-wrapper-'));
128
+ createProteumApp(path.join(repoRoot, 'apps', 'product'));
129
+
130
+ const result = spawnSync(process.execPath, [cliBin, 'runtime', 'status'], {
131
+ cwd: repoRoot,
132
+ encoding: 'utf8',
133
+ });
134
+ const payload = JSON.parse(result.stdout);
135
+
136
+ assert.equal(result.status, 1);
137
+ assert.equal(payload.ok, false);
138
+ assert.equal(payload.data.appCandidates.length, 1);
139
+ assert.match(payload.nextActions[0].command, /cd "apps\/product"/);
140
+ assert.match(payload.nextActions[0].command, /npx proteum runtime status/);
141
+ });
142
+
143
+ test('dev from a monorepo wrapper returns exact app-root start command', () => {
144
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-cli-dev-wrapper-'));
145
+ createProteumApp(path.join(repoRoot, 'apps', 'product'));
146
+
147
+ const result = spawnSync(process.execPath, [cliBin, 'dev', 'list'], {
148
+ cwd: repoRoot,
149
+ encoding: 'utf8',
150
+ });
151
+ const payload = JSON.parse(result.stdout);
152
+
153
+ assert.equal(result.status, 1);
154
+ assert.equal(payload.ok, false);
155
+ assert.match(payload.nextActions[0].command, /cd "apps\/product"/);
156
+ assert.match(payload.nextActions[0].command, /npx proteum dev --session-file/);
157
+ });
158
+
159
+ test('runtime status manifest guard points to explain manifest', () => {
160
+ const result = spawnSync(process.execPath, [cliBin, 'runtime', 'status', '--manifest'], {
161
+ cwd: coreRoot,
162
+ encoding: 'utf8',
163
+ });
164
+ const payload = JSON.parse(result.stdout);
165
+
166
+ assert.equal(result.status, 1);
167
+ assert.equal(payload.ok, false);
168
+ assert.match(payload.summary, /not supported/);
169
+ assert.match(payload.nextActions[0].command, /proteum explain --manifest/);
170
+ });
171
+
172
+ test('runtime status reports occupied configured port without probing page bodies', async () => {
173
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-cli-port-'));
174
+ const otherRoot = path.join(repoRoot, 'apps', 'other');
175
+ const appRoot = path.join(repoRoot, 'apps', 'product');
176
+ const server = http.createServer((req, res) => {
177
+ if (req.url && req.url.startsWith('/__proteum/explain')) {
178
+ res.setHeader('content-type', 'application/json');
179
+ res.end(
180
+ JSON.stringify({
181
+ app: {
182
+ root: otherRoot,
183
+ identity: { identifier: 'OtherApp', name: 'Other' },
184
+ },
185
+ }),
186
+ );
187
+ return;
188
+ }
189
+
190
+ res.setHeader('content-type', 'text/html');
191
+ res.end('<html><body>large wrong app page that should never be included</body></html>');
192
+ });
193
+ const occupiedPort = await listen(server);
194
+
195
+ try {
196
+ createProteumApp(appRoot, { routerPort: occupiedPort });
197
+
198
+ const result = await runCli(['runtime', 'status'], {
199
+ cwd: appRoot,
200
+ });
201
+ const payload = JSON.parse(result.stdout);
202
+
203
+ assert.equal(result.status, 0);
204
+ assert.equal(payload.ok, true);
205
+ assert.equal(payload.data.configuredDevPort.router.port, occupiedPort);
206
+ assert.equal(payload.data.configuredDevPort.router.available, false);
207
+ assert.equal(payload.data.configuredDevPort.router.proteum, true);
208
+ assert.equal(payload.data.configuredDevPort.router.matchesApp, false);
209
+ assert.equal(payload.data.configuredDevPort.router.app.identifier, 'OtherApp');
210
+ assert.notEqual(payload.data.configuredDevPort.recommendedPort, occupiedPort);
211
+ assert.match(payload.summary, /occupied by OtherApp/);
212
+ assert.match(payload.nextActions[0].command, /(npx )?proteum dev --session-file/);
213
+ assert.doesNotMatch(payload.nextActions[0].command, new RegExp(`--port ${occupiedPort}(\\D|$)`));
214
+ assert.match(payload.nextActions[0].reason, /do not probe page bodies/);
215
+ assert.doesNotMatch(result.stdout, /large wrong app page/);
216
+ assert.doesNotMatch(result.stdout, /<html>/);
217
+ } finally {
218
+ await closeServer(server);
219
+ }
220
+ });
221
+
222
+ test('runtime status avoids starting a second dev server when the same app owns the port', async () => {
223
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-cli-same-port-'));
224
+ const appRoot = path.join(repoRoot, 'apps', 'product');
225
+ const server = http.createServer((req, res) => {
226
+ if (req.url && req.url.startsWith('/__proteum/explain')) {
227
+ res.setHeader('content-type', 'application/json');
228
+ res.end(
229
+ JSON.stringify({
230
+ app: {
231
+ root: appRoot,
232
+ identity: { identifier: 'ProductApp', name: 'Product' },
233
+ },
234
+ }),
235
+ );
236
+ return;
237
+ }
238
+
239
+ res.setHeader('content-type', 'text/html');
240
+ res.end('<html><body>same app page body that should not be included</body></html>');
241
+ });
242
+ const occupiedPort = await listen(server);
243
+
244
+ try {
245
+ createProteumApp(appRoot, { routerPort: occupiedPort });
246
+
247
+ const result = await runCli(['runtime', 'status'], {
248
+ cwd: appRoot,
249
+ });
250
+ const payload = JSON.parse(result.stdout);
251
+
252
+ assert.equal(result.status, 0);
253
+ assert.equal(payload.data.configuredDevPort.router.matchesApp, true);
254
+ assert.equal(payload.nextActions[0].label, 'Use Existing Runtime');
255
+ assert.match(payload.nextActions[0].command, new RegExp(`proteum diagnose "/" --port ${occupiedPort}`));
256
+ assert.match(payload.nextActions[0].reason, /Do not start a second dev server/);
257
+ assert.equal(payload.nextActions.some((action) => action.label === 'Start Dev'), false);
258
+ assert.doesNotMatch(result.stdout, /same app page body/);
259
+ } finally {
260
+ await closeServer(server);
261
+ }
262
+ });