@sdods/cli 0.12.1 → 0.13.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/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 +26 -6
- package/dist/commands/steps.js +5 -3
- package/dist/commands/trace.js +3 -1
- package/dist/commands/watch.js +9 -4
- 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 +1 -11
- package/dist/workspace-bin.js +2 -26
- package/package.json +13 -12
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,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 { workspaceBin } from '../workspace-bin.js';
|
|
9
|
+
import { toolCommand, workspaceBin } from '../workspace-bin.js';
|
|
10
|
+
import { openInBrowser, resolveOpenWhen } from '../open.js';
|
|
10
11
|
import { browserStatuses, ensureBrowsers } from './browsers.js';
|
|
11
12
|
import { gateFailedError, printGates } from '../gates.js';
|
|
12
13
|
import { maybeNotify, notifyRun } from '../notify.js';
|
|
@@ -62,6 +63,7 @@ function addRunOptions(cmd) {
|
|
|
62
63
|
.option('--ui', 'interactive UI mode')
|
|
63
64
|
.option('--debug', 'step debugger')
|
|
64
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)')
|
|
65
67
|
.option('--repeat-each <n>', 'repeat each test n times', parseIntFlag('repeat-each'))
|
|
66
68
|
.option('--fail-on-flaky', 'exit 1 when any test is flaky')
|
|
67
69
|
.option('--max-failures <n>', 'stop after n failures', parseIntFlag('max-failures'))
|
|
@@ -339,7 +341,7 @@ export async function runCommand(flags, cmd) {
|
|
|
339
341
|
if (gen.exitCode !== 0) {
|
|
340
342
|
if (ctx.opts.quiet && gen.stderr)
|
|
341
343
|
process.stderr.write(gen.stderr + '\n');
|
|
342
|
-
throw new SdodsError('RUN_FAILED', '
|
|
344
|
+
throw new SdodsError('RUN_FAILED', 'Could not generate specs from the features.', {
|
|
343
345
|
hint: 'Run `sdods lint -p <slug> --undefined-steps` to find undefined steps.',
|
|
344
346
|
exitCode: 2,
|
|
345
347
|
});
|
|
@@ -419,9 +421,11 @@ export async function runCommand(flags, cmd) {
|
|
|
419
421
|
out(` ${p.name}--setup`);
|
|
420
422
|
out(` ${p.name}`);
|
|
421
423
|
}
|
|
422
|
-
const
|
|
424
|
+
const [listFile, ...listArgv] = toolCommand(ctx.rootDir, args);
|
|
425
|
+
const listed = await execa(listFile, listArgv, {
|
|
423
426
|
cwd: ctx.rootDir,
|
|
424
|
-
|
|
427
|
+
// The SDODS terminal reporter lists scenarios by feature and title instead of reporting.
|
|
428
|
+
env: { ...childEnv, SDODS_LIST_ONLY: '1' },
|
|
425
429
|
stdio: 'inherit',
|
|
426
430
|
reject: false,
|
|
427
431
|
});
|
|
@@ -432,10 +436,15 @@ export async function runCommand(flags, cmd) {
|
|
|
432
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(',')}` : ''}`));
|
|
433
437
|
out(pc.dim(`projects: ${runnerProjects.map((p) => p.name).join(', ')}`));
|
|
434
438
|
const started = Date.now();
|
|
435
|
-
const
|
|
439
|
+
const [runFile, ...runArgv] = toolCommand(ctx.rootDir, args);
|
|
440
|
+
const interactive = Boolean(flags.ui || flags.debug);
|
|
441
|
+
const child = execa(runFile, runArgv, {
|
|
436
442
|
cwd: ctx.rootDir,
|
|
437
443
|
env: childEnv,
|
|
438
|
-
|
|
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'],
|
|
439
448
|
reject: false,
|
|
440
449
|
});
|
|
441
450
|
const onSignal = () => child.kill('SIGTERM');
|
|
@@ -574,6 +583,17 @@ export async function runCommand(flags, cmd) {
|
|
|
574
583
|
out(pc.dim(`artifacts: ${runDir}`));
|
|
575
584
|
out(pc.dim(`html report: ${join(runDir, runFiles.htmlReport, 'index.html')}`));
|
|
576
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}`)}`);
|
|
577
597
|
}
|
|
578
598
|
if (gates && !gates.passed)
|
|
579
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,8 +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 { workspaceBin } from '../workspace-bin.js';
|
|
7
|
+
import { toolCommand, workspaceBin } from '../workspace-bin.js';
|
|
8
8
|
import { collect, out } from '../ui.js';
|
|
9
|
+
import { ensureBrowsers } from './browsers.js';
|
|
9
10
|
/**
|
|
10
11
|
* Spec generation runs in watch mode while the interactive UI
|
|
11
12
|
* re-runs them. Both children share the same SDODS_* environment that `sdods run` uses.
|
|
@@ -53,13 +54,15 @@ export function register(program) {
|
|
|
53
54
|
SDODS_REPORTER_MODE: 'quiet',
|
|
54
55
|
};
|
|
55
56
|
const configPath = join(ctx.rootDir, 'sdods.runner.config.ts');
|
|
56
|
-
const
|
|
57
|
+
const targets = listGeneratedProjects(ctx.registry, selection);
|
|
58
|
+
const names = targets.map((p) => p.name);
|
|
57
59
|
if (!names.length) {
|
|
58
60
|
throw new SdodsError('CONFIG_INVALID', 'Nothing to watch: no run targets match the selection.', {
|
|
59
61
|
hint: 'Check --layer/--browser against the project yaml.',
|
|
60
62
|
exitCode: 2,
|
|
61
63
|
});
|
|
62
64
|
}
|
|
65
|
+
await ensureBrowsers(targets.map((p) => p.browser ?? 'chromium'), { cwd: ctx.rootDir, install: process.env.SDODS_AUTO_INSTALL_BROWSERS === '1' });
|
|
63
66
|
out(pc.cyan(`Watching ${entry.slug} (${cfg.env.name}) → ${names.join(', ')}`));
|
|
64
67
|
const children = [];
|
|
65
68
|
const stop = () => {
|
|
@@ -77,7 +80,8 @@ export function register(program) {
|
|
|
77
80
|
reject: false,
|
|
78
81
|
});
|
|
79
82
|
if (gen.exitCode !== 0) {
|
|
80
|
-
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.`,
|
|
81
85
|
exitCode: 1,
|
|
82
86
|
});
|
|
83
87
|
}
|
|
@@ -103,7 +107,8 @@ export function register(program) {
|
|
|
103
107
|
else {
|
|
104
108
|
pwArgs.push('--ui');
|
|
105
109
|
}
|
|
106
|
-
const
|
|
110
|
+
const [pwFile, ...pwArgv] = toolCommand(ctx.rootDir, pwArgs);
|
|
111
|
+
const pw = execa(pwFile, pwArgv, {
|
|
107
112
|
cwd: ctx.rootDir,
|
|
108
113
|
env: childEnv,
|
|
109
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/dist/workspace-bin.d.ts
CHANGED
|
@@ -1,12 +1,2 @@
|
|
|
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[]];
|
|
1
|
+
export { toolCommand, workspaceBin } from '@sdods/core';
|
|
12
2
|
//# sourceMappingURL=workspace-bin.d.ts.map
|
package/dist/workspace-bin.js
CHANGED
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
}
|
|
1
|
+
// Moved to @sdods/core so the lint and recorder code paths resolve the runner tools the same way.
|
|
2
|
+
export { toolCommand, workspaceBin } from '@sdods/core';
|
|
27
3
|
//# sourceMappingURL=workspace-bin.js.map
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdods/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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
|
+
"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.1",
|
|
30
|
+
"@sdods/core": "0.13.1",
|
|
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
|
-
"@playwright/test": "^1.
|
|
37
|
-
"playwright-core": "^1.
|
|
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.
|
|
37
|
+
"@playwright/test": "^1.64.0",
|
|
38
|
+
"playwright-core": "^1.64.0",
|
|
39
|
+
"@sdods/integrations": "0.13.1",
|
|
40
|
+
"@sdods/mcp": "0.13.1",
|
|
41
|
+
"@sdods/agents": "0.13.1",
|
|
42
|
+
"@sdods/db": "0.13.1",
|
|
43
|
+
"csv-parse": "^7.0.3",
|
|
44
|
+
"@sdods/server": "0.13.1"
|
|
44
45
|
},
|
|
45
46
|
"homepage": "https://sdods.com",
|
|
46
47
|
"bugs": {
|