tuffgal 0.1.0-alpha.0 → 0.1.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +174 -0
- package/README.md +23 -14
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +173 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/init.d.ts +17 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/{src/commands/init.ts → dist/commands/init.js} +46 -64
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/supervise.d.ts +40 -0
- package/dist/commands/supervise.d.ts.map +1 -0
- package/dist/commands/supervise.js +183 -0
- package/dist/commands/supervise.js.map +1 -0
- package/dist/config.d.ts +227 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +220 -0
- package/dist/config.js.map +1 -0
- package/dist/coverage/flows.d.ts +14 -0
- package/dist/coverage/flows.d.ts.map +1 -0
- package/dist/coverage/flows.js +77 -0
- package/dist/coverage/flows.js.map +1 -0
- package/dist/coverage/screens.d.ts +10 -0
- package/dist/coverage/screens.d.ts.map +1 -0
- package/dist/coverage/screens.js +43 -0
- package/dist/coverage/screens.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/{src → dist}/reporter/assets/report.css +257 -126
- package/dist/reporter/assets/report.js +248 -0
- package/dist/reporter/template.d.ts +8 -0
- package/dist/reporter/template.d.ts.map +1 -0
- package/dist/reporter/template.js +398 -0
- package/dist/reporter/template.js.map +1 -0
- package/dist/reporter/writeReport.d.ts +8 -0
- package/dist/reporter/writeReport.d.ts.map +1 -0
- package/dist/reporter/writeReport.js +22 -0
- package/dist/reporter/writeReport.js.map +1 -0
- package/dist/runner/approve.d.ts +25 -0
- package/dist/runner/approve.d.ts.map +1 -0
- package/dist/runner/approve.js +54 -0
- package/dist/runner/approve.js.map +1 -0
- package/dist/runner/bridges/database.d.ts +15 -0
- package/dist/runner/bridges/database.d.ts.map +1 -0
- package/dist/runner/bridges/database.js +27 -0
- package/dist/runner/bridges/database.js.map +1 -0
- package/dist/runner/bridges/devServers.d.ts +16 -0
- package/dist/runner/bridges/devServers.d.ts.map +1 -0
- package/dist/runner/bridges/devServers.js +117 -0
- package/dist/runner/bridges/devServers.js.map +1 -0
- package/dist/runner/coverage.d.ts +27 -0
- package/dist/runner/coverage.d.ts.map +1 -0
- package/dist/runner/coverage.js +73 -0
- package/dist/runner/coverage.js.map +1 -0
- package/dist/runner/interpolate.d.ts +13 -0
- package/dist/runner/interpolate.d.ts.map +1 -0
- package/dist/runner/interpolate.js +31 -0
- package/dist/runner/interpolate.js.map +1 -0
- package/dist/runner/resolveLocator.d.ts +22 -0
- package/dist/runner/resolveLocator.d.ts.map +1 -0
- package/dist/runner/resolveLocator.js +41 -0
- package/dist/runner/resolveLocator.js.map +1 -0
- package/dist/runner/run.d.ts +18 -0
- package/dist/runner/run.d.ts.map +1 -0
- package/dist/runner/run.js +154 -0
- package/dist/runner/run.js.map +1 -0
- package/dist/runner/runAction.d.ts +31 -0
- package/dist/runner/runAction.d.ts.map +1 -0
- package/dist/runner/runAction.js +335 -0
- package/dist/runner/runAction.js.map +1 -0
- package/dist/runner/runStory.d.ts +52 -0
- package/dist/runner/runStory.d.ts.map +1 -0
- package/dist/runner/runStory.js +275 -0
- package/dist/runner/runStory.js.map +1 -0
- package/dist/runner/scheduler.d.ts +33 -0
- package/dist/runner/scheduler.d.ts.map +1 -0
- package/dist/runner/scheduler.js +215 -0
- package/dist/runner/scheduler.js.map +1 -0
- package/dist/runner/steps/click.d.ts +4 -0
- package/dist/runner/steps/click.d.ts.map +1 -0
- package/dist/runner/steps/click.js +11 -0
- package/dist/runner/steps/click.js.map +1 -0
- package/dist/runner/steps/input.d.ts +4 -0
- package/dist/runner/steps/input.d.ts.map +1 -0
- package/dist/runner/steps/input.js +11 -0
- package/dist/runner/steps/input.js.map +1 -0
- package/dist/runner/steps/intercept.d.ts +13 -0
- package/dist/runner/steps/intercept.d.ts.map +1 -0
- package/dist/runner/steps/intercept.js +28 -0
- package/dist/runner/steps/intercept.js.map +1 -0
- package/dist/runner/steps/navigate.d.ts +6 -0
- package/dist/runner/steps/navigate.d.ts.map +1 -0
- package/dist/runner/steps/navigate.js +8 -0
- package/dist/runner/steps/navigate.js.map +1 -0
- package/dist/runner/steps/read.d.ts +11 -0
- package/dist/runner/steps/read.d.ts.map +1 -0
- package/dist/runner/steps/read.js +21 -0
- package/dist/runner/steps/read.js.map +1 -0
- package/dist/runner/steps/scroll.d.ts +3 -0
- package/dist/runner/steps/scroll.d.ts.map +1 -0
- package/dist/runner/steps/scroll.js +6 -0
- package/dist/runner/steps/scroll.js.map +1 -0
- package/{src/runner/steps/type.ts → dist/runner/steps/type.d.ts} +2 -4
- package/dist/runner/steps/type.d.ts.map +1 -0
- package/dist/runner/steps/type.js +10 -0
- package/dist/runner/steps/type.js.map +1 -0
- package/dist/runner/steps/wait.d.ts +3 -0
- package/dist/runner/steps/wait.d.ts.map +1 -0
- package/dist/runner/steps/wait.js +4 -0
- package/dist/runner/steps/wait.js.map +1 -0
- package/dist/runner/steps/waitFor.d.ts +4 -0
- package/dist/runner/steps/waitFor.d.ts.map +1 -0
- package/dist/runner/steps/waitFor.js +11 -0
- package/dist/runner/steps/waitFor.js.map +1 -0
- package/dist/runner/storyFilter.d.ts +11 -0
- package/dist/runner/storyFilter.d.ts.map +1 -0
- package/dist/runner/storyFilter.js +12 -0
- package/dist/runner/storyFilter.js.map +1 -0
- package/dist/schema/action.d.ts +537 -0
- package/dist/schema/action.d.ts.map +1 -0
- package/dist/schema/action.js +174 -0
- package/dist/schema/action.js.map +1 -0
- package/dist/schema/load.d.ts +20 -0
- package/dist/schema/load.d.ts.map +1 -0
- package/dist/schema/load.js +86 -0
- package/dist/schema/load.js.map +1 -0
- package/dist/schema/result.d.ts +114 -0
- package/dist/schema/result.d.ts.map +1 -0
- package/dist/schema/result.js +26 -0
- package/dist/schema/result.js.map +1 -0
- package/dist/schema/story.d.ts +54 -0
- package/dist/schema/story.d.ts.map +1 -0
- package/dist/schema/story.js +90 -0
- package/dist/schema/story.js.map +1 -0
- package/dist/screenshots/baselineStore.d.ts +58 -0
- package/dist/screenshots/baselineStore.d.ts.map +1 -0
- package/dist/screenshots/baselineStore.js +113 -0
- package/dist/screenshots/baselineStore.js.map +1 -0
- package/{src/screenshots/capture.ts → dist/screenshots/capture.d.ts} +2 -12
- package/dist/screenshots/capture.d.ts.map +1 -0
- package/dist/screenshots/capture.js +27 -0
- package/dist/screenshots/capture.js.map +1 -0
- package/dist/screenshots/diff.d.ts +41 -0
- package/dist/screenshots/diff.d.ts.map +1 -0
- package/dist/screenshots/diff.js +64 -0
- package/dist/screenshots/diff.js.map +1 -0
- package/dist/util.d.ts +16 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +48 -0
- package/dist/util.js.map +1 -0
- package/package.json +22 -12
- package/bin/tuffgal.mjs +0 -2
- package/src/.gitkeep +0 -0
- package/src/cli.ts +0 -158
- package/src/commands/supervise.ts +0 -267
- package/src/config.ts +0 -222
- package/src/coverage/flows.ts +0 -90
- package/src/coverage/screens.ts +0 -52
- package/src/index.ts +0 -28
- package/src/reporter/assets/report.js +0 -45
- package/src/reporter/template.ts +0 -355
- package/src/reporter/writeReport.ts +0 -37
- package/src/runner/approve.ts +0 -65
- package/src/runner/bridges/database.ts +0 -34
- package/src/runner/bridges/devServers.ts +0 -174
- package/src/runner/coverage.ts +0 -76
- package/src/runner/interpolate.ts +0 -36
- package/src/runner/resolveLocator.ts +0 -47
- package/src/runner/run.ts +0 -177
- package/src/runner/runAction.ts +0 -422
- package/src/runner/runStory.ts +0 -195
- package/src/runner/scheduler.ts +0 -223
- package/src/runner/steps/click.ts +0 -16
- package/src/runner/steps/input.ts +0 -17
- package/src/runner/steps/intercept.ts +0 -28
- package/src/runner/steps/navigate.ts +0 -14
- package/src/runner/steps/read.ts +0 -20
- package/src/runner/steps/scroll.ts +0 -12
- package/src/runner/steps/wait.ts +0 -5
- package/src/runner/steps/waitFor.ts +0 -16
- package/src/schema/action.ts +0 -176
- package/src/schema/load.ts +0 -94
- package/src/schema/result.ts +0 -83
- package/src/schema/story.ts +0 -58
- package/src/screenshots/baselineStore.ts +0 -114
- package/src/screenshots/diff.ts +0 -101
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# 🪵 Changelog
|
|
2
|
+
|
|
3
|
+
**All notable project changes will be documented in this file.** The format
|
|
4
|
+
is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
5
|
+
this project uses [Pride Versioning](https://pridever.org) → `PROUD.DEFAULT.SHAME`
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
_Nothing right now_
|
|
10
|
+
|
|
11
|
+
## [0.1.0-alpha.10] — 2026-06-22
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **Per-breakpoint database isolation.** alpha.9 ran each breakpoint in its own
|
|
16
|
+
browser context but applied a story's fixtures only once, before the
|
|
17
|
+
breakpoint loop — so a story that mutated server state passed at the first
|
|
18
|
+
mode and then ran the next mode against the mutated rows (a registered user
|
|
19
|
+
collided on the second mode, an already-read link no longer showed as unread,
|
|
20
|
+
an emptied list had nothing left to empty). Fixtures are now re-applied before
|
|
21
|
+
**every** breakpoint, resetting that state to a known baseline per mode.
|
|
22
|
+
Fixtures are idempotent, so non-mutating stories are unaffected.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **`${breakpoint}` interpolation token.** The current mode name is exposed to
|
|
27
|
+
action interpolation as `${breakpoint}`, so a story can key data it creates
|
|
28
|
+
through the app (with no backing fixture to reset) per mode — e.g.
|
|
29
|
+
`fresh+${breakpoint}@example.test` registers a distinct user at each viewport
|
|
30
|
+
instead of colliding on a shared value. A story parameter literally named
|
|
31
|
+
`breakpoint` overrides the injected value.
|
|
32
|
+
|
|
33
|
+
## [0.1.0-alpha.9] — 2026-06-21
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- Breakpoint modes, replacing the single `viewport` config
|
|
38
|
+
- `mobile` 375×667 / `tablet` 768×1024 / `laptop` 1024×768 / `desktop` 1280×800
|
|
39
|
+
- Each mode runs in its own browser context + produces its own baselines
|
|
40
|
+
- Each `config.breakpoints` entry is a bare name (registry dimensions) or `{ name, width?, height? }` to override that mode's size; an omitted axis inherits the registry default
|
|
41
|
+
- Order preserved; when a name repeats, the first entry wins
|
|
42
|
+
- Omit `breakpoints` to run a single `desktop` mode (1280×800)
|
|
43
|
+
- Per-story `breakpoints` field, same shape, that **replaces** the project's modes for that story — a story may run a mode the project does not configure, and its overrides resolve against the registry
|
|
44
|
+
- Breakpoint grouping in the HTML report, each group labelled with the real capture size; a single-mode story renders as a flat list with no breakpoint chrome
|
|
45
|
+
- Pre-breakpoint baselines at `<action>/0.png` are read as fallbacks until promoted
|
|
46
|
+
|
|
47
|
+
### Removed
|
|
48
|
+
|
|
49
|
+
**Breaking:** `viewport` config field + per-story `viewport` override. Set
|
|
50
|
+
a single mode's size with `breakpoints: [{ name: 'desktop', width, height }]`
|
|
51
|
+
instead.
|
|
52
|
+
|
|
53
|
+
## [0.1.0-alpha.8] — 2026-06-20
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- Reset page scroll to origin before each full-page capture, so `position: sticky` and `fixed` elements no longer render shifted by `scrollY`
|
|
58
|
+
|
|
59
|
+
## [0.1.0-alpha.7] — 2026-06-20
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
|
|
63
|
+
- Add `npm run preview` to render a sample HTML report
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- Capitalize the report title to "Tuffgal report" and show a friendlier timestamp
|
|
68
|
+
- Convey story status as a colored, marked story name instead of a badge
|
|
69
|
+
- Make the whole action row clickable to toggle screenshots
|
|
70
|
+
- Move the expand/collapse buttons to the right of the filters; the story-count status now sits beside the filters, with filter-aware labels
|
|
71
|
+
- Rename "pass" terminology to "passed" throughout the HTML report
|
|
72
|
+
- Show the status badge as plain text (no glyphs)
|
|
73
|
+
- Validate the consumer config before resolving it, failing with the config file path instead of an opaque `TypeError`
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
|
|
77
|
+
- Guard the diff core against non-RGBA PNG pixel formats so a format change fails loudly instead of scoring garbage
|
|
78
|
+
- Memoize coverage init so concurrent workers no longer orphan the HTML report
|
|
79
|
+
- Raise subtle-text contrast to clear WCAG 1.4.3 (4.5:1) in both light and dark themes
|
|
80
|
+
- Reject non-finite or non-positive numeric CLI flags at parse time (e.g. `--idle-limit foo` no longer busy-loops the supervisor)
|
|
81
|
+
- Render the HTML report's default screenshot server-side so screenshots still show when JavaScript fails or is disabled
|
|
82
|
+
- Serialize per-baseline writes so stories sharing an action no longer race to create the same baseline under `--workers > 1`
|
|
83
|
+
|
|
84
|
+
### Removed
|
|
85
|
+
|
|
86
|
+
- Drop redundant "Failures" section at the bottom of the HTML report
|
|
87
|
+
- Stop linking to Playwright trace in the HTML report
|
|
88
|
+
|
|
89
|
+
## [0.1.0-alpha.6] — 2026-06-11
|
|
90
|
+
|
|
91
|
+
### Added
|
|
92
|
+
|
|
93
|
+
- Add per-story overrides for browser `viewport` optional config field
|
|
94
|
+
- Add `tuffgal approve --new-only` flag to limit baselines to new stories
|
|
95
|
+
- Allow reports to be filtered by passed, changed, or failed
|
|
96
|
+
- Allow reports to expand/contract all screenshots
|
|
97
|
+
|
|
98
|
+
### Changed
|
|
99
|
+
|
|
100
|
+
- Edit `tuffgal run` output to group changed, failed at end w/ report link
|
|
101
|
+
- Skip npm release if no version bump
|
|
102
|
+
|
|
103
|
+
## [0.1.0-alpha.5] — 2026-06-11
|
|
104
|
+
|
|
105
|
+
### Added
|
|
106
|
+
|
|
107
|
+
`navigate` step accepts an optional `waitUntil` field (`'load' |
|
|
108
|
+
'domcontentloaded' | 'networkidle' | 'commit'`) that overrides Playwright's
|
|
109
|
+
`page.goto` ready signal on a per-step basis. Defaults to `'networkidle'`.
|
|
110
|
+
Use `'domcontentloaded'` for dev-mode pages with long-tail external fetches
|
|
111
|
+
that prevent `networkidle` from settling.
|
|
112
|
+
|
|
113
|
+
## [0.1.0-alpha.4] — 2026-06-06
|
|
114
|
+
|
|
115
|
+
### Added
|
|
116
|
+
|
|
117
|
+
- `tuffgal approve` now updates the a11y snapshot at the same time as the screenshot
|
|
118
|
+
- `--story` works the same way on both `tuffgal run` and `tuffgal approve`
|
|
119
|
+
- Two stories with the same filename in different folders is now an error
|
|
120
|
+
|
|
121
|
+
### Changed
|
|
122
|
+
|
|
123
|
+
- Stories that get skipped b/c a prereq failed now show up in the run report + totals
|
|
124
|
+
- Screenshot comparison uses SSIM only
|
|
125
|
+
- README and `docs/` pages now match how the code actually works lol
|
|
126
|
+
|
|
127
|
+
### Removed
|
|
128
|
+
|
|
129
|
+
- **Breaking:** Removed the legacy `diff.maxDiffRatio` setting
|
|
130
|
+
- **Breaking:** Dropped the `ci` block from `TuffgalConfig` and the public API
|
|
131
|
+
- Removed stale `bin/**` and `schema/**` paths
|
|
132
|
+
|
|
133
|
+
## [0.1.0-alpha.3] — 2026-06-06
|
|
134
|
+
|
|
135
|
+
Grants clipboard permissions in browser contexts.
|
|
136
|
+
|
|
137
|
+
Without `permissions: ['clipboard-read', 'clipboard-write']` Playwright
|
|
138
|
+
default-denies clipboard API access, which breaks any story using the Web
|
|
139
|
+
Clipboard API.
|
|
140
|
+
|
|
141
|
+
## [0.1.0-alpha.2] — 2026-06-04
|
|
142
|
+
|
|
143
|
+
`npm run build` now copies `src/reporter/assets/{report.css,report.js}`
|
|
144
|
+
into `dist/reporter/assets/`. Earlier alpha builds shipped without the
|
|
145
|
+
reporter's static assets, so the generated HTML report rendered without
|
|
146
|
+
styling or interactivity.
|
|
147
|
+
|
|
148
|
+
## [0.1.0-alpha.1] — 2026-06-04
|
|
149
|
+
|
|
150
|
+
### Changed
|
|
151
|
+
|
|
152
|
+
- Package now ships compiled `dist/` instead of raw `.ts` sources
|
|
153
|
+
- CLI entry point is `./dist/cli.js`
|
|
154
|
+
|
|
155
|
+
### Fixed
|
|
156
|
+
|
|
157
|
+
- Corrected `bin` extension and publish flags for the alpha channel
|
|
158
|
+
|
|
159
|
+
## [0.1.0-alpha.0] — 2026-06-04
|
|
160
|
+
|
|
161
|
+
Initial public alpha. Tuffgal extracted from [Linklater](https://github.com/nschneble/linklater)'s
|
|
162
|
+
in-tree visual testing workspace.
|
|
163
|
+
|
|
164
|
+
[Unreleased]: https://github.com/nschneble/tuffgal/compare/v0.1.0-alpha.9...HEAD
|
|
165
|
+
[0.1.0-alpha.9]: https://github.com/nschneble/tuffgal/releases/tag/v0.1.0-alpha.9
|
|
166
|
+
[0.1.0-alpha.8]: https://github.com/nschneble/tuffgal/releases/tag/v0.1.0-alpha.8
|
|
167
|
+
[0.1.0-alpha.7]: https://github.com/nschneble/tuffgal/releases/tag/v0.1.0-alpha.7
|
|
168
|
+
[0.1.0-alpha.6]: https://github.com/nschneble/tuffgal/releases/tag/v0.1.0-alpha.6
|
|
169
|
+
[0.1.0-alpha.5]: https://github.com/nschneble/tuffgal/releases/tag/v0.1.0-alpha.5
|
|
170
|
+
[0.1.0-alpha.4]: https://github.com/nschneble/tuffgal/releases/tag/v0.1.0-alpha.4
|
|
171
|
+
[0.1.0-alpha.3]: https://github.com/nschneble/tuffgal/releases/tag/v0.1.0-alpha.3
|
|
172
|
+
[0.1.0-alpha.2]: https://github.com/nschneble/tuffgal/releases/tag/v0.1.0-alpha.2
|
|
173
|
+
[0.1.0-alpha.1]: https://github.com/nschneble/tuffgal/releases/tag/v0.1.0-alpha.1
|
|
174
|
+
[0.1.0-alpha.0]: https://github.com/nschneble/tuffgal/releases/tag/v0.1.0-alpha.0
|
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# Tuffgal
|
|
2
2
|
|
|
3
|
-
](LICENSE)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
> screenshots become a committable build artifact.
|
|
5
|
+
JSON-driven visual regression testing for web apps.
|
|
7
6
|
|
|
8
7
|
<img src="tuffgal.png" alt="Tuffgal" />
|
|
9
8
|
|
|
10
|
-
**Status:** Pre-
|
|
11
|
-
|
|
9
|
+
**Status:** Pre-1.0. Published on npm as `tuffgal@0.1.0-alpha.{n}` with
|
|
10
|
+
provenance. [Linklater](https://github.com/nschneble/linklater) is the
|
|
11
|
+
pilot consumer.
|
|
12
12
|
|
|
13
13
|
Public API is unstable until `v1.0.0`.
|
|
14
14
|
|
|
@@ -24,17 +24,21 @@ When a screenshot changes, a human reviews the diff and decides what to do.
|
|
|
24
24
|
|
|
25
25
|
## What ships in v1
|
|
26
26
|
|
|
27
|
-
-
|
|
28
|
-
`navigate`, `read`, `scroll`, `type`, `wait`, `waitFor
|
|
29
|
-
|
|
27
|
+
- 9 step primitives composed into actions: `click`, `input`, `intercept`,
|
|
28
|
+
`navigate`, `read`, `scroll`, `type`, `wait`, `waitFor`
|
|
29
|
+
- An implicit screenshot at the end of every action
|
|
30
|
+
- Named breakpoint modes (`mobile`, `tablet`, `laptop`, `desktop`) keyed to
|
|
31
|
+
Tailwind widths. Pick which to run per project or per story. Each mode
|
|
32
|
+
gets its own baseline and a per-mode group in the HTML report
|
|
30
33
|
- DAG scheduler with `needs`/`produces` labels and parallel workers
|
|
31
34
|
- SSIM-gated visual diff + pixelmatch overlay + a11y-tree snapshots
|
|
32
35
|
- Trace zip on failure (Playwright trace viewer)
|
|
33
36
|
- Clock freeze (`page.clock.install`)
|
|
34
37
|
- Storage-state persistence across stories
|
|
35
|
-
- Static HTML reporter
|
|
38
|
+
- Static HTML reporter
|
|
36
39
|
- V8 coverage (optional via `monocart-coverage-reports`)
|
|
37
|
-
- Per-
|
|
40
|
+
- Per-run DB reset + per-breakpoint fixture hooks (consumer-supplied via config),
|
|
41
|
+
plus a `${breakpoint}` interpolation token for per-mode test data
|
|
38
42
|
- Process supervisor for dev-server hot-reload rot (it happens)
|
|
39
43
|
|
|
40
44
|
## What's explicitly out of scope (v1)
|
|
@@ -45,18 +49,23 @@ When a screenshot changes, a human reviews the diff and decides what to do.
|
|
|
45
49
|
- WebDriver / Puppeteer substrate
|
|
46
50
|
- Supporting browsers other than Chromium
|
|
47
51
|
|
|
48
|
-
## Quick start
|
|
52
|
+
## Quick start
|
|
49
53
|
|
|
50
54
|
```bash
|
|
51
|
-
npm install -D tuffgal
|
|
55
|
+
npm install -D tuffgal@alpha
|
|
52
56
|
npx tuffgal init # scaffolds tuffgal.config.ts
|
|
53
57
|
npx tuffgal run # runs all stories
|
|
54
58
|
```
|
|
55
59
|
|
|
60
|
+
For CI on GitHub Actions, use the companion
|
|
61
|
+
[`nschneble/tuffgal-action`](https://github.com/nschneble/tuffgal-action)
|
|
62
|
+
composite action.
|
|
63
|
+
|
|
56
64
|
## Documentation
|
|
57
65
|
|
|
58
66
|
- [App contract](docs/app-contract.md)
|
|
59
67
|
- [Authoring guide](docs/authoring.md)
|
|
68
|
+
- [Changelog](CHANGELOG.md)
|
|
60
69
|
- [CI integration](docs/ci.md)
|
|
61
70
|
- [Config reference](docs/config.md)
|
|
62
71
|
- [Migrating from Cypress](docs/migration-cypress.md)
|
|
@@ -76,8 +85,8 @@ MIT. See [LICENSE](LICENSE).
|
|
|
76
85
|
| Core extraction | ✅ |
|
|
77
86
|
| Bridges | ✅ |
|
|
78
87
|
| Linklater migration | ✅ |
|
|
79
|
-
| GitHub Action |
|
|
80
|
-
| `v0.1.0-alpha` npm publish |
|
|
88
|
+
| GitHub Action | ✅ |
|
|
89
|
+
| `v0.1.0-alpha` npm publish | ✅ |
|
|
81
90
|
| `v1.0.0` public launch | ⏳ |
|
|
82
91
|
| `v1.1.0` AI fuzzy matching | Deferred |
|
|
83
92
|
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { approveAll } from "./runner/approve.js";
|
|
3
|
+
import { init } from "./commands/init.js";
|
|
4
|
+
import { loadConfig } from "./config.js";
|
|
5
|
+
import { runAll } from "./runner/run.js";
|
|
6
|
+
import { supervise } from "./commands/supervise.js";
|
|
7
|
+
const COMMANDS = ['approve', 'help', 'init', 'run', 'supervise'];
|
|
8
|
+
function parseArguments(argv) {
|
|
9
|
+
const [command, ...rest] = argv;
|
|
10
|
+
const parsed = {
|
|
11
|
+
command: COMMANDS.includes(command)
|
|
12
|
+
? command
|
|
13
|
+
: 'help',
|
|
14
|
+
headed: false,
|
|
15
|
+
manageServers: false,
|
|
16
|
+
coverage: false,
|
|
17
|
+
newOnly: false,
|
|
18
|
+
};
|
|
19
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
20
|
+
const arg = rest[index];
|
|
21
|
+
if (arg === undefined)
|
|
22
|
+
continue;
|
|
23
|
+
if (arg === '--headed') {
|
|
24
|
+
parsed.headed = true;
|
|
25
|
+
}
|
|
26
|
+
else if (arg === '--story') {
|
|
27
|
+
parsed.storyFilter = rest[index + 1];
|
|
28
|
+
index += 1;
|
|
29
|
+
}
|
|
30
|
+
else if (arg.startsWith('--story=')) {
|
|
31
|
+
parsed.storyFilter = arg.slice('--story='.length);
|
|
32
|
+
}
|
|
33
|
+
else if (arg === '--workers') {
|
|
34
|
+
parsed.workers = numericFlag('--workers', rest[index + 1]);
|
|
35
|
+
index += 1;
|
|
36
|
+
}
|
|
37
|
+
else if (arg.startsWith('--workers=')) {
|
|
38
|
+
parsed.workers = numericFlag('--workers', arg.slice('--workers='.length));
|
|
39
|
+
}
|
|
40
|
+
else if (arg === '--manage-servers') {
|
|
41
|
+
parsed.manageServers = true;
|
|
42
|
+
}
|
|
43
|
+
else if (arg === '--coverage') {
|
|
44
|
+
parsed.coverage = true;
|
|
45
|
+
}
|
|
46
|
+
else if (arg === '--new-only') {
|
|
47
|
+
parsed.newOnly = true;
|
|
48
|
+
}
|
|
49
|
+
else if (arg === '--healthcheck-interval') {
|
|
50
|
+
parsed.healthcheckIntervalMs = numericFlag('--healthcheck-interval', rest[index + 1]);
|
|
51
|
+
index += 1;
|
|
52
|
+
}
|
|
53
|
+
else if (arg.startsWith('--healthcheck-interval=')) {
|
|
54
|
+
parsed.healthcheckIntervalMs = numericFlag('--healthcheck-interval', arg.slice('--healthcheck-interval='.length));
|
|
55
|
+
}
|
|
56
|
+
else if (arg === '--idle-limit') {
|
|
57
|
+
parsed.idleLimitMs = numericFlag('--idle-limit', rest[index + 1]);
|
|
58
|
+
index += 1;
|
|
59
|
+
}
|
|
60
|
+
else if (arg.startsWith('--idle-limit=')) {
|
|
61
|
+
parsed.idleLimitMs = numericFlag('--idle-limit', arg.slice('--idle-limit='.length));
|
|
62
|
+
}
|
|
63
|
+
else if (arg === '--max-runtime') {
|
|
64
|
+
parsed.maxRuntimeMs = numericFlag('--max-runtime', rest[index + 1]);
|
|
65
|
+
index += 1;
|
|
66
|
+
}
|
|
67
|
+
else if (arg.startsWith('--max-runtime=')) {
|
|
68
|
+
parsed.maxRuntimeMs = numericFlag('--max-runtime', arg.slice('--max-runtime='.length));
|
|
69
|
+
}
|
|
70
|
+
else if (arg === '--max-respawns') {
|
|
71
|
+
parsed.maxRespawns = numericFlag('--max-respawns', rest[index + 1]);
|
|
72
|
+
index += 1;
|
|
73
|
+
}
|
|
74
|
+
else if (arg.startsWith('--max-respawns=')) {
|
|
75
|
+
parsed.maxRespawns = numericFlag('--max-respawns', arg.slice('--max-respawns='.length));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return parsed;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Parses a numeric CLI flag, rejecting anything non-finite or <= 0. Without
|
|
82
|
+
* this, `--idle-limit foo` became `NaN` and flowed into `setTimeout(…, NaN)`,
|
|
83
|
+
* which behaves as `0` and busy-loops the supervisor's healthcheck. Fail loudly
|
|
84
|
+
* at parse time instead.
|
|
85
|
+
*/
|
|
86
|
+
function numericFlag(flag, raw) {
|
|
87
|
+
const value = Number(raw);
|
|
88
|
+
if (raw === undefined ||
|
|
89
|
+
raw === '' ||
|
|
90
|
+
!Number.isFinite(value) ||
|
|
91
|
+
value <= 0) {
|
|
92
|
+
throw new Error(`${flag} requires a positive number (got "${raw ?? ''}")`);
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
function printHelp() {
|
|
97
|
+
process.stdout.write([
|
|
98
|
+
'Usage: tuffgal <command> [options]',
|
|
99
|
+
'',
|
|
100
|
+
'Commands:',
|
|
101
|
+
' run Run every story under the configured stories directory.',
|
|
102
|
+
' approve Promote every "changed" actual to its baseline.',
|
|
103
|
+
' init Scaffold a tuffgal.config.ts in the current directory.',
|
|
104
|
+
' supervise Long-running wrapper around devServers.command with',
|
|
105
|
+
' healthcheck restart, idle auto-term, and wall-clock cap.',
|
|
106
|
+
' help Show this message.',
|
|
107
|
+
'',
|
|
108
|
+
'Options:',
|
|
109
|
+
' --story <name> Filter to a single story (filename or story text).',
|
|
110
|
+
' --headed Show the browser while running.',
|
|
111
|
+
' --workers N Override the worker pool size (default min(cpus/2, 4)).',
|
|
112
|
+
' --manage-servers Spawn devServers.command, wait, run, then kill it.',
|
|
113
|
+
' --coverage Capture V8 JS + CSS coverage and emit a monocart report.',
|
|
114
|
+
'',
|
|
115
|
+
'Approve options:',
|
|
116
|
+
' --new-only Only promote new baselines; skip changed.',
|
|
117
|
+
'',
|
|
118
|
+
'Supervise options:',
|
|
119
|
+
' --healthcheck-interval N Probe interval in ms (default 30_000).',
|
|
120
|
+
' --idle-limit N Ms with no `tuffgal run` heartbeat before exit (default 600_000).',
|
|
121
|
+
' --max-runtime N Wall-clock cap in ms (default 3_600_000).',
|
|
122
|
+
' --max-respawns N Respawn budget after unhealthy/exit (default 3).',
|
|
123
|
+
].join('\n') + '\n');
|
|
124
|
+
}
|
|
125
|
+
async function main() {
|
|
126
|
+
const args = parseArguments(process.argv.slice(2));
|
|
127
|
+
if (args.newOnly &&
|
|
128
|
+
(args.command === 'run' ||
|
|
129
|
+
args.command === 'supervise' ||
|
|
130
|
+
args.command === 'init')) {
|
|
131
|
+
process.stderr.write('tuffgal error: --new-only is only valid with the `approve` subcommand\n');
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
if (args.command === 'help') {
|
|
135
|
+
printHelp();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (args.command === 'init') {
|
|
139
|
+
await init({ cwd: process.cwd() });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const config = await loadConfig(process.cwd());
|
|
143
|
+
if (args.command === 'run') {
|
|
144
|
+
const result = await runAll(config, {
|
|
145
|
+
storyFilter: args.storyFilter,
|
|
146
|
+
headed: args.headed,
|
|
147
|
+
workers: args.workers,
|
|
148
|
+
manageServers: args.manageServers,
|
|
149
|
+
coverage: args.coverage,
|
|
150
|
+
});
|
|
151
|
+
process.exit(result.totals.failed > 0 ? 1 : 0);
|
|
152
|
+
}
|
|
153
|
+
if (args.command === 'approve') {
|
|
154
|
+
const summary = await approveAll(config, {
|
|
155
|
+
storyFilter: args.storyFilter,
|
|
156
|
+
newOnly: args.newOnly,
|
|
157
|
+
});
|
|
158
|
+
process.stdout.write(`\nApproved ${summary.approved} baselines; skipped ${summary.skipped} actions.\n`);
|
|
159
|
+
}
|
|
160
|
+
if (args.command === 'supervise') {
|
|
161
|
+
await supervise(config, {
|
|
162
|
+
healthcheckIntervalMs: args.healthcheckIntervalMs,
|
|
163
|
+
idleLimitMs: args.idleLimitMs,
|
|
164
|
+
maxRuntimeMs: args.maxRuntimeMs,
|
|
165
|
+
maxRespawns: args.maxRespawns,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
main().catch((error) => {
|
|
170
|
+
process.stderr.write(`tuffgal error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
});
|
|
173
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface InitOptions {
|
|
2
|
+
cwd: string;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Scaffolds a starter Tuffgal layout in the given directory:
|
|
6
|
+
* - tuffgal.config.ts (commented template)
|
|
7
|
+
* - tuffgal/actions/.gitkeep
|
|
8
|
+
* - tuffgal/stories/.gitkeep
|
|
9
|
+
* - tuffgal/baselines/.gitkeep
|
|
10
|
+
* - tuffgal/.gitignore (ignores .auth/ and report/)
|
|
11
|
+
*
|
|
12
|
+
* Refuses to overwrite an existing tuffgal.config.{ts,js}. Existing
|
|
13
|
+
* scaffolded directories are left alone — a second \`init\` is safe to run
|
|
14
|
+
* after manually deleting just the config.
|
|
15
|
+
*/
|
|
16
|
+
export declare function init(options: InitOptions): Promise<void>;
|
|
17
|
+
//# sourceMappingURL=init.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AA+EA,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA6C9D"}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { mkdir, writeFile
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { join, relative } from 'node:path';
|
|
3
|
-
|
|
3
|
+
import { pathExists } from "../util.js";
|
|
4
4
|
const CONFIG_FILES = ['tuffgal.config.ts', 'tuffgal.config.js'];
|
|
5
|
-
|
|
6
5
|
const CONFIG_TEMPLATE = `import { defineConfig } from 'tuffgal';
|
|
7
6
|
|
|
8
7
|
/**
|
|
@@ -31,7 +30,14 @@ export default defineConfig({
|
|
|
31
30
|
// leave this empty — cookies always persist via Playwright storage state.
|
|
32
31
|
// storageStatePins: ['session_token'],
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
// Breakpoint modes to run, keyed to Tailwind widths:
|
|
34
|
+
// mobile 375x667 · tablet 768x1024 · laptop 1024x768 · desktop 1280x800
|
|
35
|
+
// Each mode gets its own baseline + a group in the report. Omit to run a
|
|
36
|
+
// single \`desktop\` mode (1280x800). Override a mode's size with an object,
|
|
37
|
+
// e.g. { name: 'desktop', width: 1440, height: 900 }. A story can run its own
|
|
38
|
+
// modes via its own \`breakpoints\` field, which replace these for that story.
|
|
39
|
+
// breakpoints: ['mobile', 'tablet', 'laptop', 'desktop'],
|
|
40
|
+
|
|
35
41
|
defaultTimeoutMs: 10_000,
|
|
36
42
|
navigationTimeoutMs: 15_000,
|
|
37
43
|
frozenTime: '2026-01-15T12:00:00.000Z',
|
|
@@ -57,22 +63,15 @@ export default defineConfig({
|
|
|
57
63
|
// },
|
|
58
64
|
});
|
|
59
65
|
`;
|
|
60
|
-
|
|
61
66
|
const GITKEEP_SUBDIRECTORIES = [
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
'tuffgal/actions',
|
|
68
|
+
'tuffgal/stories',
|
|
69
|
+
'tuffgal/baselines',
|
|
65
70
|
];
|
|
66
|
-
|
|
67
71
|
const REPORT_GITIGNORE = `# Generated by Tuffgal — do not commit.
|
|
68
72
|
.auth/
|
|
69
73
|
report/
|
|
70
74
|
`;
|
|
71
|
-
|
|
72
|
-
export interface InitOptions {
|
|
73
|
-
cwd: string;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
75
|
/**
|
|
77
76
|
* Scaffolds a starter Tuffgal layout in the given directory:
|
|
78
77
|
* - tuffgal.config.ts (commented template)
|
|
@@ -85,56 +84,39 @@ export interface InitOptions {
|
|
|
85
84
|
* scaffolded directories are left alone — a second \`init\` is safe to run
|
|
86
85
|
* after manually deleting just the config.
|
|
87
86
|
*/
|
|
88
|
-
export async function init(options
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
`Delete it manually if you want to start over.`,
|
|
96
|
-
);
|
|
87
|
+
export async function init(options) {
|
|
88
|
+
const { cwd } = options;
|
|
89
|
+
for (const candidate of CONFIG_FILES) {
|
|
90
|
+
if (await pathExists(join(cwd, candidate))) {
|
|
91
|
+
throw new Error(`${candidate} already exists at ${cwd}. Refusing to overwrite. ` +
|
|
92
|
+
`Delete it manually if you want to start over.`);
|
|
93
|
+
}
|
|
97
94
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
await writeFile(gitkeep, '', 'utf8');
|
|
110
|
-
process.stdout.write(`Wrote ${relative(cwd, gitkeep)}\n`);
|
|
95
|
+
const configPath = join(cwd, 'tuffgal.config.ts');
|
|
96
|
+
await writeFile(configPath, CONFIG_TEMPLATE, 'utf8');
|
|
97
|
+
process.stdout.write(`Wrote ${relative(cwd, configPath) || 'tuffgal.config.ts'}\n`);
|
|
98
|
+
for (const subdirectory of GITKEEP_SUBDIRECTORIES) {
|
|
99
|
+
const absolute = join(cwd, subdirectory);
|
|
100
|
+
await mkdir(absolute, { recursive: true });
|
|
101
|
+
const gitkeep = join(absolute, '.gitkeep');
|
|
102
|
+
if (!(await pathExists(gitkeep))) {
|
|
103
|
+
await writeFile(gitkeep, '', 'utf8');
|
|
104
|
+
process.stdout.write(`Wrote ${relative(cwd, gitkeep)}\n`);
|
|
105
|
+
}
|
|
111
106
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
process.stdout.write(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
' 3. Write your first story under tuffgal/stories/.',
|
|
127
|
-
' 4. Run `npx tuffgal run` to capture baselines.',
|
|
128
|
-
'',
|
|
129
|
-
].join('\n'),
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async function pathExists(absolute: string): Promise<boolean> {
|
|
134
|
-
try {
|
|
135
|
-
await access(absolute);
|
|
136
|
-
return true;
|
|
137
|
-
} catch {
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
107
|
+
const gitignorePath = join(cwd, 'tuffgal', '.gitignore');
|
|
108
|
+
if (!(await pathExists(gitignorePath))) {
|
|
109
|
+
await writeFile(gitignorePath, REPORT_GITIGNORE, 'utf8');
|
|
110
|
+
process.stdout.write(`Wrote ${relative(cwd, gitignorePath)}\n`);
|
|
111
|
+
}
|
|
112
|
+
process.stdout.write([
|
|
113
|
+
'',
|
|
114
|
+
'Tuffgal scaffold ready. Next steps:',
|
|
115
|
+
' 1. Edit tuffgal.config.ts to point at your dev server.',
|
|
116
|
+
' 2. Write your first action under tuffgal/actions/.',
|
|
117
|
+
' 3. Write your first story under tuffgal/stories/.',
|
|
118
|
+
' 4. Run `npx tuffgal run` to capture baselines.',
|
|
119
|
+
'',
|
|
120
|
+
].join('\n'));
|
|
140
121
|
}
|
|
122
|
+
//# sourceMappingURL=init.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,MAAM,YAAY,GAAG,CAAC,mBAAmB,EAAE,mBAAmB,CAAC,CAAC;AAEhE,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4DvB,CAAC;AAEF,MAAM,sBAAsB,GAAG;IAC7B,iBAAiB;IACjB,iBAAiB;IACjB,mBAAmB;CACpB,CAAC;AAEF,MAAM,gBAAgB,GAAG;;;CAGxB,CAAC;AAMF;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,OAAoB;IAC7C,MAAM,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC;IAExB,KAAK,MAAM,SAAS,IAAI,YAAY,EAAE,CAAC;QACrC,IAAI,MAAM,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC;YAC3C,MAAM,IAAI,KAAK,CACb,GAAG,SAAS,sBAAsB,GAAG,2BAA2B;gBAC9D,+CAA+C,CAClD,CAAC;QACJ,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,mBAAmB,CAAC,CAAC;IAClD,MAAM,SAAS,CAAC,UAAU,EAAE,eAAe,EAAE,MAAM,CAAC,CAAC;IACrD,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,SAAS,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC,IAAI,mBAAmB,IAAI,CAC9D,CAAC;IAEF,KAAK,MAAM,YAAY,IAAI,sBAAsB,EAAE,CAAC;QAClD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;QACzC,MAAM,KAAK,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3C,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;QAC3C,IAAI,CAAC,CAAC,MAAM,UAAU,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;YACjC,MAAM,SAAS,CAAC,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC;YACrC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;QAC5D,CAAC;IACH,CAAC;IAED,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC;IACzD,IAAI,CAAC,CAAC,MAAM,UAAU,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC;QACvC,MAAM,SAAS,CAAC,aAAa,EAAE,gBAAgB,EAAE,MAAM,CAAC,CAAC;QACzD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC;IAClE,CAAC;IAED,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB;QACE,EAAE;QACF,qCAAqC;QACrC,0DAA0D;QAC1D,sDAAsD;QACtD,qDAAqD;QACrD,kDAAkD;QAClD,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CACb,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ResolvedConfig } from '../config.ts';
|
|
2
|
+
export interface SuperviseOptions {
|
|
3
|
+
/** Milliseconds between port + heartbeat probes. Default 30_000. */
|
|
4
|
+
healthcheckIntervalMs?: number;
|
|
5
|
+
/**
|
|
6
|
+
* Window with no `tuffgal run` heartbeat before the supervisor self-
|
|
7
|
+
* terminates. Default 10 minutes. The heartbeat file lives at
|
|
8
|
+
* `config.paths.report/.heartbeat` and is touched by every `tuffgal
|
|
9
|
+
* run` invocation.
|
|
10
|
+
*/
|
|
11
|
+
idleLimitMs?: number;
|
|
12
|
+
/** Wall-clock cap, end-to-end. Default 60 minutes. */
|
|
13
|
+
maxRuntimeMs?: number;
|
|
14
|
+
/**
|
|
15
|
+
* How many times the supervisor may respawn `devServers.command` after
|
|
16
|
+
* an unhealthy probe or unexpected exit before giving up. Default 3.
|
|
17
|
+
*/
|
|
18
|
+
maxRespawns?: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Long-running supervisor around `config.devServers.command`. Solves
|
|
22
|
+
* three problems observed during heavy harness iteration:
|
|
23
|
+
*
|
|
24
|
+
* 1. Hot-reload rot — after many file-watch restarts the framework
|
|
25
|
+
* drifts into a broken state. Supervisor probes every declared
|
|
26
|
+
* healthcheck URL and restarts the whole tree on failure.
|
|
27
|
+
* 2. Forgotten dev servers — supervisor self-terminates after a
|
|
28
|
+
* wall-clock cap and after an idle window with no heartbeat from
|
|
29
|
+
* `tuffgal run`.
|
|
30
|
+
* 3. Manual signal management — SIGINT/SIGTERM teardown kills the
|
|
31
|
+
* whole detached process group, not just the shell wrapper.
|
|
32
|
+
*
|
|
33
|
+
* Each invocation of `tuffgal run` writes
|
|
34
|
+
* `config.paths.report/.heartbeat` (ISO timestamp). Supervisor reads
|
|
35
|
+
* its mtime every healthcheck. If `Date.now() - mtime > idleLimitMs`,
|
|
36
|
+
* supervisor shuts down. Missing heartbeat file is treated as "no runs
|
|
37
|
+
* yet", not stale, so a fresh shell does not self-terminate immediately.
|
|
38
|
+
*/
|
|
39
|
+
export declare function supervise(config: ResolvedConfig, options?: SuperviseOptions): Promise<void>;
|
|
40
|
+
//# sourceMappingURL=supervise.d.ts.map
|