proteum 2.2.2-1 → 2.2.3

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 CHANGED
@@ -82,8 +82,8 @@ Do not stop at static analysis for routing, controllers, generated code, SSR, cl
82
82
  - When validating a concrete route, controller path, or failing page on a running dev server, prefer `proteum diagnose <path> --port <port>` first. Use raw `proteum trace ...` output when you need lower-level event detail beyond the diagnose summary.
83
83
  - When the issue is latency, CPU, SQL cost, render cost, or memory drift, inspect `proteum perf top`, `proteum perf request`, `proteum perf compare`, or `proteum perf memory` against the running dev server before adding custom instrumentation.
84
84
  - When a framework change can affect shipped client code size, run `proteum build --prod --analyze` for static bundle artifacts or `proteum build --prod --analyze --analyze-serve --analyze-port auto` when you need a local analyzer URL.
85
- - For protected browser or API flows in dev, prefer `npx proteum session <email> --role <role>` to mint a dev auth cookie instead of automating the login UI. Use the login UI only when login itself is the feature under test.
86
- - When a task needs browser execution instead of the higher-level verifier, prefer `npx proteum verify browser <path>` or direct Playwright with a disposable profile. Keep auth sourced from `npx proteum session`, not UI login or shared browser state.
85
+ - For protected browser or API flows in dev, prefer `npx proteum session <email> --role <role>` or `npx proteum e2e --session-email <email> --session-role <role>` instead of automating the login UI. Use the login UI only when login itself is the feature under test.
86
+ - When a task needs browser execution instead of the higher-level verifier, prefer `npx proteum verify browser <path>` or `npx proteum e2e --port <port>` for Playwright suites. Keep auth sourced from Proteum session helpers, not UI login or shared browser state.
87
87
  - For request-time behavior, arm traces with `proteum trace arm --capture deep`, reproduce once, then inspect `proteum trace latest` or `proteum trace show <requestId>`.
88
88
  - When the framework-facing workflow itself changed, verify the CLI surface too with `proteum verify framework-change --crosspath-port <port> --product-port <port> --website-port <port>`.
89
89
  - Only the final verifier agent should usually run browser flows. Other agents should stay on `orient`, `verify owner`, `verify request`, and command-level checks unless browser execution is the only trustworthy surface.
package/README.md CHANGED
@@ -344,6 +344,7 @@ Proteum ships with a compact CLI focused on the real app lifecycle:
344
344
  | `proteum trace` | Inspect live dev-only request traces from the running SSR server |
345
345
  | `proteum command` | Run a dev-only internal command locally or against a running dev server |
346
346
  | `proteum session` | Mint a dev-only auth session token and Playwright-ready cookie payload |
347
+ | `proteum e2e` | Run Playwright with Proteum-managed `E2E_*` values instead of shell-leading env assignments |
347
348
  | `proteum verify` | Validate framework-facing workflows across one or more running dev apps; `framework-change` is the built-in cross-reference-app check |
348
349
  | `proteum init` | Scaffold a new Proteum app with built-in deterministic templates |
349
350
  | `proteum configure agents` | Interactively configure Proteum-managed instruction symlinks and confirm overwrites for standalone or monorepo apps |
@@ -386,6 +387,7 @@ proteum command proteum/diagnostics/ping
386
387
  proteum command proteum/diagnostics/ping --port 3101
387
388
  proteum session admin@example.com --role ADMIN --port 3101
388
389
  proteum session god@example.com --role GOD --json
390
+ proteum e2e --port 3101 --session-email admin@example.com --session-role ADMIN tests/e2e/features/admin.spec.ts
389
391
  proteum trace requests
390
392
  proteum trace arm --capture deep
391
393
  proteum trace latest
@@ -523,6 +525,7 @@ Proteum answers those questions with explicit artifacts:
523
525
  - the profiler `Explain`, `Doctor`, `Diagnose`, and `Perf` tabs for a human-readable view over the same diagnostics and trace-derived perf contracts
524
526
  - `proteum command ...` plus the profiler `Commands` tab for dev-only internal execution
525
527
  - `proteum session ...` for explicit authenticated dev browser or API bootstrapping without login UI automation
528
+ - `proteum e2e ...` for Playwright runs that need `E2E_BASE_URL`, `E2E_PORT`, or `E2E_AUTH_TOKEN` without shell-leading env assignments
526
529
 
527
530
  If you are an LLM or automation agent, start here:
528
531
 
@@ -533,7 +536,7 @@ If you are an LLM or automation agent, start here:
533
536
  5. Inspect `server/controllers/**` for request entrypoints.
534
537
  6. Inspect `server/services/**` for business logic.
535
538
  7. Inspect `client/pages/**` for SSR routes and page data contracts.
536
- 8. If the task touches a protected route or controller in dev and login UX is not the feature under test, use `proteum session <email> --role <role>` before Playwright or direct HTTP calls.
539
+ 8. If the task touches a protected route or controller in dev and login UX is not the feature under test, use `proteum e2e --session-email <email> --session-role <role>` for Playwright suites or `proteum session <email> --role <role>` before direct HTTP calls.
537
540
 
538
541
  For implementation rules in a real Proteum app, treat the local `AGENTS.md` files plus `proteum explain`, `proteum doctor`, `proteum diagnose`, `proteum perf`, and `proteum trace` as the task contract. This README is the framework overview, not the project-local instruction layer.
539
542
 
