@sdods/cli 0.12.1 → 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.
@@ -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 npx playwright install-deps ${engines.join(' ')}\` once on this machine.`);
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
- await execa('npx', args, { stdio: 'inherit', cwd: opts.cwd });
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
- await execa('npx', ['playwright', 'install', channel], { stdio: 'inherit', cwd: opts.cwd });
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
  /**
@@ -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
- await execa('npx', ['playwright', 'install', '--with-deps'], {
45
- stdio: 'inherit',
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
  }
@@ -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
- const res = await execa('npx', ['playwright', 'install', 'chromium'], {
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'], {
@@ -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 { stdout } = await execa('npx', ['playwright', '--version'], { cwd });
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 {
@@ -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
- async function openPath(p) {
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 res = await execa('npx', args, {
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
- await execa('npx', ['playwright', 'show-report', join(root, runId, runFiles.htmlReport)], {
472
- stdio: 'inherit',
473
- cwd: ctx.rootDir,
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
@@ -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>;
@@ -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', 'bddgen failed to generate specs.', {
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 listed = await execa('npx', args, {
424
+ const [listFile, ...listArgv] = toolCommand(ctx.rootDir, args);
425
+ const listed = await execa(listFile, listArgv, {
423
426
  cwd: ctx.rootDir,
424
- env: childEnv,
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 child = execa('npx', args, {
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
- stdio: 'inherit',
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));
@@ -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 result = await execa('npx', args, { cwd: rootDir, env, reject: false, all: true });
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', `bddgen export failed for ${slug}.`, {
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 seen by bddgen')
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')
@@ -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 res = await execa('npx', ['playwright', 'show-trace', ...zips], {
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,
@@ -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 names = listGeneratedProjects(ctx.registry, selection).map((p) => p.name);
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', 'bddgen failed; fix the errors above and retry.', {
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 pw = execa('npx', pwArgs, {
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 — automation and orchestration for reliable business workflows: BDD for UI, API and hybrid flows.')
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', `\nDocs: https://docs.sdods.com${SPONSOR_ENABLED ? ` · Sponsor: ${SPONSOR_URL}` : ''}`);
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);
@@ -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
@@ -1,27 +1,3 @@
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
- }
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.12.1",
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.12.1",
29
- "@sdods/core": "0.12.1",
29
+ "@sdods/contracts": "0.13.0",
30
+ "@sdods/core": "0.13.0",
30
31
  "commander": "^15.0.0",
31
- "execa": "^10.0.1",
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.12.1",
39
- "@sdods/mcp": "0.12.1",
40
- "@sdods/agents": "0.12.1",
41
- "@sdods/db": "0.12.1",
42
- "csv-parse": "^7.0.2",
43
- "@sdods/server": "0.12.1"
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": {