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.
- package/AGENTS.md +8 -3
- package/README.md +20 -15
- package/agents/project/AGENTS.md +15 -10
- package/agents/project/DOCUMENTATION.md +1326 -0
- package/agents/project/app-root/AGENTS.md +2 -2
- package/agents/project/diagnostics.md +9 -8
- package/agents/project/root/AGENTS.md +14 -8
- package/agents/project/tests/AGENTS.md +1 -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 +1010 -0
- package/cli/presentation/commands.ts +34 -24
- package/cli/presentation/help.ts +1 -1
- package/cli/runtime/commands.ts +129 -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 +93 -17
- package/cli/utils/appRoots.ts +232 -0
- package/common/dev/diagnostics.ts +1 -1
- package/common/dev/inspection.ts +8 -1
- package/common/dev/mcpPayloads.ts +431 -17
- package/common/dev/mcpServer.ts +33 -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 +109 -52
- 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/devMcp.ts +45 -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 +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 +0 -1
- package/tests/eslint-rules.test.cjs +0 -1
- package/tests/inspection.test.cjs +0 -1
- package/tests/mcp.test.cjs +748 -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/server/app/devMcp.ts
CHANGED
|
@@ -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
|
|
|
@@ -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
|
|
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
|
-
|
|
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
|
+
});
|