@@ -56,7 +56,7 @@ Coding style source of truth: root-level `CODING_STYLE.md`.
56
56
  - When starting a long-lived dev server for an agent task, always request elevated permissions and run `npx proteum dev` outside the sandbox. Use an explicit task/thread-scoped session file such as `var/run/proteum/dev/agents/<task>.json`, inspect `npx proteum dev list --json` plus current listeners first, for example with `lsof -nP -iTCP -sTCP:LISTEN`, then choose a port that is not currently used before starting `npx proteum dev --session-file <path> --port <port>`. After the server is ready, print the live server URL as a clickable Markdown link.
57
57
  - Use `--replace-existing` only when restarting the exact session file started by the current thread/task. Never replace another live session that belongs to a user, another thread, or an unknown owner.
58
58
  - If the current app depends on local `file:` connected projects, boot every connected producer app too, each with its own task-scoped session file and free port, and run every one of those `proteum dev` processes with elevated permissions outside the sandbox before starting or verifying the consumer app.
59
- - For raw browser automation, use `npx proteum verify browser` when it matches the task, or direct Playwright with a disposable profile when lower-level control is required. Bootstrap protected browser state through `npx proteum session`.
59
+ - For raw browser automation, use `npx proteum verify browser` when it matches the task, or `npx proteum e2e --port <port>` for targeted/full Playwright suites. Bootstrap protected browser state through `npx proteum e2e --session-email <email> --session-role <role>` or `npx proteum session`.
60
60
  - Current CLI banner contract: only the bare `proteum build` and bare `proteum dev` commands print the welcome banner and include the active Proteum installation method. Any extra argument or option skips the banner. Only `proteum dev` clears the interactive terminal before rendering, exposes `CTRL+R` reload plus `CTRL+C` shutdown hotkeys in its session UI, and reports connected app names plus successful connected `/ping` checks in the ready banner. When the app root is missing `AGENTS.md`, the bare interactive `proteum dev` start offers to launch `proteum configure agents` before the dev loop begins.
61
61
 
62
62
  ### Before Finishing
@@ -179,8 +179,8 @@ Verify at the correct layer:
179
179
  - Router or plugin changes: verify request context, auth, redirects, metrics, and validation on a running app.
180
180
  - New features or feature-behavior changes: use the cheapest trustworthy verification while iterating, then update the relevant end-to-end coverage and finish by running the full Playwright suite.
181
181
  - Generated, connected, or ownership-ambiguous changes: start with `npx proteum orient <query>` and prefer `npx proteum verify owner <query>` before broad global checks.
182
- - Browser-visible issues: prefer `npx proteum verify browser <path>` or the narrowest targeted Playwright pass only after request-level verification is insufficient.
183
- - Raw browser execution beyond `npx proteum verify browser`: use direct Playwright with a disposable profile, and keep that step for the final verifier agent unless a narrower surface cannot reproduce the issue.
182
+ - Browser-visible issues: prefer `npx proteum verify browser <path>` or the narrowest `npx proteum e2e --port <port> ...` Playwright pass only after request-level verification is insufficient.
183
+ - Raw browser execution beyond `npx proteum verify browser`: use `npx proteum e2e --port <port>` first, then direct Playwright with a disposable profile only when the wrapper cannot express the needed control. Keep that step for the final verifier agent unless a narrower surface cannot reproduce the issue.
184
184
  - For trace-first reproduction, session-based auth setup, temporary logs, and post-fix surface checks, follow root-level `diagnostics.md`.
185
185
 
186
186
  ## Implementation Rules
@@ -17,7 +17,7 @@ This file is the canonical source of truth for diagnostics, temporary instrument
17
17
  - Only the bare `npx proteum build` and bare `npx proteum dev` commands print the welcome banner and active Proteum installation method. Any extra argument or option skips the banner. Only `npx proteum dev` clears the interactive terminal before rendering and reports connected app names plus successful connected `/ping` checks in the ready banner; keep that in mind when capturing or comparing command logs during diagnosis. When the app root is missing `AGENTS.md`, the bare interactive `npx proteum dev` start offers to launch `npx proteum configure agents` before the dev loop begins.
18
18
  - For ownership or repo discovery questions, start with `npx proteum orient <query>` instead of jumping straight into source searches.
19
19
  - For request-time issues in dev, start with `npx proteum diagnose <path> --port <port>` when you have a concrete failing route, page, controller path, or request target. It combines owner lookup, manifest diagnostics, contract diagnostics, matching trace data, and buffered server logs in one pass.
20
- - Prefer focused verification before global checks: `npx proteum verify owner <query>`, `npx proteum verify request <path>`, and only then `npx proteum verify browser <path>` or targeted Playwright when the bug is browser-visible.
20
+ - Prefer focused verification before global checks: `npx proteum verify owner <query>`, `npx proteum verify request <path>`, and only then `npx proteum verify browser <path>` or `npx proteum e2e --port <port> ...` when the bug is browser-visible.
21
21
  - When diagnosing a consumer app that depends on local `file:` connected projects, boot every connected producer app too, each on its own free port and task-scoped session file, and run every one of those `proteum dev` processes with elevated permissions outside the sandbox before reproducing the consumer issue.
22
22
  - For connected-project failures, confirm the consumer app resolves the expected `connect.<Namespace>.source` and `connect.<Namespace>.urlInternal` values, the producer app exposes `GET /api/__proteum/connected/ping`, and the imported controller entries show `scope=connected` in `proteum explain`.
