@crvouga/atlas 1.19.0 → 1.21.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/dist/core/src/cli.d.ts +1 -1
  3. package/dist/core/src/cli.js +3 -0
  4. package/dist/reports/src/compose.d.ts +23 -1
  5. package/dist/reports/src/compose.js +29 -19
  6. package/dist/reports/src/report-cli.d.ts +13 -0
  7. package/dist/reports/src/report-cli.js +6 -4
  8. package/dist/reports/src/ui-catalog-cli.d.ts +10 -0
  9. package/dist/reports/src/ui-catalog-cli.js +391 -0
  10. package/dist/reports/src/ui-catalog-diff.d.ts +104 -0
  11. package/dist/reports/src/ui-catalog-diff.js +130 -0
  12. package/dist/reports/src/ui-catalog-export.d.ts +82 -0
  13. package/dist/reports/src/ui-catalog-export.js +221 -0
  14. package/dist/schema/src/api.d.ts +55 -0
  15. package/dist/schema/src/api.js +33 -0
  16. package/dist/schema/src/cli-results.d.ts +318 -1
  17. package/dist/schema/src/cli-results.js +89 -1
  18. package/dist/server/src/app.js +3 -1
  19. package/dist/server/src/openapi.d.ts +1 -1
  20. package/dist/server/src/openapi.js +24 -2
  21. package/dist/server/src/recording-verification.d.ts +34 -0
  22. package/dist/server/src/recording-verification.js +118 -0
  23. package/docs/agents.md +5 -0
  24. package/docs/reference/cli.md +56 -1
  25. package/docs/reference/http-api.md +52 -0
  26. package/docs/reference/reports.md +392 -0
  27. package/docs/reference/schema.md +82 -1
  28. package/docs/reference/server.md +39 -0
  29. package/docs/rich-evidence.md +12 -0
  30. package/docs/server.md +12 -0
  31. package/docs/ui-catalog.md +106 -12
  32. package/docs/ui.md +6 -1
  33. package/llms-full.txt +246 -11
  34. package/package.json +1 -1
  35. package/packages/schema/schemas/ui-catalog-diff-result.schema.json +201 -0
  36. package/packages/schema/schemas/ui-catalog-export-result.schema.json +83 -0
  37. package/packages/schema/schemas/ui-catalog-publish-result.schema.json +116 -0
  38. package/packages/schema/schemas/ui-catalog-status.schema.json +197 -0
  39. package/packages/server/openapi.json +527 -1
  40. package/packages/visualizer/src/catalog/Picture.tsx +32 -0
  41. package/packages/visualizer/src/catalog/UiCatalogCompare.tsx +289 -0
  42. package/packages/visualizer/src/catalog/UiCatalogPage.tsx +18 -28
  43. package/packages/visualizer/src/catalog/history.ts +61 -0
  44. package/packages/visualizer/src/catalog/search.ts +14 -3
  45. package/packages/visualizer/src/evidence/evidence-context.tsx +2 -1
  46. package/packages/visualizer/src/explorer/RunsView.tsx +32 -6
  47. package/packages/visualizer/src/explorer/catalog.tsx +24 -1
  48. package/packages/visualizer/src/explorer/recording-verification.ts +58 -0
package/CHANGELOG.md CHANGED
@@ -1,6 +1,18 @@
1
1
  # Changelog: @crvouga/atlas
2
2
 
