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 CHANGED
@@ -28,7 +28,7 @@ Proteum combines:
28
28
  - **Explicit request entrypoints.** Controllers are classes. Request access is explicit through `this.request`.
29
29
  - **Local validation.** Validate handler input inside the handler with `this.input(schema)`.
30
30
  - **Deterministic generation.** Proteum owns `.proteum/` and regenerates it from source.
31
- - **Explainability matters.** `proteum explain`, `proteum doctor`, and `proteum trace` expose the framework view of your app and its live requests.
31
+ - **Explainability matters.** `proteum explain`, `proteum doctor`, and `proteum trace` expose the framework view of your app and its live requests, and the profiler renders the same diagnostics surfaces for humans in dev.
32
32
  - **SEO is not an afterthought.** Identity, routes, layouts, and SSR data are part of the app contract.
33
33
 
34
34
  ## What a Proteum App Looks Like
@@ -289,7 +289,9 @@ Proteum ships with a compact CLI focused on the real app lifecycle:
289
289
  | `proteum explain` | Explain routes, controllers, services, layouts, conventions, and env |
290
290
  | `proteum trace` | Inspect live dev-only request traces from the running SSR server |
291
291
  | `proteum command` | Run a dev-only internal command locally or against a running dev server |
292
- | `proteum init` | Experimental project scaffolding when scaffold assets are installed |
292
+ | `proteum session` | Mint a dev-only auth session token and Playwright-ready cookie payload |
293
+ | `proteum init` | Scaffold a new Proteum app with built-in deterministic templates |
294
+ | `proteum create` | Scaffold a page, controller, command, route, or root service inside an app |
293
295
 
294
296
  Recommended daily workflow:
295
297
 
@@ -310,11 +312,25 @@ proteum explain --routes --controllers --commands
310
312
  proteum explain --all --json
311
313
  proteum command proteum/diagnostics/ping
312
314
  proteum command proteum/diagnostics/ping --port 3101
315
+ proteum session admin@example.com --role ADMIN --port 3101
316
+ proteum session god@example.com --role GOD --json
313
317
  proteum trace requests
314
318
  proteum trace arm --capture deep
315
319
  proteum trace latest
316
320
  ```
317
321
 
322
+ Useful scaffolding commands:
323
+
324
+ ```bash
325
+ proteum init my-app --name "My App"
326
+ proteum init my-app --name "My App" --dry-run --json
327
+ proteum create page marketing/faq --route /faq
328
+ proteum create controller Founder/projects --method list
329
+ proteum create service Conversion/Plans
330
+ ```
331
+
332
+ `proteum explain` and `proteum doctor` share the same manifest-backed diagnostics contract as the profiler `Explain` and `Doctor` tabs. For the full dev diagnostics model, see [docs/diagnostics.md](docs/diagnostics.md).
333
+
318
334
  ## Dev Commands
319
335
 
320
336
  Proteum includes a dev-only command surface for internal testing, debugging, and one-off execution that should not become a normal controller or route.
@@ -326,12 +342,15 @@ Proteum includes a dev-only command surface for internal testing, debugging, and
326
342
  - `proteum command foo/bar` refreshes generated artifacts, builds the dev output, starts a temporary local dev server, runs the command, prints the result, and exits
327
343
  - `proteum command foo/bar --port 3101` runs the same command against an existing `proteum dev` instance
328
344
  - the dev profiler exposes the same command list and run action through the `Commands` tab
345
+ - the same profiler also exposes `Explain` and `Doctor` tabs backed by the same manifest diagnostics contract as the CLI
329
346
 
330
347
  Proteum itself also ships a small built-in diagnostic command at `proteum/diagnostics/ping`, so the command surface is never empty in dev.
331
348
 
332
349
  ## Request Tracing
333
350
 
334
- Proteum includes a dev-only in-memory request trace buffer for routing, controller, context, SSR, and render debugging.
351
+ Proteum includes a dev-only in-memory request trace buffer for auth, routing, controller, context, SSR, and render debugging.
352
+
353
+ This is separate from `proteum explain` and `proteum doctor`: tracing is live request-time data, while explain/doctor are manifest-backed structure and diagnostics.
335
354
 
336
355
  When diagnosing or testing against an app, first read the default port from `PORT` or `./.proteum/manifest.json` and check whether a server is already running there. If it is, inspect the existing traces before reproducing the issue so you can collect past errors and their context.
337
356
 
@@ -363,7 +382,7 @@ export TRACE_PERSIST_ON_ERROR=true
363
382
  Capture modes:
364
383
 
365
384
  - `summary`: request lifecycle plus high-signal events
366
- - `resolve`: adds route resolution and controller/context steps
385
+ - `resolve`: adds auth, route resolution, and controller/context steps
367
386
  - `deep`: adds route skip reasons and deeper payload summaries for one request investigation
368
387
 
369
388
  The trace CLI talks to the running dev server over the dev-only `__proteum/trace` HTTP endpoints. Use `--port` for a different local port or `--url` when the host itself is non-standard. For the full guide, see [docs/request-tracing.md](docs/request-tracing.md).
@@ -387,6 +406,7 @@ Proteum answers those questions with explicit artifacts:
387
406
  - `.proteum/manifest.json` for machine-readable app structure
388
407
  - `proteum explain --json` for structured framework introspection
389
408
  - `proteum doctor --json` for structured diagnostics
409
+ - the profiler `Explain` and `Doctor` tabs for a human-readable view over the same manifest-backed contract
390
410
  - `proteum command ...` plus the profiler `Commands` tab for dev-only internal execution
391
411
 
392
412
  If you are an LLM or automation agent, start here:
@@ -449,15 +469,17 @@ Install in an app:
449
469
  npm install proteum
450
470
  ```
