dungbeetle 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/LICENSE +105 -0
  2. package/NOTICE +19 -0
  3. package/README.md +139 -0
  4. package/dist/api/capture.d.ts +24 -0
  5. package/dist/api/capture.js +61 -0
  6. package/dist/baselines.d.ts +7 -0
  7. package/dist/baselines.js +38 -0
  8. package/dist/brand.d.ts +2 -0
  9. package/dist/brand.js +9 -0
  10. package/dist/capture.d.ts +15 -0
  11. package/dist/capture.js +7 -0
  12. package/dist/captures/api.d.ts +2 -0
  13. package/dist/captures/api.js +114 -0
  14. package/dist/captures/check.d.ts +2 -0
  15. package/dist/captures/check.js +116 -0
  16. package/dist/captures/desktop.d.ts +2 -0
  17. package/dist/captures/desktop.js +97 -0
  18. package/dist/captures/game.d.ts +4 -0
  19. package/dist/captures/game.js +266 -0
  20. package/dist/captures/performance.d.ts +2 -0
  21. package/dist/captures/performance.js +47 -0
  22. package/dist/captures/registry.d.ts +4 -0
  23. package/dist/captures/registry.js +23 -0
  24. package/dist/captures/terminal.d.ts +2 -0
  25. package/dist/captures/terminal.js +65 -0
  26. package/dist/captures/types.d.ts +18 -0
  27. package/dist/captures/types.js +1 -0
  28. package/dist/captures/web.d.ts +3 -0
  29. package/dist/captures/web.js +248 -0
  30. package/dist/check/capture.d.ts +15 -0
  31. package/dist/check/capture.js +76 -0
  32. package/dist/check/junit.d.ts +9 -0
  33. package/dist/check/junit.js +51 -0
  34. package/dist/check/laravel.d.ts +2 -0
  35. package/dist/check/laravel.js +44 -0
  36. package/dist/check/parsers.d.ts +12 -0
  37. package/dist/check/parsers.js +278 -0
  38. package/dist/check/schema.d.ts +2 -0
  39. package/dist/check/schema.js +114 -0
  40. package/dist/cloud.d.ts +42 -0
  41. package/dist/cloud.js +334 -0
  42. package/dist/compare/shared.d.ts +42 -0
  43. package/dist/compare/shared.js +115 -0
  44. package/dist/compare.d.ts +3 -0
  45. package/dist/compare.js +33 -0
  46. package/dist/config.d.ts +146 -0
  47. package/dist/config.js +382 -0
  48. package/dist/desktop/a11y.d.ts +18 -0
  49. package/dist/desktop/a11y.js +74 -0
  50. package/dist/desktop/capture.d.ts +13 -0
  51. package/dist/desktop/capture.js +80 -0
  52. package/dist/desktop/macos.d.ts +8 -0
  53. package/dist/desktop/macos.js +98 -0
  54. package/dist/desktop/ocr.d.ts +17 -0
  55. package/dist/desktop/ocr.js +99 -0
  56. package/dist/diff/lcs.d.ts +5 -0
  57. package/dist/diff/lcs.js +42 -0
  58. package/dist/diff/numeric.d.ts +6 -0
  59. package/dist/diff/numeric.js +24 -0
  60. package/dist/diff/pixel.d.ts +23 -0
  61. package/dist/diff/pixel.js +97 -0
  62. package/dist/diff/structural.d.ts +11 -0
  63. package/dist/diff/structural.js +38 -0
  64. package/dist/diff/text.d.ts +7 -0
  65. package/dist/diff/text.js +64 -0
  66. package/dist/diff/tree.d.ts +46 -0
  67. package/dist/diff/tree.js +188 -0
  68. package/dist/doctor.d.ts +18 -0
  69. package/dist/doctor.js +57 -0
  70. package/dist/game/capture.d.ts +24 -0
  71. package/dist/game/capture.js +51 -0
  72. package/dist/game/protocol.d.ts +30 -0
  73. package/dist/game/protocol.js +146 -0
  74. package/dist/game/walkthrough.d.ts +45 -0
  75. package/dist/game/walkthrough.js +85 -0
  76. package/dist/guards.d.ts +2 -0
  77. package/dist/guards.js +15 -0
  78. package/dist/index.d.ts +2 -0
  79. package/dist/index.js +504 -0
  80. package/dist/json.d.ts +2 -0
  81. package/dist/json.js +40 -0
  82. package/dist/lifecycle.d.ts +14 -0
  83. package/dist/lifecycle.js +190 -0
  84. package/dist/normalization.d.ts +4 -0
  85. package/dist/normalization.js +27 -0
  86. package/dist/perf/ab.d.ts +6 -0
  87. package/dist/perf/ab.js +89 -0
  88. package/dist/perf/autocannon.d.ts +6 -0
  89. package/dist/perf/autocannon.js +101 -0
  90. package/dist/perf/capture.d.ts +7 -0
  91. package/dist/perf/capture.js +6 -0
  92. package/dist/perf/k6.d.ts +9 -0
  93. package/dist/perf/k6.js +44 -0
  94. package/dist/perf/parsers.d.ts +15 -0
  95. package/dist/perf/parsers.js +69 -0
  96. package/dist/perf/run.d.ts +8 -0
  97. package/dist/perf/run.js +45 -0
  98. package/dist/perf/toolOutput.d.ts +3 -0
  99. package/dist/perf/toolOutput.js +24 -0
  100. package/dist/reporters.d.ts +11 -0
  101. package/dist/reporters.js +314 -0
  102. package/dist/runner.d.ts +48 -0
  103. package/dist/runner.js +352 -0
  104. package/dist/snapshot.d.ts +48 -0
  105. package/dist/snapshot.js +37 -0
  106. package/dist/terminal/ansi.d.ts +21 -0
  107. package/dist/terminal/ansi.js +144 -0
  108. package/dist/terminal/capture.d.ts +30 -0
  109. package/dist/terminal/capture.js +91 -0
  110. package/dist/tty.d.ts +72 -0
  111. package/dist/tty.js +175 -0
  112. package/dist/web/domSnapshot.d.ts +27 -0
  113. package/dist/web/domSnapshot.js +55 -0
  114. package/dist/web/playwrightCapture.d.ts +16 -0
  115. package/dist/web/playwrightCapture.js +64 -0
  116. package/package.json +79 -0
