proteum 2.1.1 → 2.1.3-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/README.md +28 -6
- package/agents/framework/AGENTS.md +14 -1
- package/agents/project/AGENTS.md +3 -0
- package/cli/commands/command.ts +8 -0
- package/cli/commands/create.ts +5 -0
- package/cli/commands/dev.ts +2 -1
- package/cli/commands/init.ts +2 -94
- package/cli/commands/session.ts +254 -0
- package/cli/commands/sessionLocalRunner.js +188 -0
- package/cli/commands/trace.ts +8 -0
- package/cli/index.ts +1 -4
- package/cli/presentation/commands.ts +72 -10
- package/cli/presentation/devSession.ts +17 -3
- package/cli/presentation/proteum_logo_400x400_square_icon.txt +400 -0
- package/cli/runtime/commands.ts +89 -3
- package/cli/scaffold/index.ts +720 -0
- package/cli/scaffold/templates.ts +344 -0
- package/cli/scaffold/types.ts +26 -0
- package/client/dev/profiler/index.tsx +1410 -235
- package/common/dev/profiler.ts +1 -0
- package/common/dev/requestTrace.ts +10 -0
- package/common/dev/session.ts +24 -0
- package/docs/dev-commands.md +7 -0
- package/docs/diagnostics.md +88 -0
- package/docs/request-tracing.md +10 -0
- package/eslint.js +11 -6
- package/package.json +3 -2
- package/server/app/container/trace/index.ts +48 -0
- package/server/app/index.ts +2 -2
- package/server/index.ts +0 -1
- package/server/services/auth/index.ts +525 -61
- package/server/services/auth/router/index.ts +106 -7
- package/server/services/router/http/index.ts +108 -6
- package/server/services/router/response/index.ts +1 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const net = require('net');
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
const tsNode = require('ts-node');
|
|
5
|
+
|
|
6
|
+
const resultMarker = '__PROTEUM_SESSION_RESULT__';
|
|
7
|
+
|
|
8
|
+
const printPayload = (payload) => {
|
|
9
|
+
process.stdout.write(`${resultMarker}${JSON.stringify(payload)}\n`);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const fail = (error) => {
|
|
13
|
+
printPayload({ error: error instanceof Error ? error.message : String(error) });
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const getAvailablePort = async () =>
|
|
18
|
+
await new Promise((resolve, reject) => {
|
|
19
|
+
const server = net.createServer();
|
|
20
|
+
|
|
21
|
+
server.once('error', reject);
|
|
22
|
+
server.listen(0, '127.0.0.1', () => {
|
|
23
|
+
const address = server.address();
|
|
24
|
+
|
|
25
|
+
if (!address || typeof address === 'string') {
|
|
26
|
+
server.close(() => reject(new Error('Could not determine a local port for the session runner.')));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
server.close((error) => {
|
|
31
|
+
if (error) {
|
|
32
|
+
reject(error);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
resolve(address.port);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const closeMultiCompiler = async (multiCompiler) =>
|
|
42
|
+
await new Promise((resolve, reject) => {
|
|
43
|
+
multiCompiler.close((error) => {
|
|
44
|
+
if (error) {
|
|
45
|
+
reject(error);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
resolve();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const runCompiler = async (compiler) => {
|
|
54
|
+
const multiCompiler = await compiler.create();
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await new Promise((resolve, reject) => {
|
|
58
|
+
multiCompiler.run((error, stats) => {
|
|
59
|
+
if (error) {
|
|
60
|
+
reject(error);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (stats && stats.hasErrors()) {
|
|
65
|
+
reject(new Error('Compilation failed for the local dev session runner.'));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
resolve();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
} finally {
|
|
73
|
+
compiler.dispose();
|
|
74
|
+
await closeMultiCompiler(multiCompiler);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const waitForServerReady = async (child) =>
|
|
79
|
+
await new Promise((resolve, reject) => {
|
|
80
|
+
let settled = false;
|
|
81
|
+
|
|
82
|
+
const finish = (callback, value) => {
|
|
83
|
+
if (settled) return;
|
|
84
|
+
settled = true;
|
|
85
|
+
clearTimeout(timeout);
|
|
86
|
+
child.off('message', onMessage);
|
|
87
|
+
child.off('error', onError);
|
|
88
|
+
child.off('exit', onExit);
|
|
89
|
+
callback(value);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const onMessage = (message) => {
|
|
93
|
+
if (!message || message.type !== 'proteum:server-ready' || typeof message.publicUrl !== 'string') return;
|
|
94
|
+
finish(resolve, message.publicUrl);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const onError = (error) => finish(reject, error);
|
|
98
|
+
const onExit = (code, signal) =>
|
|
99
|
+
finish(reject, new Error(`Local session server exited before becoming ready (code=${code}, signal=${signal}).`));
|
|
100
|
+
const timeout = setTimeout(
|
|
101
|
+
() => finish(reject, new Error('Timed out while waiting for the local session server to become ready.')),
|
|
102
|
+
30000,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
child.on('message', onMessage);
|
|
106
|
+
child.once('error', onError);
|
|
107
|
+
child.once('exit', onExit);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const stopServerProcess = async (child) => {
|
|
111
|
+
if (!child || child.exitCode !== null || child.signalCode !== null) return;
|
|
112
|
+
|
|
113
|
+
child.kill('SIGTERM');
|
|
114
|
+
|
|
115
|
+
await new Promise((resolve) => {
|
|
116
|
+
const timeout = setTimeout(() => {
|
|
117
|
+
if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL');
|
|
118
|
+
}, 5000);
|
|
119
|
+
|
|
120
|
+
child.once('exit', () => {
|
|
121
|
+
clearTimeout(timeout);
|
|
122
|
+
resolve();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const requestSession = async (baseUrl, email, role) => {
|
|
128
|
+
const response = await fetch(`${baseUrl}/__proteum/session/start`, {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: { 'content-type': 'application/json' },
|
|
131
|
+
body: JSON.stringify(role ? { email, role } : { email }),
|
|
132
|
+
});
|
|
133
|
+
const body = await response.json();
|
|
134
|
+
|
|
135
|
+
if (response.status >= 400) {
|
|
136
|
+
throw new Error((body && body.error) || `Session request failed with status ${response.status}.`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { baseUrl, response: body };
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
(async () => {
|
|
143
|
+
const [, , appRootArg = '', email = '', role = ''] = process.argv;
|
|
144
|
+
const appRoot = path.resolve(appRootArg);
|
|
145
|
+
|
|
146
|
+
if (!appRootArg || !email) {
|
|
147
|
+
fail(new Error('sessionLocalRunner requires <appRoot> and <email>.'));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
process.chdir(appRoot);
|
|
152
|
+
|
|
153
|
+
tsNode.register({
|
|
154
|
+
transpileOnly: true,
|
|
155
|
+
project: path.join(__dirname, '..', 'tsconfig.json'),
|
|
156
|
+
files: true,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const port = await getAvailablePort();
|
|
160
|
+
const cli = require('../context.ts').default;
|
|
161
|
+
cli.setArgs({ workdir: appRoot, port: String(port), url: '', json: true });
|
|
162
|
+
|
|
163
|
+
const app = require('../app/index.ts').default;
|
|
164
|
+
const Compiler = require('../compiler/index.ts').default;
|
|
165
|
+
|
|
166
|
+
if (app.env.profile !== 'dev') {
|
|
167
|
+
fail(new Error(`Proteum sessions are only available when ENV_PROFILE=dev. Current profile: ${app.env.profile}.`));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const compiler = new Compiler('dev');
|
|
172
|
+
await runCompiler(compiler);
|
|
173
|
+
|
|
174
|
+
const serverProcess = spawn(process.execPath, ['--preserve-symlinks', path.join(app.outputPath('dev'), 'server.js')], {
|
|
175
|
+
cwd: app.paths.root,
|
|
176
|
+
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const baseUrl = await waitForServerReady(serverProcess);
|
|
181
|
+
const session = await requestSession(baseUrl, email, role);
|
|
182
|
+
printPayload({ session });
|
|
183
|
+
} finally {
|
|
184
|
+
await stopServerProcess(serverProcess);
|
|
185
|
+
}
|
|
186
|
+
})().catch((error) => {
|
|
187
|
+
fail(error);
|
|
188
|
+
});
|
package/cli/commands/trace.ts
CHANGED
|
@@ -75,6 +75,9 @@ const getTraceErrorMessage = (body: TRequestTraceErrorResponse | object | string
|
|
|
75
75
|
return `Trace request failed with status ${statusCode}.`;
|
|
76
76
|
};
|
|
77
77
|
|
|
78
|
+
const hasStructuredTraceError = (body: TRequestTraceErrorResponse | object | string | undefined): body is TRequestTraceErrorResponse =>
|
|
79
|
+
typeof body === 'object' && body !== null && 'error' in body && typeof body.error === 'string';
|
|
80
|
+
|
|
78
81
|
const requestJson = async <TResponse>(pathname: string, options?: { method?: 'GET' | 'POST'; json?: object }) => {
|
|
79
82
|
const attempts: string[] = [];
|
|
80
83
|
|
|
@@ -89,6 +92,11 @@ const requestJson = async <TResponse>(pathname: string, options?: { method?: 'GE
|
|
|
89
92
|
});
|
|
90
93
|
|
|
91
94
|
if (response.statusCode >= 400) {
|
|
95
|
+
if (response.statusCode === 404 && !hasStructuredTraceError(response.body as TRequestTraceErrorResponse | object | string | undefined)) {
|
|
96
|
+
attempts.push(`${baseUrl}${pathname}: returned 404`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
92
100
|
throw new TraceResponseError(
|
|
93
101
|
getTraceErrorMessage(response.body as TRequestTraceErrorResponse | object | string | undefined, response.statusCode),
|
|
94
102
|
);
|
package/cli/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
process.traceDeprecation = true;
|
|
2
2
|
|
|
3
|
-
import fs from 'fs';
|
|
4
3
|
import { Cli } from 'clipanion';
|
|
5
4
|
|
|
6
5
|
import cli from './context';
|
|
@@ -9,12 +8,10 @@ import { renderCliOverview, renderCommandHelp, resolveCustomHelpRequest } from '
|
|
|
9
8
|
import { normalizeHelpArgv, normalizeLegacyArgv } from './runtime/argv';
|
|
10
9
|
import { createCli, registeredCommands } from './runtime/commands';
|
|
11
10
|
|
|
12
|
-
const hasInitScaffold = () => fs.existsSync(`${cli.paths.core.cli}/skeleton`);
|
|
13
|
-
|
|
14
11
|
export const runCli = async (argv: string[] = process.argv.slice(2)) => {
|
|
15
12
|
const normalizedArgv = normalizeHelpArgv(normalizeLegacyArgv(argv), proteumCommandNames);
|
|
16
13
|
const clipanion = createCli(String(cli.packageJson.version || ''));
|
|
17
|
-
const initAvailable =
|
|
14
|
+
const initAvailable = true;
|
|
18
15
|
const helpRequest = resolveCustomHelpRequest(normalizedArgv);
|
|
19
16
|
|
|
20
17
|
if (helpRequest.kind === 'overview') {
|
|
@@ -5,6 +5,7 @@ import type { TRow } from './layout';
|
|
|
5
5
|
|
|
6
6
|
export const proteumCommandNames = [
|
|
7
7
|
'init',
|
|
8
|
+
'create',
|
|
8
9
|
'dev',
|
|
9
10
|
'refresh',
|
|
10
11
|
'build',
|
|
@@ -15,6 +16,7 @@ export const proteumCommandNames = [
|
|
|
15
16
|
'explain',
|
|
16
17
|
'trace',
|
|
17
18
|
'command',
|
|
19
|
+
'session',
|
|
18
20
|
] as const;
|
|
19
21
|
|
|
20
22
|
export type TProteumCommandName = (typeof proteumCommandNames)[number];
|
|
@@ -47,21 +49,56 @@ export const proteumRecommendedFlow: TRow[] = [
|
|
|
47
49
|
export const proteumCommandGroups: Array<{ title: string; names: TProteumCommandName[] }> = [
|
|
48
50
|
{ title: 'Daily workflow', names: ['dev', 'refresh', 'build'] },
|
|
49
51
|
{ title: 'Quality gates', names: ['typecheck', 'lint', 'check'] },
|
|
50
|
-
{ title: 'Manifest and contracts', names: ['doctor', 'explain', 'trace', 'command'] },
|
|
51
|
-
{ title: 'Project scaffolding', names: ['init'] },
|
|
52
|
+
{ title: 'Manifest and contracts', names: ['doctor', 'explain', 'trace', 'command', 'session'] },
|
|
53
|
+
{ title: 'Project scaffolding', names: ['init', 'create'] },
|
|
52
54
|
];
|
|
53
55
|
|
|
54
56
|
export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> = {
|
|
55
57
|
init: {
|
|
56
58
|
name: 'init',
|
|
57
59
|
category: 'Project scaffolding',
|
|
58
|
-
summary: 'Scaffold a new Proteum
|
|
59
|
-
usage: 'proteum init',
|
|
60
|
-
bestFor: '
|
|
61
|
-
examples: [
|
|
60
|
+
summary: 'Scaffold a new Proteum app with deterministic built-in templates.',
|
|
61
|
+
usage: 'proteum init [directory] [--name <name>] [--identifier <identifier>] [--port <port>] [--install] [--dry-run] [--json]',
|
|
62
|
+
bestFor: 'Bootstrapping a new app in a way that is explicit, machine-readable, and safe for LLM coding agents.',
|
|
63
|
+
examples: [
|
|
64
|
+
{ description: 'Create a new app in ./my-app', command: 'proteum init my-app --name "My App"' },
|
|
65
|
+
{
|
|
66
|
+
description: 'Scaffold an app and install dependencies immediately',
|
|
67
|
+
command: 'proteum init my-app --name "My App" --install',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
description: 'Emit scaffold details as JSON for an agent',
|
|
71
|
+
command: 'proteum init my-app --name "My App" --json',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
description: 'Preview the full app scaffold without writing files',
|
|
75
|
+
command: 'proteum init my-app --name "My App" --dry-run --json',
|
|
76
|
+
},
|
|
77
|
+
],
|
|
62
78
|
notes: [
|
|
63
|
-
'
|
|
64
|
-
'
|
|
79
|
+
'When Proteum is invoked from a local framework checkout, init writes a file: dependency to that checkout by default.',
|
|
80
|
+
'Use `--dry-run --json` when an agent needs a machine-readable app scaffold plan before writing files.',
|
|
81
|
+
'Without `--install`, init only writes files and does not touch the network.',
|
|
82
|
+
],
|
|
83
|
+
status: 'experimental',
|
|
84
|
+
},
|
|
85
|
+
create: {
|
|
86
|
+
name: 'create',
|
|
87
|
+
category: 'Project scaffolding',
|
|
88
|
+
summary: 'Generate a page, controller, command, route, or root service inside a Proteum app.',
|
|
89
|
+
usage: 'proteum create <page|controller|command|route|service> <target> [--route <url>] [--method <name>] [--http-method <verb>] [--dry-run] [--json]',
|
|
90
|
+
bestFor: 'Fast deterministic scaffolding inside an existing Proteum app without inventing file layouts or boilerplate by hand.',
|
|
91
|
+
examples: [
|
|
92
|
+
{ description: 'Create a new SSR page', command: 'proteum create page marketing/faq --route /faq' },
|
|
93
|
+
{ description: 'Create a new controller', command: 'proteum create controller Founder/projects --method list' },
|
|
94
|
+
{ description: 'Create a new command', command: 'proteum create command diagnostics --method ping' },
|
|
95
|
+
{ description: 'Preview a new route without writing files', command: 'proteum create route webhooks/stripe --dry-run --json' },
|
|
96
|
+
{ description: 'Create and register a new root service', command: 'proteum create service Conversion/Plans' },
|
|
97
|
+
],
|
|
98
|
+
notes: [
|
|
99
|
+
'Page scaffolds write `client/pages/**/index.tsx` and default the route path from the logical target path unless `--route` is provided.',
|
|
100
|
+
'Service scaffolds create `server/services/**/index.ts`, `service.json`, a config export under `server/config/*.ts`, and then try to register the new root service in `server/index.ts`.',
|
|
101
|
+
'Use `--dry-run --json` when an agent needs a machine-readable plan before writing files.',
|
|
65
102
|
],
|
|
66
103
|
status: 'experimental',
|
|
67
104
|
},
|
|
@@ -238,6 +275,31 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
|
|
|
238
275
|
],
|
|
239
276
|
status: 'experimental',
|
|
240
277
|
},
|
|
278
|
+
session: {
|
|
279
|
+
name: 'session',
|
|
280
|
+
category: 'Manifest and contracts',
|
|
281
|
+
summary: 'Mint a dev-only auth session token and cookie payload for a known user.',
|
|
282
|
+
usage: 'proteum session <email> [--role <role>] [--port <port>|--url <baseUrl>] [--json]',
|
|
283
|
+
bestFor:
|
|
284
|
+
'Starting browser or API automation from an authenticated state without driving the login UI, while still using the app-configured auth service.',
|
|
285
|
+
examples: [
|
|
286
|
+
{
|
|
287
|
+
description: 'Mint an admin session for a running dev server',
|
|
288
|
+
command: 'proteum session admin@example.com --role ADMIN --port 3101',
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
description: 'Mint a GOD session for unique.domains and print machine-readable cookie data',
|
|
292
|
+
command: 'proteum session god@example.com --role GOD --json',
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
notes: [
|
|
296
|
+
'Sessions are available only in dev mode and use the auth service registered on the current app router.',
|
|
297
|
+
'You must provide the target user email explicitly; Proteum does not guess your admin account universally across apps.',
|
|
298
|
+
'The command returns a token plus Playwright-ready cookie JSON so agents can inject the session into a browser context directly.',
|
|
299
|
+
'Without `--port` or `--url`, Proteum refreshes generated artifacts, builds the dev output, starts a temporary local dev server, creates the session, prints the payload, and exits.',
|
|
300
|
+
],
|
|
301
|
+
status: 'experimental',
|
|
302
|
+
},
|
|
241
303
|
};
|
|
242
304
|
|
|
243
305
|
export const isLikelyProteumAppRoot = (workdir: string) =>
|
|
@@ -248,8 +310,8 @@ export const isLikelyProteumAppRoot = (workdir: string) =>
|
|
|
248
310
|
|
|
249
311
|
export const getInitAvailabilityNote = (initAvailable: boolean) =>
|
|
250
312
|
initAvailable
|
|
251
|
-
? '
|
|
252
|
-
: '
|
|
313
|
+
? 'Init is built into the CLI and does not depend on external scaffold assets.'
|
|
314
|
+
: 'Init scaffolding is currently unavailable in this checkout.';
|
|
253
315
|
|
|
254
316
|
export const createClipanionUsage = (command: TProteumCommandDoc) => ({
|
|
255
317
|
category: command.category,
|
|
@@ -11,6 +11,8 @@ const ProteumWordmark = [
|
|
|
11
11
|
String.raw`|_| |_| \_\\___/ |_| |_____|\___/|_| |_|`,
|
|
12
12
|
];
|
|
13
13
|
|
|
14
|
+
const ProteumTagline = 'Agent-first SSR compiler and server loop.';
|
|
15
|
+
|
|
14
16
|
export const renderDevSession = async ({
|
|
15
17
|
appName,
|
|
16
18
|
appRoot,
|
|
@@ -32,9 +34,9 @@ export const renderDevSession = async ({
|
|
|
32
34
|
return createElement(
|
|
33
35
|
Box,
|
|
34
36
|
{ borderStyle: 'round', borderColor: 'cyan', paddingX: 2, paddingY: 0, flexDirection: 'column' },
|
|
35
|
-
createElement(Text, { bold: true, color: '
|
|
36
|
-
createElement(
|
|
37
|
-
createElement(
|
|
37
|
+
createElement(Text, { bold: true, backgroundColor: 'cyan', color: 'black' }, ' WELCOME TO '),
|
|
38
|
+
createElement(Box, { flexDirection: 'column' }, ...wordmark),
|
|
39
|
+
createElement(Text, { dimColor: true }, ProteumTagline),
|
|
38
40
|
);
|
|
39
41
|
}),
|
|
40
42
|
renderRows(
|
|
@@ -73,3 +75,15 @@ export const renderServerReadyBanner = async ({
|
|
|
73
75
|
createElement(Text, { dimColor: true }, `Trace latest: proteum trace latest --port ${routerPort}`),
|
|
74
76
|
);
|
|
75
77
|
});
|
|
78
|
+
|
|
79
|
+
export const renderDevShutdownBanner = async () =>
|
|
80
|
+
renderInk(({ Box, Text }) => {
|
|
81
|
+
const createElement = React.createElement;
|
|
82
|
+
|
|
83
|
+
return createElement(
|
|
84
|
+
Box,
|
|
85
|
+
{ borderStyle: 'round', borderColor: 'yellow', paddingX: 2, paddingY: 0, flexDirection: 'column' },
|
|
86
|
+
createElement(Text, { bold: true, backgroundColor: 'yellow', color: 'black' }, ' SHUTTING DOWN '),
|
|
87
|
+
createElement(Text, { bold: true, color: 'yellow' }, 'Thank you for developping with Proteum'),
|
|
88
|
+
);
|
|
89
|
+
});
|