451
471
 
452
- If the scaffold assets are available in your distribution, you can bootstrap a new app with:
472
+ You can bootstrap a new app with:
453
473
 
454
474
  ```bash
455
- npx proteum init
475
+ npx proteum init my-app --name "My App"
476
+ npx proteum init my-app --name "My App" --dry-run --json
456
477
  ```
457
478
 
458
479
  Then use the normal workflow:
459
480
 
460
481
  ```bash
482
+ npm install
461
483
  npx proteum dev
462
484
  npx proteum check
463
485
  npx proteum build --prod
@@ -66,6 +66,14 @@ Prefer structured CLI surfaces over re-deriving framework facts from source:
66
66
  - `npx proteum doctor --json`
67
67
  - `npx proteum trace ...`
68
68
  - `npx proteum command ...`
69
+ - `npx proteum create ... --dry-run --json`
70
+
71
+ Prefer scaffold commands before hand-writing boilerplate:
72
+
73
+ - Use `npx proteum init <directory> --name <name>` for new apps.
74
+ - Use `npx proteum init ... --dry-run --json` when an agent needs a machine-readable app plan before writing files.
75
+ - Use `npx proteum create page|controller|command|route|service <target>` for new app artifacts before creating the files manually.
76
+ - Use `npx proteum create ... --dry-run --json` when an agent needs a machine-readable artifact plan before writing files.
69
77
 
70
78
  ## File Contracts
71
79
 
@@ -79,6 +87,7 @@ Prefer structured CLI surfaces over re-deriving framework facts from source:
79
87
  - Business logic lives in classes that extend `Service` and use `this.services`, `this.models`, and `this.app`.
80
88
  - Keep auth, input parsing, locale, cookies, and request-derived values in controllers, then pass explicit typed arguments into services.
81
89
  - Split growing features into explicit subservices.
90
+ - `proteum create service ...` scaffolds the service file, its `service.json`, a typed config export under `server/config/*.ts`, and the root registration in `server/index.ts`; review and adapt the generated names before committing.
82
91
 
83
92
  ### Controllers
84
93
 
@@ -87,6 +96,7 @@ Prefer structured CLI surfaces over re-deriving framework facts from source:
87
96
  - Route path comes from the controller file path plus the method name.
88
97
  - `export const controllerPath = 'Custom/path'` can override the base path.
89
98
  - Generated client calls use `POST`.
99
+ - Prefer `proteum create controller ...` for new controller boilerplate, then adapt the generated method to real service calls.
90
100
 
91
101
  ### Commands
92
102
 
@@ -96,6 +106,7 @@ Prefer structured CLI surfaces over re-deriving framework facts from source:
96
106
  - `export const commandPath = 'Custom/path'` can override the base path.
97
107
  - Commands are for dev-only internal execution through `proteum command ...` or the profiler `Commands` tab.
98
108
  - Keep command logic internal; do not turn it into a normal controller unless it is a real app API.
109
+ - Prefer `proteum create command ...` for new command boilerplate.
99
110
 
100
111
  ### Client Pages
101
112
 
@@ -108,6 +119,7 @@ Prefer structured CLI surfaces over re-deriving framework facts from source:
108
119
  - `render` consumes resolved setup data and uses generated controller methods from render args or `@/client/context`.
109
120
  - Use `api.reload(...)` or `api.set(...)` only when intentionally mutating active page setup state.
110
121
  - Error pages use `Router.error(code, options, render)` in `client/pages/_messages/**`.
122
+ - Prefer `proteum create page ...` for new page boilerplate, then review the explicit route path and setup payload.
111
123
 
112
124
  ### Manual Routes
113
125
 
@@ -115,6 +127,7 @@ Prefer structured CLI surfaces over re-deriving framework facts from source:
115
127
  - Good fits include redirects, sitemap or RSS output, OAuth callbacks, webhooks, and public resources with custom semantics.
116
128
  - Import server-side app services from `@app` and use route handler context for `request`, `response`, router plugins, and custom router context.
117
129
  - If the route is a normal app API, prefer a controller.
130
+ - Prefer `proteum create route ...` for new manual-route boilerplate.
118
131
 
119
132
  ### Models And Aliases
120
133
 
@@ -161,4 +174,4 @@ Verify at the correct layer:
161
174
 
162
175
  When an app may already be running, check the default port from `PORT` or `./.proteum/manifest.json` and inspect `proteum trace requests`, `proteum trace latest`, and `proteum trace show <requestId>` before reproducing the issue. If those traces are not enough, arm `npx proteum trace arm --capture deep`, reproduce once, then inspect the new request.
163
176
 
164
- Useful commands: `proteum dev`, `npx proteum refresh`, `npx proteum typecheck`, `npx proteum lint`, `npx proteum check`, `npx proteum build prod`, `npx proteum command <path>`.
177
+ Useful commands: `npx proteum init <dir> --name <name>`, `npx proteum create <kind> <target>`, `proteum dev`, `npx proteum refresh`, `npx proteum typecheck`, `npx proteum lint`, `npx proteum check`, `npx proteum build prod`, `npx proteum command <path>`.
@@ -12,6 +12,8 @@ Coding style source of truth: `./CODING_STYLE.md`.
12
12
  ## Fast Start
13
13
 
14
14
  - Start with `./.proteum/manifest.json`, `npx proteum explain`, and `npx proteum doctor`.
15
+ - For new app or artifact boilerplate, prefer `npx proteum init ...` and `npx proteum create ...` before creating files by hand.
16
+ - Use `--dry-run --json` on scaffold commands when an agent needs a machine-readable plan before writing files.
15
17
  - For request-time issues in dev, inspect traces before adding logs.
16
18
  - If a server is already running on the default port from `PORT` or `./.proteum/manifest.json`, inspect existing traces before reproducing the issue.
17
19
  - If existing traces are insufficient, arm `npx proteum trace arm --capture deep`, reproduce once, then inspect the new request.
@@ -58,6 +60,7 @@ This is a TypeScript, Node.js, Preact, Proteum monolith:
58
60
 
59
61
  - If the user pastes raw errors without asking for a fix, do not implement changes. List likely causes and, for each one, give probability, why, and how to fix it.
60
62
  - For request-time behavior in dev, check whether a server is already running on the default port and prefer `npx proteum trace` before reproducing the issue or adding logs.
63
+ - After running `npx proteum create ...`, adapt the generated code to the real feature instead of leaving placeholder logic in place.
61
64
  - End your work with `Commit message`: one short top-level sentence.
62
65
 
63
66
  ## High-Impact Files
@@ -61,6 +61,9 @@ const getCommandErrorMessage = (body: TDevCommandErrorResponse | object | string
61
61
  return `Command request failed with status ${statusCode}.`;
62
62
  };
63
63
 
64
+ const hasStructuredCommandError = (body: TDevCommandErrorResponse | object | string | undefined): body is TDevCommandErrorResponse =>
65
+ typeof body === 'object' && body !== null && 'error' in body && typeof body.error === 'string';
66
+
64
67
  const requestJson = async <TResponse>(pathname: string, options?: { method?: 'GET' | 'POST'; json?: object }) => {
65
68
  const attempts: string[] = [];
66
69
 
@@ -75,6 +78,11 @@ const requestJson = async <TResponse>(pathname: string, options?: { method?: 'GE
75
78
  });
76
79
 
77
80
  if (response.statusCode >= 400) {
81
+ if (response.statusCode === 404 && !hasStructuredCommandError(response.body as TDevCommandErrorResponse | object | string | undefined)) {
82
+ attempts.push(`${baseUrl}${pathname}: returned 404`);
83
+ continue;
84
+ }
85
+
78
86
  throw new UsageError(
79
87
  getCommandErrorMessage(response.body as TDevCommandErrorResponse | object | string | undefined, response.statusCode),
80
88
  );
@@ -0,0 +1,5 @@
1
+ import { runCreateScaffold } from '../scaffold';
2
+
3
+ export const run = async (): Promise<void> => {
4
+ await runCreateScaffold();
5
+ };
@@ -22,7 +22,7 @@ import {
22
22
  import Compiler from '../compiler';
23
23
  import { createDevEventServer } from './devEvents';
24
24
  import { ensureProjectAgentSymlinks } from '../utils/agents';
25
- import { renderDevSession, renderServerReadyBanner } from '../presentation/devSession';
25
+ import { renderDevSession, renderServerReadyBanner, renderDevShutdownBanner } from '../presentation/devSession';
26
26
  import { logVerbose } from '../runtime/verbose';
27
27
 
28
28
  // Core
@@ -448,6 +448,7 @@ export const run = async () => {
448
448
  await stopApp(reason);
449
449
  await cleanupPersistedDevTraces(app);
450
450
  await devEventServer.close();
451
+ console.info(await renderDevShutdownBanner());
451
452
  })();
452
453
 
453
454
  return shuttingDownPromise;
@@ -1,97 +1,5 @@
1
- /*----------------------------------
2
- - DEPENDANCES
3
- ----------------------------------*/
1
+ import { runInitScaffold } from '../scaffold';
4
2
 
