@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.
@@ -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
- const { app, url } = await startServer({
16
- config: {
17
- rootDir: ctx.rootDir,
18
- port: opts.port,
19
- host: opts.host,
20
- authDisabled: opts.authDisabled ? true : undefined,
21
- },
22
- scheduler: opts.scheduler !== false,
23
- open: Boolean(opts.open),
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>)`);
@@ -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.2",
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.3.2",
27
- "@sdods/core": "0.3.2",
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.62.1",
35
- "playwright-core": "^1.62.1",
36
- "@sdods/integrations": "0.3.2",
37
- "@sdods/mcp": "0.3.2",
38
- "@sdods/agents": "0.3.2",
39
- "@sdods/db": "0.3.2",
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.3.2"
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.
@@ -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]