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
|
@@ -5,7 +5,6 @@ const net = require('node:net');
|
|
|
5
5
|
const os = require('node:os');
|
|
6
6
|
const path = require('node:path');
|
|
7
7
|
const { spawn } = require('node:child_process');
|
|
8
|
-
const test = require('node:test');
|
|
9
8
|
|
|
10
9
|
const coreRoot = path.resolve(__dirname, '..');
|
|
11
10
|
const cliBin = path.join(coreRoot, 'cli', 'bin.js');
|
|
@@ -65,6 +64,33 @@ const findAssetContaining = (appRoot, extension, marker) => {
|
|
|
65
64
|
return candidates.find((filepath) => fs.readFileSync(filepath, 'utf8').includes(marker));
|
|
66
65
|
};
|
|
67
66
|
|
|
67
|
+
const toPublicAssetUrl = (appRoot, filepath) => {
|
|
68
|
+
const publicRoot = path.join(appRoot, 'dev', 'public');
|
|
69
|
+
const relativePath = path.relative(publicRoot, filepath).split(path.sep).join('/');
|
|
70
|
+
|
|
71
|
+
return `/public/${relativePath}`;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const request = async (port, urlPath, headers = {}) => {
|
|
75
|
+
const response = await fetch(`http://127.0.0.1:${port}${urlPath}`, { headers });
|
|
76
|
+
const body = await response.text();
|
|
77
|
+
|
|
78
|
+
return { response, body };
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const waitForHeader = async (port, urlPath, headerName, expectedValue, timeoutMs = 60000) => {
|
|
82
|
+
const deadline = Date.now() + timeoutMs;
|
|
83
|
+
|
|
84
|
+
while (Date.now() < deadline) {
|
|
85
|
+
const { response } = await request(port, urlPath, { Accept: 'text/html' });
|
|
86
|
+
if (response.headers.get(headerName) === expectedValue) return response;
|
|
87
|
+
|
|
88
|
+
await sleep(250);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
throw new Error(`Timed out waiting for ${urlPath} header ${headerName}=${expectedValue}.`);
|
|
92
|
+
};
|
|
93
|
+
|
|
68
94
|
const waitForAssetContaining = async (appRoot, extension, marker, timeoutMs = 60000) => {
|
|
69
95
|
const deadline = Date.now() + timeoutMs;
|
|
70
96
|
|
|
@@ -172,9 +198,10 @@ const createSharedStyleSource = (marker) => `.shared-style-marker {
|
|
|
172
198
|
}
|
|
173
199
|
`;
|
|
174
200
|
|
|
175
|
-
const createFixture = (root, port) => {
|
|
201
|
+
const createFixture = (root, port, options = {}) => {
|
|
176
202
|
const appRoot = path.join(root, 'app');
|
|
177
203
|
const sharedRoot = path.join(root, 'shared');
|
|
204
|
+
const cacheConfigSource = options.routerCache ? ` cache: ${options.routerCache},\n` : '';
|
|
178
205
|
|
|
179
206
|
fs.mkdirSync(path.join(appRoot, 'public'), { recursive: true });
|
|
180
207
|
fs.mkdirSync(path.join(appRoot, 'client', 'assets', 'identity'), { recursive: true });
|
|
@@ -332,6 +359,7 @@ export const routerBaseConfig = {
|
|
|
332
359
|
upload: {
|
|
333
360
|
maxSize: '10mb',
|
|
334
361
|
},
|
|
362
|
+
${cacheConfigSource}
|
|
335
363
|
csp: {
|
|
336
364
|
scripts: [],
|
|
337
365
|
},
|
|
@@ -403,6 +431,31 @@ Router.page(
|
|
|
403
431
|
);
|
|
404
432
|
`,
|
|
405
433
|
);
|
|
434
|
+
if (options.staticPage) {
|
|
435
|
+
writeFile(
|
|
436
|
+
path.join(appRoot, 'client', 'pages', 'static-cache.tsx'),
|
|
437
|
+
`import Router from '@/client/router';
|
|
438
|
+
import { SharedMarker } from '@test/shared';
|
|
439
|
+
|
|
440
|
+
Router.page(
|
|
441
|
+
'/static-cache',
|
|
442
|
+
{
|
|
443
|
+
auth: false,
|
|
444
|
+
layout: false,
|
|
445
|
+
static: { urls: ['/static-cache'] },
|
|
446
|
+
},
|
|
447
|
+
null,
|
|
448
|
+
() => {
|
|
449
|
+
return (
|
|
450
|
+
<main>
|
|
451
|
+
<SharedMarker />
|
|
452
|
+
</main>
|
|
453
|
+
);
|
|
454
|
+
},
|
|
455
|
+
);
|
|
456
|
+
`,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
406
459
|
|
|
407
460
|
writeFile(
|
|
408
461
|
path.join(sharedRoot, 'package.json'),
|
|
@@ -448,13 +501,8 @@ const stopDevServer = async (child) => {
|
|
|
448
501
|
});
|
|
449
502
|
};
|
|
450
503
|
|
|
451
|
-
|
|
452
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-transpile-watch-'));
|
|
453
|
-
const port = await resolvePortPair();
|
|
454
|
-
const { appRoot, sharedRoot } = createFixture(root, port);
|
|
455
|
-
const sessionFile = path.join(appRoot, 'var', 'run', 'proteum', 'dev', 'transpile-watch-test.json');
|
|
504
|
+
const startDevServer = (appRoot, port, sessionFile) => {
|
|
456
505
|
let output = '';
|
|
457
|
-
|
|
458
506
|
const child = spawn(
|
|
459
507
|
process.execPath,
|
|
460
508
|
[cliBin, 'dev', '--cwd', appRoot, '--port', String(port), '--session-file', sessionFile, '--no-cache', '--verbose'],
|
|
@@ -476,8 +524,21 @@ test('proteum dev invalidates client assets and reloads for transpiled package s
|
|
|
476
524
|
output += chunk.toString();
|
|
477
525
|
});
|
|
478
526
|
|
|
527
|
+
return {
|
|
528
|
+
child,
|
|
529
|
+
getOutput: () => output,
|
|
530
|
+
};
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
test('proteum dev invalidates client assets and reloads for transpiled package scripts and styles', { timeout: 180000 }, async () => {
|
|
534
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-transpile-watch-'));
|
|
535
|
+
const port = await resolvePortPair();
|
|
536
|
+
const { appRoot, sharedRoot } = createFixture(root, port);
|
|
537
|
+
const sessionFile = path.join(appRoot, 'var', 'run', 'proteum', 'dev', 'transpile-watch-test.json');
|
|
538
|
+
const { child, getOutput } = startDevServer(appRoot, port, sessionFile);
|
|
539
|
+
|
|
479
540
|
try {
|
|
480
|
-
await waitForSessionReady(sessionFile, child,
|
|
541
|
+
await waitForSessionReady(sessionFile, child, getOutput);
|
|
481
542
|
|
|
482
543
|
const initialScriptAsset = await waitForAssetContaining(appRoot, '.js', 'SCRIPT_MARKER_INITIAL');
|
|
483
544
|
const initialScriptContent = fs.readFileSync(initialScriptAsset, 'utf8');
|
|
@@ -511,3 +572,50 @@ test('proteum dev invalidates client assets and reloads for transpiled package s
|
|
|
511
572
|
fs.rmSync(root, { recursive: true, force: true });
|
|
512
573
|
}
|
|
513
574
|
});
|
|
575
|
+
|
|
576
|
+
test('proteum dev applies router HTTP cache config to HTML and public assets', { timeout: 180000 }, async () => {
|
|
577
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-router-cache-'));
|
|
578
|
+
const port = await resolvePortPair();
|
|
579
|
+
const dynamicHtmlCacheControl = 'private, max-age=7';
|
|
580
|
+
const staticHtmlCacheControl = 'private, max-age=13';
|
|
581
|
+
const publicAssetCacheControl = 'private, max-age=17';
|
|
582
|
+
const { appRoot } = createFixture(root, port, {
|
|
583
|
+
staticPage: true,
|
|
584
|
+
routerCache: `{
|
|
585
|
+
html: {
|
|
586
|
+
dynamic: { cacheControl: '${dynamicHtmlCacheControl}', surrogateControl: 'dynamic-surrogate' },
|
|
587
|
+
static: { cacheControl: '${staticHtmlCacheControl}', surrogateControl: 'static-surrogate' },
|
|
588
|
+
},
|
|
589
|
+
publicAssets: {
|
|
590
|
+
dev: '${publicAssetCacheControl}',
|
|
591
|
+
versioned: '${publicAssetCacheControl}',
|
|
592
|
+
unversioned: '${publicAssetCacheControl}',
|
|
593
|
+
etag: false,
|
|
594
|
+
lastModified: false,
|
|
595
|
+
},
|
|
596
|
+
}`,
|
|
597
|
+
});
|
|
598
|
+
const sessionFile = path.join(appRoot, 'var', 'run', 'proteum', 'dev', 'router-cache-test.json');
|
|
599
|
+
const { child, getOutput } = startDevServer(appRoot, port, sessionFile);
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
await waitForSessionReady(sessionFile, child, getOutput);
|
|
603
|
+
|
|
604
|
+
const { response: dynamicResponse } = await request(port, '/', { Accept: 'text/html' });
|
|
605
|
+
assert.equal(dynamicResponse.headers.get('cache-control'), dynamicHtmlCacheControl);
|
|
606
|
+
assert.equal(dynamicResponse.headers.get('surrogate-control'), 'dynamic-surrogate');
|
|
607
|
+
|
|
608
|
+
const staticResponse = await waitForHeader(port, '/static-cache', 'cache-control', staticHtmlCacheControl);
|
|
609
|
+
assert.equal(staticResponse.headers.get('surrogate-control'), 'static-surrogate');
|
|
610
|
+
|
|
611
|
+
const asset = await waitForAssetContaining(appRoot, '.js', 'SCRIPT_MARKER_INITIAL');
|
|
612
|
+
const { response: assetResponse } = await request(port, toPublicAssetUrl(appRoot, asset));
|
|
613
|
+
|
|
614
|
+
assert.equal(assetResponse.headers.get('cache-control'), publicAssetCacheControl);
|
|
615
|
+
assert.equal(assetResponse.headers.get('etag'), null);
|
|
616
|
+
assert.equal(assetResponse.headers.get('last-modified'), null);
|
|
617
|
+
} finally {
|
|
618
|
+
await stopDevServer(child);
|
|
619
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
620
|
+
}
|
|
621
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const coreRoot = path.resolve(__dirname, '..');
|
|
5
|
+
process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
|
|
6
|
+
process.env.TS_NODE_TRANSPILE_ONLY = '1';
|
|
7
|
+
require('ts-node/register/transpile-only');
|
|
8
|
+
|
|
9
|
+
const { explainOwner } = require('../common/dev/inspection.ts');
|
|
10
|
+
|
|
11
|
+
const createRoute = (routePath, filepath) => ({
|
|
12
|
+
chunkFilepath: filepath,
|
|
13
|
+
chunkId: filepath.replace(/[^A-Za-z0-9]+/g, '_'),
|
|
14
|
+
codeRaw: routePath,
|
|
15
|
+
filepath,
|
|
16
|
+
hasData: false,
|
|
17
|
+
kind: 'page',
|
|
18
|
+
methodName: 'page',
|
|
19
|
+
normalizedOptionKeys: [],
|
|
20
|
+
path: routePath,
|
|
21
|
+
pathRaw: routePath,
|
|
22
|
+
scope: 'app',
|
|
23
|
+
sourceLocation: { line: 1, column: 1 },
|
|
24
|
+
targetResolution: 'literal',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const createManifest = (routes) => ({
|
|
28
|
+
app: {
|
|
29
|
+
coreRoot,
|
|
30
|
+
root: '/tmp/proteum-app',
|
|
31
|
+
},
|
|
32
|
+
commands: [],
|
|
33
|
+
connectedProjects: [],
|
|
34
|
+
controllers: [],
|
|
35
|
+
diagnostics: [],
|
|
36
|
+
layouts: [],
|
|
37
|
+
routes: {
|
|
38
|
+
client: routes,
|
|
39
|
+
server: [],
|
|
40
|
+
},
|
|
41
|
+
services: {
|
|
42
|
+
app: [],
|
|
43
|
+
routerPlugins: [],
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('root owner lookup does not match every dynamic route containing a slash', () => {
|
|
48
|
+
const manifest = createManifest([
|
|
49
|
+
createRoute('/domains/:slug((?!tlds$|tld$|sector$)[^/]+)', '/tmp/proteum-app/client/pages/domains/slug.tsx'),
|
|
50
|
+
createRoute('/admin/data/:tab([^/]+)', '/tmp/proteum-app/client/pages/admin/data.tsx'),
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
assert.deepEqual(explainOwner(manifest, '/').matches, []);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('root owner lookup returns only the literal root route when present', () => {
|
|
57
|
+
const manifest = createManifest([
|
|
58
|
+
createRoute('/domains/:slug((?!tlds$|tld$|sector$)[^/]+)', '/tmp/proteum-app/client/pages/domains/slug.tsx'),
|
|
59
|
+
createRoute('/', '/tmp/proteum-app/client/pages/home.tsx'),
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const matches = explainOwner(manifest, '/').matches;
|
|
63
|
+
|
|
64
|
+
assert.equal(matches.length, 1);
|
|
65
|
+
assert.equal(matches[0].label, '/');
|
|
66
|
+
});
|