5
- // Npm
6
- import fs from 'fs-extra';
7
- import path from 'path';
8
- import prompts from 'prompts';
9
- import cmd from 'node-cmd';
10
- import replaceOnce from 'replace-once';
11
- import { UsageError } from 'clipanion';
12
-
13
- // Cor elibs
14
- import cli from '..';
15
- import { ensureProjectAgentSymlinks } from '../utils/agents';
16
-
17
- // Configs
18
- const filesToConfig = ['package.json', 'identity.yaml'];
19
-
20
- /*----------------------------------
21
- - COMMANDE
22
- ----------------------------------*/
23
3
  export const run = async (): Promise<void> => {
24
- const skeletonPath = path.join(cli.paths.core.cli, 'skeleton');
25
-
26
- if (!fs.existsSync(skeletonPath)) {
27
- throw new UsageError(
28
- 'Proteum init is unavailable in this checkout because `cli/skeleton` is missing. Restore the scaffold assets or use an installed Proteum package that includes them.',
29
- );
30
- }
31
-
32
- const config = await prompts([
33
- {
34
- type: 'text',
35
- name: 'name',
36
- message: 'Project name ?',
37
- initial: 'MyProject',
38
- validate: (value) => /[a-z0-9\-\.]/i.test(value) || 'Must only include alphanumeric characters, and - . ',
39
- },
40
- {
41
- type: 'text',
42
- name: 'dirname',
43
- message: 'Folder name ?',
44
- initial: (value) => value.toLowerCase(),
45
- validate: (value) =>
46
- /[a-z0-9\-\.]/.test(value) || 'Must only include lowercase alphanumeric characters, and - . ',
47
- },
48
- {
49
- type: 'text',
50
- name: 'description',
51
- message: 'Briefly describe your project to your mom:',
52
- initial: 'It will revolutionnize the world',
53
- validate: (value) => /[a-z0-9\-\. ]/i.test(value) || 'Must only include alphanumeric characters, and - . ',
54
- },
55
- { type: 'toggle', name: 'microservice', message: 'Separate API from the UI servers ?' },
56
- ]);
57
-
58
- const placeholders = {
59
- PROJECT_NAME: config.name,
60
- PACKAGE_NAME: config.name.toLowerCase(),
61
- PROJECT_DESCRIPTION: config.description,
62
- };
63
-
64
- const paths = {
65
- skeleton: skeletonPath,
66
- project: path.join(process.cwd(), config.dirname),
67
- };
68
-
69
- // Copy skeleton to cwd/<project-name>
70
- console.info('Creating project skeleton ...');
71
- fs.copySync(paths.skeleton, paths.project);
72
-
73
- // Sync framework-owned Codex assets into the new project.
74
- ensureProjectAgentSymlinks({ appRoot: paths.project, coreRoot: cli.paths.core.root });
75
-
76
- // Replace placeholders
77
- console.info('Configuring project ...');
78
- for (const file of filesToConfig) {
79
- console.log('- ' + file);
80
-
81
- const filepath = path.join(paths.project, file);
82
- const content = fs.readFileSync(filepath, 'utf-8');
83
-
84
- const placeholders_keys = Object.keys(placeholders).map((k) => '{{ ' + k + ' }}');
85
- const values = Object.values(placeholders);
86
-
87
- fs.writeFileSync(filepath, replaceOnce(content, placeholders_keys, values));
88
- }
89
-
90
- // Npm install
91
- console.info('Installing packages ...');
92
- cmd.runSync(`cd "${paths.project}" && npm i`);
93
-
94
- // Run demo app
95
- /*console.info("Run demo ...");
96
- await cli.shell('5htp dev');*/
4
+ await runInitScaffold();
97
5
  };