23
23
  - Use `npx proteum explain owner <query>` when you need a fast ownership graph for a route, controller path, source file, or generated artifact before reading code.
@@ -28,7 +28,7 @@ This file is the canonical source of truth for diagnostics, temporary instrument
28
28
  - If existing traces are insufficient, arm `npx proteum trace arm --capture deep`, reproduce once, then inspect the new request with `npx proteum trace latest` or `npx proteum trace show <requestId>`.
29
29
  - Inspect browser console errors and warnings for frontend, SSR, hydration, and controller-call issues.
30
30
  - Inspect server startup and runtime errors.
31
- - For protected browser or API flows in dev, prefer `npx proteum session <email> --role <role>` over driving the login UI. Feed that auth into `npx proteum verify browser ...` or direct Playwright. Use the login UI only when auth UX itself is under test.
31
+ - For protected browser or API flows in dev, prefer `npx proteum session <email> --role <role>` over driving the login UI. Feed that auth into `npx proteum verify browser ...`, or use `npx proteum e2e --session-email <email> --session-role <role>` so Playwright receives the auth token through the child process environment. Use the login UI only when auth UX itself is under test.
32
32
 
33
33
  ## Temporary Instrumentation
34
34
 
@@ -53,7 +53,7 @@ This file is the canonical source of truth for diagnostics, temporary instrument
53
53
  - For compile-time or type-safety issues, start with the relevant targeted typecheck or build command. Do not run them by default for unrelated runtime, copy, docs, or local refactor changes.
54
54
  - For request/runtime issues, verify through the real page, route, generated controller call, or command on a running app.
55
55
  - Start the smallest trustworthy runtime surface first: `npx proteum orient <query>`, then the relevant real URL, generated controller call, command, or `npx proteum diagnose <path> --port <port>`. Add targeted Playwright coverage only when request-level verification is insufficient or the change is browser-visible.
56
- - Proteum does not provide a dedicated raw browser-runtime CLI. When `npx proteum verify browser` is insufficient, use direct Playwright with a disposable profile. Do not launch raw browser automation against a shared persistent profile.
56
+ - When `npx proteum verify browser` is insufficient, use `npx proteum e2e --port <port>` for targeted or full Playwright suites. Use direct Playwright with a disposable profile only when the wrapper cannot express the needed browser control. Do not launch raw browser automation against a shared persistent profile.
57
57
  - Focused verification should treat unrelated global diagnostics as visible but non-blocking by default. Use `--strict-global` only when the task explicitly requires broad clean-room validation.
58
58
  - For browser regressions, prefer a real browser repro first and add targeted Playwright coverage only when the user asks for automated coverage, when a stable regression path needs automation, or when manual/browser verification is insufficient.
59
59
  - Only the final verifier agent should usually run browser flows. Earlier agents should stay on `orient`, `verify owner`, `verify request`, `diagnose`, and command-level checks unless browser execution is the only trustworthy reproducer.
@@ -46,7 +46,7 @@ Coding style source of truth: root-level `CODING_STYLE.md`.
46
46
  - When starting a long-lived dev server for an agent task, always request elevated permissions and run `npx proteum dev` outside the sandbox. Use an explicit task/thread-scoped session file such as `var/run/proteum/dev/agents/<task>.json`, inspect `npx proteum dev list --json` plus current listeners first, for example with `lsof -nP -iTCP -sTCP:LISTEN`, then choose a port that is not currently used before starting `npx proteum dev --session-file <path> --port <port>`. After the server is ready, print the live server URL as a clickable Markdown link.
47
47
  - Use `--replace-existing` only when restarting the exact session file started by the current thread/task. Never replace another live session that belongs to a user, another thread, or an unknown owner.
48
48
  - If the current app depends on local `file:` connected projects, boot every connected producer app too, each with its own task-scoped session file and free port, and run every one of those `proteum dev` processes with elevated permissions outside the sandbox before starting or verifying the consumer app.
49
- - For raw browser automation, use `npx proteum verify browser` when it matches the task, or direct Playwright with a disposable profile when lower-level control is required. Bootstrap protected browser state through `npx proteum session`.
49
+ - For raw browser automation, use `npx proteum verify browser` when it matches the task, or `npx proteum e2e --port <port>` for targeted/full Playwright suites. Bootstrap protected browser state through `npx proteum e2e --session-email <email> --session-role <role>` or `npx proteum session`.
50
50
  - Current CLI banner contract: only the bare `proteum build` and bare `proteum dev` commands print the welcome banner and include the active Proteum installation method. Any extra argument or option skips the banner. Only `proteum dev` clears the interactive terminal before rendering, exposes `CTRL+R` reload plus `CTRL+C` shutdown hotkeys in its session UI, and reports connected app names plus successful connected `/ping` checks in the ready banner. When the app root is missing `AGENTS.md`, the bare interactive `proteum dev` start offers to launch `proteum configure agents` before the dev loop begins.
51
51
 
52
52
  ### Before Finishing
@@ -169,8 +169,8 @@ Verify at the correct layer:
169
169
  - Router or plugin changes: verify request context, auth, redirects, metrics, and validation on a running app.
170
170
  - New features or feature-behavior changes: use the cheapest trustworthy verification while iterating, then update the relevant end-to-end coverage and finish by running the full Playwright suite.
171
171
  - Generated, connected, or ownership-ambiguous changes: start with `npx proteum orient <query>` and prefer `npx proteum verify owner <query>` before broad global checks.
