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.
Files changed (59) hide show
  1. package/AGENTS.md +10 -4
  2. package/README.md +58 -15
  3. package/agents/project/AGENTS.md +53 -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 +12 -7
  7. package/agents/project/optimizations.md +1 -0
  8. package/agents/project/root/AGENTS.md +24 -9
  9. package/agents/project/tests/AGENTS.md +7 -0
  10. package/agents/project/tests/e2e/AGENTS.md +13 -0
  11. package/agents/project/tests/e2e/REAL_WORLD_JOURNEY_TESTS.md +192 -0
  12. package/cli/commands/connect.ts +40 -4
  13. package/cli/commands/dev.ts +148 -25
  14. package/cli/commands/diagnose.ts +138 -5
  15. package/cli/commands/doctor.ts +24 -4
  16. package/cli/commands/explain.ts +134 -6
  17. package/cli/commands/mcp.ts +133 -0
  18. package/cli/commands/orient.ts +93 -3
  19. package/cli/commands/perf.ts +118 -13
  20. package/cli/commands/runtime.ts +234 -0
  21. package/cli/commands/trace.ts +116 -21
  22. package/cli/mcp/router.ts +1010 -0
  23. package/cli/presentation/commands.ts +93 -26
  24. package/cli/presentation/devSession.ts +2 -0
  25. package/cli/presentation/help.ts +1 -1
  26. package/cli/runtime/commands.ts +215 -24
  27. package/cli/runtime/devSessions.ts +328 -2
  28. package/cli/runtime/mcpDaemon.ts +288 -0
  29. package/cli/runtime/ports.ts +151 -0
  30. package/cli/utils/agentOutput.ts +46 -0
  31. package/cli/utils/agents.ts +194 -51
  32. package/cli/utils/appRoots.ts +232 -0
  33. package/common/dev/diagnostics.ts +1 -1
  34. package/common/dev/inspection.ts +22 -7
  35. package/common/dev/mcpPayloads.ts +1150 -0
  36. package/common/dev/mcpServer.ts +287 -0
  37. package/docs/agent-routing.md +137 -0
  38. package/docs/dev-commands.md +2 -0
  39. package/docs/dev-sessions.md +4 -1
  40. package/docs/diagnostics.md +70 -24
  41. package/docs/mcp.md +206 -0
  42. package/docs/migrate-from-2.1.3.md +14 -6
  43. package/docs/request-tracing.md +12 -6
  44. package/package.json +11 -3
  45. package/server/app/devMcp.ts +204 -0
  46. package/server/services/router/http/cache.ts +116 -0
  47. package/server/services/router/http/index.ts +94 -35
  48. package/server/services/router/index.ts +8 -11
  49. package/server/services/router/request/ip.test.cjs +0 -1
  50. package/tests/agents-utils.test.cjs +92 -14
  51. package/tests/cli-mcp-command.test.cjs +262 -0
  52. package/tests/codex-mcp-usage.test.cjs +307 -0
  53. package/tests/dev-sessions.test.cjs +113 -0
  54. package/tests/dev-transpile-watch.test.cjs +117 -9
  55. package/tests/eslint-rules.test.cjs +0 -1
  56. package/tests/inspection.test.cjs +66 -0
  57. package/tests/mcp.test.cjs +873 -0
  58. package/tests/router-cache-config.test.cjs +73 -0
  59. 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
- test('proteum dev invalidates client assets and reloads for transpiled package scripts and styles', { timeout: 180000 }, async () => {
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, () => output);
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
+ });
@@ -1,5 +1,4 @@
1
1
  const assert = require('node:assert/strict');
2
- const test = require('node:test');
3
2
  const { Linter } = require('eslint');
4
3
 
5
4
  const { createProteumEslintConfig } = require('../eslint.js');
@@ -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
+ });