@sdods/cli 0.3.2 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/commands/browsers.d.ts +16 -2
- package/dist/commands/browsers.js +116 -13
- package/dist/commands/config.js +19 -11
- package/dist/commands/doctor.js +26 -22
- package/dist/commands/features.js +6 -4
- package/dist/commands/init.js +4 -2
- package/dist/commands/integrations.js +11 -40
- package/dist/commands/mcp.js +18 -9
- package/dist/commands/project.js +322 -6
- package/dist/commands/run.d.ts +4 -0
- package/dist/commands/run.js +156 -15
- package/dist/commands/serve.js +30 -10
- package/dist/notify.d.ts +63 -0
- package/dist/notify.js +91 -0
- package/dist/runner-compat.d.ts +19 -0
- package/dist/runner-compat.js +47 -0
- package/package.json +10 -10
- package/templates/.claude/skills/sdods-run/SKILL.md +1 -1
- package/templates/.claude/skills/sdods-start-ui/SKILL.md +313 -0
- package/templates/.env.example +1 -0
- package/templates/projects/demo-shop/sdods.project.yaml +7 -1
package/dist/commands/serve.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SdodsError } from '@sdods/core';
|
|
1
2
|
import { createContext } from '../context.js';
|
|
2
3
|
import { info, ok, parseIntFlag } from '../ui.js';
|
|
3
4
|
export function register(program) {
|
|
@@ -12,16 +13,35 @@ export function register(program) {
|
|
|
12
13
|
.action(async (opts, cmd) => {
|
|
13
14
|
const ctx = createContext(cmd);
|
|
14
15
|
const { startServer } = await import('@sdods/server');
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
let started;
|
|
17
|
+
try {
|
|
18
|
+
started = await startServer({
|
|
19
|
+
config: {
|
|
20
|
+
rootDir: ctx.rootDir,
|
|
21
|
+
port: opts.port,
|
|
22
|
+
host: opts.host,
|
|
23
|
+
authDisabled: opts.authDisabled ? true : undefined,
|
|
24
|
+
},
|
|
25
|
+
scheduler: opts.scheduler !== false,
|
|
26
|
+
open: Boolean(opts.open),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
// A failed listen used to leave the process alive: the scheduler had already started, so
|
|
31
|
+
// the event loop never drained and the terminal hung after printing the error. Say what to
|
|
32
|
+
// do about the common cause, and exit rather than waiting for Ctrl+C.
|
|
33
|
+
const e = err;
|
|
34
|
+
if (e?.code === 'EADDRINUSE') {
|
|
35
|
+
const port = opts.port ?? Number(process.env.PORT ?? 4444);
|
|
36
|
+
throw new SdodsError('CONFIG_INVALID', `Port ${port} is already in use.`, {
|
|
37
|
+
hint: `Start on another port: sdods serve --port ${port + 1}\n` +
|
|
38
|
+
`Or find what holds it: lsof -i :${port} (Windows: netstat -ano | findstr :${port})`,
|
|
39
|
+
cause: err,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
const { app, url } = started;
|
|
25
45
|
ok(`SDODS server listening on ${url}`);
|
|
26
46
|
if (app.setupState.token)
|
|
27
47
|
info(`First run: create the admin at ${url}/setup?token=${app.setupState.token} (or: sdods users create --admin --username <name> --password <pw>)`);
|
package/dist/notify.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ProjectConfig } from '@sdods/contracts';
|
|
2
|
+
import { type ProjectRegistry } from '@sdods/core';
|
|
3
|
+
import type { RunSummaryInput } from '@sdods/integrations';
|
|
4
|
+
export declare const LINKS_FILE: readonly [".sdods", "issue-links.json"];
|
|
5
|
+
export interface NotifyRow {
|
|
6
|
+
provider: string;
|
|
7
|
+
kind: string;
|
|
8
|
+
target?: string;
|
|
9
|
+
url?: string;
|
|
10
|
+
detail?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface NotifyRunOptions {
|
|
13
|
+
rootDir: string;
|
|
14
|
+
registry: Pick<ProjectRegistry, 'entry'>;
|
|
15
|
+
/** absolute artifacts root (the directory holding `<runId>/`) */
|
|
16
|
+
artifactsRoot: string;
|
|
17
|
+
runId: string;
|
|
18
|
+
projectSlug?: string;
|
|
19
|
+
reportUrl?: string;
|
|
20
|
+
dryRun?: boolean;
|
|
21
|
+
only?: string[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Publish a finished run to the project's enabled providers. Shared by
|
|
25
|
+
* `sdods integrations notify` and the notify step at the end of `sdods run`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function notifyRun(opts: NotifyRunOptions): Promise<{
|
|
28
|
+
summary: RunSummaryInput;
|
|
29
|
+
slug: string;
|
|
30
|
+
actions: NotifyRow[];
|
|
31
|
+
}>;
|
|
32
|
+
/** Names of the integrations that act on a finished run. */
|
|
33
|
+
export declare function enabledIntegrations(integrations: ProjectConfig['integrations']): string[];
|
|
34
|
+
export type AutoNotifyOutcome = {
|
|
35
|
+
ran: true;
|
|
36
|
+
actions: NotifyRow[];
|
|
37
|
+
errors: string[];
|
|
38
|
+
} | {
|
|
39
|
+
ran: false;
|
|
40
|
+
reason: string;
|
|
41
|
+
};
|
|
42
|
+
export interface MaybeNotifyInput {
|
|
43
|
+
flags: {
|
|
44
|
+
notify?: boolean;
|
|
45
|
+
};
|
|
46
|
+
integrations: ProjectConfig['integrations'];
|
|
47
|
+
totals?: {
|
|
48
|
+
total: number;
|
|
49
|
+
};
|
|
50
|
+
exitCode: number;
|
|
51
|
+
shardTotal?: number;
|
|
52
|
+
notify: () => Promise<{
|
|
53
|
+
actions: NotifyRow[];
|
|
54
|
+
}>;
|
|
55
|
+
warn: (message: string) => void;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The notify step at the end of `sdods run`. `integrations.github.createIssueOnFailure` and friends
|
|
59
|
+
* used to do nothing unless a separate `sdods integrations notify` step existed.
|
|
60
|
+
* A notify failure is reported, never thrown: it must not change the run's exit code.
|
|
61
|
+
*/
|
|
62
|
+
export declare function maybeNotify(input: MaybeNotifyInput): Promise<AutoNotifyOutcome>;
|
|
63
|
+
//# sourceMappingURL=notify.d.ts.map
|
package/dist/notify.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { SdodsError, Logger } from '@sdods/core';
|
|
4
|
+
export const LINKS_FILE = ['.sdods', 'issue-links.json'];
|
|
5
|
+
/**
|
|
6
|
+
* Publish a finished run to the project's enabled providers. Shared by
|
|
7
|
+
* `sdods integrations notify` and the notify step at the end of `sdods run`.
|
|
8
|
+
*/
|
|
9
|
+
export async function notifyRun(opts) {
|
|
10
|
+
const { buildRunSummaryFromFiles, getProviders, createIntegrationContext, FileIssueLinkStore } = await import('@sdods/integrations');
|
|
11
|
+
const runDir = join(opts.artifactsRoot, opts.runId);
|
|
12
|
+
if (!existsSync(runDir)) {
|
|
13
|
+
throw new SdodsError('RUN_FAILED', `Run directory not found: ${runDir}`, {
|
|
14
|
+
hint: 'Pass --artifacts-dir or check the run id (sdods report --last).',
|
|
15
|
+
exitCode: 2,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const summary = buildRunSummaryFromFiles(runDir, {
|
|
19
|
+
projectSlug: opts.projectSlug,
|
|
20
|
+
reportUrl: opts.reportUrl,
|
|
21
|
+
});
|
|
22
|
+
const slug = summary.run.projectSlug;
|
|
23
|
+
const entry = opts.registry.entry(slug);
|
|
24
|
+
const providers = await getProviders(entry.config, { only: opts.only, projectRoot: entry.root });
|
|
25
|
+
const ictx = createIntegrationContext({
|
|
26
|
+
store: new FileIssueLinkStore(join(opts.rootDir, ...LINKS_FILE)),
|
|
27
|
+
logger: new Logger('integrations'),
|
|
28
|
+
dryRun: Boolean(opts.dryRun),
|
|
29
|
+
artifactsRoot: opts.artifactsRoot,
|
|
30
|
+
});
|
|
31
|
+
const actions = [];
|
|
32
|
+
for (const p of providers) {
|
|
33
|
+
if (!p.enabled)
|
|
34
|
+
continue;
|
|
35
|
+
if (p.initError) {
|
|
36
|
+
actions.push({ provider: p.name, kind: 'skipped', detail: p.initError });
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const res = await p.provider.onRunFinished(summary, ictx);
|
|
41
|
+
actions.push(...res.actions);
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
actions.push({ provider: p.name, kind: 'error', detail: e.message });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { summary, slug, actions };
|
|
48
|
+
}
|
|
49
|
+
/** Names of the integrations that act on a finished run. */
|
|
50
|
+
export function enabledIntegrations(integrations) {
|
|
51
|
+
return [
|
|
52
|
+
...(integrations.github?.enabled ? ['github'] : []),
|
|
53
|
+
...(integrations.jira?.enabled ? ['jira'] : []),
|
|
54
|
+
...integrations.custom.map((c) => c.module),
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The notify step at the end of `sdods run`. `integrations.github.createIssueOnFailure` and friends
|
|
59
|
+
* used to do nothing unless a separate `sdods integrations notify` step existed.
|
|
60
|
+
* A notify failure is reported, never thrown: it must not change the run's exit code.
|
|
61
|
+
*/
|
|
62
|
+
export async function maybeNotify(input) {
|
|
63
|
+
if (input.flags.notify === false)
|
|
64
|
+
return { ran: false, reason: 'disabled by --no-notify' };
|
|
65
|
+
if (!enabledIntegrations(input.integrations).length)
|
|
66
|
+
return { ran: false, reason: 'no integration enabled' };
|
|
67
|
+
if (input.exitCode === 130)
|
|
68
|
+
return { ran: false, reason: 'run cancelled' };
|
|
69
|
+
if (!input.totals || input.totals.total === 0)
|
|
70
|
+
return { ran: false, reason: 'no scenario results to report' };
|
|
71
|
+
if ((input.shardTotal ?? 1) > 1)
|
|
72
|
+
return {
|
|
73
|
+
ran: false,
|
|
74
|
+
reason: 'sharded run: notify once after merging (sdods integrations notify --run-id <id>)',
|
|
75
|
+
};
|
|
76
|
+
try {
|
|
77
|
+
const { actions } = await input.notify();
|
|
78
|
+
const errors = actions
|
|
79
|
+
.filter((a) => a.kind === 'error')
|
|
80
|
+
.map((a) => `${a.provider}: ${a.detail ?? 'failed'}`);
|
|
81
|
+
for (const e of errors)
|
|
82
|
+
input.warn(`integrations notify: ${e}`);
|
|
83
|
+
return { ran: true, actions, errors };
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
const message = e.message;
|
|
87
|
+
input.warn(`integrations notify failed: ${message}`);
|
|
88
|
+
return { ran: true, actions: [], errors: [message] };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=notify.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The oldest Playwright whose step locations playwright-bdd can match in a workspace that installs
|
|
3
|
+
* SDODS from npm.
|
|
4
|
+
*
|
|
5
|
+
* playwright-bdd attributes a result to a Gherkin step by comparing each `test.step` location with
|
|
6
|
+
* the line bddgen wrote for it. Below 1.63, Playwright reports those locations against its own
|
|
7
|
+
* transformed copy of the generated spec instead of the source-mapped line, so no step matches:
|
|
8
|
+
* messages.ndjson (and the cucumber HTML report, and anything reading either) records every step
|
|
9
|
+
* SKIPPED, and a failure lands on a hook. Measured on 1.60.0, 1.61.1, 1.62.0 and 1.62.1 against
|
|
10
|
+
* 1.63.0, with the published @sdods packages and playwright-bdd 9.2.x (#79). A run from this
|
|
11
|
+
* monorepo's sources does not show it, so only this check stands between a pinned workspace and a
|
|
12
|
+
* report that names no failing step.
|
|
13
|
+
*/
|
|
14
|
+
export declare const MIN_PLAYWRIGHT_FOR_STEP_RESULTS = "1.63.0";
|
|
15
|
+
/** The `@playwright/test` version a workspace resolves, or undefined when it is not installed. */
|
|
16
|
+
export declare function installedPlaywrightVersion(rootDir: string): string | undefined;
|
|
17
|
+
/** A warning when `version` records every step SKIPPED, else undefined. */
|
|
18
|
+
export declare function stepResultsWarning(version: string | undefined): string | undefined;
|
|
19
|
+
//# sourceMappingURL=runner-compat.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* The oldest Playwright whose step locations playwright-bdd can match in a workspace that installs
|
|
5
|
+
* SDODS from npm.
|
|
6
|
+
*
|
|
7
|
+
* playwright-bdd attributes a result to a Gherkin step by comparing each `test.step` location with
|
|
8
|
+
* the line bddgen wrote for it. Below 1.63, Playwright reports those locations against its own
|
|
9
|
+
* transformed copy of the generated spec instead of the source-mapped line, so no step matches:
|
|
10
|
+
* messages.ndjson (and the cucumber HTML report, and anything reading either) records every step
|
|
11
|
+
* SKIPPED, and a failure lands on a hook. Measured on 1.60.0, 1.61.1, 1.62.0 and 1.62.1 against
|
|
12
|
+
* 1.63.0, with the published @sdods packages and playwright-bdd 9.2.x (#79). A run from this
|
|
13
|
+
* monorepo's sources does not show it, so only this check stands between a pinned workspace and a
|
|
14
|
+
* report that names no failing step.
|
|
15
|
+
*/
|
|
16
|
+
export const MIN_PLAYWRIGHT_FOR_STEP_RESULTS = '1.63.0';
|
|
17
|
+
/** The `@playwright/test` version a workspace resolves, or undefined when it is not installed. */
|
|
18
|
+
export function installedPlaywrightVersion(rootDir) {
|
|
19
|
+
try {
|
|
20
|
+
const require = createRequire(join(rootDir, 'package.json'));
|
|
21
|
+
return require('@playwright/test/package.json').version;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function below(version, floor) {
|
|
28
|
+
const parse = (v) => v
|
|
29
|
+
.split('-')[0]
|
|
30
|
+
.split('.')
|
|
31
|
+
.map((n) => Number(n) || 0);
|
|
32
|
+
const [a, b] = [parse(version), parse(floor)];
|
|
33
|
+
for (let i = 0; i < 3; i++) {
|
|
34
|
+
if ((a[i] ?? 0) !== (b[i] ?? 0))
|
|
35
|
+
return (a[i] ?? 0) < (b[i] ?? 0);
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
/** A warning when `version` records every step SKIPPED, else undefined. */
|
|
40
|
+
export function stepResultsWarning(version) {
|
|
41
|
+
if (!version || !below(version, MIN_PLAYWRIGHT_FOR_STEP_RESULTS))
|
|
42
|
+
return undefined;
|
|
43
|
+
return (`@playwright/test ${version} is older than ${MIN_PLAYWRIGHT_FOR_STEP_RESULTS}: messages.ndjson and the ` +
|
|
44
|
+
`cucumber report will record every step SKIPPED and attach failures to a hook. ` +
|
|
45
|
+
`Upgrade: npm i -D @playwright/test@^${MIN_PLAYWRIGHT_FOR_STEP_RESULTS}`);
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=runner-compat.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdods/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.2",
|
|
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>",
|
|
@@ -23,22 +23,22 @@
|
|
|
23
23
|
"node": ">=22"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@sdods/contracts": "0.
|
|
27
|
-
"@sdods/core": "0.
|
|
26
|
+
"@sdods/contracts": "0.5.2",
|
|
27
|
+
"@sdods/core": "0.5.2",
|
|
28
28
|
"commander": "^15.0.0",
|
|
29
29
|
"execa": "^10.0.1",
|
|
30
30
|
"picocolors": "^1.1.1",
|
|
31
31
|
"semver": "^7.8.5",
|
|
32
32
|
"yaml": "^2.9.0",
|
|
33
33
|
"zod": "^4.5.4",
|
|
34
|
-
"@playwright/test": "^1.
|
|
35
|
-
"playwright-core": "^1.
|
|
36
|
-
"@sdods/integrations": "0.
|
|
37
|
-
"@sdods/mcp": "0.
|
|
38
|
-
"@sdods/agents": "0.
|
|
39
|
-
"@sdods/db": "0.
|
|
34
|
+
"@playwright/test": "^1.63.0",
|
|
35
|
+
"playwright-core": "^1.63.0",
|
|
36
|
+
"@sdods/integrations": "0.5.2",
|
|
37
|
+
"@sdods/mcp": "0.5.2",
|
|
38
|
+
"@sdods/agents": "0.5.2",
|
|
39
|
+
"@sdods/db": "0.5.2",
|
|
40
40
|
"csv-parse": "^7.0.2",
|
|
41
|
-
"@sdods/server": "0.
|
|
41
|
+
"@sdods/server": "0.5.2"
|
|
42
42
|
},
|
|
43
43
|
"homepage": "https://sdods.com",
|
|
44
44
|
"bugs": {
|
|
@@ -12,7 +12,7 @@ description: Run SDODS test suites for a project across environments, layers (ui
|
|
|
12
12
|
| `project` | only project if one exists | `bun run sdods project list` |
|
|
13
13
|
| `env` | project `envs.default` | `bun run sdods env list -p <project>` |
|
|
14
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 |
|
|
15
|
+
| `browser` | project `browsers` | `-b chromium -b edge -b firefox -b webkit`, or `--project-matrix` for all |
|
|
16
16
|
| `tags` | none | Cucumber expression, e.g. `"@smoke and not @mock"`, `@user:admin`, `@jira:DEMO-12` |
|
|
17
17
|
| `headed` | false | `--headed` for a visible browser |
|
|
18
18
|
| `workers` | runner default | `-w 2` |
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sdods-start-ui
|
|
3
|
+
description: Start the SDODS web UI on any OS and get the first user signed in. Use when someone says "start the UI", "open the dashboard", "run the web app", "sdods serve", "I can't log in", "it says the web UI is not available", "port 4444 in use", "how do I register", or is onboarding onto SDODS for the first time.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Start the SDODS web UI
|
|
7
|
+
|
|
8
|
+
The UI is served by `sdods serve` — one Fastify process that serves the REST API, SSE run
|
|
9
|
+
streaming, MCP over HTTP, the scheduler and the React app. There is no separate UI process in
|
|
10
|
+
production. Docs: <https://docs.sdods.com/docs/getting-started/web-ui/> ·
|
|
11
|
+
<https://docs.sdods.com/docs/getting-started/installation/>
|
|
12
|
+
|
|
13
|
+
## 0. Preflight — check what is already on the machine
|
|
14
|
+
|
|
15
|
+
Most "the UI won't start" reports are really a Node or npm problem that predates SDODS. Run this
|
|
16
|
+
before installing anything; it takes a second and decides which install is safe.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
node -v; command -v node # need v22+
|
|
20
|
+
command -v bun fnm nvm volta asdf # is a version manager in charge of node?
|
|
21
|
+
npm config get prefix # where npm -g would write
|
|
22
|
+
which -a sdods # copies already installed (often more than one)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then read the four rules below. They are the failures that actually happen.
|
|
26
|
+
|
|
27
|
+
### Node must be 22+, and being below it does not say so
|
|
28
|
+
|
|
29
|
+
SDODS needs Node 22. Below that you do **not** get a clean version message — you get a crash from
|
|
30
|
+
somewhere deep in a dependency, because the code uses APIs that only exist in 22+:
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
TypeError: TEXT_ENCODINGS.union is not a function ← this is "your Node is too old"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
If you see an unexplained `TypeError` from a package you have never heard of, check `node -v` first.
|
|
37
|
+
|
|
38
|
+
### If a version manager owns Node, `npm install -g` is a trap
|
|
39
|
+
|
|
40
|
+
With fnm, nvm, volta or asdf, **global npm packages belong to the Node version that was active when
|
|
41
|
+
you installed them**. Switch versions and they are simply gone. Demonstrated on a machine with four
|
|
42
|
+
Node versions installed:
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
$ fnm exec --using=v22.22.2 npm ls -g → codex, bruno, firebase-tools, …
|
|
46
|
+
$ fnm exec --using=v24.15.0 npm ls -g → corepack, npm, openclaw ← different world
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
So `npm install -g @sdods/cli` under Node 22, then `fnm use 24`, and `sdods: command not found` —
|
|
50
|
+
with the package still on disk. Pick one:
|
|
51
|
+
|
|
52
|
+
| Your situation | Do this |
|
|
53
|
+
|---|---|
|
|
54
|
+
| a version manager is in charge (the common case) | use the installer — it defaults to **bun**, which installs to `~/.bun` and is the same regardless of which Node is active |
|
|
55
|
+
| you want npm anyway | pin the Node version first (`fnm use 22 && fnm default 22`), and re-install after any switch |
|
|
56
|
+
| no version manager, Node from the OS or Homebrew | `npm install -g @sdods/cli` is fine |
|
|
57
|
+
|
|
58
|
+
### Never `sudo npm install -g`
|
|
59
|
+
|
|
60
|
+
If `npm config get prefix` points somewhere unwritable (`/usr/local`, `/usr`), `npm -g` fails with
|
|
61
|
+
`EACCES`. `sudo` "fixes" it and leaves root-owned files that break every later install. Instead:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm config set prefix ~/.npm-global # then add ~/.npm-global/bin to PATH
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
or just use the installer, which writes only to `~/.sdods`, `~/.bun` and `~/.local/bin`.
|
|
68
|
+
|
|
69
|
+
### More than one `sdods` is normal — know which one wins
|
|
70
|
+
|
|
71
|
+
`which -a sdods` frequently returns several (an installer shim, a Homebrew copy, a bun global).
|
|
72
|
+
**The first on PATH wins**, and they drift apart. Inside a checkout none of them is the right one —
|
|
73
|
+
use `bun run sdods`. See the callout in §1.
|
|
74
|
+
|
|
75
|
+
### Already installed, and you just want it working
|
|
76
|
+
|
|
77
|
+
| Symptom | Do this |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `sdods: command not found`, but you installed it | you switched Node versions — re-install, or use the bun/installer path |
|
|
80
|
+
| unexplained `TypeError` from a dependency | `node -v`; anything under 22 is the cause |
|
|
81
|
+
| `EACCES` during install | do **not** sudo — repoint the npm prefix, or use the installer |
|
|
82
|
+
| two machines disagree | compare `sdods --version` **and** `which -a sdods` on both |
|
|
83
|
+
| everything looks installed but behaves oddly | `sdods doctor` — but run it outside a checkout, or with `bun run sdods` inside one |
|
|
84
|
+
|
|
85
|
+
Only the server runs on Node. Do not try to run it under `bun` directly — the native modules abort
|
|
86
|
+
with `panic: NAPI FATAL ERROR`. `bun run sdods` is safe because it shells out to Node via tsx.
|
|
87
|
+
|
|
88
|
+
## 1. Identify the install, because the command differs
|
|
89
|
+
|
|
90
|
+
Run both. **Where you are matters more than what is on PATH** — a global `sdods` and a source
|
|
91
|
+
checkout very often coexist, and the two are not interchangeable.
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
sdods --version || echo "not on PATH"
|
|
95
|
+
grep -m1 '"name"' package.json 2>/dev/null || echo "not in a checkout"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Read the rows in order and take the first that matches:
|
|
99
|
+
|
|
100
|
+
| Condition | You have | Start command |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `package.json` says `sdods-monorepo` — **even if `sdods` is on PATH** | a **source checkout** | `bun run web:build`, then `bun run sdods serve --open` |
|
|
103
|
+
| `sdods` on PATH, anywhere else | a **global install** (installer, npm, Homebrew) | `sdods serve --open` |
|
|
104
|
+
| neither, Docker is running | the **container** | `docker run --rm -p 8080:8080 -v sdods-data:/data ghcr.io/siri1410/sdods-server` |
|
|
105
|
+
| you want no terminal at all | the **desktop app** | install from <https://sdods.com/download>, launch it |
|
|
106
|
+
|
|
107
|
+
> **Inside a checkout, always use `bun run sdods`, never the global `sdods`.** The two drift, and
|
|
108
|
+
> the failure is confusing because both report the same version. A global CLI run against this
|
|
109
|
+
> repo's `demo-shop` dies with
|
|
110
|
+
> `✖ CONFIG_INVALID: browsers.1: Invalid option: expected one of "chromium"|"firefox"|…` — the
|
|
111
|
+
> repo's project yaml lists `edge`, which the older published schema does not know. Same version
|
|
112
|
+
> string, different schema. `which -a sdods` will often show three copies.
|
|
113
|
+
|
|
114
|
+
Prerequisites differ per row: the global install needs **Node 22+**; a source checkout also needs
|
|
115
|
+
**bun** and a completed `bun install` (running it from a directory outside the installed tree fails
|
|
116
|
+
with `Cannot find package 'tsx'`).
|
|
117
|
+
|
|
118
|
+
`sdods doctor` reports Node, bun, the runner, browsers, projects, env vars and the database, and
|
|
119
|
+
continues past *check* failures. It does not survive a bad project yaml — invalid config aborts it
|
|
120
|
+
with exit 2 before any check runs.
|
|
121
|
+
|
|
122
|
+
## 1a. Does the UI actually ship with my install?
|
|
123
|
+
|
|
124
|
+
The UI is a prebuilt bundle, not a live build, so this has a different answer per channel. Every
|
|
125
|
+
**published** channel carries it; the one that does not is the source checkout — which is the one
|
|
126
|
+
the docs hand to new contributors.
|
|
127
|
+
|
|
128
|
+
| Channel (as listed on sdods.com/install) | UI included | Why |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| macOS / Linux `install.sh` | yes | installs `@sdods/cli` globally, which depends on `@sdods/server`, which vendors the bundle |
|
|
131
|
+
| Windows `install.ps1` | yes | same global package, same vendored bundle |
|
|
132
|
+
| Homebrew | yes | the tap carries the formula only; the software comes from npm |
|
|
133
|
+
| npm `-g @sdods/cli` | yes | `@sdods/server`'s published tarball contains `web/index.html` |
|
|
134
|
+
| Docker | yes | the image builds the bundle at `RUN bun run --filter @sdods/web build` |
|
|
135
|
+
| Desktop app (winget · Scoop · apt) | yes | it bootstraps the same npm packages and supervises `sdods serve` |
|
|
136
|
+
| **source checkout** | **no** | `packages/web/dist/` is gitignored — run `bun run web:build` first |
|
|
137
|
+
|
|
138
|
+
Prove it on any install. The first command is the verdict; run the second only if it said `real UI`
|
|
139
|
+
(on a stub there is no asset to find, so it would curl `/` and report a misleading 200):
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
curl -s http://127.0.0.1:4444/ | grep -q 'not available in this install' \
|
|
143
|
+
&& echo 'STUB — no UI bundle' || echo 'real UI'
|
|
144
|
+
curl -s -o /dev/null -w '%{http_code}\n' "http://127.0.0.1:4444$(curl -s http://127.0.0.1:4444/ \
|
|
145
|
+
| grep -o 'src="/assets/[^"]*"' | head -1 | sed 's/src="//;s/"//')"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## 2. Start it, per OS
|
|
149
|
+
|
|
150
|
+
The CLI is identical on every OS; only the shell and the paths differ.
|
|
151
|
+
|
|
152
|
+
| OS / shell | Start the UI |
|
|
153
|
+
|---|---|
|
|
154
|
+
| macOS, Linux (bash/zsh) | `sdods serve --open` |
|
|
155
|
+
| Windows (PowerShell) | `sdods serve --open` |
|
|
156
|
+
| Windows (cmd) | `sdods serve --open` — the installer writes both `sdods.cmd` and `sdods.ps1` |
|
|
157
|
+
| WSL | use the Linux command inside WSL, not the PowerShell one |
|
|
158
|
+
| Docker (any host) | `docker run --rm -p 8080:8080 -v sdods-data:/data ghcr.io/siri1410/sdods-server` |
|
|
159
|
+
|
|
160
|
+
Defaults: **`http://127.0.0.1:4444`** for the CLI, **`http://localhost:8080`** for Docker (the
|
|
161
|
+
image binds `0.0.0.0:8080`). `--open` launches the browser (`open` / `start` / `xdg-open` by
|
|
162
|
+
platform). `--host 0.0.0.0` exposes it on the LAN — only do that deliberately.
|
|
163
|
+
|
|
164
|
+
On Apple silicon the published image is `linux/amd64` only, and Docker refuses the pull rather
|
|
165
|
+
than emulating. Add the platform flag:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
docker run --rm --platform linux/amd64 -p 8080:8080 -v sdods-data:/data ghcr.io/siri1410/sdods-server
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
If the run must come from a source checkout, `bun run web:build` is not optional — see
|
|
172
|
+
Troubleshooting.
|
|
173
|
+
|
|
174
|
+
## 3. Get the first user in (registration)
|
|
175
|
+
|
|
176
|
+
There are two ways in, and `serve` tells you which applies. On a database with **no users** — and
|
|
177
|
+
only when `--auth-disabled` is off — startup prints a one-time setup URL. You will see it **twice**:
|
|
178
|
+
once as this plain line from the CLI, and once more as a raw pino JSON log line with slightly
|
|
179
|
+
different wording. Same token, not two tokens:
|
|
180
|
+
|
|
181
|
+
```text
|
|
182
|
+
✔ SDODS server listening on http://127.0.0.1:4444
|
|
183
|
+
ℹ First run: create the admin at http://127.0.0.1:4444/setup?token=<token> (or: sdods users create --admin --username <name> --password <pw>)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
- **Browser path** — open that `/setup?token=…` URL and fill the form. Best for onboarding
|
|
187
|
+
someone who should never touch a terminal. The token is printed once per fresh database.
|
|
188
|
+
**On an unbuilt source checkout this URL 404s** — the stub page answers `/` only and there is no
|
|
189
|
+
SPA fallback, so run `bun run web:build` first or use the CLI path.
|
|
190
|
+
- **CLI path** — works headlessly, in CI, and in containers:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
sdods users create --admin --username admin --password '<at least 8 chars>'
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`--admin` makes a platform admin and owner of every organization without one. Add teammates with
|
|
197
|
+
`--role viewer|editor|admin`, and scope them per workspace:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
sdods users create --username sam --role editor
|
|
201
|
+
sdods users grant sam --workspace default --role editor
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Run `users create` and `serve` **from the same workspace root**. Resolution walks up from the cwd
|
|
205
|
+
looking for a workspace marker (`sdods.workspace.yaml`, `sdods.config.ts`, `sdods.config.json`) and
|
|
206
|
+
uses that directory's `.sdods/sdods.db`. With no marker above you, it falls back to `.sdods/sdods.db`
|
|
207
|
+
relative to the cwd — which is how you end up creating the admin in one database and serving another,
|
|
208
|
+
and the login then fails with entirely correct credentials. `SQLITE_PATH` overrides all of it, and is
|
|
209
|
+
the reliable way to pin both commands to one file:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
SQLITE_PATH=/path/to/sdods.db sdods users create --admin --username admin --password '<pw>'
|
|
213
|
+
SQLITE_PATH=/path/to/sdods.db sdods serve --open
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
> **Lost the token?** Restart `serve` to print a new one — a fresh database prints one every start.
|
|
217
|
+
> Current versions redirect a first-time visitor to `/setup` on their own and accept the token
|
|
218
|
+
> pasted into the form, so the terminal is not the only way in. Older ones do neither: they land you
|
|
219
|
+
> on `/login`, which only suggests `sdods users create`, and `/setup` takes the token from the query
|
|
220
|
+
> string alone. If that is what you see, use the CLI path or upgrade.
|
|
221
|
+
|
|
222
|
+
`--auth-disabled` makes every request a local admin. It is a development shortcut on a machine
|
|
223
|
+
only you can reach, never a way to skip onboarding on a shared host.
|
|
224
|
+
|
|
225
|
+
## 4. Fastest path from nothing to a working UI
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
curl -fsSL https://sdods.com/install.sh | sh -s -- --workspace ~/my-tests # macOS/Linux
|
|
229
|
+
cd ~/my-tests
|
|
230
|
+
sdods users create --admin --username admin --password 'change-me-now'
|
|
231
|
+
sdods serve --open
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Windows PowerShell, same shape:
|
|
235
|
+
|
|
236
|
+
```powershell
|
|
237
|
+
& ([scriptblock]::Create((irm https://sdods.com/install.ps1))) -Workspace C:\my-tests
|
|
238
|
+
cd C:\my-tests
|
|
239
|
+
sdods users create --admin --username admin --password 'change-me-now'
|
|
240
|
+
sdods serve --open
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`--workspace` runs `sdods init` for you, so the workspace already has the `demo-shop` project and
|
|
244
|
+
there is something to look at on the dashboard immediately. Without a project the UI is real but
|
|
245
|
+
empty, which reads as broken to a first-time user.
|
|
246
|
+
|
|
247
|
+
**If SDODS is already installed**, run the same command again — it upgrades in place rather than
|
|
248
|
+
adding a second copy, and it is the safe response to a version that looks stale. Two flags are worth
|
|
249
|
+
knowing before you run it anywhere you care about:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
sh install.sh --dry-run # prints every step, changes nothing
|
|
253
|
+
sh install.sh --version-check # prints the versions and paths it resolved here, then exits
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Paste the `--version-check` output into any support thread: it is the whole environment in one
|
|
257
|
+
block, and it settles the "which Node, which copy" questions from §0 immediately.
|
|
258
|
+
|
|
259
|
+
## 5. Confirm it is actually up
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
curl -s http://127.0.0.1:4444/api/health # {"ok":true,"version":…,"driver":"sqlite",…}
|
|
263
|
+
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4444/ # 200
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
A `200` on `/` that returns a short page saying *"The web UI is not available in this install"* is
|
|
267
|
+
the API running **without** the UI bundle — that is the source-checkout case, not a healthy start.
|
|
268
|
+
|
|
269
|
+
## Troubleshooting
|
|
270
|
+
|
|
271
|
+
| Symptom | Cause | Fix |
|
|
272
|
+
|---|---|---|
|
|
273
|
+
| A page saying the web UI was not found (**HTTP 503**) | no UI bundle. The server tries four locations: `<root>/packages/web/dist`, `<root>/node_modules/@sdods/web/dist`, the sibling `web/dist` in a checkout, then the copy vendored into `@sdods/server` at pack time. On a fresh clone none exist — `packages/web/dist/` is gitignored | `bun run web:build`, then restart `serve` |
|
|
274
|
+
| `CONFIG_INVALID: Port 4444 is already in use.` | something already holds the port, often another `serve` or the desktop app | take the port it suggests: `sdods serve --port 4445`, or find the holder (`lsof -i :4444` · `netstat -ano \| findstr :4444`). On versions before this fix the command printed `INTERNAL: listen EADDRINUSE` and then hung — Ctrl+C and upgrade |
|
|
275
|
+
| Login fails with the right password | `users create` and `serve` ran in different workspaces, so two `.sdods/sdods.db` files | run both from one workspace root; `sdods users list` in that directory should show the account |
|
|
276
|
+
| No setup token printed | the database already has users, **or** you passed `--auth-disabled`, which suppresses it | sign in, `sdods users create`, or restart without `--auth-disabled` |
|
|
277
|
+
| `/setup?token=…` returns 404 | an SDODS older than the fix that made the stub answer every route | upgrade, or `bun run web:build` and restart, or use `sdods users create --admin` |
|
|
278
|
+
| Fresh install, but `/login` says "ask an admin" | the login page does not redirect to `/setup` | open the `/setup?token=…` URL from the `serve` output, or `sdods users create --admin` |
|
|
279
|
+
| Global CLI errors on a repo project (`CONFIG_INVALID`, unknown browser) | version skew between the global CLI and the checkout | use `bun run sdods` inside a checkout; upgrade the global one otherwise |
|
|
280
|
+
| `sdods: command not found` after installing | the bin dir is not on PATH — `~/.local/bin`, or `%LOCALAPPDATA%\SDODS\bin` on Windows | add it, or re-run the installer with `--modify-path` / `-ModifyPath` |
|
|
281
|
+
| Docker: `no matching manifest for linux/arm64/v8` | amd64-only image on Apple silicon | add `--platform linux/amd64` |
|
|
282
|
+
| UI loads, runs never start | browser engines missing | `sdods browsers install -b chromium` (the positional `sdods browsers install chromium` also works on current versions, and failed with `too many arguments` before). Bare `sdods browsers install` installs all engines; on Linux `--with-deps` needs root |
|
|
283
|
+
| Node too old | SDODS needs Node 22+ | `nvm use 22` / `fnm use 22`. On macOS/Linux the installer also takes `--install-node`; **`install.ps1` has no such flag** — it exits and tells you to run `winget install OpenJS.NodeJS.LTS`, `scoop install nodejs-lts` or `nvm install 22` |
|
|
284
|
+
| Anything else | — | `sdods doctor` first; it reports what it found, not what it expected |
|
|
285
|
+
|
|
286
|
+
## Keeping it current
|
|
287
|
+
|
|
288
|
+
`upgrade` reads the npm registry and reports; it installs nothing without `--apply`.
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
sdods upgrade # what is behind
|
|
292
|
+
sdods upgrade --apply # install the latest @sdods/* packages
|
|
293
|
+
sdods browsers install --with-deps # refresh engines afterwards (Linux needs root)
|
|
294
|
+
sdods doctor # confirm
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
CI does this on a schedule: `.github/workflows/dependencies.yml` reports freshness into one rolling
|
|
298
|
+
issue every morning, and on the 1st of the month upgrades `@sdods/*` and Playwright, runs the suite,
|
|
299
|
+
and opens a PR only if it passes.
|
|
300
|
+
|
|
301
|
+
After any upgrade, restart `serve` — a running server keeps the old bundle in memory.
|
|
302
|
+
|
|
303
|
+
## Working on the UI itself
|
|
304
|
+
|
|
305
|
+
Only for developing the React app — not how anyone runs SDODS:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
bun run web:dev # Vite on :5173 with MSW mocks, no server needed
|
|
309
|
+
bun run sdods serve # in a second terminal, for the real API
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
The Vite dev proxy targets `http://127.0.0.1:4444` whether or not mocks are on, so if you start the
|
|
313
|
+
API on another port the proxied routes will not follow it.
|
package/templates/.env.example
CHANGED
|
@@ -18,6 +18,7 @@ HOST=127.0.0.1
|
|
|
18
18
|
SESSION_SECRET=change-me-32-chars-minimum-please # openssl rand -hex 32
|
|
19
19
|
# SDODS_PUBLIC_URL=https://sdods.example.com
|
|
20
20
|
# AUTH_DISABLED=1 # local dev only
|
|
21
|
+
# SDODS_TRUST_PROXY=false # true | hop count | CIDR list; only behind a reverse proxy (sets req.ip, https)
|
|
21
22
|
|
|
22
23
|
# Where projects and run artifacts live (defaults shown)
|
|
23
24
|
# SDODS_PROJECTS_DIR=projects
|
|
@@ -7,7 +7,7 @@ description: >-
|
|
|
7
7
|
organization: sdods
|
|
8
8
|
workspace: default
|
|
9
9
|
layers: [ui, api, hybrid, recorded]
|
|
10
|
-
browsers: [chromium, firefox, webkit]
|
|
10
|
+
browsers: [chromium, edge, firefox, webkit]
|
|
11
11
|
testIdAttribute: data-test # SauceDemo uses data-test="..."
|
|
12
12
|
|
|
13
13
|
# Modules: feature directories with their testing types and the tag every scenario carries.
|
|
@@ -183,3 +183,9 @@ reports:
|
|
|
183
183
|
cucumberHtml: false
|
|
184
184
|
allure: false
|
|
185
185
|
junit: false
|
|
186
|
+
|
|
187
|
+
# Edge is declared above so `-b edge` is legal and `@skip:edge` lints, but the generic browser
|
|
188
|
+
# matrix stays on the three engines Playwright downloads. msedge is a system install that a
|
|
189
|
+
# Linux runner does not have; the dedicated `edge` job in ci.yml installs it first.
|
|
190
|
+
ci:
|
|
191
|
+
browsers: [chromium, firefox, webkit]
|