172
- - Browser-visible issues: prefer `npx proteum verify browser <path>` or the narrowest targeted Playwright pass only after request-level verification is insufficient.
173
- - Raw browser execution beyond `npx proteum verify browser`: use direct Playwright with a disposable profile, and keep that step for the final verifier agent unless a narrower surface cannot reproduce the issue.
172
+ - Browser-visible issues: prefer `npx proteum verify browser <path>` or the narrowest `npx proteum e2e --port <port> ...` Playwright pass only after request-level verification is insufficient.
173
+ - Raw browser execution beyond `npx proteum verify browser`: use `npx proteum e2e --port <port>` first, then direct Playwright with a disposable profile only when the wrapper cannot express the needed control. Keep that step for the final verifier agent unless a narrower surface cannot reproduce the issue.
174
174
  - For trace-first reproduction, session-based auth setup, temporary logs, and post-fix surface checks, follow root-level `diagnostics.md`.
175
175
 
176
176
  ## Implementation Rules
@@ -10,7 +10,7 @@ Diagnostics source of truth: root-level `diagnostics.md`.
10
10
  - Understand the real user flow and the main feature branches before writing tests.
11
11
  - Test the current controller/page runtime model, not legacy `@Route` or `api.fetch(...)` behavior.
12
12
  - Verify routing, controllers, SSR, and router plugins against a running app when behavior depends on real request handling.
13
- - After implementing a new feature or changing existing feature behavior, update the end-to-end coverage for that behavior and run the full Playwright suite before finishing. Use a real browser repro against a running app during iteration when it is the fastest trustworthy loop.
13
+ - After implementing a new feature or changing existing feature behavior, update the end-to-end coverage for that behavior and run the full Playwright suite before finishing. Prefer `npx proteum e2e --port <port>` for Playwright runs so base URLs and auth tokens are passed through Proteum-managed child env instead of shell-leading environment assignments. Use a real browser repro against a running app during iteration when it is the fastest trustworthy loop.
14
14
  - Exercise real URLs, generated controller calls, or real browser flows instead of re-deriving framework internals in tests.
15
15
  - Organize end-to-end tests following the Crosspath platform layout under `tests/e2e/**`.
16
16
  - Put runnable scenario entrypoints in `tests/e2e/features/**`, `tests/e2e/specs/<domain>/**`, or `tests/e2e/journeys/**` depending on scope.
@@ -22,4 +22,4 @@ Diagnostics source of truth: root-level `diagnostics.md`.
22
22
  - Add `data-testid` where needed instead of relying on brittle selectors.
23
23
  - Keep end-to-end tests clean, well organized, and non-redundant. Prefer extending or reshaping the most relevant existing scenario over duplicating coverage, and remove or consolidate overlap when the suite becomes repetitive.
24
24
  - Reuse root catalog files from `/client/catalogs/**`, `/server/catalogs/**`, or `/common/catalogs/**` instead of duplicating catalog constants in tests.
