@sdods/cli 0.2.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/LICENSE +17 -0
- package/bin/sdods.js +19 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +11 -0
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.js +219 -0
- package/dist/commands/analyze.d.ts +20 -0
- package/dist/commands/analyze.js +216 -0
- package/dist/commands/auth.d.ts +7 -0
- package/dist/commands/auth.js +97 -0
- package/dist/commands/browsers.d.ts +17 -0
- package/dist/commands/browsers.js +103 -0
- package/dist/commands/completion.d.ts +17 -0
- package/dist/commands/completion.js +143 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.js +79 -0
- package/dist/commands/coverage.d.ts +3 -0
- package/dist/commands/coverage.js +65 -0
- package/dist/commands/data.d.ts +7 -0
- package/dist/commands/data.js +188 -0
- package/dist/commands/db.d.ts +3 -0
- package/dist/commands/db.js +241 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.js +323 -0
- package/dist/commands/env.d.ts +3 -0
- package/dist/commands/env.js +65 -0
- package/dist/commands/features.d.ts +3 -0
- package/dist/commands/features.js +57 -0
- package/dist/commands/feedback.d.ts +7 -0
- package/dist/commands/feedback.js +96 -0
- package/dist/commands/har.d.ts +3 -0
- package/dist/commands/har.js +124 -0
- package/dist/commands/heal.d.ts +4 -0
- package/dist/commands/heal.js +63 -0
- package/dist/commands/init.d.ts +38 -0
- package/dist/commands/init.js +363 -0
- package/dist/commands/insights.d.ts +7 -0
- package/dist/commands/insights.js +127 -0
- package/dist/commands/integrations.d.ts +14 -0
- package/dist/commands/integrations.js +306 -0
- package/dist/commands/lint.d.ts +3 -0
- package/dist/commands/lint.js +74 -0
- package/dist/commands/mcp.d.ts +3 -0
- package/dist/commands/mcp.js +190 -0
- package/dist/commands/project.d.ts +3 -0
- package/dist/commands/project.js +133 -0
- package/dist/commands/proposals.d.ts +3 -0
- package/dist/commands/proposals.js +106 -0
- package/dist/commands/record.d.ts +3 -0
- package/dist/commands/record.js +174 -0
- package/dist/commands/report.d.ts +3 -0
- package/dist/commands/report.js +255 -0
- package/dist/commands/run.d.ts +41 -0
- package/dist/commands/run.js +369 -0
- package/dist/commands/schedule.d.ts +3 -0
- package/dist/commands/schedule.js +337 -0
- package/dist/commands/serve.d.ts +3 -0
- package/dist/commands/serve.js +38 -0
- package/dist/commands/steps.d.ts +8 -0
- package/dist/commands/steps.js +67 -0
- package/dist/commands/tokens.d.ts +3 -0
- package/dist/commands/tokens.js +110 -0
- package/dist/commands/trace.d.ts +10 -0
- package/dist/commands/trace.js +104 -0
- package/dist/commands/upgrade.d.ts +14 -0
- package/dist/commands/upgrade.js +115 -0
- package/dist/commands/users.d.ts +3 -0
- package/dist/commands/users.js +183 -0
- package/dist/commands/watch.d.ts +7 -0
- package/dist/commands/watch.js +116 -0
- package/dist/commands/workspace.d.ts +8 -0
- package/dist/commands/workspace.js +83 -0
- package/dist/context.d.ts +17 -0
- package/dist/context.js +22 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/program.d.ts +7 -0
- package/dist/program.js +90 -0
- package/dist/templates/project.d.ts +10 -0
- package/dist/templates/project.js +115 -0
- package/dist/ui.d.ts +16 -0
- package/dist/ui.js +67 -0
- package/package.json +61 -0
- package/templates/.claude/skills/sdods-record/SKILL.md +75 -0
- package/templates/.claude/skills/sdods-run/SKILL.md +54 -0
- package/templates/.env.example +62 -0
- package/templates/docker-compose.yml +40 -0
- package/templates/projects/demo-shop/.env.example +4 -0
- package/templates/projects/demo-shop/data/common/posts.yaml +11 -0
- package/templates/projects/demo-shop/data/common/products.json +8 -0
- package/templates/projects/demo-shop/data/common/users.csv +7 -0
- package/templates/projects/demo-shop/data/factories.ts +16 -0
- package/templates/projects/demo-shop/data/staging/users.csv +6 -0
- package/templates/projects/demo-shop/envs/local.yaml +17 -0
- package/templates/projects/demo-shop/envs/staging.yaml +23 -0
- package/templates/projects/demo-shop/features/__screenshots__/demo-shop--ui--chromium/darwin/inventory.png +0 -0
- package/templates/projects/demo-shop/features/api/posts.feature +90 -0
- package/templates/projects/demo-shop/features/auth/data-driven-login.feature +17 -0
- package/templates/projects/demo-shop/features/auth/login.feature +32 -0
- package/templates/projects/demo-shop/features/auth/user-pool.feature +20 -0
- package/templates/projects/demo-shop/features/cart/cart.feature +33 -0
- package/templates/projects/demo-shop/features/hybrid/seed-then-verify.feature +28 -0
- package/templates/projects/demo-shop/features/inventory/inventory.feature +38 -0
- package/templates/projects/demo-shop/features/inventory/network-intercept.feature +29 -0
- package/templates/projects/demo-shop/features/inventory/visual.feature +10 -0
- package/templates/projects/demo-shop/har/staging/cart.har +1 -0
- package/templates/projects/demo-shop/har/staging/cart.har.json +11 -0
- package/templates/projects/demo-shop/har/staging/login.har +1 -0
- package/templates/projects/demo-shop/har/staging/login.har.json +11 -0
- package/templates/projects/demo-shop/har/staging/posts.api.har +2061 -0
- package/templates/projects/demo-shop/har/staging/posts.har.json +8 -0
- package/templates/projects/demo-shop/har/staging/products.har +1 -0
- package/templates/projects/demo-shop/har/staging/products.har.json +11 -0
- package/templates/projects/demo-shop/har/staging/site.api.har +148 -0
- package/templates/projects/demo-shop/har/staging/site.har +1 -0
- package/templates/projects/demo-shop/har/staging/site.har.json +11 -0
- package/templates/projects/demo-shop/pages/CartPage.ts +20 -0
- package/templates/projects/demo-shop/pages/InventoryPage.ts +103 -0
- package/templates/projects/demo-shop/pages/LoginPage.ts +42 -0
- package/templates/projects/demo-shop/schemas/post.schema.json +13 -0
- package/templates/projects/demo-shop/sdods.project.yaml +185 -0
- package/templates/projects/demo-shop/steps/auth.ts +7 -0
- package/templates/projects/demo-shop/steps/fixtures.ts +39 -0
package/dist/context.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ProjectRegistry, setLogJson, setLogLevel } from '@sdods/core';
|
|
2
|
+
export function globalOptions(cmd) {
|
|
3
|
+
let root = cmd;
|
|
4
|
+
while (root.parent)
|
|
5
|
+
root = root.parent;
|
|
6
|
+
return root.opts();
|
|
7
|
+
}
|
|
8
|
+
export function createContext(cmd) {
|
|
9
|
+
const opts = globalOptions(cmd);
|
|
10
|
+
const rootDir = ProjectRegistry.findRepoRoot(opts.cwd ?? process.cwd());
|
|
11
|
+
if (opts.verbose)
|
|
12
|
+
setLogLevel('debug');
|
|
13
|
+
else if (opts.quiet)
|
|
14
|
+
setLogLevel('error');
|
|
15
|
+
if (opts.json)
|
|
16
|
+
setLogJson(true);
|
|
17
|
+
if (opts.color === false)
|
|
18
|
+
process.env.NO_COLOR = '1';
|
|
19
|
+
const registry = ProjectRegistry.discover(rootDir);
|
|
20
|
+
return { rootDir, registry, opts };
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=context.js.map
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
/**
|
|
3
|
+
* All commands are registered eagerly (cheap) but each module keeps heavy imports
|
|
4
|
+
* (server, agents, DB, browser runner) inside its action via dynamic import.
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildProgram(): Command;
|
|
7
|
+
//# sourceMappingURL=program.d.ts.map
|
package/dist/program.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { VERSION } from '@sdods/core';
|
|
3
|
+
import { registerProjectCommands } from './commands/project.js';
|
|
4
|
+
import { registerEnvCommands } from './commands/env.js';
|
|
5
|
+
import { registerConfigCommands } from './commands/config.js';
|
|
6
|
+
import { registerDoctorCommand } from './commands/doctor.js';
|
|
7
|
+
import { registerWorkspaceCommands } from './commands/workspace.js';
|
|
8
|
+
import { register as registerRun } from './commands/run.js';
|
|
9
|
+
import { register as registerLint } from './commands/lint.js';
|
|
10
|
+
import { register as registerInit } from './commands/init.js';
|
|
11
|
+
import { register as registerAnalyze } from './commands/analyze.js';
|
|
12
|
+
import { register as registerRecord } from './commands/record.js';
|
|
13
|
+
import { register as registerHar } from './commands/har.js';
|
|
14
|
+
import { register as registerAuth } from './commands/auth.js';
|
|
15
|
+
import { register as registerData } from './commands/data.js';
|
|
16
|
+
import { register as registerDb } from './commands/db.js';
|
|
17
|
+
import { register as registerReport } from './commands/report.js';
|
|
18
|
+
import { register as registerHeal } from './commands/heal.js';
|
|
19
|
+
import { register as registerInsights } from './commands/insights.js';
|
|
20
|
+
import { register as registerSteps } from './commands/steps.js';
|
|
21
|
+
import { register as registerFeatures } from './commands/features.js';
|
|
22
|
+
import { register as registerCoverage } from './commands/coverage.js';
|
|
23
|
+
import { register as registerBrowsers } from './commands/browsers.js';
|
|
24
|
+
import { register as registerWatch } from './commands/watch.js';
|
|
25
|
+
import { register as registerTrace } from './commands/trace.js';
|
|
26
|
+
import { register as registerAgent } from './commands/agent.js';
|
|
27
|
+
import { register as registerProposals } from './commands/proposals.js';
|
|
28
|
+
import { register as registerMcp } from './commands/mcp.js';
|
|
29
|
+
import { register as registerServe } from './commands/serve.js';
|
|
30
|
+
import { register as registerUsers } from './commands/users.js';
|
|
31
|
+
import { register as registerTokens } from './commands/tokens.js';
|
|
32
|
+
import { register as registerSchedule } from './commands/schedule.js';
|
|
33
|
+
import { register as registerIntegrations } from './commands/integrations.js';
|
|
34
|
+
import { register as registerCompletion } from './commands/completion.js';
|
|
35
|
+
import { register as registerUpgrade } from './commands/upgrade.js';
|
|
36
|
+
import { register as registerFeedback } from './commands/feedback.js';
|
|
37
|
+
/**
|
|
38
|
+
* All commands are registered eagerly (cheap) but each module keeps heavy imports
|
|
39
|
+
* (server, agents, DB, browser runner) inside its action via dynamic import.
|
|
40
|
+
*/
|
|
41
|
+
export function buildProgram() {
|
|
42
|
+
const program = new Command('sdods');
|
|
43
|
+
program
|
|
44
|
+
.description('SDODS — automation and orchestration for reliable business workflows: BDD for UI, API and hybrid flows.')
|
|
45
|
+
.version(VERSION, '-V, --version')
|
|
46
|
+
.option('--json', 'machine-readable output')
|
|
47
|
+
.option('-q, --quiet', 'only errors')
|
|
48
|
+
.option('--verbose', 'debug logging')
|
|
49
|
+
.option('--cwd <dir>', 'repo root (default: auto-detect from the current directory)')
|
|
50
|
+
.option('--no-color', 'disable colours')
|
|
51
|
+
.showHelpAfterError('(add --help for usage)')
|
|
52
|
+
.showSuggestionAfterError()
|
|
53
|
+
.configureHelp({ sortSubcommands: true });
|
|
54
|
+
registerProjectCommands(program);
|
|
55
|
+
registerEnvCommands(program);
|
|
56
|
+
registerConfigCommands(program);
|
|
57
|
+
registerDoctorCommand(program);
|
|
58
|
+
registerWorkspaceCommands(program);
|
|
59
|
+
registerRun(program);
|
|
60
|
+
registerLint(program);
|
|
61
|
+
registerInit(program);
|
|
62
|
+
registerAnalyze(program);
|
|
63
|
+
registerRecord(program);
|
|
64
|
+
registerHar(program);
|
|
65
|
+
registerAuth(program);
|
|
66
|
+
registerData(program);
|
|
67
|
+
registerDb(program);
|
|
68
|
+
registerReport(program);
|
|
69
|
+
registerHeal(program);
|
|
70
|
+
registerInsights(program);
|
|
71
|
+
registerSteps(program);
|
|
72
|
+
registerFeatures(program);
|
|
73
|
+
registerCoverage(program);
|
|
74
|
+
registerBrowsers(program);
|
|
75
|
+
registerWatch(program);
|
|
76
|
+
registerTrace(program);
|
|
77
|
+
registerAgent(program);
|
|
78
|
+
registerProposals(program);
|
|
79
|
+
registerMcp(program);
|
|
80
|
+
registerServe(program);
|
|
81
|
+
registerUsers(program);
|
|
82
|
+
registerTokens(program);
|
|
83
|
+
registerSchedule(program);
|
|
84
|
+
registerIntegrations(program);
|
|
85
|
+
registerCompletion(program);
|
|
86
|
+
registerUpgrade(program);
|
|
87
|
+
registerFeedback(program);
|
|
88
|
+
return program;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=program.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ProjectConfig } from '@sdods/contracts';
|
|
2
|
+
export interface TemplateInput {
|
|
3
|
+
config: ProjectConfig;
|
|
4
|
+
envName: string;
|
|
5
|
+
uiUrl: string;
|
|
6
|
+
apiUrl: string;
|
|
7
|
+
}
|
|
8
|
+
/** Files written by `sdods project create` (relative path → content). */
|
|
9
|
+
export declare function projectTemplateFiles({ config, envName, uiUrl, apiUrl, }: TemplateInput): Record<string, string>;
|
|
10
|
+
//# sourceMappingURL=project.d.ts.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/** Files written by `sdods project create` (relative path → content). */
|
|
2
|
+
export function projectTemplateFiles({ config, envName, uiUrl, apiUrl, }) {
|
|
3
|
+
const slug = config.slug;
|
|
4
|
+
const pascal = slug.replace(/(^|-)(\w)/g, (_m, _p, c) => c.toUpperCase());
|
|
5
|
+
return {
|
|
6
|
+
[`envs/${envName}.yaml`]: `name: ${envName}
|
|
7
|
+
ui:
|
|
8
|
+
baseUrl: ${uiUrl}
|
|
9
|
+
api:
|
|
10
|
+
baseUrl: ${apiUrl}
|
|
11
|
+
headers: { Accept: application/json }
|
|
12
|
+
auth: { type: none }
|
|
13
|
+
# auth: { type: bearer, token: '\${API_TOKEN}' }
|
|
14
|
+
users:
|
|
15
|
+
poolSize: 2
|
|
16
|
+
vars: {}
|
|
17
|
+
`,
|
|
18
|
+
'.env.example': `# Copy to .env.${envName} (gitignored). Values referenced as \${VAR} in yaml.\n# API_TOKEN=\n`,
|
|
19
|
+
'steps/fixtures.ts': `import { test as base, createBdd } from '@sdods/core/fixtures';
|
|
20
|
+
import { HomePage } from '../pages/HomePage.js';
|
|
21
|
+
import { auth } from './auth.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Project-level test object: extends the SDODS merged fixtures with this project's
|
|
25
|
+
* auth strategy and page objects. playwright-bdd imports this file (importTestFrom).
|
|
26
|
+
*/
|
|
27
|
+
export const test = base.extend<{ homePage: HomePage }>({
|
|
28
|
+
auth: [auth, { scope: 'worker', option: true }],
|
|
29
|
+
homePage: async ({ pages }, use) => {
|
|
30
|
+
await use(pages.get(HomePage));
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const { Given, When, Then, Step, BeforeScenario, AfterScenario, BeforeStep, AfterStep, BeforeWorker, AfterWorker } =
|
|
35
|
+
createBdd(test);
|
|
36
|
+
`,
|
|
37
|
+
'steps/auth.ts': `import { defineAuth } from '@sdods/core/auth';
|
|
38
|
+
|
|
39
|
+
/** Login strategy used for storageState reuse. Replace with your app's flow. */
|
|
40
|
+
export const auth = defineAuth({
|
|
41
|
+
strategy: 'none',
|
|
42
|
+
});
|
|
43
|
+
`,
|
|
44
|
+
[`steps/${slug}.steps.ts`]: `import { expect } from '@playwright/test';
|
|
45
|
+
import { Then } from './fixtures.js';
|
|
46
|
+
|
|
47
|
+
// Project-specific steps. Generic UI/API/data steps come from @sdods/core/steps.
|
|
48
|
+
Then('the ${config.name} title should be visible', async ({ homePage }) => {
|
|
49
|
+
await expect(homePage.title).toBeVisible();
|
|
50
|
+
});
|
|
51
|
+
`,
|
|
52
|
+
'pages/HomePage.ts': `import { Fixture, Given } from 'playwright-bdd/decorators';
|
|
53
|
+
import { BasePage } from '@sdods/core/pages';
|
|
54
|
+
import type { test } from '../steps/fixtures.js';
|
|
55
|
+
|
|
56
|
+
@Fixture<typeof test>('homePage')
|
|
57
|
+
export class HomePage extends BasePage {
|
|
58
|
+
readonly title = this.heal.locator(this.page.locator('h1').first(), {
|
|
59
|
+
role: 'heading',
|
|
60
|
+
description: 'page title',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
@Given('I open the home page')
|
|
64
|
+
async open() {
|
|
65
|
+
await this.goto('home');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
`,
|
|
69
|
+
'features/health.feature': `@api @smoke
|
|
70
|
+
Feature: ${config.name} API health
|
|
71
|
+
As an engineer I want the API to answer so that the rest of the suite is meaningful.
|
|
72
|
+
|
|
73
|
+
Scenario: The API root responds
|
|
74
|
+
When I send a GET request to "/"
|
|
75
|
+
Then the response status should be 200
|
|
76
|
+
`,
|
|
77
|
+
'features/ui/home.feature': `@ui @smoke
|
|
78
|
+
Feature: ${config.name} home page
|
|
79
|
+
Scenario: The home page loads
|
|
80
|
+
Given I open the home page
|
|
81
|
+
Then the page URL should contain "/"
|
|
82
|
+
`,
|
|
83
|
+
'data/common/users.csv': `id,username,password,role\n1,user1,\${USER1_PASSWORD},standard\n2,user2,\${USER2_PASSWORD},standard\n3,admin1,\${ADMIN1_PASSWORD},admin\n`,
|
|
84
|
+
[`data/${envName}/.gitkeep`]: '',
|
|
85
|
+
'data/factories.ts': `import { defineFactories } from '@sdods/core/data';
|
|
86
|
+
|
|
87
|
+
export default defineFactories({
|
|
88
|
+
user: (f) => ({
|
|
89
|
+
email: f.internet.email(),
|
|
90
|
+
firstName: f.person.firstName(),
|
|
91
|
+
lastName: f.person.lastName(),
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
`,
|
|
95
|
+
'recorded/.gitkeep': '',
|
|
96
|
+
'har/.gitkeep': '',
|
|
97
|
+
'README.md': `# ${config.name} (${slug})
|
|
98
|
+
|
|
99
|
+
SDODS project. Run:
|
|
100
|
+
|
|
101
|
+
\`\`\`bash
|
|
102
|
+
sdods run -p ${slug} -e ${envName} -l api
|
|
103
|
+
sdods run -p ${slug} -e ${envName} -l ui -b chromium -t @smoke
|
|
104
|
+
sdods lint -p ${slug}
|
|
105
|
+
\`\`\`
|
|
106
|
+
|
|
107
|
+
- \`sdods.project.yaml\` — project settings (layers, browsers, tags, data, auth, screenshots)
|
|
108
|
+
- \`envs/\` — one yaml per environment; secrets via \`\${VAR}\` from \`.env.<env>\`
|
|
109
|
+
- \`features/\` — Gherkin; \`steps/\` — project steps + fixtures; \`pages/\` — page objects (decorators)
|
|
110
|
+
- \`data/\` — CSV/JSON/YAML per environment with \`data/common\` fallback
|
|
111
|
+
- \`${pascal}\` page object in \`pages/HomePage.ts\` shows the heal-aware locator style
|
|
112
|
+
`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=project.js.map
|
package/dist/ui.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface OutputOptions {
|
|
2
|
+
json?: boolean;
|
|
3
|
+
quiet?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare function out(text: string, opts?: OutputOptions): void;
|
|
6
|
+
export declare function json(value: unknown): void;
|
|
7
|
+
export declare function table(rows: Array<Record<string, unknown>>, columns?: string[]): void;
|
|
8
|
+
export declare function ok(msg: string): void;
|
|
9
|
+
export declare function warn(msg: string): void;
|
|
10
|
+
export declare function info(msg: string): void;
|
|
11
|
+
export declare function heading(msg: string): void;
|
|
12
|
+
/** Render an error and return the process exit code. */
|
|
13
|
+
export declare function renderError(e: unknown, asJson: boolean): number;
|
|
14
|
+
export declare function parseIntFlag(name: string): (v: string) => number;
|
|
15
|
+
export declare function collect(value: string, previous?: string[]): string[];
|
|
16
|
+
//# sourceMappingURL=ui.d.ts.map
|
package/dist/ui.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { errorToJson, isSdodsError } from '@sdods/core';
|
|
3
|
+
export function out(text, opts = {}) {
|
|
4
|
+
if (opts.quiet)
|
|
5
|
+
return;
|
|
6
|
+
process.stdout.write(text.endsWith('\n') ? text : text + '\n');
|
|
7
|
+
}
|
|
8
|
+
export function json(value) {
|
|
9
|
+
process.stdout.write(JSON.stringify(value, null, 2) + '\n');
|
|
10
|
+
}
|
|
11
|
+
export function table(rows, columns) {
|
|
12
|
+
if (rows.length === 0)
|
|
13
|
+
return out(pc.dim('(none)'));
|
|
14
|
+
const cols = columns ?? Object.keys(rows[0]);
|
|
15
|
+
const widths = cols.map((c) => Math.max(c.length, ...rows.map((r) => String(r[c] ?? '').length)));
|
|
16
|
+
const line = (vals) => vals.map((v, i) => v.padEnd(widths[i])).join(' ');
|
|
17
|
+
out(pc.bold(line(cols)));
|
|
18
|
+
out(pc.dim(line(widths.map((w) => '─'.repeat(w)))));
|
|
19
|
+
for (const r of rows)
|
|
20
|
+
out(line(cols.map((c) => String(r[c] ?? ''))));
|
|
21
|
+
}
|
|
22
|
+
export function ok(msg) {
|
|
23
|
+
out(`${pc.green('✔')} ${msg}`);
|
|
24
|
+
}
|
|
25
|
+
export function warn(msg) {
|
|
26
|
+
process.stderr.write(`${pc.yellow('⚠')} ${msg}\n`);
|
|
27
|
+
}
|
|
28
|
+
export function info(msg) {
|
|
29
|
+
out(`${pc.cyan('ℹ')} ${msg}`);
|
|
30
|
+
}
|
|
31
|
+
export function heading(msg) {
|
|
32
|
+
out(`\n${pc.bold(pc.underline(msg))}`);
|
|
33
|
+
}
|
|
34
|
+
/** Render an error and return the process exit code. */
|
|
35
|
+
export function renderError(e, asJson) {
|
|
36
|
+
const body = errorToJson(e);
|
|
37
|
+
if (asJson) {
|
|
38
|
+
process.stderr.write(JSON.stringify({ error: body }) + '\n');
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
process.stderr.write(`${pc.red('✖')} ${pc.bold(body.code)}: ${body.message}\n`);
|
|
42
|
+
if (body.hint)
|
|
43
|
+
process.stderr.write(` ${pc.dim('hint:')} ${body.hint}\n`);
|
|
44
|
+
if (body.docsUrl)
|
|
45
|
+
process.stderr.write(` ${pc.dim('docs:')} ${body.docsUrl}\n`);
|
|
46
|
+
if (process.env.SDODS_DEBUG && e instanceof Error && e.stack)
|
|
47
|
+
process.stderr.write(pc.dim(e.stack) + '\n');
|
|
48
|
+
}
|
|
49
|
+
if (isSdodsError(e))
|
|
50
|
+
return e.exitCode;
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
export function parseIntFlag(name) {
|
|
54
|
+
return (v) => {
|
|
55
|
+
const n = Number(v);
|
|
56
|
+
if (!Number.isInteger(n) || n < 0)
|
|
57
|
+
throw new Error(`--${name} expects a non-negative integer, got "${v}"`);
|
|
58
|
+
return n;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function collect(value, previous = []) {
|
|
62
|
+
return previous.concat(value
|
|
63
|
+
.split(',')
|
|
64
|
+
.map((s) => s.trim())
|
|
65
|
+
.filter(Boolean));
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=ui.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sdods/cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "SDODS command line: run, record, analyze, agents, MCP server, scheduler and the web server.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "SDODS <admin@sdods.com>",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"sdods": "./bin/sdods.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"bin",
|
|
20
|
+
"templates"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=22"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@sdods/contracts": "0.2.0",
|
|
27
|
+
"@sdods/core": "0.2.0",
|
|
28
|
+
"commander": "^15.0.0",
|
|
29
|
+
"execa": "^10.0.1",
|
|
30
|
+
"picocolors": "^1.1.1",
|
|
31
|
+
"semver": "^7.8.5",
|
|
32
|
+
"yaml": "^2.9.0",
|
|
33
|
+
"zod": "^4.5.4",
|
|
34
|
+
"@playwright/test": "^1.62.1",
|
|
35
|
+
"playwright-core": "^1.62.1",
|
|
36
|
+
"@sdods/integrations": "0.2.0",
|
|
37
|
+
"@sdods/mcp": "0.2.0",
|
|
38
|
+
"@sdods/agents": "0.2.0",
|
|
39
|
+
"@sdods/db": "0.2.0",
|
|
40
|
+
"csv-parse": "^7.0.2",
|
|
41
|
+
"@sdods/server": "0.2.0"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://sdods.com",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/siri1410/SDODS/issues"
|
|
46
|
+
},
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/siri1410/SDODS.git",
|
|
50
|
+
"directory": "packages/cli"
|
|
51
|
+
},
|
|
52
|
+
"keywords": [
|
|
53
|
+
"sdods",
|
|
54
|
+
"automation",
|
|
55
|
+
"orchestration",
|
|
56
|
+
"testing",
|
|
57
|
+
"bdd",
|
|
58
|
+
"ai-agents",
|
|
59
|
+
"mcp"
|
|
60
|
+
]
|
|
61
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sdods-record
|
|
3
|
+
description: Record a browser flow with SDODS for a given project, environment, user role and device, save it as a runnable spec, replay it, and optionally convert it into a Gherkin feature with reusable steps. Use when the user says "record", "codegen", "capture a flow", "re-record as <role>", or wants the same flow recorded across environments or users.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# SDODS record & playback skill
|
|
7
|
+
|
|
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
|
+
|
|
10
|
+
## Parameters
|
|
11
|
+
|
|
12
|
+
| Name | Required | Example | Notes |
|
|
13
|
+
|---|---|---|---|
|
|
14
|
+
| `project` | yes | `demo-shop` | `bun run sdods project list` shows slugs |
|
|
15
|
+
| `env` | yes | `staging`, `local` | `bun run sdods env list -p <project>` |
|
|
16
|
+
| `name` | yes | `checkout-standard` | becomes `recorded/<name>.spec.ts`; include the role in the name |
|
|
17
|
+
| `role` | no | `standard`, `problem`, `admin` | leases a pool user of that role and starts logged in (storageState) |
|
|
18
|
+
| `url` | no | `/inventory.html` | route path or absolute URL to open first |
|
|
19
|
+
| `device` | no | `"iPhone 15"`, `"Pixel 7"` | device name for mobile emulation |
|
|
20
|
+
| `browser` | no | `chromium` (default), `firefox`, `webkit` | |
|
|
21
|
+
| `har` | no | `true` | also capture network into `har/<env>/<name>.har` for offline replay |
|
|
22
|
+
|
|
23
|
+
## Steps
|
|
24
|
+
|
|
25
|
+
1. **Check the environment** (fail fast on missing vars):
|
|
26
|
+
```bash
|
|
27
|
+
bun run sdods doctor -p <project>
|
|
28
|
+
bun run sdods env list -p <project>
|
|
29
|
+
```
|
|
30
|
+
2. **Make sure the role has login state** (skip when no role):
|
|
31
|
+
```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
|
|
34
|
+
```
|
|
35
|
+
3. **Record** (opens the recorder; the person performs the flow and closes the window):
|
|
36
|
+
```bash
|
|
37
|
+
bun run sdods record -p <project> -e <env> --name <name> [--user <role>] [--url <url>] [--device "<device>"] [--browser <browser>] [--save-har]
|
|
38
|
+
```
|
|
39
|
+
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
|
+
4. **Play it back** on the same environment, then on another one:
|
|
41
|
+
```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>"
|
|
44
|
+
```
|
|
45
|
+
Look at `.sdods/runs/<runId>/` for screenshots and the HTML report (`bun run sdods report --last --open`).
|
|
46
|
+
5. **Repeat for other roles, environments or devices** by changing only the parameters. Use distinct names (`checkout-standard`, `checkout-problem`, `checkout-standard-iphone`).
|
|
47
|
+
6. **Convert to Gherkin** when the flow is stable (agent proposal, reviewed by a person):
|
|
48
|
+
```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>
|
|
54
|
+
```
|
|
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.
|
|
56
|
+
7. **Offline replay** (CI-friendly) when HAR was captured:
|
|
57
|
+
```bash
|
|
58
|
+
bun run sdods har replay -p <project> -e <env> --strict -t "@har:<name>"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Matrix template
|
|
62
|
+
|
|
63
|
+
Ask the user which cells to record, then run step 3 per cell:
|
|
64
|
+
|
|
65
|
+
| role \ env | local | staging |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| standard | `--user standard -e local` | `--user standard -e staging` |
|
|
68
|
+
| problem | `--user problem -e local` | `--user problem -e staging` |
|
|
69
|
+
| admin (mobile) | `--user admin -e local --device "iPhone 15"` | `--user admin -e staging --device "iPhone 15"` |
|
|
70
|
+
|
|
71
|
+
## Done when
|
|
72
|
+
|
|
73
|
+
- Each recording runs green with `-l recorded` on its environment.
|
|
74
|
+
- Names, roles and envs are visible in `sdods features list -p <project> --json` or `recorded/` file headers.
|
|
75
|
+
- Converted features pass `sdods lint` and are tagged with one layer tag and one suite tag.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sdods-run
|
|
3
|
+
description: Run SDODS test suites for a project across environments, layers (ui/api/hybrid/recorded), browsers, suites (@smoke/@regression) and user roles, then read the results. Use when the user says "run the tests", "smoke on staging", "regression on all browsers", "run as <role>", or asks what failed.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# SDODS run skill
|
|
7
|
+
|
|
8
|
+
## Parameters
|
|
9
|
+
|
|
10
|
+
| Name | Default | Notes |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| `project` | only project if one exists | `bun run sdods project list` |
|
|
13
|
+
| `env` | project `envs.default` | `bun run sdods env list -p <project>` |
|
|
14
|
+
| `layer` | all layers in the project | `-l ui`, `-l api`, `-l hybrid`, `-l recorded` (repeatable) |
|
|
15
|
+
| `browser` | project `browsers` | `-b chromium -b firefox -b webkit`, or `--project-matrix` for all |
|
|
16
|
+
| `tags` | none | Cucumber expression, e.g. `"@smoke and not @mock"`, `@user:admin`, `@jira:DEMO-12` |
|
|
17
|
+
| `headed` | false | `--headed` for a visible browser |
|
|
18
|
+
| `workers` | runner default | `-w 2` |
|
|
19
|
+
| `shard` | none | `--shard 1/3` |
|
|
20
|
+
| `har` | off | `--har-replay --strict` for offline runs, `--har-update` to refresh |
|
|
21
|
+
|
|
22
|
+
## Steps
|
|
23
|
+
|
|
24
|
+
1. Validate before running (cheap, catches config and tag mistakes):
|
|
25
|
+
```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
|
+
```
|
|
30
|
+
2. Run the requested slice. Typical combinations:
|
|
31
|
+
```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
|
|
38
|
+
```
|
|
39
|
+
3. Read results:
|
|
40
|
+
```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
|
+
```
|
|
45
|
+
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:
|
|
47
|
+
```bash
|
|
48
|
+
bun run sdods agent heal -p <project> --scenario <fingerprint> --dry-run
|
|
49
|
+
```
|
|
50
|
+
5. Repeat for other environments or roles by changing `-e` / `-t "@user:<role>"` only.
|
|
51
|
+
|
|
52
|
+
## Exit codes
|
|
53
|
+
|
|
54
|
+
`0` all passed · `1` test failures · `2` configuration or usage error · `3` lint errors · `130` cancelled.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# ── SDODS: tokens and keys ─────────────────────────────────────────────────
|
|
2
|
+
# Nothing here is mandatory for the platform itself. SDODS API tokens are
|
|
3
|
+
# self-issued and free (`sdods tokens create`). Check status: `sdods doctor`.
|
|
4
|
+
#
|
|
5
|
+
# mandatory only when the feature next to it is used
|
|
6
|
+
# one-of agents need ANY ONE of the four options in the Agents block
|
|
7
|
+
# optional turns on an integration
|
|
8
|
+
# ci-only GitHub Actions secrets, never needed locally
|
|
9
|
+
|
|
10
|
+
# ── Database (mandatory: none; DATABASE_URL only when DB_DRIVER=postgres) ─────
|
|
11
|
+
DB_DRIVER=sqlite
|
|
12
|
+
SQLITE_PATH=.sdods/sdods.db
|
|
13
|
+
# DATABASE_URL=postgres://sdods:sdods@localhost:5432/sdods
|
|
14
|
+
|
|
15
|
+
# ── Web server (`sdods serve`) — SESSION_SECRET mandatory for serve only ────
|
|
16
|
+
PORT=4444
|
|
17
|
+
HOST=127.0.0.1
|
|
18
|
+
SESSION_SECRET=change-me-32-chars-minimum-please # openssl rand -hex 32
|
|
19
|
+
# SDODS_PUBLIC_URL=https://sdods.example.com
|
|
20
|
+
# AUTH_DISABLED=1 # local dev only
|
|
21
|
+
|
|
22
|
+
# Where projects and run artifacts live (defaults shown)
|
|
23
|
+
# SDODS_PROJECTS_DIR=projects
|
|
24
|
+
# SDODS_ARTIFACTS_DIR=.sdods/runs
|
|
25
|
+
|
|
26
|
+
# ── Agents (one-of; every agent command also has --dry-run and --adapter fake) ─
|
|
27
|
+
# Pick ONE way for agents to reach a model. Auto-detection order when unset:
|
|
28
|
+
# ANTHROPIC_API_KEY → `claude` CLI on PATH → `codex` CLI on PATH → OPENAI_API_KEY
|
|
29
|
+
# → a local Ollama → fake
|
|
30
|
+
# SDODS_LLM_PROVIDER=claude-code # claude | claude-code | codex | openai-compatible | ollama | fake
|
|
31
|
+
#
|
|
32
|
+
# 1) Claude via API key (billed by Anthropic)
|
|
33
|
+
# ANTHROPIC_API_KEY=
|
|
34
|
+
#
|
|
35
|
+
# 2) Claude Code CLI login — no key needed: `npm i -g @anthropic-ai/claude-code && claude login`
|
|
36
|
+
# (adapter claude-code; optional SDODS_CLAUDE_CODE_MODEL=... to pin a model)
|
|
37
|
+
#
|
|
38
|
+
# 3) Codex CLI login — no key needed: `npm i -g @openai/codex && codex login`
|
|
39
|
+
# (adapter codex; optional SDODS_CODEX_MODEL=... to pin a model)
|
|
40
|
+
#
|
|
41
|
+
# 4) Any OpenAI-compatible endpoint (billed by the provider)
|
|
42
|
+
# OPENAI_API_KEY=
|
|
43
|
+
# OPENAI_BASE_URL= # optional: Azure, vLLM, LM Studio, llama.cpp
|
|
44
|
+
#
|
|
45
|
+
# 5) A model on this machine — no key, no per-token cost (adapter ollama)
|
|
46
|
+
# `ollama serve && ollama pull qwen2.5-coder:7b`
|
|
47
|
+
# OLLAMA_HOST=http://127.0.0.1:11434
|
|
48
|
+
# OLLAMA_MODEL=qwen2.5-coder:7b
|
|
49
|
+
# OLLAMA_CONTEXT_LENGTH=16384 # Ollama defaults to 4096 and truncates longer prompts
|
|
50
|
+
|
|
51
|
+
# ── Integrations (optional; project yaml stores only the variable NAMES) ──────
|
|
52
|
+
# GITHUB_TOKEN= # check runs, PR comments, issues
|
|
53
|
+
# JIRA_EMAIL= # Jira Cloud basic auth
|
|
54
|
+
# JIRA_API_TOKEN=
|
|
55
|
+
|
|
56
|
+
# ── SDODS's own token (optional, free): MCP over HTTP and CI ingest ─────────
|
|
57
|
+
# SDODS_SERVER_URL=http://127.0.0.1:4444
|
|
58
|
+
# SDODS_TOKEN=amx_... # sdods tokens create --user <name> --scopes runs:ingest,runs:read
|
|
59
|
+
|
|
60
|
+
# ── CI-only GitHub secrets (never needed locally) ────────────────────────────
|
|
61
|
+
# FIREBASE_SERVICE_ACCOUNT_AUTOMAX_DOCS docs deploy (Firebase Hosting)
|
|
62
|
+
# NPM_TOKEN publishing @sdods/* packages
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
services:
|
|
2
|
+
postgres:
|
|
3
|
+
image: postgres:16
|
|
4
|
+
container_name: sdods-postgres
|
|
5
|
+
environment:
|
|
6
|
+
POSTGRES_USER: sdods
|
|
7
|
+
POSTGRES_PASSWORD: sdods
|
|
8
|
+
POSTGRES_DB: sdods
|
|
9
|
+
ports:
|
|
10
|
+
- '5432:5432'
|
|
11
|
+
volumes:
|
|
12
|
+
- sdods-pgdata:/var/lib/postgresql/data
|
|
13
|
+
healthcheck:
|
|
14
|
+
test: ['CMD-SHELL', 'pg_isready -U sdods -d sdods']
|
|
15
|
+
interval: 5s
|
|
16
|
+
timeout: 3s
|
|
17
|
+
retries: 20
|
|
18
|
+
|
|
19
|
+
# Optional: the SDODS web server on Postgres. Build with `docker build -t sdods .` first.
|
|
20
|
+
sdods:
|
|
21
|
+
image: sdods:latest
|
|
22
|
+
profiles: ['server']
|
|
23
|
+
depends_on:
|
|
24
|
+
postgres:
|
|
25
|
+
condition: service_healthy
|
|
26
|
+
environment:
|
|
27
|
+
DB_DRIVER: postgres
|
|
28
|
+
DATABASE_URL: postgres://sdods:sdods@postgres:5432/sdods
|
|
29
|
+
HOST: 0.0.0.0
|
|
30
|
+
PORT: 4444
|
|
31
|
+
SESSION_SECRET: change-me-32-chars-minimum-please
|
|
32
|
+
ports:
|
|
33
|
+
- '4444:4444'
|
|
34
|
+
volumes:
|
|
35
|
+
- ./projects:/app/projects
|
|
36
|
+
- sdods-runs:/app/.sdods
|
|
37
|
+
|
|
38
|
+
volumes:
|
|
39
|
+
sdods-pgdata:
|
|
40
|
+
sdods-runs:
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Rows for the "posts" dataset (JSONPlaceholder ids and their owners).
|
|
2
|
+
rows:
|
|
3
|
+
- id: 1
|
|
4
|
+
userId: 1
|
|
5
|
+
title: sunt aut facere repellat provident occaecati excepturi optio reprehenderit
|
|
6
|
+
- id: 11
|
|
7
|
+
userId: 2
|
|
8
|
+
title: et ea vero quia laudantium autem
|
|
9
|
+
- id: 21
|
|
10
|
+
userId: 3
|
|
11
|
+
title: asperiores ea ipsam voluptatibus modi minima quia sint
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "id": 4, "name": "Sauce Labs Backpack", "price": 29.99 },
|
|
3
|
+
{ "id": 0, "name": "Sauce Labs Bike Light", "price": 9.99 },
|
|
4
|
+
{ "id": 1, "name": "Sauce Labs Bolt T-Shirt", "price": 15.99 },
|
|
5
|
+
{ "id": 5, "name": "Sauce Labs Fleece Jacket", "price": 49.99 },
|
|
6
|
+
{ "id": 2, "name": "Sauce Labs Onesie", "price": 7.99 },
|
|
7
|
+
{ "id": 3, "name": "Test.allTheThings() T-Shirt (Red)", "price": 15.99 }
|
|
8
|
+
]
|