proteum 2.4.4 → 2.5.0
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/README.md +60 -55
- package/agents/project/AGENTS.md +112 -31
- package/agents/project/CODING_STYLE.md +2 -2
- package/agents/project/app-root/AGENTS.md +1 -3
- package/agents/project/client/AGENTS.md +1 -1
- package/agents/project/client/pages/AGENTS.md +21 -9
- package/agents/project/diagnostics.md +2 -2
- package/agents/project/optimizations.md +1 -1
- package/agents/project/root/AGENTS.md +105 -22
- package/agents/project/server/routes/AGENTS.md +30 -1
- package/agents/project/tests/AGENTS.md +1 -1
- package/cli/commands/doctor.ts +54 -3
- package/cli/commands/runtime.ts +6 -0
- package/cli/commands/worktree.ts +116 -0
- package/cli/compiler/artifacts/controllers.ts +16 -15
- package/cli/compiler/artifacts/discovery.ts +129 -17
- package/cli/compiler/artifacts/routing.ts +0 -5
- package/cli/compiler/artifacts/services.ts +253 -76
- package/cli/compiler/common/controllers.ts +159 -57
- package/cli/compiler/common/generatedRouteModules.ts +457 -363
- package/cli/mcp/router.ts +47 -3
- package/cli/presentation/commands.ts +25 -15
- package/cli/runtime/commands.ts +39 -12
- package/cli/runtime/worktreeBootstrap.ts +608 -0
- package/cli/scaffold/index.ts +28 -18
- package/cli/scaffold/templates.ts +44 -33
- package/cli/utils/agents.ts +14 -1
- package/client/services/router/index.tsx +23 -3
- package/client/services/router/request/api.ts +14 -4
- package/common/dev/contractsDoctor.ts +1 -1
- package/common/dev/mcpPayloads.ts +8 -1
- package/common/env/proteumEnv.ts +14 -2
- package/common/router/contracts.ts +1 -1
- package/common/router/definitions.ts +177 -0
- package/common/router/index.ts +23 -12
- package/common/router/pageData.ts +5 -5
- package/common/router/register.ts +2 -2
- package/common/router/request/api.ts +12 -2
- package/docs/agent-routing.md +5 -2
- package/docs/diagnostics.md +2 -0
- package/docs/mcp.md +6 -3
- package/eslint.js +36 -1
- package/package.json +1 -1
- package/server/app/commands.ts +5 -1
- package/server/app/container/console/index.ts +1 -1
- package/server/app/controller/index.ts +98 -40
- package/server/app/index.ts +92 -1
- package/server/app/service/index.ts +5 -1
- package/server/index.ts +6 -2
- package/server/services/router/index.ts +47 -38
- package/server/services/router/response/index.ts +2 -2
- package/tests/agents-utils.test.cjs +14 -1
- package/tests/cli-mcp-command.test.cjs +84 -0
- package/tests/definition-contracts.test.cjs +453 -0
- package/tests/dev-transpile-watch.test.cjs +37 -28
- package/tests/eslint-rules.test.cjs +39 -1
- package/tests/mcp.test.cjs +90 -0
- package/tests/worktree-bootstrap.test.cjs +206 -0
- package/types/aliases.d.ts +0 -5
- package/types/controller-input.test.ts +23 -17
- package/types/controller-request-context.test.ts +10 -11
- package/cli/commands/migrate.ts +0 -51
- package/cli/migrate/pageContract.ts +0 -516
- package/docs/migrate-from-2.1.3.md +0 -396
- package/scripts/cleanup-generated-controllers.ts +0 -62
- package/scripts/fix-reference-app-typing.ts +0 -490
- package/scripts/format-router-registrations.ts +0 -119
- package/scripts/migrate-explicit-controllers-and-request.ts +0 -423
- package/scripts/refactor-client-app-imports.ts +0 -244
- package/scripts/refactor-client-pages.ts +0 -587
- package/scripts/refactor-server-controllers.ts +0 -471
- package/scripts/refactor-server-runtime-aliases.ts +0 -360
- package/scripts/restore-client-app-import-files.ts +0 -41
|
@@ -370,24 +370,26 @@ ${cacheConfigSource}
|
|
|
370
370
|
);
|
|
371
371
|
writeFile(
|
|
372
372
|
path.join(appRoot, 'server', 'index.ts'),
|
|
373
|
-
`import {
|
|
373
|
+
`import { defineApplication } from '@server/app';
|
|
374
374
|
import Router from '@server/services/router';
|
|
375
375
|
import SchemaRouter from '@server/services/schema/router';
|
|
376
376
|
|
|
377
377
|
import * as appConfig from '@/server/config/app';
|
|
378
378
|
|
|
379
|
-
export default
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
379
|
+
export default defineApplication({
|
|
380
|
+
services: (app) => ({
|
|
381
|
+
Router: new Router(
|
|
382
|
+
app,
|
|
383
|
+
{
|
|
384
|
+
...appConfig.routerBaseConfig,
|
|
385
|
+
plugins: {
|
|
386
|
+
schema: new SchemaRouter({}, app),
|
|
387
|
+
},
|
|
386
388
|
},
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
)
|
|
390
|
-
}
|
|
389
|
+
app,
|
|
390
|
+
),
|
|
391
|
+
}),
|
|
392
|
+
});
|
|
391
393
|
`,
|
|
392
394
|
);
|
|
393
395
|
writeFile(
|
|
@@ -411,48 +413,48 @@ export default class TranspileWatchClient extends ClientApplication {
|
|
|
411
413
|
);
|
|
412
414
|
writeFile(
|
|
413
415
|
path.join(appRoot, 'client', 'pages', 'index.tsx'),
|
|
414
|
-
`import
|
|
416
|
+
`import { definePageRoute } from '@common/router/definitions';
|
|
415
417
|
import { SharedMarker } from '@test/shared';
|
|
416
418
|
|
|
417
|
-
|
|
418
|
-
'/',
|
|
419
|
-
{
|
|
419
|
+
export default definePageRoute({
|
|
420
|
+
path: '/',
|
|
421
|
+
options: {
|
|
420
422
|
auth: false,
|
|
421
423
|
layout: false,
|
|
422
424
|
},
|
|
423
|
-
null,
|
|
424
|
-
() => {
|
|
425
|
+
data: null,
|
|
426
|
+
render: () => {
|
|
425
427
|
return (
|
|
426
428
|
<main>
|
|
427
429
|
<SharedMarker />
|
|
428
430
|
</main>
|
|
429
431
|
);
|
|
430
432
|
},
|
|
431
|
-
);
|
|
433
|
+
});
|
|
432
434
|
`,
|
|
433
435
|
);
|
|
434
436
|
if (options.staticPage) {
|
|
435
437
|
writeFile(
|
|
436
438
|
path.join(appRoot, 'client', 'pages', 'static-cache.tsx'),
|
|
437
|
-
`import
|
|
439
|
+
`import { definePageRoute } from '@common/router/definitions';
|
|
438
440
|
import { SharedMarker } from '@test/shared';
|
|
439
441
|
|
|
440
|
-
|
|
441
|
-
'/static-cache',
|
|
442
|
-
{
|
|
442
|
+
export default definePageRoute({
|
|
443
|
+
path: '/static-cache',
|
|
444
|
+
options: {
|
|
443
445
|
auth: false,
|
|
444
446
|
layout: false,
|
|
445
447
|
static: { urls: ['/static-cache'] },
|
|
446
448
|
},
|
|
447
|
-
null,
|
|
448
|
-
() => {
|
|
449
|
+
data: null,
|
|
450
|
+
render: () => {
|
|
449
451
|
return (
|
|
450
452
|
<main>
|
|
451
453
|
<SharedMarker />
|
|
452
454
|
</main>
|
|
453
455
|
);
|
|
454
456
|
},
|
|
455
|
-
);
|
|
457
|
+
});
|
|
456
458
|
`,
|
|
457
459
|
);
|
|
458
460
|
}
|
|
@@ -539,8 +541,13 @@ test('proteum dev invalidates client assets and reloads for transpiled package s
|
|
|
539
541
|
|
|
540
542
|
try {
|
|
541
543
|
await waitForSessionReady(sessionFile, child, getOutput);
|
|
544
|
+
await request(port, '/', { Accept: 'text/html' });
|
|
542
545
|
|
|
543
|
-
const initialScriptAsset = await waitForAssetContaining(appRoot, '.js', 'SCRIPT_MARKER_INITIAL')
|
|
546
|
+
const initialScriptAsset = await waitForAssetContaining(appRoot, '.js', 'SCRIPT_MARKER_INITIAL').catch(
|
|
547
|
+
(error) => {
|
|
548
|
+
throw new Error(`${error.message}\n${getOutput()}`);
|
|
549
|
+
},
|
|
550
|
+
);
|
|
544
551
|
const initialScriptContent = fs.readFileSync(initialScriptAsset, 'utf8');
|
|
545
552
|
const scriptReloadStream = await connectToReloadStream(port + 1);
|
|
546
553
|
|
|
@@ -608,7 +615,9 @@ test('proteum dev applies router HTTP cache config to HTML and public assets', {
|
|
|
608
615
|
const staticResponse = await waitForHeader(port, '/static-cache', 'cache-control', staticHtmlCacheControl);
|
|
609
616
|
assert.equal(staticResponse.headers.get('surrogate-control'), 'static-surrogate');
|
|
610
617
|
|
|
611
|
-
const asset = await waitForAssetContaining(appRoot, '.js', 'SCRIPT_MARKER_INITIAL')
|
|
618
|
+
const asset = await waitForAssetContaining(appRoot, '.js', 'SCRIPT_MARKER_INITIAL').catch((error) => {
|
|
619
|
+
throw new Error(`${error.message}\n${getOutput()}`);
|
|
620
|
+
});
|
|
612
621
|
const { response: assetResponse } = await request(port, toPublicAssetUrl(appRoot, asset));
|
|
613
622
|
|
|
614
623
|
assert.equal(assetResponse.headers.get('cache-control'), publicAssetCacheControl);
|
|
@@ -11,6 +11,17 @@ const lint = (code) => {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
const swallowedErrorRuleId = 'proteum/no-swallowed-caught-error';
|
|
14
|
+
const noAppImportRuleId = 'proteum/no-app-import';
|
|
15
|
+
|
|
16
|
+
test('proteum lint rejects contextual @app imports', () => {
|
|
17
|
+
const messages = lint(`
|
|
18
|
+
import { Router } from '@app';
|
|
19
|
+
|
|
20
|
+
export const route = Router;
|
|
21
|
+
`);
|
|
22
|
+
|
|
23
|
+
assert.equal(messages.some((message) => message.ruleId === noAppImportRuleId), true);
|
|
24
|
+
});
|
|
14
25
|
|
|
15
26
|
test('proteum lint rejects empty catch blocks', () => {
|
|
16
27
|
const messages = lint(`
|
|
@@ -50,6 +61,32 @@ test('proteum lint rejects generic catch feedback that drops original error deta
|
|
|
50
61
|
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
51
62
|
});
|
|
52
63
|
|
|
64
|
+
test('proteum lint rejects console calls as caught error preservation', () => {
|
|
65
|
+
for (const method of ['error', 'warn']) {
|
|
66
|
+
const messages = lint(`
|
|
67
|
+
export const run = async () => {
|
|
68
|
+
try {
|
|
69
|
+
await Investor.api.getDashboard();
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.${method}(error);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
`);
|
|
75
|
+
|
|
76
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('proteum lint rejects direct console promise catch handlers', () => {
|
|
81
|
+
const messages = lint(`
|
|
82
|
+
export const run = () => {
|
|
83
|
+
Investor.api.ensureApiKey().catch(console.log);
|
|
84
|
+
};
|
|
85
|
+
`);
|
|
86
|
+
|
|
87
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
88
|
+
});
|
|
89
|
+
|
|
53
90
|
test('proteum lint allows rethrowing the caught error', () => {
|
|
54
91
|
const messages = lint(`
|
|
55
92
|
export const run = async () => {
|
|
@@ -99,8 +136,9 @@ test('proteum lint allows surfacing a message derived from the caught error', ()
|
|
|
99
136
|
test('proteum lint allows routing promise failures to app error handling', () => {
|
|
100
137
|
const messages = lint(`
|
|
101
138
|
export const run = () => {
|
|
139
|
+
const context = useContext();
|
|
102
140
|
Investor.api.ensureApiKey().catch((error) => {
|
|
103
|
-
|
|
141
|
+
context.app.handleError(error);
|
|
104
142
|
});
|
|
105
143
|
};
|
|
106
144
|
`);
|
package/tests/mcp.test.cjs
CHANGED
|
@@ -186,6 +186,14 @@ test('instruction routing promotes triggered full instruction files', () => {
|
|
|
186
186
|
);
|
|
187
187
|
assert.equal(payload.data.fullReadPolicy.default.includes('read-only'), true);
|
|
188
188
|
assert.equal(payload.data.selected.some((entry) => entry.fullRead === 'full-before-action'), true);
|
|
189
|
+
|
|
190
|
+
const bugFixPayload = resolveInstructionRouting({ appRoot, query: 'fix OAuth redirect bug' });
|
|
191
|
+
assert.equal(
|
|
192
|
+
bugFixPayload.data.selected.some(
|
|
193
|
+
(entry) => entry.file === path.join(appRoot, 'DOCUMENTATION.md') && /Bug fix/.test(entry.reason),
|
|
194
|
+
),
|
|
195
|
+
true,
|
|
196
|
+
);
|
|
189
197
|
});
|
|
190
198
|
|
|
191
199
|
test('workflow start payload combines compact runtime, instructions, owner, and duplicate guidance', () => {
|
|
@@ -777,6 +785,88 @@ test('machine MCP router resolves offline monorepo app candidates before dev is
|
|
|
777
785
|
await server.close();
|
|
778
786
|
});
|
|
779
787
|
|
|
788
|
+
test('machine MCP workflow_start blocks offline unbootstrapped Codex worktrees', async (t) => {
|
|
789
|
+
const previousRegistryDir = process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
|
|
790
|
+
const registryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-worktree-offline-'));
|
|
791
|
+
process.env.PROTEUM_MACHINE_DEV_SESSION_DIR = registryDir;
|
|
792
|
+
t.onTestFinished(() => {
|
|
793
|
+
if (previousRegistryDir === undefined) delete process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
|
|
794
|
+
else process.env.PROTEUM_MACHINE_DEV_SESSION_DIR = previousRegistryDir;
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-worktree-repo-'));
|
|
798
|
+
const appRoot = path.join(repoRoot, '.codex', 'worktrees', 'product');
|
|
799
|
+
writeProteumAppFixture(appRoot, { identifier: 'ProductApp', name: 'Product', routerPort: 3020 });
|
|
800
|
+
|
|
801
|
+
const server = createProteumMachineMcpServer({ version: 'test' });
|
|
802
|
+
const client = new Client({ name: 'machine-mcp-test', version: '1.0.0' });
|
|
803
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
804
|
+
|
|
805
|
+
await server.connect(serverTransport);
|
|
806
|
+
await client.connect(clientTransport);
|
|
807
|
+
|
|
808
|
+
const workflow = await client.callTool({ name: 'workflow_start', arguments: { cwd: appRoot, task: 'runtime health' } });
|
|
809
|
+
const payload = JSON.parse(workflow.content[0].text);
|
|
810
|
+
|
|
811
|
+
assert.equal(payload.ok, false);
|
|
812
|
+
assert.match(payload.summary, /has not completed Proteum worktree bootstrap/);
|
|
813
|
+
assert.equal(payload.nextActions.length, 1);
|
|
814
|
+
assert.match(payload.nextActions[0].command, /proteum worktree init --source <source-app-root>/);
|
|
815
|
+
|
|
816
|
+
await client.close();
|
|
817
|
+
await server.close();
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
test('machine MCP workflow_start blocks live unbootstrapped Codex worktrees before forwarding', async (t) => {
|
|
821
|
+
const previousRegistryDir = process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
|
|
822
|
+
const registryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-worktree-live-'));
|
|
823
|
+
process.env.PROTEUM_MACHINE_DEV_SESSION_DIR = registryDir;
|
|
824
|
+
t.onTestFinished(() => {
|
|
825
|
+
if (previousRegistryDir === undefined) delete process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
|
|
826
|
+
else process.env.PROTEUM_MACHINE_DEV_SESSION_DIR = previousRegistryDir;
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-worktree-live-repo-'));
|
|
830
|
+
const appRoot = path.join(repoRoot, '.codex', 'worktrees', 'product');
|
|
831
|
+
writeProteumAppFixture(appRoot, { identifier: 'ProductApp', name: 'Product', routerPort: 3020 });
|
|
832
|
+
await writeMachineDevSessionRecord({
|
|
833
|
+
...createDevSessionRecord({
|
|
834
|
+
appRoot,
|
|
835
|
+
port: 3020,
|
|
836
|
+
sessionFilePath: path.join(appRoot, 'var/run/proteum/dev/3020.json'),
|
|
837
|
+
}),
|
|
838
|
+
publicUrl: 'http://localhost:3020',
|
|
839
|
+
state: 'ready',
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
let forwarded = false;
|
|
843
|
+
const server = createProteumMachineMcpServer({
|
|
844
|
+
createDevMcpClient: async () => ({
|
|
845
|
+
callTool: async () => {
|
|
846
|
+
forwarded = true;
|
|
847
|
+
throw new Error('should not forward');
|
|
848
|
+
},
|
|
849
|
+
close: async () => {},
|
|
850
|
+
}),
|
|
851
|
+
version: 'test',
|
|
852
|
+
});
|
|
853
|
+
const client = new Client({ name: 'machine-mcp-test', version: '1.0.0' });
|
|
854
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
855
|
+
|
|
856
|
+
await server.connect(serverTransport);
|
|
857
|
+
await client.connect(clientTransport);
|
|
858
|
+
|
|
859
|
+
const workflow = await client.callTool({ name: 'workflow_start', arguments: { cwd: appRoot, task: 'runtime health' } });
|
|
860
|
+
const payload = JSON.parse(workflow.content[0].text);
|
|
861
|
+
|
|
862
|
+
assert.equal(payload.ok, false);
|
|
863
|
+
assert.equal(forwarded, false);
|
|
864
|
+
assert.match(payload.nextActions[0].command, /proteum worktree init --source <source-app-root>/);
|
|
865
|
+
|
|
866
|
+
await client.close();
|
|
867
|
+
await server.close();
|
|
868
|
+
});
|
|
869
|
+
|
|
780
870
|
test('machine MCP offline resolution inspects occupied ports before suggesting dev start', async (t) => {
|
|
781
871
|
const previousRegistryDir = process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
|
|
782
872
|
const registryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-offline-port-registry-'));
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const coreRoot = path.resolve(__dirname, '..');
|
|
7
|
+
process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
|
|
8
|
+
process.env.TS_NODE_TRANSPILE_ONLY = '1';
|
|
9
|
+
require('ts-node/register/transpile-only');
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
createWorktreeBootstrapDiagnostics,
|
|
13
|
+
getWorktreeBootstrapStatus,
|
|
14
|
+
runWorktreeBootstrapInit,
|
|
15
|
+
worktreeBootstrapMarkerRelativePath,
|
|
16
|
+
} = require('../cli/runtime/worktreeBootstrap.ts');
|
|
17
|
+
|
|
18
|
+
const writeFile = (filepath, content) => {
|
|
19
|
+
fs.mkdirSync(path.dirname(filepath), { recursive: true });
|
|
20
|
+
fs.writeFileSync(filepath, content);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const createCodexAppRoot = () => {
|
|
24
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-worktree-bootstrap-'));
|
|
25
|
+
const appRoot = path.join(root, '.codex', 'worktrees', 'fixture-app');
|
|
26
|
+
|
|
27
|
+
fs.mkdirSync(appRoot, { recursive: true });
|
|
28
|
+
return appRoot;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const writeBootstrapFixture = (appRoot, { env = true, manifest = true, nodeModules = true } = {}) => {
|
|
32
|
+
writeFile(path.join(appRoot, 'package.json'), '{"name":"fixture"}\n');
|
|
33
|
+
writeFile(path.join(appRoot, 'package-lock.json'), '{"lockfileVersion":3}\n');
|
|
34
|
+
writeFile(path.join(appRoot, 'proteum.config.ts'), 'export default {};\n');
|
|
35
|
+
writeFile(path.join(appRoot, 'AGENTS.md'), '# Agents\n');
|
|
36
|
+
if (env) writeFile(path.join(appRoot, '.env'), 'PORT=3020\n');
|
|
37
|
+
if (manifest) writeFile(path.join(appRoot, '.proteum', 'manifest.json'), '{"version":10}\n');
|
|
38
|
+
if (nodeModules) fs.mkdirSync(path.join(appRoot, 'node_modules'), { recursive: true });
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const noOpRefresh = async () => ({ stdout: '', stderr: '', summary: 'refresh ok' });
|
|
42
|
+
const noOpRuntime = async () => ({ stdout: '', stderr: '', summary: 'runtime ok' });
|
|
43
|
+
const noOpDeps = async () => {};
|
|
44
|
+
|
|
45
|
+
test('worktree bootstrap status blocks Codex worktrees without a marker', () => {
|
|
46
|
+
const appRoot = createCodexAppRoot();
|
|
47
|
+
writeBootstrapFixture(appRoot);
|
|
48
|
+
|
|
49
|
+
const status = getWorktreeBootstrapStatus({ appRoot, proteumVersion: 'test' });
|
|
50
|
+
|
|
51
|
+
assert.equal(status.guarded, true);
|
|
52
|
+
assert.equal(status.blocking, true);
|
|
53
|
+
assert.equal(status.state, 'missing');
|
|
54
|
+
assert.equal(status.staleReasons[0].code, 'worktree-bootstrap/missing-marker');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('worktree bootstrap init writes a fresh marker with hashes and runtime status', async () => {
|
|
58
|
+
const appRoot = createCodexAppRoot();
|
|
59
|
+
writeBootstrapFixture(appRoot);
|
|
60
|
+
|
|
61
|
+
const result = await runWorktreeBootstrapInit({
|
|
62
|
+
appRoot,
|
|
63
|
+
coreRoot,
|
|
64
|
+
proteumVersion: 'test',
|
|
65
|
+
runDependencies: noOpDeps,
|
|
66
|
+
runRefresh: noOpRefresh,
|
|
67
|
+
runRuntimeStatus: noOpRuntime,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
assert.equal(fs.existsSync(path.join(appRoot, worktreeBootstrapMarkerRelativePath)), true);
|
|
71
|
+
assert.equal(result.status.blocking, false);
|
|
72
|
+
assert.equal(result.marker.proteumVersion, 'test');
|
|
73
|
+
assert.equal(result.marker.refresh.status, 'ok');
|
|
74
|
+
assert.equal(result.marker.runtimeStatus.summary, 'runtime ok');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('worktree bootstrap becomes stale when tracked inputs change', async () => {
|
|
78
|
+
const appRoot = createCodexAppRoot();
|
|
79
|
+
writeBootstrapFixture(appRoot);
|
|
80
|
+
|
|
81
|
+
await runWorktreeBootstrapInit({
|
|
82
|
+
appRoot,
|
|
83
|
+
coreRoot,
|
|
84
|
+
proteumVersion: 'test',
|
|
85
|
+
runDependencies: noOpDeps,
|
|
86
|
+
runRefresh: noOpRefresh,
|
|
87
|
+
runRuntimeStatus: noOpRuntime,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
writeFile(path.join(appRoot, 'package-lock.json'), '{"lockfileVersion":3,"changed":true}\n');
|
|
91
|
+
|
|
92
|
+
const status = getWorktreeBootstrapStatus({ appRoot, proteumVersion: 'test' });
|
|
93
|
+
|
|
94
|
+
assert.equal(status.blocking, true);
|
|
95
|
+
assert.equal(
|
|
96
|
+
status.staleReasons.some((reason) => reason.code === 'worktree-bootstrap/package-lock-changed'),
|
|
97
|
+
true,
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('worktree bootstrap accepts intentional dependency skips with a reason', async () => {
|
|
102
|
+
const appRoot = createCodexAppRoot();
|
|
103
|
+
writeBootstrapFixture(appRoot, { nodeModules: false });
|
|
104
|
+
|
|
105
|
+
const result = await runWorktreeBootstrapInit({
|
|
106
|
+
appRoot,
|
|
107
|
+
coreRoot,
|
|
108
|
+
proteumVersion: 'test',
|
|
109
|
+
reason: 'dependencies are shared by the parent workspace',
|
|
110
|
+
runDependencies: noOpDeps,
|
|
111
|
+
runRefresh: noOpRefresh,
|
|
112
|
+
runRuntimeStatus: noOpRuntime,
|
|
113
|
+
skipDeps: true,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
assert.equal(result.status.blocking, false);
|
|
117
|
+
assert.equal(result.marker.dependencies.status, 'skipped');
|
|
118
|
+
assert.equal(result.marker.skips.dependencies.reason, 'dependencies are shared by the parent workspace');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('worktree bootstrap requires a source env only when .env is missing', async () => {
|
|
122
|
+
const appRoot = createCodexAppRoot();
|
|
123
|
+
writeBootstrapFixture(appRoot, { env: false });
|
|
124
|
+
|
|
125
|
+
await assert.rejects(
|
|
126
|
+
() =>
|
|
127
|
+
runWorktreeBootstrapInit({
|
|
128
|
+
appRoot,
|
|
129
|
+
coreRoot,
|
|
130
|
+
proteumVersion: 'test',
|
|
131
|
+
runDependencies: noOpDeps,
|
|
132
|
+
runRefresh: noOpRefresh,
|
|
133
|
+
runRuntimeStatus: noOpRuntime,
|
|
134
|
+
}),
|
|
135
|
+
/missing \.env/,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const sourceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-worktree-source-'));
|
|
139
|
+
writeFile(path.join(sourceRoot, '.env'), 'PORT=3021\n');
|
|
140
|
+
|
|
141
|
+
const result = await runWorktreeBootstrapInit({
|
|
142
|
+
appRoot,
|
|
143
|
+
coreRoot,
|
|
144
|
+
proteumVersion: 'test',
|
|
145
|
+
runDependencies: noOpDeps,
|
|
146
|
+
runRefresh: noOpRefresh,
|
|
147
|
+
runRuntimeStatus: noOpRuntime,
|
|
148
|
+
source: sourceRoot,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
assert.equal(fs.readFileSync(path.join(appRoot, '.env'), 'utf8'), 'PORT=3021\n');
|
|
152
|
+
assert.equal(result.marker.env.copied, true);
|
|
153
|
+
assert.equal(result.status.blocking, false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('worktree bootstrap detects missing env manifest node_modules and version changes', async () => {
|
|
157
|
+
const appRoot = createCodexAppRoot();
|
|
158
|
+
writeBootstrapFixture(appRoot);
|
|
159
|
+
|
|
160
|
+
await runWorktreeBootstrapInit({
|
|
161
|
+
appRoot,
|
|
162
|
+
coreRoot,
|
|
163
|
+
proteumVersion: 'test',
|
|
164
|
+
runDependencies: noOpDeps,
|
|
165
|
+
runRefresh: noOpRefresh,
|
|
166
|
+
runRuntimeStatus: noOpRuntime,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
fs.rmSync(path.join(appRoot, '.env'));
|
|
170
|
+
fs.rmSync(path.join(appRoot, '.proteum', 'manifest.json'));
|
|
171
|
+
fs.rmSync(path.join(appRoot, 'node_modules'), { force: true, recursive: true });
|
|
172
|
+
|
|
173
|
+
const status = getWorktreeBootstrapStatus({ appRoot, proteumVersion: 'next' });
|
|
174
|
+
const codes = status.staleReasons.map((reason) => reason.code);
|
|
175
|
+
|
|
176
|
+
assert.equal(status.blocking, true);
|
|
177
|
+
assert.equal(codes.includes('worktree-bootstrap/env-missing'), true);
|
|
178
|
+
assert.equal(codes.includes('worktree-bootstrap/manifest-missing'), true);
|
|
179
|
+
assert.equal(codes.includes('worktree-bootstrap/node-modules-missing'), true);
|
|
180
|
+
assert.equal(codes.includes('worktree-bootstrap/proteum-version-changed'), true);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('worktree bootstrap bypass keeps status visible without blocking', () => {
|
|
184
|
+
const appRoot = createCodexAppRoot();
|
|
185
|
+
const previous = process.env.PROTEUM_ALLOW_UNBOOTSTRAPPED_WORKTREE;
|
|
186
|
+
process.env.PROTEUM_ALLOW_UNBOOTSTRAPPED_WORKTREE = '1';
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
writeBootstrapFixture(appRoot);
|
|
190
|
+
const status = getWorktreeBootstrapStatus({ appRoot, proteumVersion: 'test' });
|
|
191
|
+
|
|
192
|
+
assert.equal(status.bypassed, true);
|
|
193
|
+
assert.equal(status.blocking, false);
|
|
194
|
+
assert.equal(status.state, 'bypassed');
|
|
195
|
+
assert.equal(status.staleReasons.length > 0, true);
|
|
196
|
+
assert.equal(
|
|
197
|
+
createWorktreeBootstrapDiagnostics({ appRoot, status }).some(
|
|
198
|
+
(diagnostic) => diagnostic.code === 'worktree-bootstrap/bypassed',
|
|
199
|
+
),
|
|
200
|
+
true,
|
|
201
|
+
);
|
|
202
|
+
} finally {
|
|
203
|
+
if (previous === undefined) delete process.env.PROTEUM_ALLOW_UNBOOTSTRAPPED_WORKTREE;
|
|
204
|
+
else process.env.PROTEUM_ALLOW_UNBOOTSTRAPPED_WORKTREE = previous;
|
|
205
|
+
}
|
|
206
|
+
});
|
package/types/aliases.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
defineAction,
|
|
3
|
+
defineController,
|
|
4
|
+
schema,
|
|
5
|
+
type TControllerActionInput,
|
|
6
|
+
} from '@server/app/controller';
|
|
2
7
|
|
|
3
8
|
type Assert<T extends true> = T;
|
|
4
9
|
|
|
@@ -8,25 +13,26 @@ type Equals<TLeft, TRight> = (<T>() => T extends TLeft ? 1 : 2) extends <T>() =>
|
|
|
8
13
|
: false
|
|
9
14
|
: false;
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
schema.object({
|
|
16
|
+
const controller = defineController({
|
|
17
|
+
actions: {
|
|
18
|
+
fromShape: defineAction({
|
|
19
|
+
input: {
|
|
20
|
+
name: schema.string(),
|
|
21
|
+
age: schema.number().optional(),
|
|
22
|
+
},
|
|
23
|
+
handler: ({ input }) => input,
|
|
24
|
+
}),
|
|
25
|
+
fromSchema: defineAction({
|
|
26
|
+
input: schema.object({
|
|
22
27
|
slug: schema.string(),
|
|
23
28
|
}),
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
29
|
+
handler: ({ input }) => input,
|
|
30
|
+
}),
|
|
31
|
+
},
|
|
32
|
+
});
|
|
27
33
|
|
|
28
|
-
type TFromShape =
|
|
29
|
-
type TFromSchema =
|
|
34
|
+
type TFromShape = TControllerActionInput<typeof controller, 'fromShape'>;
|
|
35
|
+
type TFromSchema = TControllerActionInput<typeof controller, 'fromSchema'>;
|
|
30
36
|
|
|
31
37
|
type _AssertShapeInference = Assert<
|
|
32
38
|
Equals<
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Application } from '@server/app';
|
|
2
|
-
import
|
|
2
|
+
import type { TControllerActionContext } from '@server/app/controller';
|
|
3
3
|
import type { TServerRouter } from '@server/services/router';
|
|
4
4
|
import type { TServiceModelsClient, TServiceRequestContext } from '@server/app/service';
|
|
5
5
|
|
|
@@ -38,18 +38,17 @@ interface TTestApp extends Application {
|
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
type TRequestAuth = TypedRequestController['request']['auth'];
|
|
48
|
-
type TAuthCheckResult = ReturnType<TypedRequestController['useAuth']>;
|
|
41
|
+
type TControllerContext = TControllerActionContext<undefined, TTestApp>;
|
|
42
|
+
type TControllerContextAuth = TControllerContext['auth'];
|
|
43
|
+
type TControllerModelsClient = TControllerContext['models'];
|
|
49
44
|
type TServiceContextAuth = TServiceRequestContext<TTestApp>['auth'];
|
|
50
45
|
type TModelsClient = TServiceModelsClient<TTestApp>;
|
|
51
46
|
|
|
52
|
-
type
|
|
53
|
-
|
|
47
|
+
type _AssertTypedControllerRequestService = Assert<
|
|
48
|
+
Equals<TControllerContextAuth['check'], TTypedAuthRequestService['check']>
|
|
49
|
+
>;
|
|
54
50
|
type _AssertTypedServiceRequestContext = Assert<Equals<TServiceContextAuth['check'], TTypedAuthRequestService['check']>>;
|
|
51
|
+
type _AssertTypedControllerModelsClient = Assert<
|
|
52
|
+
Equals<ReturnType<TControllerModelsClient['post']['findMany']>, 'posts'>
|
|
53
|
+
>;
|
|
55
54
|
type _AssertTypedModelsClient = Assert<Equals<ReturnType<TModelsClient['post']['findMany']>, 'posts'>>;
|
package/cli/commands/migrate.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
|
|
3
|
-
import cli from '..';
|
|
4
|
-
import { runPageContractMigration } from '../migrate/pageContract';
|
|
5
|
-
|
|
6
|
-
const printHuman = (summary: ReturnType<typeof runPageContractMigration>) => {
|
|
7
|
-
console.log(`Proteum migrate page-contract`);
|
|
8
|
-
console.log(`App root: ${summary.appRoot}`);
|
|
9
|
-
console.log(`Scanned files: ${summary.scannedFiles}`);
|
|
10
|
-
console.log(`Changed files: ${summary.changedFiles.length}${summary.dryRun ? ' (dry run)' : ''}`);
|
|
11
|
-
|
|
12
|
-
if (summary.changedFiles.length > 0) {
|
|
13
|
-
console.log('');
|
|
14
|
-
console.log('Rewritten files:');
|
|
15
|
-
for (const filepath of summary.changedFiles) {
|
|
16
|
-
console.log(`- ${path.relative(summary.appRoot, filepath)}`);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (summary.manualFixes.length > 0) {
|
|
21
|
-
console.log('');
|
|
22
|
-
console.log('Manual fixes required:');
|
|
23
|
-
for (const fix of summary.manualFixes) {
|
|
24
|
-
console.log(
|
|
25
|
-
`- ${path.relative(summary.appRoot, fix.filepath)}:${fix.line}:${fix.column} ${fix.routeLabel} :: ${fix.reason}`,
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export const run = async (): Promise<void> => {
|
|
32
|
-
const action = String(cli.args.action || '').trim();
|
|
33
|
-
if (action !== 'page-contract') {
|
|
34
|
-
throw new Error(`Unsupported migrate action "${action}". Expected "page-contract".`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const summary = runPageContractMigration({
|
|
38
|
-
appRoot: String(cli.args.workdir || process.cwd()),
|
|
39
|
-
dryRun: cli.args.dryRun === true,
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
if (cli.args.json === true) {
|
|
43
|
-
console.log(JSON.stringify(summary, null, 2));
|
|
44
|
-
} else {
|
|
45
|
-
printHuman(summary);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (summary.manualFixes.length > 0) {
|
|
49
|
-
throw new Error(`Page-contract migration requires manual fixes in ${summary.manualFixes.length} location(s).`);
|
|
50
|
-
}
|
|
51
|
-
};
|