25
- - For protected dev flows, prefer `npx proteum session <email> --role <role>` over automating login unless the login flow itself is under test.
25
+ - For protected dev flows, prefer `npx proteum e2e --session-email <email> --session-role <role>` or `npx proteum session <email> --role <role>` over automating login unless the login flow itself is under test.
package/cli/app/index.ts CHANGED
@@ -36,6 +36,12 @@ const parseRouterPortOverride = (rawPort: string | boolean | string[] | undefine
36
36
 
37
37
  const normalizeModulePath = (value: string) => value.replace(/\\/g, '/').replace(/\/$/, '');
38
38
 
39
+ const resolveSideTsconfig = (appRoot: string, side: TAppSide) => {
40
+ const candidates = [path.join(appRoot, side, 'tsconfig.json'), path.join(appRoot, side, 'app.tsconfig.json')];
41
+
42
+ return candidates.find((candidate) => fs.existsSync(candidate));
43
+ };
44
+
39
45
  const resolveTranspileModuleDirectories = ({
40
46
  moduleNames,
41
47
  resolvePackageRoot,
@@ -183,17 +189,21 @@ export class App {
183
189
  ----------------------------------*/
184
190
 
185
191
  public aliases = {
186
- client: new TsAlias({
187
- rootDir: this.paths.root + '/client',
188
- modulesDir: [cli.paths.framework.appNodeModulesRoot, cli.paths.framework.frameworkNodeModulesRoot],
189
- debug: false,
190
- }),
191
- server: new TsAlias({
192
- rootDir: this.paths.root + '/server',
192
+ client: this.createSideAliases('client'),
193
+ server: this.createSideAliases('server'),
194
+ };
195
+
196
+ private createSideAliases(side: TAppSide) {
197
+ const tsconfigFilepath = resolveSideTsconfig(this.paths.root, side);
198
+
199
+ if (!tsconfigFilepath) return new TsAlias({ aliases: [] });
200
+
201
+ return new TsAlias({
202
+ rootDir: tsconfigFilepath,
193
203
  modulesDir: [cli.paths.framework.appNodeModulesRoot, cli.paths.framework.frameworkNodeModulesRoot],
194
204
  debug: false,
195
- }),
196
- };
205
+ });
206
+ }
197
207
 
198
208
  private loadPkg() {
199
209
  return fs.readJSONSync(this.paths.root + '/package.json');
@@ -1,5 +1,5 @@
1
1
  import cli from '..';
2
- import { refreshGeneratedTypings, runAppLint, runAppTypecheck } from '../utils/check';
2
+ import { hasAppConfig, refreshGeneratedTypings, runAppLint, runAppTypecheck } from '../utils/check';
3
3
  import { renderRows } from '../presentation/layout';
4
4
  import { renderStep, renderSuccess, renderTitle } from '../presentation/ink';
5
5
 
@@ -23,8 +23,12 @@ export const run = async (): Promise<void> => {
23
23
  renderRows([{ label: 'app', value: cli.paths.appRoot === process.cwd() ? '.' : cli.paths.appRoot }]),
24
24
  ].join('\n\n'),
25
25
  );
26
- console.info(await renderStep('[1/3]', 'Refreshing generated typings.'));
27
- await refreshGeneratedTypings();
26
+ if (hasAppConfig()) {
27
+ console.info(await renderStep('[1/3]', 'Refreshing generated typings.'));
28
+ await refreshGeneratedTypings();
29
+ } else {
30
+ console.info(await renderStep('[1/3]', 'Skipping generated typings: no Proteum app config found.'));
31
+ }
28
32
  console.info(await renderStep('[2/3]', 'Running TypeScript typechecking.'));
29
33
  await runAppTypecheck();
30
34
  console.info(await renderStep('[3/3]', 'Running ESLint.'));
@@ -13,7 +13,7 @@ import cli from '..';
13
13
  import { renderRows } from '../presentation/layout';
14
14
  import { isLikelyProteumAppRoot } from '../presentation/commands';
15
15
  import { renderStep, renderSuccess, renderTitle, renderWarning } from '../presentation/ink';
16
- import { configureProjectAgentSymlinks, type TConfigureProjectAgentSymlinksResult } from '../utils/agents';
16
+ import { configureProjectAgentInstructions, type TConfigureProjectAgentInstructionsResult } from '../utils/agents';
17
17
 
18
18
  /*----------------------------------
19
19
  - HELPERS
@@ -93,7 +93,12 @@ const promptBlockedOverwritePaths = async (blockedPaths: string[]) => {
93
93
  if (blockedPaths.length === 0) return [];
94
94
 
95
95
  console.info(await renderWarning('Proteum found existing non-managed instruction paths.'));
96
- console.info(['Choose whether to overwrite each path with a Proteum-managed symlink:', ...blockedPaths.map((entry) => `- ${entry}`)].join('\n'));
96
+ console.info(
97
+ [
98
+ 'Choose whether to overwrite each path with a Proteum-managed instruction stub:',
99
+ ...blockedPaths.map((entry) => `- ${entry}`),
100
+ ].join('\n'),
101
+ );
97
102
 
98
103
  const overwriteBlockedPaths: string[] = [];
99
104
 
@@ -118,7 +123,7 @@ const promptBlockedOverwritePaths = async (blockedPaths: string[]) => {
118
123
  return overwriteBlockedPaths;
119
124
  };
120
125
 
121
- const renderConfigureResultSections = (result: TConfigureProjectAgentSymlinksResult) => {
126
+ const renderConfigureResultSections = (result: TConfigureProjectAgentInstructionsResult) => {
122
127
  const sections: string[] = [];
123
128
 
124
129
  sections.push(
@@ -178,7 +183,7 @@ export const runConfigureAgentsWizard = async ({
178
183
  : undefined;
179
184
  console.info(
180
185
  [
181
- await renderTitle('PROTEUM CONFIGURE AGENTS', 'Configure Proteum-managed instruction symlinks.'),
186
+ await renderTitle('PROTEUM CONFIGURE AGENTS', 'Configure Proteum-managed instruction stubs.'),
182
187
  renderRows([{ label: 'app', value: appRoot === process.cwd() ? '.' : appRoot }]),
183
188
  ].join('\n\n'),
184
189
  );
@@ -204,7 +209,7 @@ export const runConfigureAgentsWizard = async ({
204
209
  })
205
210
  : undefined;
206
211
 
207
- const preview = configureProjectAgentSymlinks({
212
+ const preview = configureProjectAgentInstructions({
208
213
  appRoot,
209
214
  coreRoot,
210
215
  dryRun: true,
@@ -216,12 +221,12 @@ export const runConfigureAgentsWizard = async ({
216
221
  await renderStep(
217
222
  '[1/1]',
218
223
  isMonorepo
219
- ? `Writing monorepo-aware instruction symlinks using ${monorepoRoot}.`
220
- : 'Writing standalone instruction symlinks.',
224
+ ? `Writing monorepo-aware instruction stubs using ${monorepoRoot}.`
225
+ : 'Writing standalone instruction stubs.',
221
226
  ),
222
227
  );
223
228
 
224
- const result = configureProjectAgentSymlinks({
229
+ const result = configureProjectAgentInstructions({
225
230
  appRoot,
226
231
  coreRoot,
227
232
  monorepoRoot,
@@ -229,7 +234,7 @@ export const runConfigureAgentsWizard = async ({
229
234
  });
230
235
  const sections = renderConfigureResultSections(result);
231
236
 
232
- console.info(await renderSuccess('Proteum-managed instruction symlinks are configured.'));
237
+ console.info(await renderSuccess('Proteum-managed instruction stubs are configured.'));
233
238
 
234
239
  if (sections.length > 0) console.info(`\n${sections.join('\n\n')}`);
235
240
  };
@@ -0,0 +1,204 @@
1
+ import { spawn } from 'child_process';
2
+ import dotenv from 'dotenv';
3
+ import fs from 'fs-extra';
4
+ import got from 'got';
5
+ import path from 'path';
6
+ import { UsageError } from 'clipanion';
7
+
8
+ import cli from '..';
9
+ import type { TDevSessionErrorResponse, TDevSessionStartResponse } from '../../common/dev/session';
10
+
11
+ type TPlaywrightInvocation = {
12
+ command: string;
13
+ args: string[];
14
+ };
15
+
16
+ const normalizeBaseUrl = (value: string) => value.replace(/\/+$/, '');
17
+
18
+ const getRouterPortFromManifest = () => {
19
+ const manifestFilepath = path.join(cli.args.workdir as string, '.proteum', 'manifest.json');
20
+ if (!fs.existsSync(manifestFilepath)) return undefined;
21
+
22
+ const manifest = fs.readJsonSync(manifestFilepath, { throws: false }) as
23
+ | { env?: { resolved?: { routerPort?: number } } }
24
+ | undefined;
25
+ const port = manifest?.env?.resolved?.routerPort;
26
+
27
+ if (typeof port !== 'number' || port <= 0) return undefined;
28
+
29
+ return String(port);
30
+ };
31
+
32
+ const getRouterPort = () => {
33
+ const overridePort = typeof cli.args.port === 'string' && cli.args.port ? cli.args.port : '';
34
+ if (overridePort) return overridePort;
35
+
36
+ const manifestPort = getRouterPortFromManifest();
37
+ if (manifestPort) return manifestPort;
38
+
39
+ return '';
40
+ };
41
+
42
+ const getBaseUrlCandidates = () => {
43
+ const explicitUrl = typeof cli.args.url === 'string' && cli.args.url ? cli.args.url.trim() : '';
44
+ if (explicitUrl) return [normalizeBaseUrl(explicitUrl)];
45
+
46
+ const port = getRouterPort();
47
+ if (!port) return [];
48
+
49
+ return [...new Set([`http://localhost:${port}`, `http://127.0.0.1:${port}`, `http://[::1]:${port}`])];
50
+ };
51
+
52
+ const getSessionErrorMessage = (body: TDevSessionErrorResponse | object | string | undefined, statusCode: number) => {
53
+ if (typeof body === 'object' && body !== null && 'error' in body && typeof body.error === 'string') {
54
+ return body.error;
55
+ }
56
+
57
+ return `Session request failed with status ${statusCode}.`;
58
+ };
59
+
60
+ const hasStructuredSessionError = (body: TDevSessionErrorResponse | object | string | undefined): body is TDevSessionErrorResponse =>
61
+ typeof body === 'object' && body !== null && 'error' in body && typeof body.error === 'string';
62
+
63
+ const requestSession = async ({ email, role }: { email: string; role: string }) => {
64
+ const attempts: string[] = [];
65
+
66
+ for (const baseUrl of getBaseUrlCandidates()) {
67
+ try {
68
+ const response = await got(`${baseUrl}/__proteum/session/start`, {
69
+ method: 'POST',
70
+ json: role ? { email, role } : { email },
71
+ responseType: 'json',
72
+ throwHttpErrors: false,
73
+ retry: { limit: 0 },
74
+ });
75
+
76
+ if (response.statusCode >= 400) {
77
+ if (response.statusCode === 404 && !hasStructuredSessionError(response.body as TDevSessionErrorResponse | object | string | undefined)) {
78
+ attempts.push(`${baseUrl}/__proteum/session/start: returned 404`);
79
+ continue;
80
+ }
81
+
82
+ throw new UsageError(
83
+ getSessionErrorMessage(response.body as TDevSessionErrorResponse | object | string | undefined, response.statusCode),
84
+ );
85
+ }
86
+
87
+ return {
88
+ baseUrl,
89
+ token: (response.body as TDevSessionStartResponse).session.token,
90
+ };
91
+ } catch (error) {
92
+ if (error instanceof UsageError) throw error;
93
+
94
+ const message = error instanceof Error ? error.message : String(error);
95
+ attempts.push(`${baseUrl}/__proteum/session/start: ${message}`);
96
+ }
97
+ }
98
+
99
+ throw new UsageError(
100
+ [
101
+ 'Could not reach the Proteum session server.',
102
+ ...attempts.map((attempt) => `- ${attempt}`),
103
+ 'Start the app with `proteum dev`, then pass --port or --url to `proteum e2e`.',
104
+ ].join('\n'),
105
+ );
106
+ };
107
+
108
+ const resolveBaseUrl = (sessionBaseUrl?: string) => {
109
+ if (sessionBaseUrl) return sessionBaseUrl;
110
+
111
+ const [baseUrl] = getBaseUrlCandidates();
112
+ if (baseUrl) return baseUrl;
113
+
114
+ throw new UsageError('Could not determine E2E_BASE_URL. Pass --port or --url to `proteum e2e`.');
115
+ };
116
+
117
+ const parseEnvPair = (value: string) => {
118
+ const separatorIndex = value.indexOf('=');
119
+ if (separatorIndex <= 0) {
120
+ throw new UsageError(`Invalid --env value "${value}". Expected KEY=value.`);
121
+ }
122
+
123
+ const key = value.slice(0, separatorIndex).trim();
124
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
125
+ throw new UsageError(`Invalid --env key "${key}".`);
126
+ }
127
+
128
+ return { key, value: value.slice(separatorIndex + 1) };
129
+ };
130
+
131
+ const readEnvFiles = (filepaths: string[]) => {
132
+ const env: Record<string, string> = {};
133
+
134
+ for (const filepath of filepaths) {
135
+ const absoluteFilepath = path.resolve(cli.args.workdir as string, filepath);
136
+
137
+ if (!fs.existsSync(absoluteFilepath)) {
138
+ throw new UsageError(`Env file does not exist: ${absoluteFilepath}`);
139
+ }
140
+
141
+ Object.assign(env, dotenv.parse(fs.readFileSync(absoluteFilepath)));
142
+ }
143
+
144
+ return env;
145
+ };
146
+
147
+ const resolvePlaywrightInvocation = (appRoot: string): TPlaywrightInvocation => {
148
+ const binaryName = process.platform === 'win32' ? 'playwright.cmd' : 'playwright';
149
+ const localBinary = path.join(appRoot, 'node_modules', '.bin', binaryName);
150
+
151
+ if (fs.existsSync(localBinary)) {
152
+ return { command: localBinary, args: ['test'] };
153
+ }
154
+
155
+ return { command: 'npx', args: ['playwright', 'test'] };
156
+ };
157
+
158
+ const runPlaywright = async ({ env, playwrightArgs }: { env: Record<string, string>; playwrightArgs: string[] }) => {
159
+ const appRoot = cli.args.workdir as string;
160
+ const invocation = resolvePlaywrightInvocation(appRoot);
161
+
162
+ return await new Promise<number | null>((resolve, reject) => {
163
+ const child = spawn(invocation.command, [...invocation.args, ...playwrightArgs], {
164
+ cwd: appRoot,
165
+ env: {
166
+ ...process.env,
167
+ ...env,
168
+ },
169
+ stdio: 'inherit',
170
+ });
171
+
172
+ child.once('error', reject);
173
+ child.once('close', (exitCode) => resolve(exitCode));
174
+ });
175
+ };
176
+
177
+ export const run = async () => {
178
+ const sessionEmail = typeof cli.args.sessionEmail === 'string' ? cli.args.sessionEmail.trim() : '';
179
+ const sessionRole = typeof cli.args.sessionRole === 'string' ? cli.args.sessionRole.trim() : '';
180
+ const envFilepaths = Array.isArray(cli.args.envFile) ? cli.args.envFile : [];
181
+ const envPairs = Array.isArray(cli.args.env) ? cli.args.env : [];
182
+ const playwrightArgs = Array.isArray(cli.args.playwrightArgs) ? cli.args.playwrightArgs : [];
183
+ const explicitPort = getRouterPort();
184
+
185
+ const explicitEnv = readEnvFiles(envFilepaths);
186
+ for (const pair of envPairs) {
187
+ const parsed = parseEnvPair(pair);
188
+ explicitEnv[parsed.key] = parsed.value;
189
+ }
190
+
191
+ const session = sessionEmail ? await requestSession({ email: sessionEmail, role: sessionRole }) : undefined;
192
+ const baseUrl = resolveBaseUrl(session?.baseUrl);
193
+ const exitCode = await runPlaywright({
194
+ env: {
195
+ ...explicitEnv,
196
+ E2E_BASE_URL: baseUrl,
197
+ ...(explicitPort ? { E2E_PORT: explicitPort } : {}),
198
+ ...(session?.token ? { E2E_AUTH_TOKEN: session.token } : {}),
199
+ },
200
+ playwrightArgs,
201
+ });
202
+
203
+ return exitCode ?? 1;
204
+ };
@@ -1,5 +1,5 @@
1
1
  import cli from '..';
2
- import { refreshGeneratedTypings, runAppTypecheck } from '../utils/check';
2
+ import { hasAppConfig, refreshGeneratedTypings, runAppTypecheck } from '../utils/check';
3
3
  import { renderRows } from '../presentation/layout';
4
4
  import { renderStep, renderSuccess, renderTitle } from '../presentation/ink';
5
5
 
@@ -23,8 +23,12 @@ export const run = async (): Promise<void> => {
23
23
  renderRows([{ label: 'app', value: cli.paths.appRoot === process.cwd() ? '.' : cli.paths.appRoot }]),
24
24
  ].join('\n\n'),
25
25
  );
26
- console.info(await renderStep('[1/2]', 'Refreshing generated typings.'));
27
- await refreshGeneratedTypings();
26
+ if (hasAppConfig()) {
27
+ console.info(await renderStep('[1/2]', 'Refreshing generated typings.'));
28
+ await refreshGeneratedTypings();
29
+ } else {
30
+ console.info(await renderStep('[1/2]', 'Skipping generated typings: no Proteum app config found.'));
31
+ }
28
32
  console.info(await renderStep('[2/2]', 'Running TypeScript typechecking.'));
29
33
  await runAppTypecheck();
30
34
  console.info(await renderSuccess('Typecheck passed.'));
@@ -14,6 +14,7 @@ export const proteumCommandNames = [
14
14
  'typecheck',
15
15
  'lint',
16
16
  'check',
17
+ 'e2e',
17
18
  'connect',
18
19
  'doctor',
19
20
  'explain',
@@ -55,7 +56,7 @@ export const proteumRecommendedFlow: TRow[] = [
55
56
 
56
57
  export const proteumCommandGroups: Array<{ title: string; names: TProteumCommandName[] }> = [
57
58
  { title: 'Daily workflow', names: ['dev', 'refresh', 'build'] },
58
- { title: 'Quality gates', names: ['typecheck', 'lint', 'check'] },
59
+ { title: 'Quality gates', names: ['typecheck', 'lint', 'check', 'e2e'] },
59
60
  { title: 'Manifest and contracts', names: ['connect', 'doctor', 'explain', 'orient', 'diagnose', 'perf', 'trace', 'command', 'session', 'verify'] },
60
61
  { title: 'Project scaffolding', names: ['init', 'configure', 'create', 'migrate'] },
61
62
  ];
@@ -112,22 +113,22 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
112
113
  configure: {
113
114
  name: 'configure',
114
115
  category: 'Project scaffolding',
115
- summary: 'Interactively configure Proteum-managed instruction symlinks for a standalone app or monorepo app root.',
116
+ summary: 'Interactively configure Proteum-managed instruction stubs for a standalone app or monorepo app root.',
116
117
  usage: 'proteum configure agents',
117
118
  bestFor:
118
- 'Creating or switching the managed `AGENTS.md` instruction layout intentionally instead of having `init` or `dev` write symlinks implicitly.',
119
+ 'Creating or switching the managed `AGENTS.md` instruction layout intentionally instead of having `init` or `dev` write instruction files implicitly.',
119
120
  examples: [
120
121
  {
121
- description: 'Configure instruction symlinks for the current standalone app',
122
+ description: 'Configure instruction stubs for the current standalone app',
122
123
  command: 'proteum configure agents',
123
124
  },
124
125
  ],
125
126
  notes: [
126
- 'This command is interactive. It asks whether the current Proteum app belongs to a monorepo and, if so, which ancestor path should receive the reusable root `AGENTS.md` symlink.',
127
+ 'This command is interactive. It asks whether the current Proteum app belongs to a monorepo and, if so, which ancestor path should receive the reusable root `AGENTS.md` stub.',
127
128
  'Standalone mode writes the full app-root instruction set into the current Proteum app root.',
128
129
  'Monorepo mode writes the reusable root `AGENTS.md` into the chosen monorepo root and switches the current app root `AGENTS.md` to the app-root addendum.',
129
- 'If a target path already contains a non-managed file or foreign symlink, the interactive flow asks whether to overwrite it with the Proteum-managed symlink.',
130
- 'Declined non-managed paths are left untouched; Proteum still creates missing symlinks and updates symlinks it already manages.',
130
+ 'If a target path already contains a non-managed file or foreign symlink, the interactive flow asks whether to overwrite it with the Proteum-managed stub.',
131
+ 'Declined non-managed paths are left untouched; Proteum still creates missing stubs and updates stubs or symlinks it already manages.',
131
132
  ],
132
133
  status: 'experimental',
133
134
  },
@@ -276,6 +277,35 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
276
277
  notes: ['This command executes refresh, typecheck, then lint in that order.'],
277
278
  status: 'stable',
278
279
  },
280
+ e2e: {
281
+ name: 'e2e',
282
+ category: 'Quality gates',
283
+ summary: 'Run app Playwright tests with Proteum-managed E2E environment values.',
284
+ usage:
285
+ 'proteum e2e [--cwd <path>] [--port <port>|--url <url>] [--session-email <email>] [--session-role <role>] [--env KEY=value] [--env-file <path>] [--grep <text>] [--project <name>] [specs...]',
286
+ bestFor:
287
+ 'Running targeted or full Playwright suites without shell-leading environment assignments for base URLs, auth tokens, or per-run values.',
288
+ examples: [
289
+ {
290
+ description: 'Run the full suite against a local dev server',
291
+ command: 'proteum e2e --port 3101',
292
+ },
293
+ {
294
+ description: 'Run one spec with a dev auth token minted internally',
295
+ command: 'proteum e2e --port 3101 --session-email admin@example.com --session-role ADMIN tests/e2e/features/admin.spec.ts',
296
+ },
297
+ {
298
+ description: 'Load extra dotenv values before Playwright starts',
299
+ command: 'proteum e2e --url http://localhost:3101 --env-file .proteum/e2e.env --grep smoke',
300
+ },
301
+ ],
302
+ notes: [
303
+ '`proteum e2e` spawns Playwright with `E2E_BASE_URL`, optional `E2E_PORT`, optional `E2E_AUTH_TOKEN`, and any `--env`/`--env-file` values in the child process environment.',
304
+ 'Common Playwright flags are exposed directly: `--config`, `--debug`, `--grep`, `--headed`, `--list`, `--project`, `--reporter`, `--retries`, `--timeout`, `--ui`, and `--workers`.',
305
+ 'The shell command itself stays `proteum e2e ...`, so Codex does not need to run `FOO=bar npx playwright test ...`.',
306
+ ],
307
+ status: 'experimental',
308
+ },
279
309
  connect: {
280
310
  name: 'connect',
281
311
  category: 'Manifest and contracts',
@@ -4,11 +4,11 @@ import cli, { type TArgsObject } from '../context';
4
4
  import { createClipanionUsage, proteumCommands, type TProteumCommandName } from '../presentation/commands';
5
5
  import { createArgs } from './argv';
6
6
 
7
- type TRunModule = { run: () => Promise<void> };
7
+ type TRunModule = { run: () => Promise<number | void> };
8
8
 
9
9
  export const runCommandModule = async (loader: () => Promise<TRunModule>) => {
10
10
  const module = await loader();
11
- await module.run();
11
+ return await module.run();
12
12
  };
13
13
 
14
14
  export abstract class ProteumCommand extends Command {