proteum 2.2.2 → 2.2.6
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 +5 -5
- package/README.md +4 -1
- package/agents/project/AGENTS.md +9 -8
- package/agents/project/diagnostics.md +10 -8
- package/agents/project/optimizations.md +2 -2
- package/agents/project/root/AGENTS.md +8 -7
- package/agents/project/tests/AGENTS.md +3 -2
- package/cli/app/index.ts +19 -9
- package/cli/commands/check.ts +7 -3
- package/cli/commands/configure.ts +14 -9
- package/cli/commands/e2e.ts +204 -0
- package/cli/commands/typecheck.ts +7 -3
- package/cli/presentation/commands.ts +37 -7
- package/cli/runtime/command.ts +2 -2
- package/cli/runtime/commands.ts +59 -0
- package/cli/scaffold/index.ts +1 -1
- package/cli/utils/agents.ts +175 -80
- package/cli/utils/check.ts +32 -4
- package/docs/dev-sessions.md +11 -2
- package/docs/diagnostics.md +2 -1
- package/package.json +1 -1
- package/scripts/update-codex-agents.ts +2 -2
- package/server/services/router/request/index.ts +2 -1
- package/server/services/router/request/ip.test.cjs +60 -0
- package/server/services/router/request/ip.ts +71 -0
|
@@ -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
|
-
|
|
27
|
-
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
130
|
-
'Declined non-managed paths are left untouched; Proteum still creates missing
|
|
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',
|
package/cli/runtime/command.ts
CHANGED
|
@@ -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 {
|
package/cli/runtime/commands.ts
CHANGED
|
@@ -259,6 +259,63 @@ class CheckCommand extends ProteumCommand {
|
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
class E2eCommand extends ProteumCommand {
|
|
263
|
+
public static paths = [['e2e']];
|
|
264
|
+
|
|
265
|
+
public static usage = buildUsage('e2e');
|
|
266
|
+
|
|
267
|
+
public cwd = Option.String('--cwd', { description: 'Run Playwright against another Proteum app root.' });
|
|
268
|
+
public port = Option.String('--port', { description: 'Set E2E_BASE_URL from a local router port.' });
|
|
269
|
+
public url = Option.String('--url', { description: 'Set E2E_BASE_URL from an explicit base URL.' });
|
|
270
|
+
public sessionEmail = Option.String('--session-email', {
|
|
271
|
+
description: 'Mint a dev session before Playwright starts and pass it as E2E_AUTH_TOKEN.',
|
|
272
|
+
});
|
|
273
|
+
public sessionRole = Option.String('--session-role', { description: 'Require the dev session user to have this role.' });
|
|
274
|
+
public env = Option.Array('--env', [], { description: 'Pass an environment value to Playwright as KEY=value.' });
|
|
275
|
+
public envFile = Option.Array('--env-file', [], { description: 'Load environment values from a dotenv file before Playwright starts.' });
|
|
276
|
+
public config = Option.String('--config', { description: 'Playwright config file.' });
|
|
277
|
+
public debug = Option.Boolean('--debug', false, { description: 'Run Playwright in debug mode.' });
|
|
278
|
+
public grep = Option.String('--grep', { description: 'Playwright grep filter.' });
|
|
279
|
+
public headed = Option.Boolean('--headed', false, { description: 'Run browsers in headed mode.' });
|
|
280
|
+
public list = Option.Boolean('--list', false, { description: 'List Playwright tests without running them.' });
|
|
281
|
+
public project = Option.Array('--project', [], { description: 'Playwright project name. Can be repeated.' });
|
|
282
|
+
public reporter = Option.String('--reporter', { description: 'Playwright reporter.' });
|
|
283
|
+
public retries = Option.String('--retries', { description: 'Playwright retry count.' });
|
|
284
|
+
public timeout = Option.String('--timeout', { description: 'Playwright per-test timeout.' });
|
|
285
|
+
public ui = Option.Boolean('--ui', false, { description: 'Run Playwright in UI mode.' });
|
|
286
|
+
public workers = Option.String('--workers', { description: 'Playwright worker count.' });
|
|
287
|
+
public specs = Option.Rest();
|
|
288
|
+
|
|
289
|
+
public async execute() {
|
|
290
|
+
const playwrightArgs = [
|
|
291
|
+
...(this.config ? ['--config', this.config] : []),
|
|
292
|
+
...(this.debug ? ['--debug'] : []),
|
|
293
|
+
...(this.grep ? ['--grep', this.grep] : []),
|
|
294
|
+
...(this.headed ? ['--headed'] : []),
|
|
295
|
+
...(this.list ? ['--list'] : []),
|
|
296
|
+
...this.project.flatMap((project) => ['--project', project]),
|
|
297
|
+
...(this.reporter ? ['--reporter', this.reporter] : []),
|
|
298
|
+
...(this.retries ? ['--retries', this.retries] : []),
|
|
299
|
+
...(this.timeout ? ['--timeout', this.timeout] : []),
|
|
300
|
+
...(this.ui ? ['--ui'] : []),
|
|
301
|
+
...(this.workers ? ['--workers', this.workers] : []),
|
|
302
|
+
...this.specs,
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
this.setCliArgs({
|
|
306
|
+
env: this.env,
|
|
307
|
+
envFile: this.envFile,
|
|
308
|
+
playwrightArgs,
|
|
309
|
+
port: this.port ?? '',
|
|
310
|
+
sessionEmail: this.sessionEmail ?? '',
|
|
311
|
+
sessionRole: this.sessionRole ?? '',
|
|
312
|
+
url: this.url ?? '',
|
|
313
|
+
workdir: this.cwd ?? '',
|
|
314
|
+
});
|
|
315
|
+
return await runCommandModule(() => import('../commands/e2e'));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
262
319
|
class ConnectCommand extends ProteumCommand {
|
|
263
320
|
public static paths = [['connect']];
|
|
264
321
|
|
|
@@ -608,6 +665,7 @@ export const registeredCommands = {
|
|
|
608
665
|
typecheck: TypecheckCommand,
|
|
609
666
|
lint: LintCommand,
|
|
610
667
|
check: CheckCommand,
|
|
668
|
+
e2e: E2eCommand,
|
|
611
669
|
connect: ConnectCommand,
|
|
612
670
|
doctor: DoctorCommand,
|
|
613
671
|
explain: ExplainCommand,
|
|
@@ -640,6 +698,7 @@ export const createCli = (version: string) => {
|
|
|
640
698
|
clipanion.register(TypecheckCommand);
|
|
641
699
|
clipanion.register(LintCommand);
|
|
642
700
|
clipanion.register(CheckCommand);
|
|
701
|
+
clipanion.register(E2eCommand);
|
|
643
702
|
clipanion.register(ConnectCommand);
|
|
644
703
|
clipanion.register(DoctorCommand);
|
|
645
704
|
clipanion.register(ExplainCommand);
|
package/cli/scaffold/index.ts
CHANGED
|
@@ -727,7 +727,7 @@ export const runInitScaffold = async () => {
|
|
|
727
727
|
? 'Run `npm run dev` in the new app directory.'
|
|
728
728
|
: 'Run `npm install`, then `npm run dev` in the new app directory.',
|
|
729
729
|
);
|
|
730
|
-
result.nextSteps.push('Run `proteum configure agents` when you want Proteum-managed instruction
|
|
730
|
+
result.nextSteps.push('Run `proteum configure agents` when you want Proteum-managed instruction stubs.');
|
|
731
731
|
result.nextSteps.push('Use `proteum create page|controller|command|route|service ...` to add app artifacts.');
|
|
732
732
|
|
|
733
733
|
printResult(result, createInitSummary(result, config));
|