@@ -0,0 +1,254 @@
1
+ import got from 'got';
2
+ import path from 'path';
3
+ import { spawn } from 'child_process';
4
+ import { UsageError } from 'clipanion';
5
+
6
+ import cli from '..';
7
+ import type { TDevSessionErrorResponse, TDevSessionStartResponse } from '../../common/dev/session';
8
+
9
+ const localSessionResultMarker = '__PROTEUM_SESSION_RESULT__';
10
+
11
+ type TResolvedSessionOutput = {
12
+ baseUrl: string;
13
+ user: TDevSessionStartResponse['user'];
14
+ session: TDevSessionStartResponse['session'];
15
+ browserCookie: string;
16
+ curlCookieHeader: string;
17
+ playwright: {
18
+ cookies: Array<{
19
+ name: string;
20
+ value: string;
21
+ url: string;
22
+ expires: number;
23
+ httpOnly: boolean;
24
+ secure: boolean;
25
+ sameSite: 'Lax';
26
+ }>;
27
+ };
28
+ };
29
+
30
+ const normalizeBaseUrl = (value: string) => value.replace(/\/+$/, '');
31
+
32
+ const getRouterPortFromManifest = () => {
33
+ const manifestFilepath = path.join(cli.args.workdir as string, '.proteum', 'manifest.json');
34
+ if (!require('fs-extra').existsSync(manifestFilepath)) return undefined;
35
+
36
+ const manifest = require('fs-extra').readJsonSync(manifestFilepath, { throws: false }) as
37
+ | { env?: { resolved?: { routerPort?: number } } }
38
+ | undefined;
39
+ const port = manifest?.env?.resolved?.routerPort;
40
+
41
+ if (typeof port !== 'number' || port <= 0) return undefined;
42
+
43
+ return String(port);
44
+ };
45
+
46
+ const getRouterPort = () => {
47
+ const overridePort = typeof cli.args.port === 'string' && cli.args.port ? cli.args.port : '';
48
+ if (overridePort) return overridePort;
49
+
50
+ const envPort = process.env.PORT?.trim();
51
+ if (envPort) return envPort;
52
+
53
+ const manifestPort = getRouterPortFromManifest();
54
+ if (manifestPort) return manifestPort;
55
+
56
+ throw new UsageError(
57
+ `Could not determine the router port from PORT or .proteum/manifest.json in ${cli.args.workdir as string}. Pass --port or --url explicitly.`,
58
+ );
59
+ };
60
+
61
+ const getRouterBaseUrls = () => {
62
+ const explicitUrl = typeof cli.args.url === 'string' && cli.args.url ? cli.args.url.trim() : '';
63
+ if (explicitUrl) return [normalizeBaseUrl(explicitUrl)];
64
+
65
+ const port = getRouterPort();
66
+ return [...new Set([`http://127.0.0.1:${port}`, `http://localhost:${port}`, `http://[::1]:${port}`])];
67
+ };
68
+
69
+ const getSessionErrorMessage = (body: TDevSessionErrorResponse | object | string | undefined, statusCode: number) => {
70
+ if (typeof body === 'object' && body !== null && 'error' in body && typeof body.error === 'string') {
71
+ return body.error;
72
+ }
73
+
74
+ return `Session request failed with status ${statusCode}.`;
75
+ };
76
+
77
+ const hasStructuredSessionError = (body: TDevSessionErrorResponse | object | string | undefined): body is TDevSessionErrorResponse =>
78
+ typeof body === 'object' && body !== null && 'error' in body && typeof body.error === 'string';
79
+
80
+ const requestSession = async (email: string, role: string) => {
81
+ const attempts: string[] = [];
82
+
83
+ for (const baseUrl of getRouterBaseUrls()) {
84
+ try {
85
+ const response = await got(`${baseUrl}/__proteum/session/start`, {
86
+ method: 'POST',
87
+ json: role ? { email, role } : { email },
88
+ responseType: 'json',
89
+ throwHttpErrors: false,
90
+ retry: { limit: 0 },
91
+ });
92
+
93
+ if (response.statusCode >= 400) {
94
+ if (response.statusCode === 404 && !hasStructuredSessionError(response.body as TDevSessionErrorResponse | object | string | undefined)) {
95
+ attempts.push(`${baseUrl}/__proteum/session/start: returned 404`);
96
+ continue;
97
+ }
98
+
99
+ throw new UsageError(
100
+ getSessionErrorMessage(response.body as TDevSessionErrorResponse | object | string | undefined, response.statusCode),
101
+ );
102
+ }
103
+
104
+ return {
105
+ baseUrl,
106
+ response: response.body as TDevSessionStartResponse,
107
+ };
108
+ } catch (error) {
109
+ if (error instanceof UsageError) throw error;
110
+
111
+ const message = error instanceof Error ? error.message : String(error);
112
+ attempts.push(`${baseUrl}/__proteum/session/start: ${message}`);
113
+ }
114
+ }
115
+
116
+ throw new UsageError(
117
+ [
118
+ 'Could not reach the Proteum session server.',
119
+ ...attempts.map((attempt) => `- ${attempt}`),
120
+ 'Make sure the app is running with `proteum dev`, or omit --port/--url to run the session request locally.',
121
+ ].join('\n'),
122
+ );
123
+ };
124
+
125
+ const buildSessionOutput = ({
126
+ baseUrl,
127
+ response,
128
+ }: {
129
+ baseUrl: string;
130
+ response: TDevSessionStartResponse;
131
+ }): TResolvedSessionOutput => {
132
+ const expires = Math.floor(Date.parse(response.session.expiresAt) / 1000);
133
+ const secure = new URL(baseUrl).protocol === 'https:';
134
+
135
+ return {
136
+ baseUrl,
137
+ user: response.user,
138
+ session: response.session,
139
+ browserCookie: `${response.session.cookieName}=${response.session.token}; Path=/`,
140
+ curlCookieHeader: `Cookie: ${response.session.cookieName}=${response.session.token}`,
141
+ playwright: {
142
+ cookies: [
143
+ {
144
+ name: response.session.cookieName,
145
+ value: response.session.token,
146
+ url: baseUrl,
147
+ expires,
148
+ httpOnly: false,
149
+ secure,
150
+ sameSite: 'Lax',
151
+ },
152
+ ],
153
+ },
154
+ };
155
+ };
156
+
157
+ const printJson = (value: object) => {
158
+ console.log(JSON.stringify(value, null, 2));
159
+ };
160
+
161
+ const renderSession = (value: TResolvedSessionOutput) =>
162
+ [
163
+ `Session ${value.user.email}`,
164
+ `- baseUrl=${value.baseUrl}`,
165
+ `- roles=${value.user.roles.join(',')}`,
166
+ `- expiresAt=${value.session.expiresAt}`,
167
+ 'Token',
168
+ value.session.token,
169
+ 'Playwright',
170
+ JSON.stringify(value.playwright, null, 2),
171
+ 'Browser Cookie',
172
+ value.browserCookie,
173
+ ].join('\n');
174
+
175
+ const runLocalSession = async (email: string, role: string) => {
176
+ const runnerFilepath = path.join(cli.paths.core.root, 'cli', 'commands', 'sessionLocalRunner.js');
177
+
178
+ return await new Promise<{ baseUrl: string; response: TDevSessionStartResponse }>((resolve, reject) => {
179
+ const stdoutChunks: Buffer[] = [];
180
+ const stderrChunks: Buffer[] = [];
181
+ const child = spawn(process.execPath, [runnerFilepath, cli.args.workdir as string, email, role], {
182
+ cwd: cli.args.workdir as string,
183
+ env: { ...process.env },
184
+ stdio: ['ignore', 'pipe', 'pipe'],
185
+ });
186
+
187
+ child.stdout.on('data', (chunk: Buffer | string) => {
188
+ stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
189
+ });
190
+
191
+ child.stderr.on('data', (chunk: Buffer | string) => {
192
+ stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
193
+ });
194
+
195
+ child.on('error', (error) => reject(error));
196
+ child.on('close', () => {
197
+ const stdout = Buffer.concat(stdoutChunks).toString('utf8');
198
+ const stderr = Buffer.concat(stderrChunks).toString('utf8');
199
+ const markerLine = stdout
200
+ .split(/\r?\n/)
201
+ .find((line) => line.startsWith(localSessionResultMarker));
202
+
203
+ if (stderr.trim()) {
204
+ process.stderr.write(stderr.endsWith('\n') ? stderr : `${stderr}\n`);
205
+ }
206
+
207
+ if (!markerLine) {
208
+ reject(
209
+ new Error(
210
+ ['Local session runner exited without returning a structured result.', stdout.trim() ? `stdout:\n${stdout.trim()}` : undefined]
211
+ .filter(Boolean)
212
+ .join('\n\n'),
213
+ ),
214
+ );
215
+ return;
216
+ }
217
+
218
+ const payload = JSON.parse(markerLine.slice(localSessionResultMarker.length)) as
219
+ | { session: { baseUrl: string; response: TDevSessionStartResponse } }
220
+ | { error: string };
221
+
222
+ if ('session' in payload) {
223
+ resolve(payload.session);
224
+ return;
225
+ }
226
+
227
+ reject(new Error(payload.error || 'Session runner failed.'));
228
+ });
229
+ });
230
+ };
231
+
232
+ export const run = async () => {
233
+ const email = typeof cli.args.email === 'string' ? cli.args.email.trim() : '';
234
+ const role = typeof cli.args.role === 'string' ? cli.args.role.trim() : '';
235
+ const shouldPrintJson = cli.args.json === true;
236
+ const shouldUseRemoteServer =
237
+ (typeof cli.args.port === 'string' && cli.args.port.length > 0) ||
238
+ (typeof cli.args.url === 'string' && cli.args.url.length > 0);
239
+
240
+ if (!email) {
241
+ throw new UsageError('An email is required. Example: proteum session admin@example.com --role ADMIN');
242
+ }
243
+
244
+ const resolved = buildSessionOutput(
245
+ shouldUseRemoteServer ? await requestSession(email, role) : await runLocalSession(email, role),
246
+ );
247
+
248
+ if (shouldPrintJson) {
249
+ printJson(resolved);
250
+ return;
251
+ }
252
+
253
+ console.log(renderSession(resolved));
254
+ };