package/LICENSE ADDED
@@ -0,0 +1,105 @@
1
+ # Functional Source License, Version 1.1, ALv2 Future License
2
+
3
+ ## Abbreviation
4
+
5
+ FSL-1.1-ALv2
6
+
7
+ ## Notice
8
+
9
+ Copyright 2026 DungbeetleDev
10
+
11
+ ## Terms and Conditions
12
+
13
+ ### Licensor ("We")
14
+
15
+ The party offering the Software under these Terms and Conditions.
16
+
17
+ ### The Software
18
+
19
+ The "Software" is each version of the software that we make available under
20
+ these Terms and Conditions, as indicated by our inclusion of these Terms and
21
+ Conditions with the Software.
22
+
23
+ ### License Grant
24
+
25
+ Subject to your compliance with this License Grant and the Patents,
26
+ Redistribution and Trademark clauses below, we hereby grant you the right to
27
+ use, copy, modify, create derivative works, publicly perform, publicly display
28
+ and redistribute the Software for any Permitted Purpose identified below.
29
+
30
+ ### Permitted Purpose
31
+
32
+ A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
33
+ means making the Software available to others in a commercial product or
34
+ service that:
35
+
36
+ 1. substitutes for the Software;
37
+
38
+ 2. substitutes for any other product or service we offer using the Software
39
+ that exists as of the date we make the Software available; or
40
+
41
+ 3. offers the same or substantially similar functionality as the Software.
42
+
43
+ Permitted Purposes specifically include using the Software:
44
+
45
+ 1. for your internal use and access;
46
+
47
+ 2. for non-commercial education;
48
+
49
+ 3. for non-commercial research; and
50
+
51
+ 4. in connection with professional services that you provide to a licensee
52
+ using the Software in accordance with these Terms and Conditions.
53
+
54
+ ### Patents
55
+
56
+ To the extent your use for a Permitted Purpose would necessarily infringe our
57
+ patents, the license grant above includes a license under our patents. If you
58
+ make a claim against any party that the Software infringes or contributes to
59
+ the infringement of any patent, then your patent license to the Software ends
60
+ immediately.
61
+
62
+ ### Redistribution
63
+
64
+ The Terms and Conditions apply to all copies, modifications and derivatives of
65
+ the Software.
66
+
67
+ If you redistribute any copies, modifications or derivatives of the Software,
68
+ you must include a copy of or a link to these Terms and Conditions and not
69
+ remove any copyright notices provided in or with the Software.
70
+
71
+ ### Disclaimer
72
+
73
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
74
+ IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
75
+ PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
76
+
77
+ IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
78
+ SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
79
+ EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
80
+
81
+ ### Trademarks
82
+
83
+ Except for displaying the License Details and identifying us as the origin of
84
+ the Software, you have no right under these Terms and Conditions to use our
85
+ trademarks, trade names, service marks or product names.
86
+
87
+ ## Grant of Future License
88
+
89
+ We hereby irrevocably grant you an additional license to use the Software under
90
+ the Apache License, Version 2.0 that is effective on the second anniversary of
91
+ the date we make the Software available. On or after that date, you may use the
92
+ Software under the Apache License, Version 2.0, in which case the following
93
+ will apply:
94
+
95
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
96
+ this file except in compliance with the License.
97
+
98
+ You may obtain a copy of the License at
99
+
100
+ http://www.apache.org/licenses/LICENSE-2.0
101
+
102
+ Unless required by applicable law or agreed to in writing, software distributed
103
+ under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
104
+ CONDITIONS OF ANY KIND, either express or implied. See the License for the
105
+ specific language governing permissions and limitations under the License.
package/NOTICE ADDED
@@ -0,0 +1,19 @@
1
+ Dungbeetle
2
+ Copyright 2026 DungbeetleDev
3
+
4
+ This product includes software developed as part of the Dungbeetle project
5
+ (https://github.com/DungbeetleTech/client).
6
+
7
+ The command-line tool and core engine in this repository (the `dungbeetle`
8
+ package at the repository root) are licensed under the Functional Source License,
9
+ Version 1.1 (FSL-1.1-ALv2): source-available and free for any use except offering
10
+ a competing product or service, with an automatic conversion to the Apache License,
11
+ Version 2.0 two years after each release. See the LICENSE file for the full text.
12
+
13
+ The cloud server in the `server/` directory is licensed separately under the
14
+ Business Source License 1.1 (BUSL-1.1). See `server/LICENSE` and LICENSING.md for
15
+ details and the change-license terms.
16
+
17
+ "Dungbeetle" and the Dungbeetle logo are trademarks of DungbeetleDev. The licenses
18
+ above grant rights to the software but not to the marks; see TRADEMARK.md for the
19
+ brand-use policy.
package/README.md ADDED
@@ -0,0 +1,139 @@
1
+ <p align="center">
2
+ <img src="brand/logo.svg" alt="Dungbeetle" width="220">
3
+ </p>
4
+
5
+ # Dungbeetle
6
+
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/dungbeetle"><img src="https://img.shields.io/npm/v/dungbeetle" alt="npm version"></a>
9
+ <a href="https://github.com/DungbeetleTech/client/actions/workflows/ci.yml"><img src="https://github.com/DungbeetleTech/client/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
10
+ <img src="https://img.shields.io/node/v/dungbeetle" alt="Node.js version">
11
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-FSL--1.1--ALv2-blue" alt="License: FSL-1.1-ALv2"></a>
12
+ </p>
13
+
14
+ > Web, desktop, terminal, and eventually anything — zero adoption cost and runs anywhere.
15
+
16
+ Dungbeetle captures your app's output as stable, reviewable JSON and produces
17
+ **semantic diffs** instead of brittle pixel comparisons. Output is normalized (timestamps,
18
+ UUIDs, and temp paths are masked) so baselines stay readable and survive cosmetic
19
+ churn.
20
+
21
+ - 🖥️ **Terminal** — capture with ANSI normalization
22
+ - 🌐 **Web** — structured DOM snapshots (`url`/`html` fetch by default, or a
23
+ `playwright` driver when a browser is available)
24
+ - ⚡ **Performance** — baselines from k6 metrics, compared with numeric tolerance
25
+ - 🪟 **Desktop** — accessibility-tree snapshots (role/name/state), with an
26
+ experimental native macOS driver (`driver: "macos-ax"`)
27
+ - 🔌 **API** — REST/GraphQL response snapshots (status, allow-listed headers,
28
+ parsed JSON body) with structural diffs
29
+ - 🎮 **Game** — scripted walkthroughs snapshotting semantic game state at named
30
+ markers, deterministic and headless (Godot 4.x first; see `adapters/godot`)
31
+ - 🎭 **Shared masking** for dynamic values, and stable JSON diffs you can review
32
+ in a pull request
33
+
34
+ Baselines are committed under `dungbeetle.snapshots/` so changes show up in code
35
+ review. An optional **Dungbeetle cloud** service (a separate product) can store runs
36
+ and baselines centrally when you'd rather not commit them.
37
+
38
+ ## 📚 Documentation
39
+
40
+ Full documentation — guides, capture types, configuration, and the CLI
41
+ reference — lives at the Dungbeetle docs site:
42
+
43
+ **→ <https://dungbeetle.dev>**
44
+
45
+ <!-- The docs domain is a placeholder until the site goes live. When the
46
+ production URL is set, update this one link (and the matching pointers in
47
+ SUPPORT.md and CONTRIBUTING.md). -->
48
+
49
+ ## Requirements
50
+
51
+ - **Node.js 22.5.0 or newer.**
52
+ - No native build step and no external services for the CLI.
53
+
54
+ ## Installation
55
+
56
+ ```sh
57
+ npm install -g dungbeetle # global `dungbeetle` binary
58
+ # or, per-project:
59
+ npm install --save-dev dungbeetle && npx dungbeetle --help
60
+ ```
61
+
62
+ ## Quick start
63
+
64
+ Scaffold a config, then add at least one **capture target** — `init` writes an
65
+ empty `lifecycle.capture` array (unless it detects a Laravel app), so you tell
66
+ Dungbeetle what to snapshot:
67
+
68
+ ```sh
69
+ dungbeetle init # scaffold dungbeetle.config.json
70
+ ```
71
+
72
+ Add a target to `lifecycle.capture` in `dungbeetle.config.json` — here a minimal
73
+ terminal snapshot (see the [docs](https://dungbeetle.dev) for web, desktop, game,
74
+ API, and check targets):
75
+
76
+ ```json
77
+ {
78
+ "lifecycle": {
79
+ "capture": [{ "kind": "terminal", "name": "hello", "command": "echo hello world" }]
80
+ }
81
+ }
82
+ ```
83
+
84
+ Then capture and compare:
85
+
86
+ ```sh
87
+ dungbeetle update # run your capture targets and write the first baselines
88
+ dungbeetle test # compare current output against the baselines
89
+ ```
90
+
91
+ `update` writes baselines under `dungbeetle.snapshots/` (commit these). `test`
92
+ compares current output to them and exits non-zero on any difference, so it drops
93
+ straight into CI. Run `dungbeetle doctor` any time to check your config and
94
+ targets. For machine-readable CI output:
95
+
96
+ ```sh
97
+ dungbeetle ci --json report.json --html report.html
98
+ ```
99
+
100
+ Run `dungbeetle doctor` to validate your config, paths, targets, and optional browser
101
+ setup. See the [documentation](https://dungbeetle.dev) for the next steps.
102
+
103
+ ## Dungbeetle cloud
104
+
105
+ An optional **Dungbeetle cloud** service stores runs and baselines centrally with a
106
+ review/approve UI — useful when you'd rather not commit baselines to your repo. It
107
+ is a **separate product** (not part of this repository); see
108
+ <https://dungbeetle.dev> for details. Production self-hosting is available to
109
+ enterprise customers on request.
110
+
111
+ ## Contributing
112
+
113
+ ```sh
114
+ npm install
115
+ npm run check # lint + typecheck + test (run before a PR)
116
+ git commit -s # sign off your commits (DCO — required)
117
+ ```
118
+
119
+ We welcome contributions! Please read [CONTRIBUTING.md](CONTRIBUTING.md) (it
120
+ covers the **DCO sign-off** every commit needs) and our
121
+ [Code of Conduct](CODE_OF_CONDUCT.md). Questions go to
122
+ [Discussions](https://github.com/DungbeetleTech/client/discussions); bugs and
123
+ features to [Issues](https://github.com/DungbeetleTech/client/issues/new/choose);
124
+ security reports to [SECURITY.md](SECURITY.md). See also [SUPPORT.md](SUPPORT.md)
125
+ and [GOVERNANCE.md](GOVERNANCE.md).
126
+
127
+ ## License
128
+
129
+ Dungbeetle's **CLI & core engine** (`dungbeetle`, this repository) is
130
+ **source-available** under the **Functional Source License 1.1**
131
+ ([`LICENSE`](LICENSE)) — free for any use, including internal and commercial,
132
+ except offering it to others as a competing product or service; each release
133
+ converts to **Apache-2.0** two years later.
134
+
135
+ The **Dungbeetle cloud** server is a separate product under the **Business Source
136
+ License 1.1** and lives in its own repository.
137
+
138
+ See [LICENSING.md](LICENSING.md) for the rationale and [TRADEMARK.md](TRADEMARK.md)
139
+ for the brand-use policy. "Dungbeetle" and the Dungbeetle logo are trademarks of DungbeetleDev.
@@ -0,0 +1,24 @@
1
+ import type { MaskRule } from "../config.js";
2
+ export type ApiSnapshot = {
3
+ kind: "api";
4
+ status: number;
5
+ headers: Record<string, string>;
6
+ bodyType: "json" | "text";
7
+ body: unknown;
8
+ };
9
+ export type ApiCaptureOptions = {
10
+ url: string;
11
+ method: string;
12
+ headers: Record<string, string>;
13
+ body?: string;
14
+ includeHeaders: string[];
15
+ timeoutMs: number;
16
+ maskRules: MaskRule[];
17
+ };
18
+ export declare function maskJsonValue(value: unknown, rules: MaskRule[]): unknown;
19
+ export declare function normalizeApiResponse(input: {
20
+ status: number;
21
+ headers: Headers;
22
+ text: string;
23
+ }, options: Pick<ApiCaptureOptions, "includeHeaders" | "maskRules">): ApiSnapshot;
24
+ export declare function captureApi(options: ApiCaptureOptions): Promise<ApiSnapshot>;
@@ -0,0 +1,61 @@
1
+ import { isRecord } from "../guards.js";
2
+ import { applyMaskRules } from "../normalization.js";
3
+ // Apply mask rules to every string leaf of a parsed JSON value.
4
+ export function maskJsonValue(value, rules) {
5
+ if (typeof value === "string") {
6
+ return applyMaskRules(value, rules);
7
+ }
8
+ if (Array.isArray(value)) {
9
+ return value.map((entry) => maskJsonValue(entry, rules));
10
+ }
11
+ if (isRecord(value)) {
12
+ return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, maskJsonValue(nested, rules)]));
13
+ }
14
+ return value;
15
+ }
16
+ // Turn a fetched response into the snapshot shape. Split from `captureApi` so
17
+ // tests can exercise the normalization without a live server.
18
+ export function normalizeApiResponse(input, options) {
19
+ const headers = {};
20
+ for (const name of options.includeHeaders) {
21
+ const value = input.headers.get(name);
22
+ if (value !== null) {
23
+ headers[name.toLowerCase()] = applyMaskRules(value, options.maskRules);
24
+ }
25
+ }
26
+ const contentType = input.headers.get("content-type") ?? "";
27
+ if (contentType.includes("json")) {
28
+ try {
29
+ return {
30
+ kind: "api",
31
+ status: input.status,
32
+ headers,
33
+ bodyType: "json",
34
+ body: maskJsonValue(JSON.parse(input.text), options.maskRules)
35
+ };
36
+ }
37
+ catch {
38
+ // Declared JSON but unparseable — fall through and snapshot the raw text,
39
+ // which is itself the signal a reviewer needs to see.
40
+ }
41
+ }
42
+ return {
43
+ kind: "api",
44
+ status: input.status,
45
+ headers,
46
+ bodyType: "text",
47
+ body: applyMaskRules(input.text, options.maskRules)
48
+ };
49
+ }
50
+ // Note: a non-2xx status is NOT an error here — the status is part of the
51
+ // snapshot, so an endpoint whose contract is `404` can be baselined and a
52
+ // drift to `200` shows up as a diff.
53
+ export async function captureApi(options) {
54
+ const response = await fetch(options.url, {
55
+ method: options.method,
56
+ headers: options.headers,
57
+ body: options.body,
58
+ signal: AbortSignal.timeout(options.timeoutMs)
59
+ });
60
+ return normalizeApiResponse({ status: response.status, headers: response.headers, text: await response.text() }, options);
61
+ }
@@ -0,0 +1,7 @@
1
+ export declare class MissingBaselineError extends Error {
2
+ readonly baselinePath: string;
3
+ constructor(baselinePath: string);
4
+ }
5
+ export declare function baselinePathForTarget(baselinesDir: string, targetName: string, cwd?: string): string;
6
+ export declare function readBaseline(filePath: string): Promise<unknown>;
7
+ export declare function writeBaseline(filePath: string, value: unknown): Promise<void>;
@@ -0,0 +1,38 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { parseJsonFile, stableStringify } from "./json.js";
4
+ import { isErrno } from "./guards.js";
5
+ export class MissingBaselineError extends Error {
6
+ baselinePath;
7
+ constructor(baselinePath) {
8
+ super(`Missing baseline at ${baselinePath}`);
9
+ this.baselinePath = baselinePath;
10
+ }
11
+ }
12
+ export function baselinePathForTarget(baselinesDir, targetName, cwd = process.cwd()) {
13
+ return path.join(path.resolve(cwd, baselinesDir), `${slugifyTargetName(targetName)}.json`);
14
+ }
15
+ export async function readBaseline(filePath) {
16
+ let raw;
17
+ try {
18
+ raw = await readFile(filePath, "utf8");
19
+ }
20
+ catch (error) {
21
+ if (isErrno(error, "ENOENT")) {
22
+ throw new MissingBaselineError(filePath);
23
+ }
24
+ throw error;
25
+ }
26
+ return parseJsonFile(raw, filePath);
27
+ }
28
+ export async function writeBaseline(filePath, value) {
29
+ await mkdir(path.dirname(filePath), { recursive: true });
30
+ await writeFile(filePath, `${stableStringify(value)}\n`, "utf8");
31
+ }
32
+ function slugifyTargetName(value) {
33
+ const slug = value
34
+ .toLowerCase()
35
+ .replace(/[^a-z0-9._-]+/g, "-")
36
+ .replace(/^-+|-+$/g, "");
37
+ return slug || "snapshot";
38
+ }
@@ -0,0 +1,2 @@
1
+ export declare const BRAND_NAME: string;
2
+ export declare const BRAND_SUBTITLE = "Web, desktop, terminal, and eventually anything \u2014 zero adoption cost and runs anywhere.";
package/dist/brand.js ADDED
@@ -0,0 +1,9 @@
1
+ // SPDX-License-Identifier: FSL-1.1-ALv2
2
+ // Display name for the CLI, overridable via env so the tool can be white-labelled
3
+ // when self-hosted without editing source. Structural identifiers (the `dungbeetle`
4
+ // binary name, `dungbeetle.config.json`) are intentionally NOT driven by this —
5
+ // they stay fixed regardless of the display brand.
6
+ export const BRAND_NAME = process.env.BRAND_NAME?.trim() || "Dungbeetle";
7
+ // Shared brand copy — one source, mirrored by the README/docs (which build
8
+ // separately and can't import this).
9
+ export const BRAND_SUBTITLE = "Web, desktop, terminal, and eventually anything — zero adoption cost and runs anywhere.";
@@ -0,0 +1,15 @@
1
+ import type { ApiSnapshot } from "./api/capture.js";
2
+ import type { CheckSnapshot } from "./check/capture.js";
3
+ import type { CaptureTarget, DungbeetleConfig } from "./config.js";
4
+ import type { DesktopSnapshot } from "./desktop/a11y.js";
5
+ import type { GameSnapshot } from "./game/capture.js";
6
+ import type { PerformanceSnapshot } from "./perf/k6.js";
7
+ import type { TerminalCapture } from "./terminal/capture.js";
8
+ import type { DomSnapshot, WebFileSnapshot } from "./web/domSnapshot.js";
9
+ import type { PlaywrightWebSnapshot } from "./web/playwrightCapture.js";
10
+ export type SnapshotArtifact = ApiSnapshot | CheckSnapshot | TerminalCapture | DomSnapshot | WebFileSnapshot | PlaywrightWebSnapshot | PerformanceSnapshot | DesktopSnapshot | GameSnapshot;
11
+ export type CaptureContext = {
12
+ config: DungbeetleConfig;
13
+ cwd: string;
14
+ };
15
+ export declare function captureTarget(target: CaptureTarget, options: CaptureContext): Promise<SnapshotArtifact>;
@@ -0,0 +1,7 @@
1
+ import { captureTypes } from "./captures/registry.js";
2
+ // Thin dispatcher: each capture kind owns its full lifecycle in its own
3
+ // `src/captures/<kind>.ts` module; this routes to the registered handler. The
4
+ // web sub-dispatch (playwright/html/url) lives inside the web module's capture.
5
+ export function captureTarget(target, options) {
6
+ return captureTypes[target.kind].capture(target, options);
7
+ }
@@ -0,0 +1,2 @@
1
+ import type { CaptureType } from "./types.js";
2
+ export declare const api: CaptureType;
@@ -0,0 +1,114 @@
1
+ import { captureApi } from "../api/capture.js";
2
+ import { coerceSnapshot, render } from "../compare/shared.js";
3
+ import { structuralChanges } from "../diff/structural.js";
4
+ import { canonicalizeSnapshot } from "../snapshot.js";
5
+ // Response headers kept in the snapshot unless the target says otherwise.
6
+ // Everything else (dates, request ids, rate-limit counters) is volatile.
7
+ const DEFAULT_INCLUDE_HEADERS = ["content-type"];
8
+ // GraphQL sugar: a `query` (+ optional `variables`) becomes the standard
9
+ // `{query, variables}` JSON POST. A raw `body` is sent verbatim with the
10
+ // user's own headers.
11
+ function requestBody(target) {
12
+ if (target.query) {
13
+ return JSON.stringify({ query: target.query, variables: target.variables ?? {} });
14
+ }
15
+ return target.body;
16
+ }
17
+ function requestHeaders(target) {
18
+ const headers = { ...(target.headers ?? {}) };
19
+ const hasContentType = Object.keys(headers).some((name) => name.toLowerCase() === "content-type");
20
+ if (target.query && !hasContentType) {
21
+ headers["content-type"] = "application/json";
22
+ }
23
+ return headers;
24
+ }
25
+ function requestMethod(target) {
26
+ return target.method ?? (target.query || target.body ? "POST" : "GET");
27
+ }
28
+ function compareApi(baseline, candidate, options) {
29
+ const sections = [];
30
+ if (baseline.status !== candidate.status) {
31
+ sections.push(`~ status: ${String(baseline.status)} → ${String(candidate.status)}`);
32
+ }
33
+ const changes = structuralChanges({ headers: baseline.headers, body: baseline.body }, { headers: candidate.headers, body: candidate.body }, { numericTolerance: options.comparison.numericTolerance });
34
+ for (const change of changes) {
35
+ sections.push(`~ ${change.path}: ${render(change.before)} → ${render(change.after)}`);
36
+ }
37
+ return {
38
+ equal: sections.length === 0,
39
+ rendered: sections.join("\n")
40
+ };
41
+ }
42
+ function parsesAsUrl(value) {
43
+ try {
44
+ new URL(value);
45
+ return true;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ function validateApiTarget(target) {
52
+ if (!target.url || !parsesAsUrl(target.url)) {
53
+ return {
54
+ name: "api-target",
55
+ severity: "fail",
56
+ target: target.name,
57
+ message: `API target "${target.name}" must have a valid "url".`
58
+ };
59
+ }
60
+ if (target.body && target.query) {
61
+ return {
62
+ name: "api-target",
63
+ severity: "fail",
64
+ target: target.name,
65
+ message: `API target "${target.name}" sets both "body" and "query" — use one.`
66
+ };
67
+ }
68
+ return {
69
+ name: "api-target",
70
+ severity: "pass",
71
+ target: target.name,
72
+ message: `API target "${target.name}" points at ${target.url}.`
73
+ };
74
+ }
75
+ export const api = {
76
+ kind: "api",
77
+ parallelSafe: true,
78
+ capture: (target, { config }) => {
79
+ const apiTarget = target;
80
+ return captureApi({
81
+ url: apiTarget.url,
82
+ method: requestMethod(apiTarget),
83
+ headers: requestHeaders(apiTarget),
84
+ body: requestBody(apiTarget),
85
+ includeHeaders: apiTarget.includeHeaders ?? DEFAULT_INCLUDE_HEADERS,
86
+ timeoutMs: apiTarget.timeoutMs ?? config.lifecycle.wait.timeoutMs,
87
+ maskRules: config.normalization.masks
88
+ });
89
+ },
90
+ canonicalize: (value) => ({
91
+ kind: value.kind,
92
+ status: value.status,
93
+ headers: value.headers,
94
+ bodyType: value.bodyType,
95
+ body: canonicalizeSnapshot(value.body)
96
+ }),
97
+ compare: (baseline, candidate, options) => compareApi(coerceSnapshot(baseline), coerceSnapshot(candidate), options),
98
+ validateConfig: (target, { label, issues }) => {
99
+ const apiTarget = target;
100
+ if (!apiTarget.url || typeof apiTarget.url !== "string") {
101
+ issues.push(`${label} (api "${apiTarget.name}") must have a "url".`);
102
+ }
103
+ else if (!parsesAsUrl(apiTarget.url)) {
104
+ issues.push(`${label} (api "${apiTarget.name}") has an invalid "url".`);
105
+ }
106
+ if (apiTarget.body && apiTarget.query) {
107
+ issues.push(`${label} (api "${apiTarget.name}") sets both "body" and "query" — use one.`);
108
+ }
109
+ if (apiTarget.variables && !apiTarget.query) {
110
+ issues.push(`${label} (api "${apiTarget.name}") sets "variables" without a "query".`);
111
+ }
112
+ },
113
+ doctorChecks: (target) => [validateApiTarget(target)]
114
+ };
@@ -0,0 +1,2 @@
1
+ import type { CaptureType } from "./types.js";
2
+ export declare const check: CaptureType;