@sdods/cli 0.12.0 → 0.13.0
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/auth.js +5 -0
- package/dist/commands/browsers.js +10 -3
- package/dist/commands/doctor.js +5 -3
- package/dist/commands/init.js +2 -6
- package/dist/commands/record.js +10 -1
- package/dist/commands/report.js +13 -9
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.js +28 -6
- package/dist/commands/steps.js +5 -3
- package/dist/commands/trace.js +3 -1
- package/dist/commands/watch.js +12 -5
- package/dist/open.d.ts +10 -0
- package/dist/open.js +26 -0
- package/dist/program.js +5 -2
- package/dist/workspace-bin.d.ts +2 -0
- package/dist/workspace-bin.js +3 -0
- package/package.json +11 -10
package/dist/commands/auth.js
CHANGED
|
@@ -2,6 +2,7 @@ import { execa } from 'execa';
|
|
|
2
2
|
import { SdodsError } from '@sdods/core';
|
|
3
3
|
import { createContext } from '../context.js';
|
|
4
4
|
import { json, ok, table } from '../ui.js';
|
|
5
|
+
import { ensureBrowsers } from './browsers.js';
|
|
5
6
|
/**
|
|
6
7
|
* Login-state commands. Capture/list are thin wrappers over `@sdods/core/auth/capture`
|
|
7
8
|
* (the same library `sdods record --user` uses) so there is a single implementation.
|
|
@@ -26,6 +27,10 @@ export function register(program) {
|
|
|
26
27
|
const ctx = createContext(cmd);
|
|
27
28
|
const cfg = ctx.registry.resolve(opts.project, opts.env);
|
|
28
29
|
const { captureAuth, poolUsers } = await import('@sdods/core/auth/capture');
|
|
30
|
+
await ensureBrowsers([opts.browser ?? 'chromium'], {
|
|
31
|
+
cwd: ctx.rootDir,
|
|
32
|
+
install: process.env.SDODS_AUTO_INSTALL_BROWSERS === '1',
|
|
33
|
+
});
|
|
29
34
|
const roles = opts.user
|
|
30
35
|
? [opts.user]
|
|
31
36
|
: [...new Set((await poolUsers(cfg)).map((u) => u.role))];
|
|
@@ -7,6 +7,7 @@ import { BrowserSchema } from '@sdods/contracts';
|
|
|
7
7
|
import { SdodsError } from '@sdods/core';
|
|
8
8
|
import { createContext } from '../context.js';
|
|
9
9
|
import { collect, info, json, ok, table, warn } from '../ui.js';
|
|
10
|
+
import { toolCommand } from '../workspace-bin.js';
|
|
10
11
|
// Typed against BrowserName (not `string`) so a new browser is a compile error here rather than a
|
|
11
12
|
// silent fall-through to chromium.
|
|
12
13
|
const ENGINE_OF = {
|
|
@@ -186,7 +187,7 @@ export async function ensureBrowsers(names, opts) {
|
|
|
186
187
|
}
|
|
187
188
|
if (process.platform === 'linux') {
|
|
188
189
|
warn('If the browser then fails to launch with missing libraries, run ' +
|
|
189
|
-
`\`sudo
|
|
190
|
+
`\`sudo sdods browsers install --with-deps ${engines.map((e) => `-b ${e}`).join(' ')}\` once on this machine.`);
|
|
190
191
|
}
|
|
191
192
|
ok(`Installed ${engines.join(', ')}`);
|
|
192
193
|
return engines;
|
|
@@ -198,12 +199,18 @@ export async function installBrowsers(opts = {}) {
|
|
|
198
199
|
const channels = [...new Set(names.map((b) => CHANNEL_OF[b]).filter((c) => !!c))];
|
|
199
200
|
if (engines.length) {
|
|
200
201
|
const args = ['playwright', 'install', ...(opts.withDeps ? ['--with-deps'] : []), ...engines];
|
|
201
|
-
|
|
202
|
+
const [file, ...argv] = toolCommand(opts.cwd ?? process.cwd(), args);
|
|
203
|
+
await execa(file, argv, { stdio: 'inherit', cwd: opts.cwd });
|
|
202
204
|
}
|
|
203
205
|
// Channels are a system install run by the vendor's own installer, so `--with-deps` does not
|
|
204
206
|
// apply and they cannot share the engine invocation.
|
|
205
207
|
for (const channel of channels) {
|
|
206
|
-
|
|
208
|
+
const [file, ...argv] = toolCommand(opts.cwd ?? process.cwd(), [
|
|
209
|
+
'playwright',
|
|
210
|
+
'install',
|
|
211
|
+
channel,
|
|
212
|
+
]);
|
|
213
|
+
await execa(file, argv, { stdio: 'inherit', cwd: opts.cwd });
|
|
207
214
|
}
|
|
208
215
|
}
|
|
209
216
|
/**
|
package/dist/commands/doctor.js
CHANGED
|
@@ -4,7 +4,7 @@ import { execa } from 'execa';
|
|
|
4
4
|
import pc from 'picocolors';
|
|
5
5
|
import { collectVarRefs, describeWorkers, effectiveWorkers, loadDotEnvLayer, loadEnvFile, poolAccountsByEnv, } from '@sdods/core';
|
|
6
6
|
import { createContext } from '../context.js';
|
|
7
|
-
import { browserStatuses } from './browsers.js';
|
|
7
|
+
import { browserStatuses, installBrowsers } from './browsers.js';
|
|
8
8
|
import { MIN_PLAYWRIGHT_FOR_STEP_RESULTS, installedPlaywrightVersion, stepResultsWarning, } from '../runner-compat.js';
|
|
9
9
|
import { json, out, parseIntFlag } from '../ui.js';
|
|
10
10
|
export function registerDoctorCommand(program) {
|
|
@@ -41,8 +41,10 @@ export function registerDoctorCommand(program) {
|
|
|
41
41
|
checks.push(...browsers);
|
|
42
42
|
if (opts.fix && browsers.some((b) => !b.ok)) {
|
|
43
43
|
out(pc.cyan('Installing browsers…'));
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
// The same path as `sdods browsers install`, so the engines land where runs look for them.
|
|
45
|
+
await installBrowsers({
|
|
46
|
+
browsers: browsers.filter((b) => !b.ok).map((b) => b.name),
|
|
47
|
+
withDeps: process.platform === 'linux',
|
|
46
48
|
cwd: ctx.rootDir,
|
|
47
49
|
}).catch(() => undefined);
|
|
48
50
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -6,6 +6,7 @@ import pc from 'picocolors';
|
|
|
6
6
|
import { OrganizationSchema, SlugSchema, WorkspaceSchema } from '@sdods/contracts';
|
|
7
7
|
import { SdodsError, VERSION, WORKSPACE_FILE } from '@sdods/core';
|
|
8
8
|
import { globalOptions } from '../context.js';
|
|
9
|
+
import { installBrowsers } from './browsers.js';
|
|
9
10
|
import { installSkills } from '../skills-catalog.js';
|
|
10
11
|
import { json, ok, out, warn } from '../ui.js';
|
|
11
12
|
/** Root of the SDODS monorepo this CLI runs from (used by --link, which needs a real checkout). */
|
|
@@ -338,12 +339,7 @@ Docs: https://docs.sdods.com
|
|
|
338
339
|
}
|
|
339
340
|
let browsersInstalled = false;
|
|
340
341
|
if (flags.install && flags.browsers && installed) {
|
|
341
|
-
|
|
342
|
-
cwd: target,
|
|
343
|
-
stdio: 'inherit',
|
|
344
|
-
reject: false,
|
|
345
|
-
});
|
|
346
|
-
browsersInstalled = res.exitCode === 0;
|
|
342
|
+
browsersInstalled = await installBrowsers({ browsers: ['chromium'], cwd: target }).then(() => true, () => false);
|
|
347
343
|
}
|
|
348
344
|
if (flags.claude) {
|
|
349
345
|
const res = await execa('npx', ['-y', '@sdods/cli', 'agent', 'install-claude'], {
|
package/dist/commands/record.js
CHANGED
|
@@ -5,6 +5,8 @@ import pc from 'picocolors';
|
|
|
5
5
|
import { SdodsError, VERSION } from '@sdods/core';
|
|
6
6
|
import { createContext } from '../context.js';
|
|
7
7
|
import { json, ok, out, warn } from '../ui.js';
|
|
8
|
+
import { toolCommand } from '../workspace-bin.js';
|
|
9
|
+
import { ensureBrowsers } from './browsers.js';
|
|
8
10
|
function addRecordOptions(cmd) {
|
|
9
11
|
return cmd
|
|
10
12
|
.option('-p, --project <slug>', 'project slug')
|
|
@@ -30,6 +32,12 @@ async function recordAction(flags, cmd) {
|
|
|
30
32
|
const { AuthStateCache } = await import('@sdods/core');
|
|
31
33
|
const name = flags.name ?? `rec-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}`;
|
|
32
34
|
const browser = (flags.browser ?? 'chromium');
|
|
35
|
+
// Before the recorder opens: otherwise a missing engine surfaces as the runner's own install
|
|
36
|
+
// instructions, which install to the wrong place.
|
|
37
|
+
await ensureBrowsers([browser], {
|
|
38
|
+
cwd: config.runtime.repoRoot,
|
|
39
|
+
install: process.env.SDODS_AUTO_INSTALL_BROWSERS === '1',
|
|
40
|
+
});
|
|
33
41
|
// logged-in start: make sure the first user of the role has a fresh storage state
|
|
34
42
|
let storageStatePath;
|
|
35
43
|
let userName;
|
|
@@ -164,7 +172,8 @@ export function register(program) {
|
|
|
164
172
|
}
|
|
165
173
|
async function playwrightVersion(cwd) {
|
|
166
174
|
try {
|
|
167
|
-
const
|
|
175
|
+
const [file, ...argv] = toolCommand(cwd, ['playwright', '--version']);
|
|
176
|
+
const { stdout } = await execa(file, argv, { cwd });
|
|
168
177
|
return stdout.trim().replace(/^Version\s+/i, '');
|
|
169
178
|
}
|
|
170
179
|
catch {
|
package/dist/commands/report.js
CHANGED
|
@@ -8,6 +8,8 @@ import { SdodsError, collectGateEvidence, evaluateGates, gateReportsFromRunnerJs
|
|
|
8
8
|
import { createContext } from '../context.js';
|
|
9
9
|
import { gateFailedError, printGates } from '../gates.js';
|
|
10
10
|
import { json, ok, out, table, warn } from '../ui.js';
|
|
11
|
+
import { toolCommand } from '../workspace-bin.js';
|
|
12
|
+
import { openInBrowser } from '../open.js';
|
|
11
13
|
function artifactsRoot(rootDir) {
|
|
12
14
|
return resolve(rootDir, process.env.SDODS_ARTIFACTS_DIR ?? '.sdods/runs');
|
|
13
15
|
}
|
|
@@ -33,10 +35,7 @@ function readJsonFile(file) {
|
|
|
33
35
|
return null;
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
|
-
|
|
37
|
-
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
38
|
-
await execa(cmd, [p], { shell: process.platform === 'win32' }).catch(() => undefined);
|
|
39
|
-
}
|
|
38
|
+
const openPath = openInBrowser;
|
|
40
39
|
/**
|
|
41
40
|
* The process `report merge` judges: `--process` (with `-p`, or the project in a run.json), or the
|
|
42
41
|
* process a run.json names when it declares gates. `undefined` when there is nothing to judge.
|
|
@@ -337,7 +336,8 @@ export function register(program) {
|
|
|
337
336
|
reporters.join(','),
|
|
338
337
|
workspace.reports,
|
|
339
338
|
];
|
|
340
|
-
const
|
|
339
|
+
const [file, ...argv] = toolCommand(ctx.rootDir, args);
|
|
340
|
+
const res = await execa(file, argv, {
|
|
341
341
|
cwd: ctx.rootDir,
|
|
342
342
|
reject: false,
|
|
343
343
|
env: {
|
|
@@ -459,6 +459,7 @@ export function register(program) {
|
|
|
459
459
|
.command('show-report')
|
|
460
460
|
.description('Open the HTML report of a run')
|
|
461
461
|
.option('--run <id>', 'run id (default: latest)')
|
|
462
|
+
.option('--last', 'the latest run (the default; accepted so `report --last` habits work here)')
|
|
462
463
|
.action(async (opts, cmd) => {
|
|
463
464
|
const ctx = createContext(cmd);
|
|
464
465
|
const root = artifactsRoot(ctx.rootDir);
|
|
@@ -468,10 +469,13 @@ export function register(program) {
|
|
|
468
469
|
const html = join(root, runId, runFiles.htmlReport, 'index.html');
|
|
469
470
|
if (!existsSync(html))
|
|
470
471
|
throw new SdodsError('RUN_FAILED', `No HTML report at ${html}.`, { exitCode: 2 });
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
472
|
+
const [file, ...argv] = toolCommand(ctx.rootDir, [
|
|
473
|
+
'playwright',
|
|
474
|
+
'show-report',
|
|
475
|
+
join(root, runId, runFiles.htmlReport),
|
|
476
|
+
]);
|
|
477
|
+
out(pc.cyan(`Serving the report of run ${runId}. Press Ctrl+C to stop.`));
|
|
478
|
+
await execa(file, argv, { stdio: 'inherit', cwd: ctx.rootDir });
|
|
475
479
|
});
|
|
476
480
|
}
|
|
477
481
|
//# sourceMappingURL=report.js.map
|
package/dist/commands/run.d.ts
CHANGED
|
@@ -42,6 +42,7 @@ export interface RunFlags {
|
|
|
42
42
|
notify?: boolean;
|
|
43
43
|
allowPoolContention?: boolean;
|
|
44
44
|
installBrowsers?: boolean;
|
|
45
|
+
open?: string;
|
|
45
46
|
}
|
|
46
47
|
export declare function register(program: Command): void;
|
|
47
48
|
export declare function runCommand(flags: RunFlags, cmd: Command): Promise<number>;
|
package/dist/commands/run.js
CHANGED
|
@@ -6,6 +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 { toolCommand, workspaceBin } from '../workspace-bin.js';
|
|
10
|
+
import { openInBrowser, resolveOpenWhen } from '../open.js';
|
|
9
11
|
import { browserStatuses, ensureBrowsers } from './browsers.js';
|
|
10
12
|
import { gateFailedError, printGates } from '../gates.js';
|
|
11
13
|
import { maybeNotify, notifyRun } from '../notify.js';
|
|
@@ -61,6 +63,7 @@ function addRunOptions(cmd) {
|
|
|
61
63
|
.option('--ui', 'interactive UI mode')
|
|
62
64
|
.option('--debug', 'step debugger')
|
|
63
65
|
.option('--list', 'list the run targets and tests that would run')
|
|
66
|
+
.option('--open <when>', 'open the SDODS dashboard after the run: always | on-failure | never (default: on-failure at an interactive terminal, never in CI or under an agent; or set SDODS_OPEN)')
|
|
64
67
|
.option('--repeat-each <n>', 'repeat each test n times', parseIntFlag('repeat-each'))
|
|
65
68
|
.option('--fail-on-flaky', 'exit 1 when any test is flaky')
|
|
66
69
|
.option('--max-failures <n>', 'stop after n failures', parseIntFlag('max-failures'))
|
|
@@ -328,7 +331,8 @@ export async function runCommand(flags, cmd) {
|
|
|
328
331
|
// Recorded specs are plain runner tests: no Gherkin to generate.
|
|
329
332
|
const recordedOnly = runnerProjects.every((p) => p.layer === 'recorded');
|
|
330
333
|
if (!recordedOnly) {
|
|
331
|
-
const
|
|
334
|
+
const [bddgen, ...bddgenArgs] = workspaceBin(ctx.rootDir, 'playwright-bdd', 'bddgen');
|
|
335
|
+
const gen = await execa(bddgen, [...bddgenArgs, '-c', configPath], {
|
|
332
336
|
cwd: ctx.rootDir,
|
|
333
337
|
env: childEnv,
|
|
334
338
|
stdio: ctx.opts.quiet ? 'pipe' : 'inherit',
|
|
@@ -337,7 +341,7 @@ export async function runCommand(flags, cmd) {
|
|
|
337
341
|
if (gen.exitCode !== 0) {
|
|
338
342
|
if (ctx.opts.quiet && gen.stderr)
|
|
339
343
|
process.stderr.write(gen.stderr + '\n');
|
|
340
|
-
throw new SdodsError('RUN_FAILED', '
|
|
344
|
+
throw new SdodsError('RUN_FAILED', 'Could not generate specs from the features.', {
|
|
341
345
|
hint: 'Run `sdods lint -p <slug> --undefined-steps` to find undefined steps.',
|
|
342
346
|
exitCode: 2,
|
|
343
347
|
});
|
|
@@ -417,9 +421,11 @@ export async function runCommand(flags, cmd) {
|
|
|
417
421
|
out(` ${p.name}--setup`);
|
|
418
422
|
out(` ${p.name}`);
|
|
419
423
|
}
|
|
420
|
-
const
|
|
424
|
+
const [listFile, ...listArgv] = toolCommand(ctx.rootDir, args);
|
|
425
|
+
const listed = await execa(listFile, listArgv, {
|
|
421
426
|
cwd: ctx.rootDir,
|
|
422
|
-
|
|
427
|
+
// The SDODS terminal reporter lists scenarios by feature and title instead of reporting.
|
|
428
|
+
env: { ...childEnv, SDODS_LIST_ONLY: '1' },
|
|
423
429
|
stdio: 'inherit',
|
|
424
430
|
reject: false,
|
|
425
431
|
});
|
|
@@ -430,10 +436,15 @@ export async function runCommand(flags, cmd) {
|
|
|
430
436
|
out(pc.dim(`run ${runId} · project ${entry.slug} · env ${cfg.env.name}${tags ? ` · tags ${tags}` : ''}${proc ? ` · process ${proc.name}` : ''}${modules.length ? ` · modules ${modules.join(',')}` : ''}`));
|
|
431
437
|
out(pc.dim(`projects: ${runnerProjects.map((p) => p.name).join(', ')}`));
|
|
432
438
|
const started = Date.now();
|
|
433
|
-
const
|
|
439
|
+
const [runFile, ...runArgv] = toolCommand(ctx.rootDir, args);
|
|
440
|
+
const interactive = Boolean(flags.ui || flags.debug);
|
|
441
|
+
const child = execa(runFile, runArgv, {
|
|
434
442
|
cwd: ctx.rootDir,
|
|
435
443
|
env: childEnv,
|
|
436
|
-
|
|
444
|
+
// stdin is only for the runner's interactive modes. Given a terminal on stdin, the HTML
|
|
445
|
+
// reporter ends every run with "To open last HTML report run: npx playwright show-report",
|
|
446
|
+
// a command for a tool the user never installed by name; SDODS prints its own below.
|
|
447
|
+
stdio: [interactive ? 'inherit' : 'ignore', 'inherit', 'inherit'],
|
|
437
448
|
reject: false,
|
|
438
449
|
});
|
|
439
450
|
const onSignal = () => child.kill('SIGTERM');
|
|
@@ -572,6 +583,17 @@ export async function runCommand(flags, cmd) {
|
|
|
572
583
|
out(pc.dim(`artifacts: ${runDir}`));
|
|
573
584
|
out(pc.dim(`html report: ${join(runDir, runFiles.htmlReport, 'index.html')}`));
|
|
574
585
|
out(pc.dim(`dashboard: ${join(runDir, runFiles.dashboard, 'index.html')}`));
|
|
586
|
+
out('');
|
|
587
|
+
out(` open the results: ${pc.cyan(`sdods report --run ${runId} --open`)}`);
|
|
588
|
+
const openWhen = resolveOpenWhen(flags.open, false);
|
|
589
|
+
const failed = finalExit !== 0 && finalExit !== 130;
|
|
590
|
+
const dashboard = join(runDir, runFiles.dashboard, 'index.html');
|
|
591
|
+
if ((openWhen === 'always' || (openWhen === 'on-failure' && failed)) && existsSync(dashboard)) {
|
|
592
|
+
out(pc.dim(` opening the dashboard in your browser (--open never to stop this)`));
|
|
593
|
+
await openInBrowser(dashboard);
|
|
594
|
+
}
|
|
595
|
+
if (finalExit !== 0 && finalExit !== 130)
|
|
596
|
+
out(` step through it: ${pc.cyan(`sdods trace --run ${runId}`)}`);
|
|
575
597
|
}
|
|
576
598
|
if (gates && !gates.passed)
|
|
577
599
|
renderError(gateFailedError(gates, join(runDir, runFiles.gates)), Boolean(ctx.opts.json));
|
package/dist/commands/steps.js
CHANGED
|
@@ -3,6 +3,7 @@ import { execa } from 'execa';
|
|
|
3
3
|
import { SdodsError, coreStepsDir } from '@sdods/core';
|
|
4
4
|
import { createContext } from '../context.js';
|
|
5
5
|
import { json, table } from '../ui.js';
|
|
6
|
+
import { toolCommand } from '../workspace-bin.js';
|
|
6
7
|
/** Parse `bddgen export` output (`* Given ...` lines) into StepDef[]. */
|
|
7
8
|
export async function listSteps(rootDir, slug, opts = {}) {
|
|
8
9
|
const env = {
|
|
@@ -14,9 +15,10 @@ export async function listSteps(rootDir, slug, opts = {}) {
|
|
|
14
15
|
const args = ['bddgen', 'export', '-c', join(rootDir, 'sdods.runner.config.ts')];
|
|
15
16
|
if (opts.unused)
|
|
16
17
|
args.push('--unused-steps');
|
|
17
|
-
const
|
|
18
|
+
const [file, ...argv] = toolCommand(rootDir, args);
|
|
19
|
+
const result = await execa(file, argv, { cwd: rootDir, env, reject: false, all: true });
|
|
18
20
|
if (result.exitCode !== 0) {
|
|
19
|
-
throw new SdodsError('RUN_FAILED', `
|
|
21
|
+
throw new SdodsError('RUN_FAILED', `Could not list the step definitions of ${slug}.`, {
|
|
20
22
|
details: { output: result.all?.slice(-2000) },
|
|
21
23
|
});
|
|
22
24
|
}
|
|
@@ -46,7 +48,7 @@ export function register(program) {
|
|
|
46
48
|
.description('Inspect step definitions available to a project');
|
|
47
49
|
steps
|
|
48
50
|
.command('list')
|
|
49
|
-
.description('List all step definitions (core library + project) as
|
|
51
|
+
.description('List all step definitions (core library + project) as the run sees them')
|
|
50
52
|
.requiredOption('-p, --project <slug>', 'project slug')
|
|
51
53
|
.option('--unused', 'only steps not used by any feature')
|
|
52
54
|
.option('--grep <text>', 'filter by substring')
|
package/dist/commands/trace.js
CHANGED
|
@@ -5,6 +5,7 @@ import pc from 'picocolors';
|
|
|
5
5
|
import { SdodsError, DEFAULT_ARTIFACTS_DIR } from '@sdods/core';
|
|
6
6
|
import { createContext } from '../context.js';
|
|
7
7
|
import { json, out, table } from '../ui.js';
|
|
8
|
+
import { toolCommand } from '../workspace-bin.js';
|
|
8
9
|
/** Recursively find `trace.zip` files (runner output) below a directory. */
|
|
9
10
|
export function findTraceZips(dir, depth = 8) {
|
|
10
11
|
const found = [];
|
|
@@ -93,7 +94,8 @@ export function register(program) {
|
|
|
93
94
|
return;
|
|
94
95
|
}
|
|
95
96
|
out(pc.cyan(`Opening ${zips.length} trace(s) from ${source}`));
|
|
96
|
-
const
|
|
97
|
+
const [file, ...argv] = toolCommand(ctx.rootDir, ['playwright', 'show-trace', ...zips]);
|
|
98
|
+
const res = await execa(file, argv, {
|
|
97
99
|
cwd: ctx.rootDir,
|
|
98
100
|
stdio: 'inherit',
|
|
99
101
|
reject: false,
|
package/dist/commands/watch.js
CHANGED
|
@@ -4,7 +4,9 @@ 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 { toolCommand, workspaceBin } from '../workspace-bin.js';
|
|
7
8
|
import { collect, out } from '../ui.js';
|
|
9
|
+
import { ensureBrowsers } from './browsers.js';
|
|
8
10
|
/**
|
|
9
11
|
* Spec generation runs in watch mode while the interactive UI
|
|
10
12
|
* re-runs them. Both children share the same SDODS_* environment that `sdods run` uses.
|
|
@@ -52,13 +54,15 @@ export function register(program) {
|
|
|
52
54
|
SDODS_REPORTER_MODE: 'quiet',
|
|
53
55
|
};
|
|
54
56
|
const configPath = join(ctx.rootDir, 'sdods.runner.config.ts');
|
|
55
|
-
const
|
|
57
|
+
const targets = listGeneratedProjects(ctx.registry, selection);
|
|
58
|
+
const names = targets.map((p) => p.name);
|
|
56
59
|
if (!names.length) {
|
|
57
60
|
throw new SdodsError('CONFIG_INVALID', 'Nothing to watch: no run targets match the selection.', {
|
|
58
61
|
hint: 'Check --layer/--browser against the project yaml.',
|
|
59
62
|
exitCode: 2,
|
|
60
63
|
});
|
|
61
64
|
}
|
|
65
|
+
await ensureBrowsers(targets.map((p) => p.browser ?? 'chromium'), { cwd: ctx.rootDir, install: process.env.SDODS_AUTO_INSTALL_BROWSERS === '1' });
|
|
62
66
|
out(pc.cyan(`Watching ${entry.slug} (${cfg.env.name}) → ${names.join(', ')}`));
|
|
63
67
|
const children = [];
|
|
64
68
|
const stop = () => {
|
|
@@ -68,18 +72,20 @@ export function register(program) {
|
|
|
68
72
|
process.on('SIGINT', stop);
|
|
69
73
|
process.on('SIGTERM', stop);
|
|
70
74
|
// initial generation so UI mode has specs to show
|
|
71
|
-
const
|
|
75
|
+
const [bddgen, ...bddgenArgs] = workspaceBin(ctx.rootDir, 'playwright-bdd', 'bddgen');
|
|
76
|
+
const gen = await execa(bddgen, [...bddgenArgs, '-c', configPath], {
|
|
72
77
|
cwd: ctx.rootDir,
|
|
73
78
|
env: childEnv,
|
|
74
79
|
stdio: 'inherit',
|
|
75
80
|
reject: false,
|
|
76
81
|
});
|
|
77
82
|
if (gen.exitCode !== 0) {
|
|
78
|
-
throw new SdodsError('RUN_FAILED', '
|
|
83
|
+
throw new SdodsError('RUN_FAILED', 'Could not generate specs from the features; fix the errors above and retry.', {
|
|
84
|
+
hint: `Run \`sdods lint -p ${entry.slug} --undefined-steps\` to list them.`,
|
|
79
85
|
exitCode: 1,
|
|
80
86
|
});
|
|
81
87
|
}
|
|
82
|
-
const watcher = execa(
|
|
88
|
+
const watcher = execa(bddgen, [...bddgenArgs, '-c', configPath, '--watch'], {
|
|
83
89
|
cwd: ctx.rootDir,
|
|
84
90
|
env: childEnv,
|
|
85
91
|
stdio: 'inherit',
|
|
@@ -101,7 +107,8 @@ export function register(program) {
|
|
|
101
107
|
else {
|
|
102
108
|
pwArgs.push('--ui');
|
|
103
109
|
}
|
|
104
|
-
const
|
|
110
|
+
const [pwFile, ...pwArgv] = toolCommand(ctx.rootDir, pwArgs);
|
|
111
|
+
const pw = execa(pwFile, pwArgv, {
|
|
105
112
|
cwd: ctx.rootDir,
|
|
106
113
|
env: childEnv,
|
|
107
114
|
stdio: 'inherit',
|
package/dist/open.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Open a file or URL in the default browser or viewer. Never throws: opening is a courtesy. */
|
|
2
|
+
export declare function openInBrowser(target: string): Promise<void>;
|
|
3
|
+
export type OpenWhen = 'always' | 'on-failure' | 'never';
|
|
4
|
+
/**
|
|
5
|
+
* When `sdods run` opens its results. `--open` wins, then SDODS_OPEN; otherwise on failure, and only
|
|
6
|
+
* for a person at a terminal: never in CI, under a coding agent (the same check the runner makes),
|
|
7
|
+
* for --json, or when output is piped to the server, the desktop app or a log.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveOpenWhen(flag: string | undefined, json: boolean): OpenWhen;
|
|
10
|
+
//# sourceMappingURL=open.d.ts.map
|
package/dist/open.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
/** Open a file or URL in the default browser or viewer. Never throws: opening is a courtesy. */
|
|
3
|
+
export async function openInBrowser(target) {
|
|
4
|
+
const [cmd, args] = process.platform === 'darwin'
|
|
5
|
+
? ['open', [target]]
|
|
6
|
+
: process.platform === 'win32'
|
|
7
|
+
? // `start` takes its first quoted argument as a window title, so give it an empty one.
|
|
8
|
+
['cmd', ['/c', 'start', '""', target]]
|
|
9
|
+
: ['xdg-open', [target]];
|
|
10
|
+
await execa(cmd, args, { stdio: 'ignore', windowsVerbatimArguments: true }).catch(() => undefined);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* When `sdods run` opens its results. `--open` wins, then SDODS_OPEN; otherwise on failure, and only
|
|
14
|
+
* for a person at a terminal: never in CI, under a coding agent (the same check the runner makes),
|
|
15
|
+
* for --json, or when output is piped to the server, the desktop app or a log.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveOpenWhen(flag, json) {
|
|
18
|
+
const explicit = flag ?? process.env.SDODS_OPEN;
|
|
19
|
+
if (explicit === 'always' || explicit === 'on-failure' || explicit === 'never')
|
|
20
|
+
return explicit;
|
|
21
|
+
const agent = Boolean(process.env.CLAUDECODE || process.env.COPILOT_CLI);
|
|
22
|
+
if (json || process.env.CI || agent || !process.stdout.isTTY)
|
|
23
|
+
return 'never';
|
|
24
|
+
return 'on-failure';
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=open.js.map
|
package/dist/program.js
CHANGED
|
@@ -46,7 +46,7 @@ import { register as registerMatrix } from './commands/matrix.js';
|
|
|
46
46
|
export function buildProgram() {
|
|
47
47
|
const program = new Command('sdods');
|
|
48
48
|
program
|
|
49
|
-
.description('SDODS —
|
|
49
|
+
.description('SDODS — BDD test automation for UI, API and hybrid flows that leaves release evidence behind every run.')
|
|
50
50
|
.version(VERSION, '-V, --version')
|
|
51
51
|
.option('--json', 'machine-readable output')
|
|
52
52
|
.option('-q, --quiet', 'only errors')
|
|
@@ -57,7 +57,10 @@ export function buildProgram() {
|
|
|
57
57
|
.showSuggestionAfterError()
|
|
58
58
|
.configureHelp({ sortSubcommands: true })
|
|
59
59
|
// Root help only: 'after' is not inherited by subcommands.
|
|
60
|
-
.addHelpText('after',
|
|
60
|
+
.addHelpText('after',
|
|
61
|
+
// The credit NOTICE requires in spirit: SDODS wraps the runner's commands, not its authorship.
|
|
62
|
+
`\nDocs: https://docs.sdods.com${SPONSOR_ENABLED ? ` · Sponsor: ${SPONSOR_URL}` : ''}\n` +
|
|
63
|
+
'Runs on Playwright (Apache-2.0) and playwright-bdd (MIT); see NOTICE.');
|
|
61
64
|
registerProjectCommands(program);
|
|
62
65
|
registerEnvCommands(program);
|
|
63
66
|
registerConfigCommands(program);
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdods/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "SDODS command line: run, record, analyze, agents, MCP server, scheduler and the web server.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
+
"funding": "https://sdods.com/sponsor/",
|
|
6
7
|
"author": "SDODS <admin@sdods.com>",
|
|
7
8
|
"type": "module",
|
|
8
9
|
"bin": {
|
|
@@ -25,22 +26,22 @@
|
|
|
25
26
|
"node": ">=22"
|
|
26
27
|
},
|
|
27
28
|
"dependencies": {
|
|
28
|
-
"@sdods/contracts": "0.
|
|
29
|
-
"@sdods/core": "0.
|
|
29
|
+
"@sdods/contracts": "0.13.0",
|
|
30
|
+
"@sdods/core": "0.13.0",
|
|
30
31
|
"commander": "^15.0.0",
|
|
31
|
-
"execa": "^10.0
|
|
32
|
+
"execa": "^10.1.0",
|
|
32
33
|
"picocolors": "^1.1.1",
|
|
33
34
|
"semver": "^7.8.5",
|
|
34
35
|
"yaml": "^2.9.1",
|
|
35
36
|
"zod": "^4.6.5",
|
|
36
37
|
"@playwright/test": "^1.63.0",
|
|
37
38
|
"playwright-core": "^1.63.0",
|
|
38
|
-
"@sdods/integrations": "0.
|
|
39
|
-
"@sdods/mcp": "0.
|
|
40
|
-
"@sdods/agents": "0.
|
|
41
|
-
"@sdods/db": "0.
|
|
42
|
-
"csv-parse": "^7.0.
|
|
43
|
-
"@sdods/server": "0.
|
|
39
|
+
"@sdods/integrations": "0.13.0",
|
|
40
|
+
"@sdods/mcp": "0.13.0",
|
|
41
|
+
"@sdods/agents": "0.13.0",
|
|
42
|
+
"@sdods/db": "0.13.0",
|
|
43
|
+
"csv-parse": "^7.0.3",
|
|
44
|
+
"@sdods/server": "0.13.0"
|
|
44
45
|
},
|
|
45
46
|
"homepage": "https://sdods.com",
|
|
46
47
|
"bugs": {
|