@sdods/cli 0.11.3 → 0.12.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/dist/.tsbuildinfo +1 -1
- package/dist/commands/browsers.d.ts +32 -1
- package/dist/commands/browsers.js +120 -21
- package/dist/commands/doctor.js +3 -8
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.js +17 -3
- package/dist/commands/watch.js +4 -2
- package/dist/workspace-bin.d.ts +12 -0
- package/dist/workspace-bin.js +27 -0
- package/package.json +8 -8
- package/skills/sdods-run/SKILL.md +7 -1
|
@@ -9,7 +9,38 @@ export interface BrowserStatus {
|
|
|
9
9
|
executable: string | null;
|
|
10
10
|
playwrightVersion: string;
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Whether the download that produced `executable` finished.
|
|
14
|
+
*
|
|
15
|
+
* Playwright writes `INSTALLATION_COMPLETE` last. An executable without it is a download that was
|
|
16
|
+
* cut off, or one an antivirus scanner is still holding, and it does not launch reliably. Headless
|
|
17
|
+
* Chromium runs launch the separate headless shell of the same revision, so that must be complete
|
|
18
|
+
* too; a cache holding only one of the two fails every run with "Executable doesn't exist".
|
|
19
|
+
*
|
|
20
|
+
* A path outside Playwright's cache layout (a custom `executablePath`) has no marker to check, so
|
|
21
|
+
* the executable existing is all this can say.
|
|
22
|
+
*/
|
|
23
|
+
export declare function installationComplete(executable: string): boolean;
|
|
24
|
+
export declare function browserStatuses(names?: string[], opts?: {
|
|
25
|
+
cwd?: string;
|
|
26
|
+
}): Promise<BrowserStatus[]>;
|
|
27
|
+
/**
|
|
28
|
+
* Make sure every downloaded engine a run needs is on disk, installing the missing ones when
|
|
29
|
+
* `install` is set.
|
|
30
|
+
*
|
|
31
|
+
* This is what stands between a run and Playwright's "Executable doesn't exist … run npx
|
|
32
|
+
* playwright install". That advice is wrong for SDODS: run from anywhere but the workspace it
|
|
33
|
+
* installs a different Playwright's revision, and without PLAYWRIGHT_BROWSERS_PATH it installs
|
|
34
|
+
* into a folder the desktop app never reads. Installing here, from the workspace, with the
|
|
35
|
+
* environment the run itself inherits, lands the exact revision where the runner will look, on
|
|
36
|
+
* every OS.
|
|
37
|
+
*
|
|
38
|
+
* Channel browsers (Edge) are a system install and stay with the check in `run`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function ensureBrowsers(names: string[], opts: {
|
|
41
|
+
cwd: string;
|
|
42
|
+
install: boolean;
|
|
43
|
+
}): Promise<string[]>;
|
|
13
44
|
/** Install browser engines and channels (used by `browsers install`, `doctor --fix`, `init`). */
|
|
14
45
|
export declare function installBrowsers(opts?: {
|
|
15
46
|
browsers?: string[];
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { join } from 'node:path';
|
|
3
|
+
import { basename, dirname, join } from 'node:path';
|
|
4
4
|
import { execa } from 'execa';
|
|
5
5
|
import pc from 'picocolors';
|
|
6
6
|
import { BrowserSchema } from '@sdods/contracts';
|
|
7
7
|
import { SdodsError } from '@sdods/core';
|
|
8
8
|
import { createContext } from '../context.js';
|
|
9
|
-
import { collect, json, ok, table } from '../ui.js';
|
|
9
|
+
import { collect, info, json, ok, table, warn } from '../ui.js';
|
|
10
10
|
// Typed against BrowserName (not `string`) so a new browser is a compile error here rather than a
|
|
11
11
|
// silent fall-through to chromium.
|
|
12
12
|
const ENGINE_OF = {
|
|
@@ -54,30 +54,83 @@ function channelExecutable(channel) {
|
|
|
54
54
|
}
|
|
55
55
|
return null;
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
/**
|
|
58
|
+
* The playwright-core the RUNNER will launch, not the CLI's own copy.
|
|
59
|
+
*
|
|
60
|
+
* `@sdods/cli` depends on playwright-core, and the workspace depends on `@playwright/test`, which
|
|
61
|
+
* brings its own. When the two versions differ they want different browser revisions, and asking
|
|
62
|
+
* the CLI's copy would report `chromium-1243` present while the runner looks for `chromium-1250`.
|
|
63
|
+
* So resolve through the workspace's `@playwright/test` first, the same package `npx playwright
|
|
64
|
+
* install` runs from, and fall back to the CLI's copy only when there is no workspace install.
|
|
65
|
+
*/
|
|
66
|
+
function loadPlaywrightCore(cwd) {
|
|
67
|
+
const attempts = [];
|
|
68
|
+
if (cwd) {
|
|
69
|
+
attempts.push(() => {
|
|
70
|
+
const ws = createRequire(join(cwd, 'package.json'));
|
|
71
|
+
const test = createRequire(ws.resolve('@playwright/test/package.json'));
|
|
72
|
+
return createRequire(test.resolve('playwright/package.json'));
|
|
73
|
+
});
|
|
74
|
+
attempts.push(() => createRequire(join(cwd, 'package.json')));
|
|
64
75
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
attempts.push(() => createRequire(import.meta.url));
|
|
77
|
+
for (const attempt of attempts) {
|
|
78
|
+
try {
|
|
79
|
+
const req = attempt();
|
|
80
|
+
const pw = req('playwright-core');
|
|
81
|
+
const { version } = req('playwright-core/package.json');
|
|
82
|
+
return { pw, version };
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// try the next location
|
|
86
|
+
}
|
|
74
87
|
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
/** `chromium-1243`, `firefox-1543`, `webkit-2359`: the folder Playwright downloads a revision into. */
|
|
91
|
+
const REVISION_DIR = /^(chromium|firefox|webkit)-(\d+)$/;
|
|
92
|
+
/**
|
|
93
|
+
* Whether the download that produced `executable` finished.
|
|
94
|
+
*
|
|
95
|
+
* Playwright writes `INSTALLATION_COMPLETE` last. An executable without it is a download that was
|
|
96
|
+
* cut off, or one an antivirus scanner is still holding, and it does not launch reliably. Headless
|
|
97
|
+
* Chromium runs launch the separate headless shell of the same revision, so that must be complete
|
|
98
|
+
* too; a cache holding only one of the two fails every run with "Executable doesn't exist".
|
|
99
|
+
*
|
|
100
|
+
* A path outside Playwright's cache layout (a custom `executablePath`) has no marker to check, so
|
|
101
|
+
* the executable existing is all this can say.
|
|
102
|
+
*/
|
|
103
|
+
export function installationComplete(executable) {
|
|
104
|
+
if (!existsSync(executable))
|
|
105
|
+
return false;
|
|
106
|
+
let dir = dirname(executable);
|
|
107
|
+
while (!REVISION_DIR.test(basename(dir))) {
|
|
108
|
+
const parent = dirname(dir);
|
|
109
|
+
if (parent === dir)
|
|
110
|
+
return true;
|
|
111
|
+
dir = parent;
|
|
112
|
+
}
|
|
113
|
+
const [, engine, revision] = REVISION_DIR.exec(basename(dir));
|
|
114
|
+
if (!existsSync(join(dir, 'INSTALLATION_COMPLETE')))
|
|
115
|
+
return false;
|
|
116
|
+
if (engine !== 'chromium')
|
|
117
|
+
return true;
|
|
118
|
+
const shell = join(dirname(dir), `chromium_headless_shell-${revision}`);
|
|
119
|
+
return existsSync(join(shell, 'INSTALLATION_COMPLETE'));
|
|
120
|
+
}
|
|
121
|
+
export async function browserStatuses(names = DOWNLOADED_BROWSERS, opts = {}) {
|
|
122
|
+
const loaded = loadPlaywrightCore(opts.cwd);
|
|
123
|
+
const version = loaded?.version ?? 'unknown';
|
|
75
124
|
return names.map((name) => {
|
|
76
125
|
const engine = ENGINE_OF[name] ?? 'chromium';
|
|
77
126
|
const channel = CHANNEL_OF[name] ?? null;
|
|
78
127
|
let executable;
|
|
79
128
|
try {
|
|
80
|
-
executable = channel
|
|
129
|
+
executable = channel
|
|
130
|
+
? channelExecutable(channel)
|
|
131
|
+
: loaded
|
|
132
|
+
? loaded.pw[engine].executablePath()
|
|
133
|
+
: null;
|
|
81
134
|
}
|
|
82
135
|
catch {
|
|
83
136
|
executable = null;
|
|
@@ -86,12 +139,58 @@ export async function browserStatuses(names = DOWNLOADED_BROWSERS) {
|
|
|
86
139
|
name,
|
|
87
140
|
engine,
|
|
88
141
|
channel,
|
|
89
|
-
installed: Boolean(executable && existsSync(executable)),
|
|
142
|
+
installed: Boolean(executable && (channel ? existsSync(executable) : installationComplete(executable))),
|
|
90
143
|
executable,
|
|
91
144
|
playwrightVersion: version,
|
|
92
145
|
};
|
|
93
146
|
});
|
|
94
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Make sure every downloaded engine a run needs is on disk, installing the missing ones when
|
|
150
|
+
* `install` is set.
|
|
151
|
+
*
|
|
152
|
+
* This is what stands between a run and Playwright's "Executable doesn't exist … run npx
|
|
153
|
+
* playwright install". That advice is wrong for SDODS: run from anywhere but the workspace it
|
|
154
|
+
* installs a different Playwright's revision, and without PLAYWRIGHT_BROWSERS_PATH it installs
|
|
155
|
+
* into a folder the desktop app never reads. Installing here, from the workspace, with the
|
|
156
|
+
* environment the run itself inherits, lands the exact revision where the runner will look, on
|
|
157
|
+
* every OS.
|
|
158
|
+
*
|
|
159
|
+
* Channel browsers (Edge) are a system install and stay with the check in `run`.
|
|
160
|
+
*/
|
|
161
|
+
export async function ensureBrowsers(names, opts) {
|
|
162
|
+
const wanted = [...new Set(names)].filter((n) => !CHANNEL_OF[n]);
|
|
163
|
+
if (!wanted.length)
|
|
164
|
+
return [];
|
|
165
|
+
const missing = (await browserStatuses(wanted, { cwd: opts.cwd })).filter((s) => !s.installed);
|
|
166
|
+
if (!missing.length)
|
|
167
|
+
return [];
|
|
168
|
+
// One engine can back several browsers (mobile-chrome is chromium); install each engine once.
|
|
169
|
+
const engines = [...new Set(missing.map((s) => s.engine))];
|
|
170
|
+
if (!opts.install) {
|
|
171
|
+
throw new SdodsError('NOT_SUPPORTED', `The ${engines.join(', ')} test browser${engines.length > 1 ? 's are' : ' is'} not installed.`, {
|
|
172
|
+
hint: `Run \`sdods browsers install ${engines.map((e) => `-b ${e}`).join(' ')}\` from the workspace, ` +
|
|
173
|
+
'or pass --install-browsers to install missing browsers before the run.',
|
|
174
|
+
exitCode: 2,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
info(`Installing the ${engines.join(', ')} test browser${engines.length > 1 ? 's' : ''} for this run…`);
|
|
178
|
+
await installBrowsers({ browsers: engines, cwd: opts.cwd });
|
|
179
|
+
const still = (await browserStatuses(engines, { cwd: opts.cwd })).filter((s) => !s.installed);
|
|
180
|
+
if (still.length) {
|
|
181
|
+
throw new SdodsError('NOT_SUPPORTED', `Installing ${still.map((s) => s.name).join(', ')} did not complete.`, {
|
|
182
|
+
hint: 'Check the network or proxy (HTTPS_PROXY) and any antivirus quarantine, then run ' +
|
|
183
|
+
`\`sdods browsers install ${still.map((s) => `-b ${s.name}`).join(' ')}\`.`,
|
|
184
|
+
exitCode: 2,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (process.platform === 'linux') {
|
|
188
|
+
warn('If the browser then fails to launch with missing libraries, run ' +
|
|
189
|
+
`\`sudo npx playwright install-deps ${engines.join(' ')}\` once on this machine.`);
|
|
190
|
+
}
|
|
191
|
+
ok(`Installed ${engines.join(', ')}`);
|
|
192
|
+
return engines;
|
|
193
|
+
}
|
|
95
194
|
/** Install browser engines and channels (used by `browsers install`, `doctor --fix`, `init`). */
|
|
96
195
|
export async function installBrowsers(opts = {}) {
|
|
97
196
|
const names = (opts.browsers?.length ? opts.browsers : DOWNLOADED_BROWSERS);
|
|
@@ -183,7 +282,7 @@ export function register(program) {
|
|
|
183
282
|
: DOWNLOADED_BROWSERS;
|
|
184
283
|
for (const b of names)
|
|
185
284
|
BrowserSchema.parse(b);
|
|
186
|
-
const statuses = await browserStatuses(names);
|
|
285
|
+
const statuses = await browserStatuses(names, { cwd: ctx.rootDir });
|
|
187
286
|
if (ctx.opts.json)
|
|
188
287
|
return json(statuses);
|
|
189
288
|
table(statuses.map((s) => ({
|
package/dist/commands/doctor.js
CHANGED
|
@@ -37,7 +37,7 @@ export function registerDoctorCommand(program) {
|
|
|
37
37
|
fix: `npm i -D @playwright/test@^${MIN_PLAYWRIGHT_FOR_STEP_RESULTS} (or bun add -d)`,
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
|
-
const browsers = await browserCheck();
|
|
40
|
+
const browsers = await browserCheck(ctx.rootDir);
|
|
41
41
|
checks.push(...browsers);
|
|
42
42
|
if (opts.fix && browsers.some((b) => !b.ok)) {
|
|
43
43
|
out(pc.cyan('Installing browsers…'));
|
|
@@ -239,11 +239,6 @@ function tokenMatrix(cliStatus, localLlm) {
|
|
|
239
239
|
requirement: 'optional',
|
|
240
240
|
present: has('SDODS_TOKEN'),
|
|
241
241
|
detail: 'self-issued, free: MCP over HTTP and CI result ingest (`sdods tokens create`)',
|
|
242
|
-
}, {
|
|
243
|
-
name: 'FIREBASE_SERVICE_ACCOUNT_AUTOMAX_DOCS',
|
|
244
|
-
requirement: 'ci-only',
|
|
245
|
-
present: has('FIREBASE_SERVICE_ACCOUNT_AUTOMAX_DOCS'),
|
|
246
|
-
detail: 'GitHub secret for the docs deploy workflow',
|
|
247
242
|
}, {
|
|
248
243
|
name: 'NPM_TOKEN',
|
|
249
244
|
requirement: 'ci-only',
|
|
@@ -351,11 +346,11 @@ async function versionCheck(name, args, fix, bin = name) {
|
|
|
351
346
|
return { name, ok: false, detail: 'not found', fix };
|
|
352
347
|
}
|
|
353
348
|
}
|
|
354
|
-
async function browserCheck() {
|
|
349
|
+
async function browserCheck(cwd) {
|
|
355
350
|
// Reuses `browsers list`'s detection so the two commands can never disagree about what is
|
|
356
351
|
// installed — and so channel browsers are probed by path rather than by the bundled engine's
|
|
357
352
|
// executable, which exists whether or not the branded browser does.
|
|
358
|
-
const statuses = await browserStatuses(['chromium', 'edge', 'firefox', 'webkit']);
|
|
353
|
+
const statuses = await browserStatuses(['chromium', 'edge', 'firefox', 'webkit'], { cwd });
|
|
359
354
|
return statuses.map((s) => ({
|
|
360
355
|
name: `browser:${s.name}`,
|
|
361
356
|
ok: s.installed,
|
package/dist/commands/run.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export interface RunFlags {
|
|
|
41
41
|
allowEmpty?: boolean;
|
|
42
42
|
notify?: boolean;
|
|
43
43
|
allowPoolContention?: boolean;
|
|
44
|
+
installBrowsers?: boolean;
|
|
44
45
|
}
|
|
45
46
|
export declare function register(program: Command): void;
|
|
46
47
|
export declare function runCommand(flags: RunFlags, cmd: Command): Promise<number>;
|
package/dist/commands/run.js
CHANGED
|
@@ -6,7 +6,8 @@ import { RecordingModeSchema, newRunId, runFiles, } from '@sdods/contracts';
|
|
|
6
6
|
import { SdodsError, CLI_OVERRIDES_ENV, VERSION, checkPoolCapacity, collectGateEvidence, effectiveWorkers, evaluateGates, formatFindings, hasGates, lintProject, listGeneratedProjects, moduleByName, moduleDir, normalizeTagExpr, parseTagExpr, poolAccountsFor, poolDemandByRole, poolTooSmallError, redactRunTraces, serializeCliOverrides, setupTierOf, } from '@sdods/core';
|
|
7
7
|
import { analyzeChangeImpact } from '@sdods/mcp';
|
|
8
8
|
import { createContext } from '../context.js';
|
|
9
|
-
import {
|
|
9
|
+
import { workspaceBin } from '../workspace-bin.js';
|
|
10
|
+
import { browserStatuses, ensureBrowsers } from './browsers.js';
|
|
10
11
|
import { gateFailedError, printGates } from '../gates.js';
|
|
11
12
|
import { maybeNotify, notifyRun } from '../notify.js';
|
|
12
13
|
import { installedPlaywrightVersion, stepResultsWarning } from '../runner-compat.js';
|
|
@@ -36,6 +37,7 @@ function addRunOptions(cmd) {
|
|
|
36
37
|
.option('--process <name>', 'run a named process (recipe) from the project or workspace')
|
|
37
38
|
.option('--project-matrix', 'run every browser declared in the project yaml')
|
|
38
39
|
.option('--device <name>', 'device name for mobile emulation, e.g. "iPhone 15"')
|
|
40
|
+
.option('--install-browsers', 'download any test browser the run needs that is missing (also SDODS_AUTO_INSTALL_BROWSERS=1)')
|
|
39
41
|
.option('--headed', 'run headed')
|
|
40
42
|
.option('-w, --workers <n>', 'parallel workers', parseIntFlag('workers'))
|
|
41
43
|
.option('--allow-pool-contention', 'run even when a @user: role has fewer pool accounts than the workers that need it')
|
|
@@ -138,7 +140,7 @@ export async function runCommand(flags, cmd) {
|
|
|
138
140
|
// selected browser here would fail runs that would have worked.
|
|
139
141
|
const usesBrowser = !layers.length || layers.some((l) => l !== 'api');
|
|
140
142
|
const missing = usesBrowser
|
|
141
|
-
? (await browserStatuses(browsers)).filter((s) => s.channel && !s.installed)
|
|
143
|
+
? (await browserStatuses(browsers, { cwd: ctx.rootDir })).filter((s) => s.channel && !s.installed)
|
|
142
144
|
: [];
|
|
143
145
|
if (missing.length) {
|
|
144
146
|
const names = missing.map((s) => s.name);
|
|
@@ -313,10 +315,22 @@ export async function runCommand(flags, cmd) {
|
|
|
313
315
|
exitCode: 2,
|
|
314
316
|
});
|
|
315
317
|
}
|
|
318
|
+
// Every run target launches a browser, including @api ones: the BDD fixtures are shared across
|
|
319
|
+
// layers, so an api target opens the runner's default engine (chromium). Checking here, against
|
|
320
|
+
// the targets actually generated, catches a missing or half-downloaded engine before bddgen, and
|
|
321
|
+
// either installs it (desktop app, --install-browsers) or stops with the SDODS command rather
|
|
322
|
+
// than Playwright's "npx playwright install", which installs to the wrong place.
|
|
323
|
+
if (!flags.list) {
|
|
324
|
+
await ensureBrowsers(runnerProjects.map((p) => p.browser ?? 'chromium'), {
|
|
325
|
+
cwd: ctx.rootDir,
|
|
326
|
+
install: Boolean(flags.installBrowsers) || process.env.SDODS_AUTO_INSTALL_BROWSERS === '1',
|
|
327
|
+
});
|
|
328
|
+
}
|
|
316
329
|
// Recorded specs are plain runner tests: no Gherkin to generate.
|
|
317
330
|
const recordedOnly = runnerProjects.every((p) => p.layer === 'recorded');
|
|
318
331
|
if (!recordedOnly) {
|
|
319
|
-
const
|
|
332
|
+
const [bddgen, ...bddgenArgs] = workspaceBin(ctx.rootDir, 'playwright-bdd', 'bddgen');
|
|
333
|
+
const gen = await execa(bddgen, [...bddgenArgs, '-c', configPath], {
|
|
320
334
|
cwd: ctx.rootDir,
|
|
321
335
|
env: childEnv,
|
|
322
336
|
stdio: ctx.opts.quiet ? 'pipe' : 'inherit',
|
package/dist/commands/watch.js
CHANGED
|
@@ -4,6 +4,7 @@ import pc from 'picocolors';
|
|
|
4
4
|
import { newRunId } from '@sdods/contracts';
|
|
5
5
|
import { SdodsError, listGeneratedProjects, normalizeTagExpr } from '@sdods/core';
|
|
6
6
|
import { createContext } from '../context.js';
|
|
7
|
+
import { workspaceBin } from '../workspace-bin.js';
|
|
7
8
|
import { collect, out } from '../ui.js';
|
|
8
9
|
/**
|
|
9
10
|
* Spec generation runs in watch mode while the interactive UI
|
|
@@ -68,7 +69,8 @@ export function register(program) {
|
|
|
68
69
|
process.on('SIGINT', stop);
|
|
69
70
|
process.on('SIGTERM', stop);
|
|
70
71
|
// initial generation so UI mode has specs to show
|
|
71
|
-
const
|
|
72
|
+
const [bddgen, ...bddgenArgs] = workspaceBin(ctx.rootDir, 'playwright-bdd', 'bddgen');
|
|
73
|
+
const gen = await execa(bddgen, [...bddgenArgs, '-c', configPath], {
|
|
72
74
|
cwd: ctx.rootDir,
|
|
73
75
|
env: childEnv,
|
|
74
76
|
stdio: 'inherit',
|
|
@@ -79,7 +81,7 @@ export function register(program) {
|
|
|
79
81
|
exitCode: 1,
|
|
80
82
|
});
|
|
81
83
|
}
|
|
82
|
-
const watcher = execa(
|
|
84
|
+
const watcher = execa(bddgen, [...bddgenArgs, '-c', configPath, '--watch'], {
|
|
83
85
|
cwd: ctx.rootDir,
|
|
84
86
|
env: childEnv,
|
|
85
87
|
stdio: 'inherit',
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How to run a package's CLI from the workspace: `[node, <its bin script>]` when the package is
|
|
3
|
+
* installed there, else `['npx', <bin>]`.
|
|
4
|
+
*
|
|
5
|
+
* `npx <bin>` only finds a tool through a `node_modules/.bin` shim. Some layouts have the package
|
|
6
|
+
* but no shim npx recognises (bun on Windows writes `.bunx` shims), and npx then downloads an
|
|
7
|
+
* unrelated registry package of the same name: `npx bddgen` fetched `bddgen@1.0.5` and every run
|
|
8
|
+
* failed with "bddgen failed to generate specs". Resolving the package and running its bin with
|
|
9
|
+
* this Node works in every layout.
|
|
10
|
+
*/
|
|
11
|
+
export declare function workspaceBin(rootDir: string, pkg: string, bin: string): [string, ...string[]];
|
|
12
|
+
//# sourceMappingURL=workspace-bin.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* How to run a package's CLI from the workspace: `[node, <its bin script>]` when the package is
|
|
6
|
+
* installed there, else `['npx', <bin>]`.
|
|
7
|
+
*
|
|
8
|
+
* `npx <bin>` only finds a tool through a `node_modules/.bin` shim. Some layouts have the package
|
|
9
|
+
* but no shim npx recognises (bun on Windows writes `.bunx` shims), and npx then downloads an
|
|
10
|
+
* unrelated registry package of the same name: `npx bddgen` fetched `bddgen@1.0.5` and every run
|
|
11
|
+
* failed with "bddgen failed to generate specs". Resolving the package and running its bin with
|
|
12
|
+
* this Node works in every layout.
|
|
13
|
+
*/
|
|
14
|
+
export function workspaceBin(rootDir, pkg, bin) {
|
|
15
|
+
try {
|
|
16
|
+
const manifest = createRequire(join(rootDir, 'package.json')).resolve(`${pkg}/package.json`);
|
|
17
|
+
const { bin: bins } = JSON.parse(readFileSync(manifest, 'utf8'));
|
|
18
|
+
const rel = typeof bins === 'string' ? bins : bins?.[bin];
|
|
19
|
+
if (rel)
|
|
20
|
+
return [process.execPath, join(dirname(manifest), rel)];
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// not resolvable from here: fall back to npx
|
|
24
|
+
}
|
|
25
|
+
return ['npx', bin];
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=workspace-bin.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdods/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "SDODS command line: run, record, analyze, agents, MCP server, scheduler and the web server.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "SDODS <admin@sdods.com>",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"node": ">=22"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@sdods/contracts": "0.
|
|
29
|
-
"@sdods/core": "0.
|
|
28
|
+
"@sdods/contracts": "0.12.1",
|
|
29
|
+
"@sdods/core": "0.12.1",
|
|
30
30
|
"commander": "^15.0.0",
|
|
31
31
|
"execa": "^10.0.1",
|
|
32
32
|
"picocolors": "^1.1.1",
|
|
@@ -35,12 +35,12 @@
|
|
|
35
35
|
"zod": "^4.6.5",
|
|
36
36
|
"@playwright/test": "^1.63.0",
|
|
37
37
|
"playwright-core": "^1.63.0",
|
|
38
|
-
"@sdods/integrations": "0.
|
|
39
|
-
"@sdods/mcp": "0.
|
|
40
|
-
"@sdods/agents": "0.
|
|
41
|
-
"@sdods/db": "0.
|
|
38
|
+
"@sdods/integrations": "0.12.1",
|
|
39
|
+
"@sdods/mcp": "0.12.1",
|
|
40
|
+
"@sdods/agents": "0.12.1",
|
|
41
|
+
"@sdods/db": "0.12.1",
|
|
42
42
|
"csv-parse": "^7.0.2",
|
|
43
|
-
"@sdods/server": "0.
|
|
43
|
+
"@sdods/server": "0.12.1"
|
|
44
44
|
},
|
|
45
45
|
"homepage": "https://sdods.com",
|
|
46
46
|
"bugs": {
|
|
@@ -32,13 +32,19 @@ an SDODS source checkout use `bun run sdods`.
|
|
|
32
32
|
```
|
|
33
33
|
2. Run the requested slice. Typical combinations:
|
|
34
34
|
```bash
|
|
35
|
-
sdods run -p <project> -e <env> -l api # API only
|
|
35
|
+
sdods run -p <project> -e <env> -l api # API only (still launches chromium: shared fixtures)
|
|
36
36
|
sdods run -p <project> -e <env> -l ui -b chromium -t @smoke # quick UI smoke
|
|
37
37
|
sdods run -p <project> -e <env> -t @regression --project-matrix # every browser in the yaml
|
|
38
38
|
sdods run -p <project> -e <env> -t "@user:admin" # scenarios that lease admin users
|
|
39
39
|
sdods run -p <project> -e <env> -l hybrid -w 2 # API→UI chaining
|
|
40
40
|
sdods run -p <project> -e <env> --har-replay --strict -t @smoke # offline, CI-style
|
|
41
|
+
sdods run -p <project> -e <env> -b firefox --install-browsers # download missing browsers first
|
|
41
42
|
```
|
|
43
|
+
A run whose browser is missing or half-downloaded stops before generating specs with the
|
|
44
|
+
`sdods browsers install -b <engine>` command; `--install-browsers` (or
|
|
45
|
+
`SDODS_AUTO_INSTALL_BROWSERS=1`, which the desktop app sets) downloads it instead. Never answer
|
|
46
|
+
Playwright's "run npx playwright install" by running that from another folder: it installs a
|
|
47
|
+
different revision into a cache the runner does not read.
|
|
42
48
|
3. Read results:
|
|
43
49
|
```bash
|
|
44
50
|
sdods report --last # summary: totals, failed, flaky, report paths
|