@sdods/cli 0.6.0 → 0.7.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.
@@ -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 { installSkills } from '../skills-catalog.js';
9
10
  import { json, ok, out, warn } from '../ui.js';
10
11
  /** Root of the SDODS monorepo this CLI runs from (used by --link, which needs a real checkout). */
11
12
  export function sourceRepoRoot() {
@@ -278,11 +279,12 @@ Docs: https://docs.sdods.com
278
279
  else {
279
280
  write('projects/.gitkeep', '');
280
281
  }
281
- const skillsSrc = join(src, '.claude', 'skills');
282
- if (existsSync(skillsSrc)) {
283
- cpSync(skillsSrc, join(target, '.claude', 'skills'), { recursive: true });
284
- files.push('.claude/skills/');
285
- }
282
+ // The user-facing skills, for Claude Code (.claude/skills) and every agent that reads the shared
283
+ // .agents/skills. Copied from the bundled set, never from this repository's own .claude/skills,
284
+ // which also holds skills for releasing SDODS itself.
285
+ const skills = installSkills({ rootDir: target });
286
+ if (skills.some((s) => s.status !== 'skipped'))
287
+ files.push('.claude/skills/', '.agents/skills/');
286
288
  if (flags.from) {
287
289
  try {
288
290
  const analyze = (await import('@sdods/core/analyze'));
@@ -1,9 +1,9 @@
1
1
  import pc from 'picocolors';
2
2
  import { SdodsError } from '@sdods/core';
3
- import { ALL_CAPABILITIES, DEFAULT_CAPABILITIES } from '@sdods/mcp';
3
+ import { ALL_CAPABILITIES, DEFAULT_CAPABILITIES, MCP_CLIENTS } from '@sdods/mcp';
4
4
  import { createContext } from '../context.js';
5
5
  import { json, ok, out } from '../ui.js';
6
- const CLIENTS = ['claude', 'codex', 'cursor', 'vscode', 'windsurf'];
6
+ const CLIENTS = MCP_CLIENTS;
7
7
  /**
8
8
  * The default capability list, derived rather than restated. It used to be spelled out in three
9
9
  * places in this file — the fallback, the flag default and the "did the user change it" sentinel —
@@ -80,7 +80,7 @@ export function register(program) {
80
80
  });
81
81
  mcp
82
82
  .command('install <client>')
83
- .description('Register the SDODS MCP server with a client: claude | codex | cursor | vscode | windsurf (merges, never clobbers)')
83
+ .description('Register the SDODS MCP server with a client: claude | codex | cursor | vscode | windsurf | gemini (merges, never clobbers)')
84
84
  .option('-p, --project <slug>')
85
85
  .option('-e, --env <name>')
86
86
  .option('--caps <list>')
@@ -167,12 +167,27 @@ async function registerViaClientCli(client, o, cwd) {
167
167
  });
168
168
  }
169
169
  };
170
+ // A remote server without credentials answers 401 to every call, so the token is part of the
171
+ // registration. Neither form writes the secret: Claude Code expands ${SDODS_TOKEN} from the
172
+ // environment when it reads .mcp.json, and Codex reads the variable named by
173
+ // --bearer-token-env-var at connect time.
170
174
  const addArgs = client === 'claude'
171
175
  ? o.httpUrl
172
- ? ['mcp', 'add', '-s', 'project', '--transport', 'http', 'sdods', o.httpUrl]
176
+ ? [
177
+ 'mcp',
178
+ 'add',
179
+ '-s',
180
+ 'project',
181
+ '--transport',
182
+ 'http',
183
+ 'sdods',
184
+ o.httpUrl,
185
+ '--header',
186
+ 'Authorization: Bearer ${SDODS_TOKEN}',
187
+ ]
173
188
  : ['mcp', 'add', '-s', 'project', 'sdods', '--', ...stdio]
174
189
  : o.httpUrl
175
- ? ['mcp', 'add', 'sdods', '--url', o.httpUrl]
190
+ ? ['mcp', 'add', 'sdods', '--url', o.httpUrl, '--bearer-token-env-var', 'SDODS_TOKEN']
176
191
  : ['mcp', 'add', 'sdods', '--', ...stdio];
177
192
  const ok1 = await run(addArgs);
178
193
  if (!ok1)
@@ -0,0 +1,3 @@
1
+ import type { Command } from 'commander';
2
+ export declare function register(program: Command): void;
3
+ //# sourceMappingURL=skills.d.ts.map
@@ -0,0 +1,69 @@
1
+ import { relative } from 'node:path';
2
+ import pc from 'picocolors';
3
+ import { SdodsError } from '@sdods/core';
4
+ import { createContext } from '../context.js';
5
+ import { DEFAULT_SKILL_TARGETS, SKILL_TARGETS, installSkills, listBundledSkills, } from '../skills-catalog.js';
6
+ import { info, json, ok, out, table } from '../ui.js';
7
+ function parseList(raw) {
8
+ return (raw ?? '')
9
+ .split(',')
10
+ .map((s) => s.trim())
11
+ .filter(Boolean);
12
+ }
13
+ export function register(program) {
14
+ const skills = program
15
+ .command('skills')
16
+ .description('Agent Skills for Claude Code, Codex, Cursor, Copilot, Gemini CLI and others');
17
+ skills
18
+ .command('list')
19
+ .description('List the skills bundled with SDODS')
20
+ .action((_opts, cmd) => {
21
+ const ctx = createContext(cmd);
22
+ const rows = listBundledSkills().map(({ name, description }) => ({ name, description }));
23
+ if (ctx.opts.json)
24
+ return json(rows);
25
+ table(rows);
26
+ });
27
+ skills
28
+ .command('install')
29
+ .description('Copy the bundled skills where your coding agents read them (default: .claude/skills and .agents/skills)')
30
+ .option('--agent <list>', `comma-separated: ${Object.keys(SKILL_TARGETS).join(', ')} (default ${DEFAULT_SKILL_TARGETS.join(',')})`)
31
+ .option('-g, --global', 'install into your home directory instead of the project')
32
+ .option('--skill <names>', 'only these skills (comma-separated)')
33
+ .option('--force', 'replace skills that are already installed')
34
+ .action((opts, cmd) => {
35
+ const ctx = createContext(cmd);
36
+ const targets = parseList(opts.agent);
37
+ const bad = targets.filter((t) => !(t in SKILL_TARGETS));
38
+ if (bad.length)
39
+ throw new SdodsError('CONFIG_INVALID', `Unknown agent ${bad.join(', ')}.`, {
40
+ hint: `Use one or more of ${Object.keys(SKILL_TARGETS).join(', ')}.`,
41
+ exitCode: 2,
42
+ });
43
+ let results;
44
+ try {
45
+ results = installSkills({
46
+ rootDir: ctx.rootDir,
47
+ targets: targets,
48
+ global: opts.global === true,
49
+ skills: parseList(opts.skill),
50
+ force: opts.force === true,
51
+ });
52
+ }
53
+ catch (e) {
54
+ throw new SdodsError('CONFIG_INVALID', e.message, { exitCode: 2 });
55
+ }
56
+ if (ctx.opts.json)
57
+ return json(results);
58
+ const base = opts.global ? (process.env.HOME ?? '') : ctx.rootDir;
59
+ for (const r of results) {
60
+ const where = relative(base, r.path) || r.path;
61
+ if (r.status === 'skipped')
62
+ out(pc.dim(`- ${where} (already installed; --force replaces it)`));
63
+ else
64
+ ok(`${r.status === 'replaced' ? 'Replaced' : 'Installed'} ${where}`);
65
+ }
66
+ info('Restart or reload your agent so it picks the skills up.');
67
+ });
68
+ }
69
+ //# sourceMappingURL=skills.js.map
package/dist/program.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import { VERSION } from '@sdods/core';
3
+ import { SPONSOR_ENABLED, SPONSOR_URL } from '@sdods/contracts/sponsor';
3
4
  import { registerProjectCommands } from './commands/project.js';
4
5
  import { registerEnvCommands } from './commands/env.js';
5
6
  import { registerConfigCommands } from './commands/config.js';
@@ -26,6 +27,7 @@ import { register as registerTrace } from './commands/trace.js';
26
27
  import { register as registerAgent } from './commands/agent.js';
27
28
  import { register as registerProposals } from './commands/proposals.js';
28
29
  import { register as registerMcp } from './commands/mcp.js';
30
+ import { register as registerSkills } from './commands/skills.js';
29
31
  import { register as registerServe } from './commands/serve.js';
30
32
  import { register as registerUsers } from './commands/users.js';
31
33
  import { register as registerTokens } from './commands/tokens.js';
@@ -50,7 +52,9 @@ export function buildProgram() {
50
52
  .option('--no-color', 'disable colours')
51
53
  .showHelpAfterError('(add --help for usage)')
52
54
  .showSuggestionAfterError()
53
- .configureHelp({ sortSubcommands: true });
55
+ .configureHelp({ sortSubcommands: true })
56
+ // Root help only: 'after' is not inherited by subcommands.
57
+ .addHelpText('after', `\nDocs: https://docs.sdods.com${SPONSOR_ENABLED ? ` · Sponsor: ${SPONSOR_URL}` : ''}`);
54
58
  registerProjectCommands(program);
55
59
  registerEnvCommands(program);
56
60
  registerConfigCommands(program);
@@ -77,6 +81,7 @@ export function buildProgram() {
77
81
  registerAgent(program);
78
82
  registerProposals(program);
79
83
  registerMcp(program);
84
+ registerSkills(program);
80
85
  registerServe(program);
81
86
  registerUsers(program);
82
87
  registerTokens(program);
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The Agent Skills SDODS ships to users. `packages/cli/skills/` is the one source: `sdods init`
3
+ * copies it into new workspaces, `sdods skills install` copies it into any project or home
4
+ * directory, the npm tarball carries it, and scripts/build-agent-plugin.ts builds the public
5
+ * skills repository and Claude Code plugin from it.
6
+ */
7
+ export declare function bundledSkillsDir(): string;
8
+ export interface BundledSkill {
9
+ name: string;
10
+ description: string;
11
+ dir: string;
12
+ }
13
+ export declare function listBundledSkills(dir?: string): BundledSkill[];
14
+ /** The `name:` and `description:` lines of a SKILL.md frontmatter block (single-line values). */
15
+ export declare function frontmatter(text: string): Record<string, string>;
16
+ /**
17
+ * Where each agent family looks for skills, relative to a project root (or to the home directory
18
+ * with `global`). `.agents/skills` is the shared location read by Codex, Cursor, Gemini CLI,
19
+ * GitHub Copilot, OpenCode, Amp and Goose; Claude Code reads `.claude/skills`.
20
+ */
21
+ export declare const SKILL_TARGETS: {
22
+ readonly claude: ".claude/skills";
23
+ readonly agents: ".agents/skills";
24
+ readonly cursor: ".cursor/skills";
25
+ readonly copilot: ".github/skills";
26
+ readonly gemini: ".gemini/skills";
27
+ };
28
+ export type SkillTarget = keyof typeof SKILL_TARGETS;
29
+ export declare const DEFAULT_SKILL_TARGETS: SkillTarget[];
30
+ export interface InstallSkillsOptions {
31
+ rootDir: string;
32
+ targets?: SkillTarget[];
33
+ global?: boolean;
34
+ skills?: string[];
35
+ force?: boolean;
36
+ home?: string;
37
+ source?: string;
38
+ }
39
+ export interface InstalledSkill {
40
+ skill: string;
41
+ target: SkillTarget;
42
+ path: string;
43
+ status: 'installed' | 'replaced' | 'skipped';
44
+ }
45
+ export declare function installSkills(o: InstallSkillsOptions): InstalledSkill[];
46
+ //# sourceMappingURL=skills-catalog.d.ts.map
@@ -0,0 +1,75 @@
1
+ import { cpSync, existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ /**
6
+ * The Agent Skills SDODS ships to users. `packages/cli/skills/` is the one source: `sdods init`
7
+ * copies it into new workspaces, `sdods skills install` copies it into any project or home
8
+ * directory, the npm tarball carries it, and scripts/build-agent-plugin.ts builds the public
9
+ * skills repository and Claude Code plugin from it.
10
+ */
11
+ export function bundledSkillsDir() {
12
+ // src/skills-catalog.ts and dist/skills-catalog.js both sit one level below packages/cli
13
+ return resolve(dirname(fileURLToPath(import.meta.url)), '..', 'skills');
14
+ }
15
+ export function listBundledSkills(dir = bundledSkillsDir()) {
16
+ if (!existsSync(dir))
17
+ return [];
18
+ return readdirSync(dir, { withFileTypes: true })
19
+ .filter((e) => e.isDirectory() && existsSync(join(dir, e.name, 'SKILL.md')))
20
+ .map((e) => {
21
+ const fm = frontmatter(readFileSync(join(dir, e.name, 'SKILL.md'), 'utf8'));
22
+ return { name: fm.name ?? e.name, description: fm.description ?? '', dir: join(dir, e.name) };
23
+ })
24
+ .sort((a, b) => a.name.localeCompare(b.name));
25
+ }
26
+ /** The `name:` and `description:` lines of a SKILL.md frontmatter block (single-line values). */
27
+ export function frontmatter(text) {
28
+ const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
29
+ const out = {};
30
+ for (const line of (m?.[1] ?? '').split(/\r?\n/)) {
31
+ const kv = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
32
+ if (kv)
33
+ out[kv[1]] = kv[2].replace(/^['"]|['"]$/g, '');
34
+ }
35
+ return out;
36
+ }
37
+ /**
38
+ * Where each agent family looks for skills, relative to a project root (or to the home directory
39
+ * with `global`). `.agents/skills` is the shared location read by Codex, Cursor, Gemini CLI,
40
+ * GitHub Copilot, OpenCode, Amp and Goose; Claude Code reads `.claude/skills`.
41
+ */
42
+ export const SKILL_TARGETS = {
43
+ claude: '.claude/skills',
44
+ agents: '.agents/skills',
45
+ cursor: '.cursor/skills',
46
+ copilot: '.github/skills',
47
+ gemini: '.gemini/skills',
48
+ };
49
+ export const DEFAULT_SKILL_TARGETS = ['claude', 'agents'];
50
+ /** GitHub Copilot's user-level directory is `~/.copilot/skills`, not `~/.github/skills`. */
51
+ const GLOBAL_OVERRIDES = { copilot: '.copilot/skills' };
52
+ export function installSkills(o) {
53
+ const available = listBundledSkills(o.source);
54
+ const wanted = o.skills?.length ? o.skills : available.map((s) => s.name);
55
+ const unknown = wanted.filter((w) => !available.some((s) => s.name === w));
56
+ if (unknown.length)
57
+ throw new Error(`Unknown skill ${unknown.join(', ')}. Bundled: ${available.map((s) => s.name).join(', ')}`);
58
+ const base = o.global ? (o.home ?? homedir()) : o.rootDir;
59
+ const results = [];
60
+ for (const target of o.targets?.length ? o.targets : DEFAULT_SKILL_TARGETS) {
61
+ const rel = (o.global && GLOBAL_OVERRIDES[target]) || SKILL_TARGETS[target];
62
+ for (const skill of available.filter((s) => wanted.includes(s.name))) {
63
+ const path = join(base, rel, skill.name);
64
+ const exists = existsSync(join(path, 'SKILL.md'));
65
+ if (exists && !o.force) {
66
+ results.push({ skill: skill.name, target, path, status: 'skipped' });
67
+ continue;
68
+ }
69
+ cpSync(skill.dir, path, { recursive: true, force: true });
70
+ results.push({ skill: skill.name, target, path, status: exists ? 'replaced' : 'installed' });
71
+ }
72
+ }
73
+ return results;
74
+ }
75
+ //# sourceMappingURL=skills-catalog.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdods/cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.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>",
@@ -17,14 +17,15 @@
17
17
  "files": [
18
18
  "dist",
19
19
  "bin",
20
- "templates"
20
+ "templates",
21
+ "skills"
21
22
  ],
22
23
  "engines": {
23
24
  "node": ">=22"
24
25
  },
25
26
  "dependencies": {
26
- "@sdods/contracts": "0.6.0",
27
- "@sdods/core": "0.6.0",
27
+ "@sdods/contracts": "0.7.1",
28
+ "@sdods/core": "0.7.1",
28
29
  "commander": "^15.0.0",
29
30
  "execa": "^10.0.1",
30
31
  "picocolors": "^1.1.1",
@@ -33,12 +34,12 @@
33
34
  "zod": "^4.5.4",
34
35
  "@playwright/test": "^1.63.0",
35
36
  "playwright-core": "^1.63.0",
36
- "@sdods/integrations": "0.6.0",
37
- "@sdods/mcp": "0.6.0",
38
- "@sdods/agents": "0.6.0",
39
- "@sdods/db": "0.6.0",
37
+ "@sdods/integrations": "0.7.1",
38
+ "@sdods/mcp": "0.7.1",
39
+ "@sdods/agents": "0.7.1",
40
+ "@sdods/db": "0.7.1",
40
41
  "csv-parse": "^7.0.2",
41
- "@sdods/server": "0.6.0"
42
+ "@sdods/server": "0.7.1"
42
43
  },
43
44
  "homepage": "https://sdods.com",
44
45
  "bugs": {
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: sdods
3
+ description: Work in an SDODS test-automation workspace — BDD features for UI, API and hybrid flows. Use when writing, fixing or reviewing Gherkin features, steps, page objects or project/env YAML, running suites, reading results, or using the sdods CLI or its MCP tools.
4
+ ---
5
+
6
+ # SDODS
7
+
8
+ SDODS runs BDD scenarios for UI, API and hybrid flows from one workspace. A workspace has
9
+ `sdods.workspace.yaml` at its root; each project is `projects/<slug>/sdods.project.yaml` plus
10
+ `envs/<env>.yaml`, and features live in `projects/<slug>/features/<module>/`.
11
+
12
+ Commands below use `sdods`. Without a global install use `npx -y @sdods/cli` in its place; inside
13
+ an SDODS source checkout use `bun run sdods`. When the `sdods` MCP server is connected, prefer its
14
+ tools (`project_list`, `feature_list`, `step_find`, `run_tests`, `run_get`, `analyze_failure`, …)
15
+ over shelling out.
16
+
17
+ ## Rules
18
+
19
+ - Every scenario carries exactly one layer tag (`@ui`, `@api`, `@hybrid`) and exactly one suite tag
20
+ (`@smoke`, `@regression`, `@sanity`). Optional: `@visual @a11y @perf @mock @data-driven @pool`,
21
+ and value tags `@user:<role> @data:<dataset> @har:<name> @jira:KEY @skip:<browser>`.
22
+ - Reuse existing steps first (`sdods steps list -p <slug>`, or `step_find`). Add a step only when no
23
+ phrasing fits, and name it in the same style.
24
+ - Locator priority: role + name > label > test id > placeholder > text > CSS. Never XPath.
25
+ - Page objects use step decorators (`@Fixture`, `@Given/@When/@Then`) and heal-aware locators:
26
+ `this.heal.locator(primary, { role, name, testId, description })`.
27
+ - Keep scenarios independent and deterministic, with no sleeps. Seed through the API rather than the
28
+ UI, and clean up what you create.
29
+ - Secrets are never literals; reference `${VAR}`.
30
+ - When acting through SDODS agents or MCP write tools, changes are proposals (`feature_write`,
31
+ `proposals`) that a person accepts with `sdods proposals accept <id>`.
32
+
33
+ ## Quick reference
34
+
35
+ | Task | Command |
36
+ | --- | --- |
37
+ | Projects and hierarchy | `sdods project list`, `sdods workspace tree` |
38
+ | Resolved config, with where each value came from | `sdods config show -p <slug> -e <env> --explain` |
39
+ | Validate features | `sdods lint -p <slug>` |
40
+ | Run a slice | `sdods run -p <slug> -e <env> -l api` · `-l ui -b chromium -t @smoke` · `--process pr-check` |
41
+ | Results | `sdods report --last`, `sdods heal report --last`, `sdods trace --last` |
42
+ | Available steps | `sdods steps list -p <slug>` |
43
+ | Record and replay | `sdods record -p <slug> -e <env> --user <role> --name <name>` |
44
+ | Agents | `sdods agent plan\|generate\|heal\|upgrade\|review -p <slug> --dry-run`, `sdods proposals list` |
45
+ | Health check | `sdods doctor` |
46
+
47
+ ## Writing a scenario
48
+
49
+ 1. Pick the module directory under `features/` and tag the Feature with one layer and one suite tag.
50
+ 2. Reuse steps from `sdods steps list`; add project steps in `steps/<module>.steps.ts` only if needed.
51
+ 3. Get data from datasets (`Given I load dataset "users" row 1`) or `@user:<role>`, never literals.
52
+ 4. `sdods lint -p <slug>`, then run just that scenario and check the before/after screenshots in
53
+ `.sdods/runs/<runId>/`.
54
+
55
+ ## Verify before claiming done
56
+
57
+ ```bash
58
+ sdods lint -p <slug>
59
+ ```
60
+
61
+ ```bash
62
+ sdods run -p <slug> -e <env> -l api
63
+ ```
64
+
65
+ ```bash
66
+ sdods run -p <slug> -e <env> -l ui -b chromium -t @smoke
67
+ ```
68
+
69
+ Exit codes: `0` passed · `1` test failures · `2` configuration or usage error · `3` lint errors.
@@ -7,12 +7,15 @@ description: Record a browser flow with SDODS for a given project, environment,
7
7
 
8
8
  Re-runnable recipe. Ask for the parameters you do not have, then follow the steps. Never edit `main` directly; recordings land in `projects/<project>/recorded/` and conversions become proposals.
9
9
 
10
+ Commands below use `sdods`. Without a global install use `npx -y @sdods/cli` in its place; inside
11
+ an SDODS source checkout use `bun run sdods`.
12
+
10
13
  ## Parameters
11
14
 
12
15
  | Name | Required | Example | Notes |
13
16
  |---|---|---|---|
14
- | `project` | yes | `demo-shop` | `bun run sdods project list` shows slugs |
15
- | `env` | yes | `staging`, `local` | `bun run sdods env list -p <project>` |
17
+ | `project` | yes | `demo-shop` | `sdods project list` shows slugs |
18
+ | `env` | yes | `staging`, `local` | `sdods env list -p <project>` |
16
19
  | `name` | yes | `checkout-standard` | becomes `recorded/<name>.spec.ts`; include the role in the name |
17
20
  | `role` | no | `standard`, `problem`, `admin` | leases a pool user of that role and starts logged in (storageState) |
18
21
  | `url` | no | `/inventory.html` | route path or absolute URL to open first |
@@ -24,38 +27,38 @@ Re-runnable recipe. Ask for the parameters you do not have, then follow the step
24
27
 
25
28
  1. **Check the environment** (fail fast on missing vars):
26
29
  ```bash
27
- bun run sdods doctor -p <project>
28
- bun run sdods env list -p <project>
30
+ sdods doctor -p <project>
31
+ sdods env list -p <project>
29
32
  ```
30
33
  2. **Make sure the role has login state** (skip when no role):
31
34
  ```bash
32
- bun run sdods auth list -p <project> -e <env>
33
- bun run sdods auth capture -p <project> -e <env> --user <role> # only if missing or expired
35
+ sdods auth list -p <project> -e <env>
36
+ sdods auth capture -p <project> -e <env> --user <role> # only if missing or expired
34
37
  ```
35
38
  3. **Record** (opens the recorder; the person performs the flow and closes the window):
36
39
  ```bash
37
- bun run sdods record -p <project> -e <env> --name <name> [--user <role>] [--url <url>] [--device "<device>"] [--browser <browser>] [--save-har]
40
+ sdods record -p <project> -e <env> --name <name> [--user <role>] [--url <url>] [--device "<device>"] [--browser <browser>] [--save-har]
38
41
  ```
39
42
  Output: `projects/<project>/recorded/<name>.spec.ts`, post-processed (SDODS fixtures, routes instead of absolute URLs, tags `@recorded @ui @regression`, header comment with project/env/role/device).
40
43
  4. **Play it back** on the same environment, then on another one:
41
44
  ```bash
42
- bun run sdods run -p <project> -e <env> -l recorded --grep "<name>"
43
- bun run sdods run -p <project> -e <other-env> -l recorded --grep "<name>"
45
+ sdods run -p <project> -e <env> -l recorded --grep "<name>"
46
+ sdods run -p <project> -e <other-env> -l recorded --grep "<name>"
44
47
  ```
45
- Look at `.sdods/runs/<runId>/` for screenshots and the HTML report (`bun run sdods report --last --open`).
48
+ Look at `.sdods/runs/<runId>/` for screenshots and the HTML report (`sdods report --last --open`).
46
49
  5. **Repeat for other roles, environments or devices** by changing only the parameters. Use distinct names (`checkout-standard`, `checkout-problem`, `checkout-standard-iphone`).
47
50
  6. **Convert to Gherkin** when the flow is stable (agent proposal, reviewed by a person):
48
51
  ```bash
49
- bun run sdods record convert projects/<project>/recorded/<name>.spec.ts
50
- bun run sdods proposals list
51
- bun run sdods proposals show <id>
52
- bun run sdods proposals accept <id> --branch sdods/<id> # after review
53
- bun run sdods lint -p <project>
52
+ sdods record convert projects/<project>/recorded/<name>.spec.ts
53
+ sdods proposals list
54
+ sdods proposals show <id>
55
+ sdods proposals accept <id> --branch sdods/<id> # after review
56
+ sdods lint -p <project>
54
57
  ```
55
- The proposal reuses existing steps first (`bun run sdods steps list -p <project>`), adds only unmatched steps, and creates a page object with heal-aware locators.
58
+ The proposal reuses existing steps first (`sdods steps list -p <project>`), adds only unmatched steps, and creates a page object with heal-aware locators.
56
59
  7. **Offline replay** (CI-friendly) when HAR was captured:
57
60
  ```bash
58
- bun run sdods har replay -p <project> -e <env> --strict -t "@har:<name>"
61
+ sdods har replay -p <project> -e <env> --strict -t "@har:<name>"
59
62
  ```
60
63
 
61
64
  ## Matrix template
@@ -5,12 +5,15 @@ description: Run SDODS test suites for a project across environments, layers (ui
5
5
 
6
6
  # SDODS run skill
7
7
 
8
+ Commands below use `sdods`. Without a global install use `npx -y @sdods/cli` in its place; inside
9
+ an SDODS source checkout use `bun run sdods`.
10
+
8
11
  ## Parameters
9
12
 
10
13
  | Name | Default | Notes |
11
14
  |---|---|---|
12
- | `project` | only project if one exists | `bun run sdods project list` |
13
- | `env` | project `envs.default` | `bun run sdods env list -p <project>` |
15
+ | `project` | only project if one exists | `sdods project list` |
16
+ | `env` | project `envs.default` | `sdods env list -p <project>` |
14
17
  | `layer` | all layers in the project | `-l ui`, `-l api`, `-l hybrid`, `-l recorded` (repeatable) |
15
18
  | `browser` | project `browsers` | `-b chromium -b edge -b firefox -b webkit`, or `--project-matrix` for all |
16
19
  | `tags` | none | Cucumber expression, e.g. `"@smoke and not @mock"`, `@user:admin`, `@jira:DEMO-12` |
@@ -23,29 +26,29 @@ description: Run SDODS test suites for a project across environments, layers (ui
23
26
 
24
27
  1. Validate before running (cheap, catches config and tag mistakes):
25
28
  ```bash
26
- bun run sdods config validate
27
- bun run sdods lint -p <project>
28
- bun run sdods run -p <project> -e <env> --list # shows the run targets that would run
29
+ sdods config validate
30
+ sdods lint -p <project>
31
+ sdods run -p <project> -e <env> --list # shows the run targets that would run
29
32
  ```
30
33
  2. Run the requested slice. Typical combinations:
31
34
  ```bash
32
- bun run sdods run -p <project> -e <env> -l api # API only, no browser
33
- bun run sdods run -p <project> -e <env> -l ui -b chromium -t @smoke # quick UI smoke
34
- bun run sdods run -p <project> -e <env> -t @regression --project-matrix # every browser in the yaml
35
- bun run sdods run -p <project> -e <env> -t "@user:admin" # scenarios that lease admin users
36
- bun run sdods run -p <project> -e <env> -l hybrid -w 2 # API→UI chaining
37
- bun run sdods run -p <project> -e <env> --har-replay --strict -t @smoke # offline, CI-style
35
+ sdods run -p <project> -e <env> -l api # API only, no browser
36
+ sdods run -p <project> -e <env> -l ui -b chromium -t @smoke # quick UI smoke
37
+ sdods run -p <project> -e <env> -t @regression --project-matrix # every browser in the yaml
38
+ sdods run -p <project> -e <env> -t "@user:admin" # scenarios that lease admin users
39
+ sdods run -p <project> -e <env> -l hybrid -w 2 # API→UI chaining
40
+ sdods run -p <project> -e <env> --har-replay --strict -t @smoke # offline, CI-style
38
41
  ```
39
42
  3. Read results:
40
43
  ```bash
41
- bun run sdods report --last # summary: totals, failed, flaky, report paths
42
- bun run sdods report --last --open # HTML report + dashboard
43
- bun run sdods heal report --last # locators that were healed
44
+ sdods report --last # summary: totals, failed, flaky, report paths
45
+ sdods report --last --open # HTML report + dashboard
46
+ sdods heal report --last # locators that were healed
44
47
  ```
45
48
  Artifacts: `.sdods/runs/<runId>/<project>/<fingerprint>/r<retry>/` holds `scenario-start.png`, `NN-before.png`/`NN-after.png` per UI step (policy by suite tag), `api/*.json` per API call, `heal.jsonl`.
46
- 4. If something failed, collect: the scenario name, the error from `summary.json`, the `failure.png`, and the trace (`bun run sdods trace --last`). Then either fix the feature/page object or hand it to the healer agent:
49
+ 4. If something failed, collect: the scenario name, the error from `summary.json`, the `failure.png`, and the trace (`sdods trace --last`). Then either fix the feature/page object or hand it to the healer agent:
47
50
  ```bash
48
- bun run sdods agent heal -p <project> --scenario <fingerprint> --dry-run
51
+ sdods agent heal -p <project> --scenario <fingerprint> --dry-run
49
52
  ```
50
53
  5. Repeat for other environments or roles by changing `-e` / `-t "@user:<role>"` only.
51
54