3
- ## 1.19.0 (2026-10-29)
3
+ ## 1.21.0 (2026-10-29)
4
+
5
+ ### Features
6
+
7
+ - add an atlas ui-catalog command group, a single-file export and comparison of commits and profiles ([3159315](https://github.com/crvouga/atlas/commit/3159315e9a1b057309b5b1eb2d745426885adfa8))
8
+
9
+ ## 1.20.0 (2026-10-28)
10
+
11
+ ### Features
12
+
13
+ - mark stored-then-unavailable recordings in the Runs tab from a server read-back ([8c9370a](https://github.com/crvouga/atlas/commit/8c9370aecf67e2b2e690ba25ed512be36a137875))
14
+
15
+ ## 1.19.0 (2026-10-28)
4
16
 
5
17
  ### Features
6
18
 
@@ -9,6 +9,6 @@
9
9
  * @see docs/reference/cli.md
10
10
  */
11
11
  /** The text `atlas --help` prints: every command, and the flags of the statechart commands. */
12
- export declare const USAGE = "atlas \u2014 executable statecharts\n\n atlas lint [--config atlas.config.ts | --specs <dir>] [--require-implementations]\n atlas plan [--config atlas.config.ts] [--state <name>]\n atlas run [--config atlas.config.ts] [--mode fast|showcase] [--workers n] [--state <name>]\n [--seed <name>]... [--paths id,id] [--shard i/n] [--rerun-failed | --reuse <run-dir>]\n [--no-retry] [--output <dir>]\n atlas merge [--config atlas.config.ts] [--output <dir>] <run-dir> <run-dir>...\n atlas export --specs <dir> --format scxml|mermaid|markdown|json [--out <file>]\n atlas server [--port 8080] one Atlas node: API, projects and client; needs ATLAS_S3_* (atlas serve --help)\n atlas client build [--no-data] [--out <dir>] build the project-agnostic Atlas client\n atlas dev [--port 5180] develop the Atlas server and client on one host\n atlas init [--server <url>] [--write] integrate this project: detect its suites, print the plan (atlas init --help)\n atlas report publish|stream|status|merge|doctor|policy \u2026 layered evidence for any kind of spec (atlas report --help)\n\n --workers path attempts that run at once (default: one per CPU, capped by the driver)\n --seed only paths that start from this seed (\"start\" for paths setup starts); repeatable\n --shard 2/4 run the second quarter of the plan; merge the shards' runs afterwards\n --rerun-failed keep the newest run's passed paths and run only the rest\n";
12
+ export declare const USAGE = "atlas \u2014 executable statecharts\n\n atlas lint [--config atlas.config.ts | --specs <dir>] [--require-implementations]\n atlas plan [--config atlas.config.ts] [--state <name>]\n atlas run [--config atlas.config.ts] [--mode fast|showcase] [--workers n] [--state <name>]\n [--seed <name>]... [--paths id,id] [--shard i/n] [--rerun-failed | --reuse <run-dir>]\n [--no-retry] [--output <dir>]\n atlas merge [--config atlas.config.ts] [--output <dir>] <run-dir> <run-dir>...\n atlas export --specs <dir> --format scxml|mermaid|markdown|json [--out <file>]\n atlas server [--port 8080] one Atlas node: API, projects and client; needs ATLAS_S3_* (atlas serve --help)\n atlas client build [--no-data] [--out <dir>] build the project-agnostic Atlas client\n atlas dev [--port 5180] develop the Atlas server and client on one host\n atlas init [--server <url>] [--write] integrate this project: detect its suites, print the plan (atlas init --help)\n atlas report publish|stream|status|merge|doctor|policy \u2026 layered evidence for any kind of spec (atlas report --help)\n atlas ui-catalog plan|publish|status|diff|export \u2026 the UI catalog of captured states (atlas ui-catalog --help)\n\n --workers path attempts that run at once (default: one per CPU, capped by the driver)\n --seed only paths that start from this seed (\"start\" for paths setup starts); repeatable\n --shard 2/4 run the second quarter of the plan; merge the shards' runs afterwards\n --rerun-failed keep the newest run's passed paths and run only the rest\n";
13
13
  /** Runs `atlas` with `argv` (default: the process arguments) and returns the exit code. Throws for a command that is wrong, which the `atlas` binary reports as a message and exit code 2. */
14
14
  export declare function main(argv?: string[]): Promise<number>;
@@ -33,6 +33,7 @@ export const USAGE = `atlas — executable statecharts
33
33
  atlas dev [--port 5180] develop the Atlas server and client on one host
34
34
  atlas init [--server <url>] [--write] integrate this project: detect its suites, print the plan (atlas init --help)
35
35
  atlas report publish|stream|status|merge|doctor|policy … layered evidence for any kind of spec (atlas report --help)
36
+ atlas ui-catalog plan|publish|status|diff|export … the UI catalog of captured states (atlas ui-catalog --help)
36
37
 
37
38
  --workers path attempts that run at once (default: one per CPU, capped by the driver)
38
39
  --seed only paths that start from this seed ("start" for paths setup starts); repeatable
@@ -105,6 +106,8 @@ export async function main(argv = process.argv.slice(2)) {
105
106
  return (await import('../../reports/src/init.js')).initMain(argv.slice(1), { out: text => process.stdout.write(text) });
106
107
  if (command === 'report')
107
108
  return (await import('../../reports/src/report-cli.js')).runReportCommand(argv.slice(1));
109
+ if (command === 'ui-catalog')
110
+ return (await import('../../reports/src/ui-catalog-cli.js')).runUiCatalogCommand(argv.slice(1));
108
111
  const configFile = flag(argv, 'config') ?? 'atlas.config.ts';
109
112
  const specs = flag(argv, 'specs');
110
113
  const output = flag(argv, 'output');
@@ -1,4 +1,5 @@
1
- import type { BackendInfo, LayerHeader, StatusCounts } from './evidence-types.js';
1
+ import type { BackendInfo, LayerEvidence, LayerHeader, StatusCounts } from './evidence-types.js';
2
+ import type { SpecEntry } from './explorer-types.js';
2
3
  import { type CombinedEvidence, type EvidenceResult } from './layer-merge.js';
3
4
  import type { ReportOutline } from './layer-types.js';
4
5
  import type { ObjectStore } from './object-store.js';
@@ -58,6 +59,27 @@ export declare function countResults(all: readonly EvidenceResult[]): StatusCoun
58
59
  };
59
60
  /** Counts a combined report's winning results in total and per environment, and lists the specs that have no result at all. */
60
61
  export declare function summarize(combined: CombinedEvidence): ReportSummary;
62
+ /** Every layer of some backends, read but not yet composed. */
63
+ export type BackendLayers = {
64
+ /** Each layer that could be read, with the specs it recorded. A layer several backends hold appears once per backend. */
65
+ readonly loaded: readonly {
66
+ readonly evidence: LayerEvidence;
67
+ readonly specs: readonly SpecEntry[];
68
+ }[];
69
+ /** Every backend given, sorted by id, with whether it could be listed. */
70
+ readonly backends: readonly BackendInfo[];
71
+ /** What could not be read, sorted and without repeats. */
72
+ readonly notices: readonly string[];
73
+ };
74
+ /**
75
+ * Reads every layer of every backend, superseded ones included, without composing them. This is what {@link combineBackends}
76
+ * starts from; a caller that needs layers a newer layer has replaced (to compare two commits, for example) reads them here.
77
+ * A backend that cannot be listed, or a layer that cannot be understood, is a notice, never an omission that looks like a pass.
78
+ * Throws for an invalid or repeated backend `id`, before anything is read.
79
+ */
80
+ export declare function readBackendLayers(backends: readonly BackendInput[], options?: {
81
+ readonly concurrency?: number;
82
+ }): Promise<BackendLayers>;
61
83
  /**
62
84
  * Reads every layer of every backend and lays them over the specs. Layers are the only input:
63
85
  * a report is a function of the set of layers, so the order of backends and layers, and how they
@@ -45,25 +45,12 @@ export function summarize(combined) {
45
45
  };
46
46
  }
47
47
  /**
48
- * Reads every layer of every backend and lays them over the specs. Layers are the only input:
49
- * a report is a function of the set of layers, so the order of backends and layers, and how they
50
- * were split across machines, branches and runs, does not change it.
51
- *
52
- * A backend that cannot be read, or a layer that cannot be understood, is named in `notices` and
53
- * leaves everything else usable; a gate must treat any notice as unknown, never as a pass. A layer held by
54
- * several backends is one layer. Only an invalid backend `id` (not lowercase letters, digits and hyphens, or
55
- * repeated) throws, before anything is read. Backends are only read from.
56
- *
57
- * When `options.outline` is omitted, the specs are the ones the included layers recorded (the newest
58
- * recording of each spec path wins), with no commit sha.
59
- *
60
- * @example
61
- * const outline = await loadDirectoryOutline('./specs'); // optional: without it the layers' own specs are used
62
- * const report = await combineBackends([{ id: 'ci', store: directoryStore('./atlas-reports') }], { outline });
63
- * report.summary.checks.failed; // number of slots whose winner failed
64
- * report.notices; // non-empty means something could not be read
48
+ * Reads every layer of every backend, superseded ones included, without composing them. This is what {@link combineBackends}
49
+ * starts from; a caller that needs layers a newer layer has replaced (to compare two commits, for example) reads them here.
50
+ * A backend that cannot be listed, or a layer that cannot be understood, is a notice, never an omission that looks like a pass.
51
+ * Throws for an invalid or repeated backend `id`, before anything is read.
65
52
  */
66
- export async function combineBackends(backends, options = {}) {
53
+ export async function readBackendLayers(backends, options = {}) {
67
54
  const seen = new Set();
68
55
  for (const backend of backends) {
69
56
  if (!BACKEND_ID.test(backend.id) || seen.has(backend.id))
@@ -96,6 +83,29 @@ export async function combineBackends(backends, options = {}) {
96
83
  notices.push(`${backend.label ?? backend.id}: a layer could not be read, so its outcome is unknown (${error instanceof Error ? error.message : String(error)})`);
97
84
  }
98
85
  });
86
+ return { loaded, backends: info.sort((a, b) => (a.id < b.id ? -1 : 1)), notices: [...new Set(notices)].sort() };
87
+ }
88
+ /**
89
+ * Reads every layer of every backend and lays them over the specs. Layers are the only input:
90
+ * a report is a function of the set of layers, so the order of backends and layers, and how they
91
+ * were split across machines, branches and runs, does not change it.
92
+ *
93
+ * A backend that cannot be read, or a layer that cannot be understood, is named in `notices` and
94
+ * leaves everything else usable; a gate must treat any notice as unknown, never as a pass. A layer held by
95
+ * several backends is one layer. Only an invalid backend `id` (not lowercase letters, digits and hyphens, or
96
+ * repeated) throws, before anything is read. Backends are only read from.
97
+ *
98
+ * When `options.outline` is omitted, the specs are the ones the included layers recorded (the newest
99
+ * recording of each spec path wins), with no commit sha.
100
+ *
101
+ * @example
102
+ * const outline = await loadDirectoryOutline('./specs'); // optional: without it the layers' own specs are used
103
+ * const report = await combineBackends([{ id: 'ci', store: directoryStore('./atlas-reports') }], { outline });
104
+ * report.summary.checks.failed; // number of slots whose winner failed
105
+ * report.notices; // non-empty means something could not be read
106
+ */
107
+ export async function combineBackends(backends, options = {}) {
108
+ const { loaded, backends: info, notices } = await readBackendLayers(backends, options.concurrency === undefined ? {} : { concurrency: options.concurrency });
99
109
  const include = options.include;
100
110
  const kept = include ? loaded.filter(item => include(item.evidence.layer)) : loaded;
101
111
  const layers = kept.map(item => item.evidence);
@@ -107,5 +117,5 @@ export async function combineBackends(backends, options = {}) {
107
117
  const outline = options.outline ?? { specs: recorded.entries(), sha: null, changedFiles: [] };
108
118
  const combined = combineEvidence(outline, layers);
109
119
  const headers = distinctHeaders(layers.map(evidence => evidence.layer)).sort((a, b) => b.recordedAtMs - a.recordedAtMs || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
110
- return { combined, layers: headers, backends: info.sort((a, b) => (a.id < b.id ? -1 : 1)), notices: [...new Set(notices)].sort(), summary: summarize(combined) };
120
+ return { combined, layers: headers, backends: info, notices, summary: summarize(combined) };
111
121
  }
@@ -12,6 +12,7 @@
12
12
  * @see docs/integrating.md
13
13
  * @see docs/agents.md
14
14
  */
15
+ import { type LayerRun } from '../../schema/src/layer.js';
15
16
  /** The text `atlas report --help` prints: every subcommand, its flags, what each does and the exit codes. Also the source of the command reference in `docs/reference/cli.md`. */
16
17
  export declare const REPORT_USAGE = "atlas report \u2014 layered evidence for any kind of spec\n\n atlas report publish [--store <dir|s3://bucket/prefix>] [--producer <name>]\n ( --ctrf <file> | --junit <file> | --manifest <file> | --checks <file.json> )\n [--environment <name>] [--branch <b>] [--sha <s>] [--run-url <u>]\n [--run-id <id>] [--run-title <t>] [--run-started-at <iso|ms>] [--run-ended-at <iso|ms>]\n [--spec <path> | --specs <dir>] [--upload-run]\n [--param key=value]... [--kind <k>] [--title <t>] [--recorded-at <iso|ms>]\n [--attach [role=]<file>]... [--label key=value]... [--link [name=]<url>]...\n [--attach-root <dir>]... [--max-attachment-mb <n>] [--max-total-attachment-mb <n>]\n [--require-recordings]\n atlas report stream [--store <dir|s3://\u2026|https://atlas.example.com>] --producer <name> [--environment <name>]\n [--branch <b>] [--sha <s>] [--run-url <u>] [--kind <k>] [--param key=value]...\n [--run-id <id>] [--run-title <t>]\n [--flush-ms 1000] [--batch 100] (checks as JSON lines on standard input)\n atlas report status [--store <dir|s3://\u2026|id=url>]... [--backends <module>]\n [--specs <dir>] [--format text|json|markdown]\n [--require-environments a,b] [--require-all-specs]\n [--branch <name>]... [--ignore-modified]\n atlas report verify-recordings [--store <dir|s3://\u2026|id=url>]... [--backends <module>] [--layer <id>]...\n [--limit <n>] [--branch <name>]... [--ignore-modified] [--format text|json]\n atlas report merge [--store \u2026]... [--backends <module>] --into <dir|s3://\u2026>\n atlas report doctor [--store \u2026]... [--role publisher|reader|both]\n atlas report policy --store s3://bucket/prefix [--role publisher|reader|both]\n\n A store is a directory or s3://bucket/prefix (add ?region=\u2026&endpoint=\u2026 for R2, MinIO and the\n rest). Select a project on an Atlas node with --project <slug> or ATLAS_PROJECT. This scopes specs, reports and recordings together.\n When no --store is given, ATLAS_STORE is used. Keys never go in the URL: set\n AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or sign in with `aws sso login`, or use a role.\n\n publish Write one immutable layer: the checks of one producer, on one environment. Safe to repeat.\n --producer is required, unless the --checks file names one\n --spec <path> every check in the report belongs to this spec (a report per feature file)\n --specs <dir> link checks to the spec files in <dir> by path, or by the suite, class or\n file name the report gives; also publishes the specs themselves, so the\n node can show them without a checkout\n --run-id <id> name this execution (a local or manual run with no CI run URL; also ATLAS_RUN_ID).\n Layers that share an id are one run, however many shards or batches uploaded them;\n another invocation at the same commit gets another id. Never part of what a check is\n --run-title <t> a human name for the run (also ATLAS_RUN_TITLE); --run-started-at and --run-ended-at\n record when it ran. Without --run-id a run is its CI run URL, else branch@sha\n --upload-run with --manifest: also upload the run's screenshots and clips for the viewer\n --attach <file> attach a file to the whole layer: a CI log, a coverage report, a trace archive\n (screenshots, videos and traces of a single test go in its CTRF or checks file)\n --label k=v a fact about the run, not part of what it checks: build, os, browser\n --link [n=]<url> a link to the run, a dashboard, an issue\n Files a report names (CTRF attachments, JUnit [[ATTACHMENT|path]] lines) come from the report's own\n folder and each --attach-root, and never from a hidden file or a link out: a report is data from a\n test run, not instructions. One that is missing or not allowed is skipped with a warning.\n --require-recordings exit 1 when a check that declared recordingExpectation \"required\" has no\n uploaded video (absent, left out as too large or not allowed, or refused). The layer is\n still written and the checks keep their results; only this command fails. The receipt of\n what was uploaded (counts, sizes, checksums) is in the JSON result under layer.recordings.\n From a checkout with uncommitted changes the layer is marked \"modified\".\n stream Send checks as they finish: one JSON object per line on standard input (the fields of one\n check: id, title, status, and any evidence). Each second's worth becomes one layer, so a server\n shows the run while it is still going. A check sent again with the same id (running, then\n passed) is replaced by the later. Ends when the input does; exit 1 if anything could not be sent.\n Point it at an Atlas server with --store https://\u2026 and a token in ATLAS_TOKEN.\n status Combine every layer of every store, and exit 1 unless the report passes.\n --specs <dir> lay the results over these spec files (default: the specs in the store)\n --require-all-specs fail while any spec has no results\n --branch <name> only layers recorded on this branch (repeatable)\n --ignore-modified leave out layers from modified checkouts and unknown origins\n verify-recordings Read back the videos the layers published and check they are still there as published: the\n layer still lists the file, its blob is still stored, its size and SHA-256 are the recorded ones, and it\n begins like the container its media type names. Exit 1 when any video is missing, unreadable, corrupt or\n not that container, naming the layer and check; test results are never changed. This checks stored bytes,\n not playback in a browser. Videos given only by address are counted, not checked.\n --layer <id> only the layer whose id starts with this (repeatable; at least 7 digits)\n --limit <n> only the newest n layers\n merge Copy the layers of several stores into one.\n doctor Check that a store works: credentials, listing, writing, and what it holds.\n policy Print the least-privilege IAM policy for publishers, readers, or both.\n\n --backends <module> A TypeScript or JavaScript module whose default export is a list of\n { id, label?, store } (or a function returning it): a device lab, anything\n that implements ObjectStore.\n\n --format json (alias --json) publish, stream, merge, doctor, status and verify-recordings print one JSON document instead of prose\n (schemas: publish-result, stream-result, merge-result, doctor-result, report-status, verify-recordings-result). A command\n that is wrong is {\"ok\":false,\"error\":{\"message\":\u2026,\"code\":\"usage\"}} on standard error.\n A store may be an Atlas server (https://\u2026): ATLAS_STORE=https://atlas.example.com, with ATLAS_TOKEN.\n\n Exit codes: 0 ok, 1 the report does not pass (or a check failed), 2 the command was wrong.\n";
17
18
  type Env = Readonly<Record<string, string | undefined>>;
@@ -41,11 +42,23 @@ export declare function detectFreshness(origin: {
41
42
  sha: string | null;
42
43
  ci: boolean;
43
44
  }, git?: Git): 'modified' | 'unknown' | undefined;
45
+ /** Splits `key=value` (the form of `--param` and `--label`), or throws a message naming `flag`. */
46
+ export declare function keyValue(pair: string, flag: string): [string, string];
47
+ /** An ISO date or epoch milliseconds from a flag, `null` when the flag was not given. Throws a message naming `flag` for anything else. */
48
+ export declare function parseTime(text: string | undefined, flag?: string): number | null;
49
+ /**
50
+ * The execution the producer named, from `--run-id`, `--run-title`, `--run-started-at`, `--run-ended-at` and then
51
+ * `ATLAS_RUN_ID` / `ATLAS_RUN_TITLE`, laid over a `run` the checks file gave. `undefined` when nothing names one, so the
52
+ * documented grouping (CI run URL, else branch@sha) applies. A title or time without an id is refused: it would have nothing to belong to.
53
+ */
54
+ export declare function executionOf(rest: readonly string[], env: Env, given?: unknown): LayerRun | undefined;
44
55
  /**
45
56
  * `atlas report …` for a terminal: a mistake in the command or a path is a message on stderr and exit
46
57
  * code 2, so a CI job can tell a broken invocation (2) from a report that does not pass (1).
47
58
  */
48
59
  export declare function runReportCommand(argv: readonly string[]): Promise<number>;
60
+ /** Does the command ask for a JSON document (`--format json` or `--json`) instead of prose? */
61
+ export declare function wantsJson(argv: readonly string[]): boolean;
49
62
  /** `atlas report …`. Returns the exit code: 0 passes, 1 does not. Throws on a mistake in the command (see {@link runReportCommand}). */
50
63
  export declare function reportMain(argv: readonly string[], io?: ReportIo): Promise<number>;
51
64
  export {};
@@ -149,7 +149,8 @@ export function detectFreshness(origin, git = gitOutput) {
149
149
  // `git diff` is empty (so null) when clean; untracked files (test output, build products) do not count.
150
150
  return git(['status', '--porcelain', '--untracked-files=no']) ? 'modified' : undefined;
151
151
  }
152
- function keyValue(pair, flag) {
152
+ /** Splits `key=value` (the form of `--param` and `--label`), or throws a message naming `flag`. */
153
+ export function keyValue(pair, flag) {
153
154
  const at = pair.indexOf('=');
154
155
  if (at < 1)
155
156
  throw new Error(`${flag} takes key=value (got "${pair}")`);
@@ -173,7 +174,8 @@ function linkSpec(spec) {
173
174
  const named = /^([^=/:]+)=(.+)$/.exec(spec);
174
175
  return named ? { name: named[1], url: named[2] } : { url: spec };
175
176
  }
176
- function parseTime(text, flag = '--recorded-at') {
177
+ /** An ISO date or epoch milliseconds from a flag, `null` when the flag was not given. Throws a message naming `flag` for anything else. */
178
+ export function parseTime(text, flag = '--recorded-at') {
177
179
  if (text === undefined)
178
180
  return null;
179
181
  const numeric = Number(text);
@@ -187,7 +189,7 @@ function parseTime(text, flag = '--recorded-at') {
187
189
  * `ATLAS_RUN_ID` / `ATLAS_RUN_TITLE`, laid over a `run` the checks file gave. `undefined` when nothing names one, so the
188
190
  * documented grouping (CI run URL, else branch@sha) applies. A title or time without an id is refused: it would have nothing to belong to.
189
191
  */
190
- function executionOf(rest, env, given) {
192
+ export function executionOf(rest, env, given) {
191
193
  const file = typeof given === 'object' && given !== null && !Array.isArray(given) ? given : {};
192
194
  const id = value(rest, 'run-id') ?? env.ATLAS_RUN_ID ?? (typeof file.id === 'string' ? file.id : undefined);
193
195
  const title = value(rest, 'run-title') ?? env.ATLAS_RUN_TITLE;
@@ -235,7 +237,7 @@ export async function runReportCommand(argv) {
235
237
  }
236
238
  }
237
239
  /** Does the command ask for a JSON document (`--format json` or `--json`) instead of prose? */
238
- function wantsJson(argv) {
240
+ export function wantsJson(argv) {
239
241
  return value(argv, 'format') === 'json' || argv.includes('--json');
240
242
  }
241
243
  function printChecks(out, label, checks) {
@@ -0,0 +1,10 @@
1
+ import { type ReportIo } from './report-cli.js';
2
+ /** The text `atlas ui-catalog --help` prints: every subcommand, its flags, what each does and the exit codes. Also the source of its entry in `docs/reference/cli.md`. */
3
+ export declare const UI_CATALOG_USAGE = "atlas ui-catalog \u2014 a browsable inventory of captured UI states, from any collector\n\n atlas ui-catalog plan --states <file.json> --producer <name> [--store <dir|s3://bucket/prefix|https://\u2026>]\n [--environment <name>] [--param key=value]... [--shard <label>]\n [--branch <b>] [--sha <s>] [--run-url <u>] [--run-id <id>] [--run-title <t>] [--recorded-at <iso|ms>]\n atlas ui-catalog publish --captures <file.json> --producer <name> [the same flags as plan]\n [--attach-root <dir>]... [--max-picture-mb <n>]\n atlas ui-catalog status [--store <dir|s3://\u2026|id=url>]... [--backends <module>] [filters] [--branch <name>]...\n [--ignore-modified] [--format text|json|markdown]\n atlas ui-catalog diff --from <commit> --to <commit> [--store \u2026]... [--backends <module>] [filters]\n [--fail-on-change] [--format text|json|markdown]\n atlas ui-catalog export --out <file.html> [--store \u2026]... [--backends <module>] [filters]\n [--images inline|none] [--max-mb <n>] [--manifest <file.json>] [--title <t>]\n\n filters (status, diff, export): --q <words> --coverage a,b --profile <key|label>... --area a --route r --cohort c\n --flag f --surface s --producer p\n\n plan Write one layer of kind ui-plan: the states (and with \"inventory\": true, the surfaces) that should exist under\n this profile. Every entry is not-run, so a plan never counts as a pass. The file is {\"states\":[{\"id\":\u2026,\n \"title\"?,\"route\"?,\"fixture\"?,\"cohort\"?,\"area\"?,\"flags\"?,\"covers\"?,\"inventory\"?}]} or just that list.\n publish Write one layer of kind ui-state: the states one collector or shard captured under this profile. The file is\n {\"states\":[{\"id\":\u2026,\"title\"?,\"status\"?:\"passed|failed|running|not-run\",\"detail\"?,\"route\"?,\"fixture\"?,\"cohort\"?,\n \"area\"?,\"flags\"?,\"covers\"?,\"labels\"?,\"tags\"?,\"durationMs\"?,\"screenshot\"?:\"file.png\"|{\"path\"|\"uri\",\n \"width\"?,\"height\"?,\"contentType\"?}}]}. A picture file is read from beside the file and each --attach-root, never\n from a hidden file or a link out. A passed state whose picture cannot be read is published as failed, with the\n reason: a state without its picture is not shown as captured.\n --environment and each --param key=value are the profile (a viewport, a color scheme, a device): another\n profile is another result side by side. --shard only describes the layer; it never splits a result.\n --recorded-at defaults to the modification time of the file, so publishing the same file twice is one layer.\n status Read every store and print the catalog's summary. Exit 1 unless the catalog holds a state and there is nothing\n to attend to: a failed capture, a state a plan expects with nothing captured, a capture still in progress, a\n backend or layer that could not be read. Without a plan nothing is expected, so nothing is missing.\n --profile takes a profile's key or its label, as printed by status.\n diff Set the catalog recorded at one commit (--from) against another (--to): each state in each profile is paired\n with itself and is unchanged, changed in coverage, changed in picture (the stored bytes differ), unverified (a\n side has no content hash), only in --to (added) or only in --from (removed). A commit is the whole id or at\n least 7 digits of it. Exit 0 unless a store could not be read, or, with --fail-on-change, anything differs.\n export Write the catalog as one HTML file with its pictures embedded and its manifest in it: no server, no network.\n It is the catalog at the moment it was made. --images none leaves the pictures out. --max-mb (default 32) limits\n the embedded pictures; those past it are listed in the page as left out, and the manifest stays complete.\n --manifest writes the catalog's JSON manifest too.\n\n --format json (alias --json) every subcommand prints one JSON document instead of prose\n (schemas: ui-catalog-publish-result, ui-catalog-status, ui-catalog-diff-result, ui-catalog-export-result).\n A command that is wrong is {\"ok\":false,\"error\":{\"message\":\u2026,\"code\":\"usage\"}} on standard error.\n A store is a directory, s3://bucket/prefix or an Atlas server (https://\u2026), exactly as for atlas report; select a project\n with --project <slug> or ATLAS_PROJECT. When no --store is given, ATLAS_STORE is used.\n\n Exit codes: 0 ok, 1 the answer is no (see each command), 2 the command was wrong.\n";
4
+ /** `atlas ui-catalog …`. Returns the exit code. Throws on a mistake in the command (see {@link runUiCatalogCommand}). */
5
+ export declare function uiCatalogMain(argv: readonly string[], io?: ReportIo): Promise<number>;
6
+ /**
7
+ * `atlas ui-catalog …` for a terminal: a mistake in the command or a path is a message on stderr and exit code 2, so a CI job can
8
+ * tell a broken invocation (2) from a catalog that needs attention (1). With `--format json` the mistake is a JSON document.
9
+ */
10
+ export declare function runUiCatalogCommand(argv: readonly string[]): Promise<number>;