staysfixed 0.7.2 → 0.9.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 (65) hide show
  1. package/CHANGELOG.md +429 -0
  2. package/README.md +193 -57
  3. package/docs/design-v2.md +24 -4
  4. package/docs/getting-started.md +19 -6
  5. package/docs/guards.md +2 -2
  6. package/docs/how-v2-works.md +12 -11
  7. package/docs/mcp.md +17 -8
  8. package/docs/settings.md +564 -0
  9. package/docs/watching.md +10 -4
  10. package/examples/staysfixed.config.electron.js +17 -6
  11. package/examples/staysfixed.config.web.js +22 -5
  12. package/package.json +2 -1
  13. package/src/cli/index.js +55 -46
  14. package/src/cli/status.js +45 -1
  15. package/src/cli/watch-flags.js +54 -0
  16. package/src/core/config.js +54 -3
  17. package/src/core/paths.js +15 -0
  18. package/src/guard/run.js +70 -3
  19. package/src/report/console.js +50 -6
  20. package/src/run.js +11 -0
  21. package/src/types.js +3 -0
  22. package/src/v2/adapters/android-driver.js +6 -1
  23. package/src/v2/adapters/android.js +97 -2
  24. package/src/v2/adapters/child.js +101 -0
  25. package/src/v2/adapters/contract.js +42 -5
  26. package/src/v2/adapters/electron.js +72 -6
  27. package/src/v2/adapters/http.js +18 -11
  28. package/src/v2/adapters/ios-driver.js +64 -14
  29. package/src/v2/adapters/ios.js +247 -25
  30. package/src/v2/adapters/process.js +783 -71
  31. package/src/v2/adapters/python.js +495 -0
  32. package/src/v2/adapters/source.js +373 -18
  33. package/src/v2/adapters/web-driver.js +134 -24
  34. package/src/v2/adapters/web.js +149 -18
  35. package/src/v2/adapters/windows.js +18 -1
  36. package/src/v2/browsers.js +66 -3
  37. package/src/v2/cause.js +61 -17
  38. package/src/v2/check.js +653 -69
  39. package/src/v2/ci.js +130 -35
  40. package/src/v2/cli.js +65 -42
  41. package/src/v2/cluster.js +220 -14
  42. package/src/v2/coverage.js +43 -176
  43. package/src/v2/detect.js +308 -60
  44. package/src/v2/doctor.js +353 -54
  45. package/src/v2/escalate.js +5 -1
  46. package/src/v2/init.js +183 -66
  47. package/src/v2/intent.js +9 -23
  48. package/src/v2/journeys/from-suite.js +336 -30
  49. package/src/v2/journeys/index.js +99 -6
  50. package/src/v2/mcp/tools.js +90 -16
  51. package/src/v2/normalise.js +169 -23
  52. package/src/v2/observation.js +19 -33
  53. package/src/v2/rank.js +216 -23
  54. package/src/v2/reference.js +160 -24
  55. package/src/v2/remote.js +113 -18
  56. package/src/v2/run.js +103 -14
  57. package/src/v2/sealed.js +0 -20
  58. package/src/v2/selfcheck.js +190 -13
  59. package/src/v2/ship.js +55 -5
  60. package/src/v2/store.js +67 -1
  61. package/src/v2/types.js +12 -2
  62. package/src/v2/waiver.js +64 -54
  63. package/src/v2/watch/events.js +60 -215
  64. package/src/v2/watch/focus.js +14 -4
  65. package/src/v2/watch/panel.js +167 -17
@@ -0,0 +1,564 @@
1
+ # The settings file, and what each kind of product needs
2
+
3
+ Two questions answered in one place, because they are really one question:
4
+
5
+ 1. **What can I put in the settings file?** Every option, per kind of product.
6
+ 2. **What has to be on this machine before that option can work?** Every program,
7
+ every environment variable, every licence a person has to accept — with the
8
+ exact command, not "you may need Xcode".
9
+
10
+ You should not have to read this. `staysfixed init` reads the repository and
11
+ writes the file for you, with an explanation beside every option, and
12
+ `staysfixed doctor` tells you what is missing and how to get it. This page is
13
+ here for when you want to add something `init` could not know, and for an agent
14
+ setting this up on somebody's machine that needs to be sure before it speaks.
15
+
16
+ ---
17
+
18
+ ## Where the file goes
19
+
20
+ The first of these that exists, searched from the folder you run in and then
21
+ upwards:
22
+
23
+ ```
24
+ staysfixed.config.js
25
+ staysfixed.config.mjs
26
+ staysfixed.config.json
27
+ .staysfixed/config.js
28
+ .staysfixed/config.mjs
29
+ .staysfixed/config.json
30
+ ```
31
+
32
+ `--config <file>` overrides all of it and names the project. JSON works
33
+ everywhere; JavaScript is only needed when a value has to be worked out at the
34
+ moment it is read.
35
+
36
+ The shape is one product name and one block per kind of thing your repository
37
+ makes:
38
+
39
+ ```js
40
+ export default {
41
+ product: 'your-app',
42
+ source: { /* reading the code */ },
43
+ process: { /* commands and libraries */ },
44
+ http: { /* a server */ },
45
+ web: { /* a website */ },
46
+ electron: { /* a desktop app */ },
47
+ android: { /* an APK */ },
48
+ ios: { /* a simulator build */ },
49
+ windows: { /* a native Windows program */ },
50
+ };
51
+ ```
52
+
53
+ A repository that makes four things has four blocks. Leaving a block out is how
54
+ you say "there is none of that here" — it is never an error, and the run says
55
+ which kinds it did not check.
56
+
57
+ **Two things are not settings, and never will be.** There is no tolerance: how
58
+ much your product disagrees with itself is measured by running the new build
59
+ twice, and subtracted. And there is nothing to approve: the build you say
60
+ `staysfixed ship` about becomes what "working" means.
61
+
62
+ ---
63
+
64
+
65
+ ### `process.alsoWatch`
66
+
67
+ A list of extra folders a command is allowed to touch, so that what it writes there is
68
+ watched rather than counted as a surprise. The tool's own run output names this option, and
69
+ until 2026-08-30 it appeared nowhere here — in a page whose promise is every option, per kind
70
+ of product.
71
+
72
+ ```js
73
+ process: {
74
+ commands: [{ name: 'build', run: 'npm run build' }],
75
+ alsoWatch: ['dist', '../shared/generated'],
76
+ }
77
+ ```
78
+
79
+ ## `product`
80
+
81
+ ```js
82
+ product: 'your-app',
83
+ ```
84
+
85
+ The name the record is kept under. One repository can build five things and each
86
+ keeps its own idea of what "working" means, so the name is how two of them are
87
+ told apart. Change it and you start again from nothing.
88
+
89
+ ---
90
+
91
+ ## `keepBuilds` — how much history is kept
92
+
93
+ ```js
94
+ keepBuilds: 5,
95
+ ```
96
+
97
+ Every check writes a record of the build it walked into `.staysfixed/`, and that
98
+ folder belongs in git — it is the record of what "working" means, not throwaway
99
+ evidence. So it grows: one record per check, in your repository, for ever. A
100
+ project checked on every commit was accumulating that record with nothing ever
101
+ removing it.
102
+
103
+ At the end of a successful check the older records are cleared out, in three
104
+ tiers. Three things are outside the count entirely and are never touched, whatever
105
+ this is set to: the reference — the build you shipped, which is the whole point of
106
+ the store — the build this run just walked, and anything named by `--against`.
107
+
108
+ After those:
109
+
110
+ - the newest `keepBuilds` are kept whole, every recording intact;
111
+ - the next `keepBuilds` × 4 are thinned to one recording per journey, so they can
112
+ still say what that build did without keeping every take of it;
113
+ - everything behind that is removed, folder and all, oldest first.
114
+
115
+ The count is builds, not days, and that is deliberate. A project checked on every
116
+ commit writes fifty folders a day, so "keep thirty days" is fifteen hundred
117
+ folders in somebody's git history — and the same rule throws away a two-month-old
118
+ build on a project checked twice a week. The thinned middle tier is the grace
119
+ period, and it is cheap: fifteen thinned builds cost about what one untouched one
120
+ costs, and they buy back the case a plain cap gets wrong — thirty checks in an
121
+ afternoon, then `--against` the build from before lunch.
122
+
123
+ If any stored record could not be read, **nothing is removed or thinned at all**
124
+ and the run says so. Deleting on an incomplete listing is how you lose the one
125
+ record that mattered, and a thinned folder can be walked again where a deleted one
126
+ cannot.
127
+
128
+ The run prints one line saying what it cleared. Set it higher if you use
129
+ `staysfixed check --against` on older builds a lot; there is no reason to set it
130
+ lower than 2.
131
+
132
+ ---
133
+
134
+ ## `suite` — walking your own test suite
135
+
136
+ ```js
137
+ suite: { budgetMs: 90000 },
138
+ ```
139
+
140
+ `staysfixed check --journeys suite` runs the project's own test suite as well as
141
+ everything else: each test file twice inside the scratch copy, every check
142
+ reported by name, why each failure failed, and a fingerprint of the test file
143
+ itself — so an edited test says plainly that the change is yours.
144
+
145
+ `budgetMs` is how long that is allowed to take. It defaults to 90 seconds, which
146
+ is a statement about how long anyone waits inside an edit-and-check loop before
147
+ switching the tool off, not a guess at how long suites take. **Every file the
148
+ budget did not reach is named, one by one**, in the report and in the coverage
149
+ ledger — never "some tests were skipped" — and every run says which budget was
150
+ applied and where it came from.
151
+
152
+ Set `0` for no budget at all, and the whole suite is walked however long it takes.
153
+ Raise it when your suite is slower than 90 seconds and you would rather wait than
154
+ have the tail named as uncovered.
155
+
156
+ The harvest is off unless you ask for it. Running a stranger's whole suite twice
157
+ on every check is not a thing to do by default, and a check nobody can afford to
158
+ run is a check nobody runs.
159
+
160
+ ---
161
+
162
+ ## `source` — reading the code
163
+
164
+ Free, exact, runs nothing, and it is the only channel that can see a door nobody
165
+ has ever opened.
166
+
167
+ | Option | What it does |
168
+ | --- | --- |
169
+ | `folders` | Which folders to read. Left out, it reads the usual ones — `src`, `lib`, `app`, `bin`, `server`, `pages`, `api`, `electron`, `main`, `packages`. That is right for a repository that makes one thing and **misses whole products in a repository that makes several**, so `init` writes the real list. |
170
+ | `surface` | Read the source as though it belonged to one kind of product, when the guess is wrong. |
171
+
172
+ **Needs on the machine:** nothing.
173
+
174
+ ---
175
+
176
+ ## `process` — commands and libraries
177
+
178
+ Each command runs in a **throwaway copy of your project**, never your working
179
+ copy, with the clock stopped and every outbound connection recorded and then
180
+ refused.
181
+
182
+ | Option | What it does |
183
+ | --- | --- |
184
+ | `commands[]` | The commands worth running. See the entry shape below. |
185
+ | `imports[]` | Modules to import and compare the exports of: `{ name, module }`. |
186
+ | `skip[]` | Folder names to leave out of the scratch copy. See the warning below. |
187
+
188
+ A `commands` entry:
189
+
190
+ ```js
191
+ process: {
192
+ commands: [
193
+ {
194
+ name: 'demo --help', // what it is called in the report
195
+ run: 'node ./cli.js --help', // the command itself
196
+ describe: 'ask it to print its help', // one line, for a person reading the result
197
+ cwd: 'packages/cli', // optional, relative to the project
198
+ stdin: 'yes\n', // optional, what to type at it
199
+ env: { LOG_LEVEL: 'debug' }, // optional, added to the frozen set below
200
+ timeoutMs: 60000, // optional
201
+ irreversible: false, // true = it spends money or sends something
202
+ },
203
+ ],
204
+ },
205
+ ```
206
+
207
+ `irreversible: true` means the command is watched **asking** — the same charge,
208
+ the same amount, the same place — and never allowed to actually do it. That is
209
+ reported as a gap in coverage, never as a pass.
210
+
211
+ `init` only ever fills in `--help` commands by itself, deliberately: a command
212
+ named in a manifest could deploy, publish or wipe a database, and running one
213
+ because it was listed would be this tool causing the exact damage it exists to
214
+ catch. Add the others yourself.
215
+
216
+ ### `process.skip` — and the bar for using it
217
+
218
+ ```js
219
+ process: { skip: ['DerivedData', '.gradle'] },
220
+ ```
221
+
222
+ Every run copies your project into a scratch folder. That copy is **cloned**, not
223
+ copied: on a Mac and on most Linux filesystems it is one call per file that moves
224
+ no bytes and shares the blocks until something writes to them. Measured on a
225
+ twelve-gigabyte project, most of it an iOS build folder: 41.5 seconds and no disk
226
+ used. Where the filesystem cannot clone it falls back to a real copy — slower,
227
+ never wrong. If a copy takes over twenty seconds the run says so and points you
228
+ at this setting.
229
+
230
+ **Only put something here if it is regenerated on demand and read by nothing.**
231
+ Caches, coverage reports, scratch output. What you skip is missing from **both**
232
+ builds, so a product that actually needed it fails the same way twice, there is no
233
+ difference to report, and the run comes back clean — a pass for the wrong reason,
234
+ which is the single failure this whole tool exists to prevent. Build output,
235
+ `node_modules`, lockfiles, fixtures and configuration are all copied every time.
236
+ When in doubt, leave it in and let the run be slower.
237
+
238
+ `skip` can only **add** to what is already left behind: `.git`, `.staysfixed`,
239
+ `.turbo`, `.nyc_output`, `coverage`, `.pytest_cache`, `__pycache__`, `.DS_Store`.
240
+ There is no way to switch one of those back on, because that could only ever make
241
+ runs slower.
242
+
243
+ ### Needs on the machine
244
+
245
+ | What | Why | Who |
246
+ | --- | --- | --- |
247
+ | Node 22 or newer | The tool itself | The agent: install Node 22+ |
248
+ | A POSIX shell (`/bin/sh`) | Every command is run through a shell | Already there on Mac and Linux |
249
+ | `cp` | The scratch clone. Without it the copy still happens, byte by byte, and is slower | Already there on Mac and Linux; Windows goes straight to the plain copy |
250
+ | `git` | Working out how far a difference is from the code you changed, and putting an old build back | The agent: install git. Without it every difference is ranked as though you edited nothing |
251
+
252
+ **What every command is given, and nothing else:** `PATH`, `SHELL=/bin/sh`,
253
+ `HOME`, `TMPDIR`/`TEMP`/`TMP` (all pointed at scratch folders), `TZ=UTC`, `LANG`,
254
+ `LC_ALL`, `TERM=dumb`, `NO_COLOR=1`, `FORCE_COLOR=0`, `CI=''`, `COLUMNS=80`,
255
+ `LINES=24`, `STAYSFIXED=1`, `STAYSFIXED_SEED`, `STAYSFIXED_CLOCK`, plus anything
256
+ in the entry's own `env`. Everything else in your shell is deliberately not there:
257
+ a command that only works because of a variable you happened to have set is a
258
+ command that does not work on anybody else's machine.
259
+
260
+ **One limit worth knowing:** what a command *sent out into the world* — calls
261
+ made, files written, processes started — is only watched when the program being
262
+ run is Node. A Python or Go command is compared on what it printed, what it
263
+ answered and how it exited.
264
+
265
+ ---
266
+
267
+ ## `http` — a server or an API
268
+
269
+ Every route is read out of your own source, so a route nobody links to is checked
270
+ like any other. Booted on a spare port, one build at a time, never two at once.
271
+
272
+ | Option | What it does |
273
+ | --- | --- |
274
+ | `start` | The command that starts it. **It must listen on the `PORT` it is given** — see below. |
275
+ | `restore` | A command that puts the data back how it was, so both builds see the same rows. Without it the second run sees whatever the first one wrote. |
276
+ | `samples` | One real value per changing part of a route: `{ id: '1', slug: 'a-real-one' }`. A route with a part nobody has given a value for is reported as never looked at, never quietly skipped. |
277
+ | `requests[]` | Extra requests the source cannot show — anything needing a body or a header: `{ name, method, url, headers, body, describe, irreversible }`. |
278
+ | `irreversible[]` | Route names that spend money, send a message or destroy data. Watched at the moment they are asked for, and stopped before anything happens. |
279
+ | `env` | Extra environment for the start command. |
280
+ | `nodeEnv` | What `NODE_ENV` is set to. **Defaults to `production`.** |
281
+ | `startTimeoutMs` | How long to wait for it to start listening. |
282
+ | `watch` | Folders to notice files being written in. |
283
+
284
+ **The one contract you must meet.** The tool picks a free port and passes it as
285
+ `PORT`, with `HOST=127.0.0.1`. Your `start` command has to bind that port. A
286
+ server that hard-codes 3000 will be started, will not be found, and the run will
287
+ say it could not reach it — because two builds cannot both own port 3000, which
288
+ is the whole reason the port is chosen rather than configured.
289
+
290
+ **A `restore` command that looks like it destroys data it cannot rebuild is
291
+ refused, not run.** The refusal is reported; it is never a pass.
292
+
293
+ ### Needs on the machine
294
+
295
+ | What | Why | Who |
296
+ | --- | --- | --- |
297
+ | A POSIX shell | To run `start` and `restore` | Already there |
298
+ | Docker, **or** a dump file | Only if your server has a database. Both builds have to see identical rows or every difference is really a data difference | A person: install Docker, or point `restore` at a dump the tool may restore |
299
+
300
+ ---
301
+
302
+ ## `web` — a website
303
+
304
+ Opened in a throwaway browser with the clock stopped, motion killed, randomness
305
+ seeded and the internet cut off. What is compared is what the screen **means** —
306
+ the roles, names and states a screen reader would read — never the markup, so a
307
+ restyled page reports nothing at all.
308
+
309
+ | Option | What it does |
310
+ | --- | --- |
311
+ | `start` | The command that boots this build, listening on the `PORT` it is given. **Much better than an address** — see below. |
312
+ | `url` / `baseUrl` | Where it already is, if you accept the weaker answer. |
313
+ | `screens[]` | `{ name, url }` — the pages to visit. |
314
+ | `journeys[]` | Walks through the site: `{ name, steps: [{ click: 'Save' }, …] }`. |
315
+ | `samples` | One real value per changing part of a page address. |
316
+ | `viewport` | `{ width, height, deviceScaleFactor }`. |
317
+ | `colorScheme` | `light` or `dark`. |
318
+ | `locale`, `timezone` | Pinned so text is drawn the same way every run. |
319
+ | `allowHosts[]` / `allowed[]` | Addresses the page is allowed to reach. Everything else is refused. |
320
+ | `refuse[]` | Addresses that must never be reached even if they would work. |
321
+ | `allowWrites` | Let the page write to disk. |
322
+ | `network` | How outside calls are handled — recorded, replayed or refused. |
323
+ | `restore` | A command that puts the data back between runs. |
324
+ | `env`, `nodeEnv` | Environment for the start command. |
325
+ | `startTimeoutMs`, `timeoutMs`, `settleTimeoutMs` | How long to wait to start, to answer, and to stop moving. |
326
+ | `everyStep` | Photograph every step, not just the end of each journey. |
327
+
328
+ **Give it `start`, not `url`, whenever you can.** One address can only ever serve
329
+ one build, so with an address alone both halves of the comparison read the *same*
330
+ running copy and a paired run proves nothing. The tool says so on every run that
331
+ had only an address.
332
+
333
+ ### Needs on the machine
334
+
335
+ A browser. `npm install staysfixed` brings the **driver** — `playwright-core`,
336
+ about 13MB — and deliberately not a browser: most people checking a command-line
337
+ tool should not wait for a 570MB download they will never use.
338
+
339
+ | What | The command | Who |
340
+ | --- | --- | --- |
341
+ | The driver, if it somehow is not there | `npm install playwright-core` | The agent, without asking |
342
+ | A browser, when there is none on the machine at all | `npx playwright install chromium` | The agent, without asking |
343
+
344
+ Those are two separate failures and the tool tells them apart — "the driver is
345
+ missing" and "the driver is here and there is no browser for it to open" need
346
+ different commands, and being given the wrong one is how twenty minutes
347
+ disappear.
348
+
349
+ **It looks for a browser you already have first.** Chrome for Testing, Chromium,
350
+ Edge or Chrome, preferring one that is *not* the browser you actually use. Only
351
+ when there is nothing at all is the download asked for. And a project that already
352
+ drives its own tests with the full `playwright` package is used as it is, never
353
+ asked to install a second copy of the same driver.
354
+
355
+ The download is about 570MB in a shared cache outside your project — 371MB for
356
+ Chrome for Testing and 196MB for its headless shell — once per machine, not once
357
+ per project.
358
+
359
+ **Checks never open the browser you use** if there is any other on this machine,
360
+ and never your profile. `staysfixed browsers` says which one would be opened and
361
+ why; `staysfixed browsers --clean` clears up after a run that was killed.
362
+
363
+ Environment variables it reads, if you need to point it somewhere unusual:
364
+ `PLAYWRIGHT_BROWSERS_PATH` (where browsers were downloaded),
365
+ `STAYSFIXED_BROWSER` and `STAYSFIXED_CHROME` (use exactly this binary), and
366
+ `LOCALAPPDATA` on Windows.
367
+
368
+ ---
369
+
370
+ ## `electron` — a desktop app
371
+
372
+ Opened on its own — own settings folder, own ports, own name — and read on both
373
+ sides: the window, and the private channels behind it.
374
+
375
+ | Option | What it does |
376
+ | --- | --- |
377
+ | `binary` (or `app`) | **The built app.** On a Mac the `.app`; on Windows the `.exe`. This adapter never builds it for you. |
378
+ | `identityEnv` | If your app tells a server who it is, name the setting that carries the id: `{ YOUR_APP_DEVICE_ID: '{identity}' }`. Without this two runs can claim the same slot and fight over it — which looks exactly like a bug in your product. |
379
+ | `exercise[]` | Private channels to ask to answer: `['settings:read', 'sessions:list']`. A channel is only ever asked when it is named here, because knocking on an unknown door could do anything. |
380
+ | `journeys[]` | Walks through the window: `{ name, steps: [{ click: 'New session' }] }`. |
381
+ | `contract` | Where to read the declared doors from, when it is not obvious. |
382
+ | `appId` | The app's identifier, when it cannot be read from the bundle. |
383
+ | `args[]`, `env` | Extra arguments and environment for the app. |
384
+ | `windowMatch` | Which window to read, when the app opens more than one. |
385
+ | `startTimeoutMs`, `settleTries`, `settleGapMs` | How long to wait for it to open, and how many times to re-read the screen until two readings agree. |
386
+
387
+ **Note:** `doctor` sometimes says "set `app.binary`". That is version 1's picture
388
+ setting. The difference engine reads **`electron.binary`** (or `electron.app`).
389
+ Set the one under `electron`.
390
+
391
+ ### Needs on the machine
392
+
393
+ | What | Why | Who |
394
+ | --- | --- | --- |
395
+ | Nothing to download | The app is its own Chromium and is driven over its own debugging port | — |
396
+ | A **built** app | The adapter never builds it | You: build it the way you normally do — often `npm run build` then `npm run package` — and point `electron.binary` at the result |
397
+ | Two free TCP ports per run | One to drive the window, one to read the main process | Automatic |
398
+ | `/bin/ps` | Finding and clearing anything an interrupted run left running | Already there on Mac and Linux |
399
+
400
+ The app is launched with its own `HOME`, `TMPDIR`, `XDG_*` folders, `TZ=UTC` and a
401
+ fixed seed and clock, so it cannot read your real settings and cannot leave
402
+ anything behind in them.
403
+
404
+ ---
405
+
406
+ ## `android` — an APK on an emulator
407
+
408
+ Installed on an emulator of its own, walked, then removed. Compared against the
409
+ stored record: whether two emulator snapshots come back byte for byte is unproven,
410
+ and every run says which mode it used.
411
+
412
+ | Option | What it does |
413
+ | --- | --- |
414
+ | `apk` | The built package. Left out, it looks in the usual build folders. |
415
+ | `avd` | Which emulator to use. Left out, it takes the first one that is **not** a Play Store image. |
416
+ | `serial` | A device already plugged in, or an emulator already running. |
417
+ | `journeys[]` / `screens[]` | Walks through the app: `{ name, steps: [{ tap: 'Sign in' }] }`. Left out, it opens the app and reads the first screen. |
418
+ | `headless` | `false` shows the emulator window. |
419
+ | `reset` | `'uninstall'` removes the app between runs. |
420
+ | `locale`, `timezone` | Pinned. |
421
+ | `allowTo[]` | Addresses the app is allowed to reach. |
422
+ | `settleTries` | How many times to re-read the screen until two readings agree. |
423
+
424
+ ### Needs on the machine
425
+
426
+ **Java is not needed.** The APK is read directly. That surprises people, so it is
427
+ worth saying: everything the package declares can be listed with nothing
428
+ installed at all.
429
+
430
+ To actually run one:
431
+
432
+ | What | The command | Who |
433
+ | --- | --- | --- |
434
+ | The Android command line tools | On a Mac: `brew install --cask android-commandlinetools`, then `sdkmanager "platform-tools" "emulator"` | The agent — except **Google's licence has to be accepted once, by a person** |
435
+ | The emulator, if the SDK is there without it | `sdkmanager --install emulator` | The agent |
436
+ | A system image and an emulator to run it | `sdkmanager --install "system-images;android-33;google_apis;arm64-v8a"` then `avdmanager create avd -n staysfixed -k "system-images;android-33;google_apis;arm64-v8a"` | The agent |
437
+ | A built APK | Usually `./gradlew assembleRelease`, or point `android.apk` at one you have | You |
438
+
439
+ **Pick a plain Google APIs image, never a Play Store one.** A Play Store device
440
+ refuses root forever, and without root the files the app writes cannot be seen at
441
+ all — so that whole channel goes dark and the run never mentions it as a reason
442
+ things look quiet. This is the most likely way to lose coverage on Android without
443
+ noticing.
444
+
445
+ Environment variables it reads: `ANDROID_HOME`, `ANDROID_SDK_ROOT`,
446
+ `ANDROID_AVD_HOME`. Without them it looks in `~/Library/Android/sdk`,
447
+ `~/Android/Sdk`, `~/AppData/Local/Android/Sdk`, `/usr/local/share/android-sdk`
448
+ and `/opt/android-sdk`.
449
+
450
+ Outbound calls are stopped with a device-wide proxy, set before the run and
451
+ cleared after it. Nothing inside an encrypted connection is read and **no
452
+ certificate is installed on the emulator**.
453
+
454
+ ---
455
+
456
+ ## `ios` — an app on the simulator
457
+
458
+ Installed on a simulator, opened, and read — the same roles, names and states a
459
+ person hears read out to them. One build at a time. macOS only: nothing on Linux
460
+ or Windows can run an iOS simulator.
461
+
462
+ | Option | What it does |
463
+ | --- | --- |
464
+ | `app` | The built app bundle for the simulator. Left out, it looks where builds land. |
465
+ | `deviceType`, `runtime` | Which simulator and which system: `'iPhone 17'`, `'iOS 26.4'`. Left out it picks a sensible one and says which. |
466
+ | `device` | An exact simulator that already exists, by name or id. |
467
+ | `journeys[]` | Walks through the app: `{ name, steps: [{ tap: 'Sign in' }] }`. |
468
+ | `openUrls[]` | Addresses to open the app with, for a screen only reached by a link. |
469
+ | `appearance` | `light` or `dark`. |
470
+ | `reset` | `'erase'` wipes the simulator between runs. |
471
+ | `logProcess` | Which process to read the logs of, when it is not the app itself. |
472
+
473
+ ### Needs on the machine
474
+
475
+ | What | The command | Who |
476
+ | --- | --- | --- |
477
+ | Xcode | **A person.** It is a free download from the Mac App Store, it is about 10 gigabytes, and it has a licence to accept the first time it opens. Once it is installed nothing else here needs doing by hand | A person |
478
+ | The command line tools | `xcode-select --install` | The agent |
479
+ | An iOS runtime to run | `xcodebuild -downloadPlatform iOS` | The agent |
480
+ | A built app | `xcodebuild -scheme <YourScheme> -sdk iphonesimulator -derivedDataPath build`, or point `ios.app` at one | You. The adapter never builds it: guessing a scheme produces a build nobody asked for |
481
+
482
+ The programs it uses, so you can check for them yourself: `xcrun`, `simctl`,
483
+ `xcode-select`, `xcodebuild`, `clang` and `plutil`.
484
+
485
+ **`clang` is the one that goes quiet.** Reading what a control on screen *means*
486
+ is done by a small reader compiled for the simulator SDK at run time. Without the
487
+ command line tools that compile fails, the meaning channel is dark, and what is
488
+ left is thinner than it looks. `staysfixed doctor` says so; a run that has already
489
+ started does not always.
490
+
491
+ If `simctl` is installed but does not answer, open `Simulator.app` once, or run
492
+ `sudo xcode-select --reset`.
493
+
494
+ ---
495
+
496
+ ## `windows` — a native Windows program
497
+
498
+ A Windows window can only be read from Windows, so this needs a machine running
499
+ one. An ssh host you already have counts, including a WSL shell on a Windows
500
+ machine, and **nothing is installed on that machine**.
501
+
502
+ | Option | What it does |
503
+ | --- | --- |
504
+ | `host` | The ssh host that reaches a logged-in Windows desktop. |
505
+ | `remoteExe` | The built program already on that machine: `'C:\\Users\\you\\YourApp\\YourApp.exe'`. Much faster than copying. |
506
+ | `exe` | The built program here, to be copied over before each run. |
507
+ | `watchDirs[]` | Folders on that machine to watch for files the app writes. |
508
+ | `journeys[]` | Walks through the window. |
509
+ | `args[]`, `cwd` | Arguments and working folder for the program. |
510
+
511
+ ### Needs on the machine
512
+
513
+ **On your machine:**
514
+
515
+ | What | Why |
516
+ | --- | --- |
517
+ | `ssh` | Everything goes down one ssh connection |
518
+ | `tar` | How a local build is streamed across — not `scp` |
519
+ | An entry in `~/.ssh/config` with **key-based** login | The connection is made with `BatchMode=yes`, so a host that wants a password is reported as unreachable rather than sitting there waiting for one nobody will type. Test it with `ssh <host> true` |
520
+
521
+ **On the far machine:**
522
+
523
+ | What | Why | Who |
524
+ | --- | --- | --- |
525
+ | `node` | The small program that reads the screen is sent down the connection each run and disappears when it closes. Nothing is written to that machine's disk | The agent: `ssh <host> 'sudo apt-get install -y nodejs'`, or whatever that machine installs packages with |
526
+ | `powershell.exe` | Reading a Windows window. It is found at one of three known paths, not by asking the shell — that question answers "no" on a machine with Windows right there | Already there on any Windows machine |
527
+ | **Somebody signed in** | UI Automation reads a desktop session. There is nothing to read on a desktop nobody has signed into | A person: sign in once and leave the session running. Locking the screen afterwards is fine; signing out is not |
528
+ | The screen unlocked | Only for full-screen pictures. Everything else works locked | A person |
529
+
530
+ **Two builds can never run at once**, because Windows shows one desktop. Runs are
531
+ one after the other and the comparison is genuinely weaker here than anywhere
532
+ else. If your Windows product is Electron — most are — use the `electron` block
533
+ instead: it is driven over the debug port, from any machine, and this adapter
534
+ refuses Electron apps on purpose so nobody gets the weaker answer by accident.
535
+
536
+ ---
537
+
538
+ ## Environment variables, in one place
539
+
540
+ Ones you might want to set:
541
+
542
+ | Variable | What it does |
543
+ | --- | --- |
544
+ | `NO_COLOR`, `STAYSFIXED_NO_COLOR` | Plain text, no colour. `--no-color` does the same thing. |
545
+ | `STAYSFIXED_OFFLINE` | Any value stops `doctor` dialling other machines. |
546
+ | `PLAYWRIGHT_BROWSERS_PATH` | Where browsers were downloaded. |
547
+ | `STAYSFIXED_BROWSER`, `STAYSFIXED_CHROME` | Use exactly this browser binary. |
548
+ | `ANDROID_HOME`, `ANDROID_SDK_ROOT`, `ANDROID_AVD_HOME` | Where the Android SDK and its emulators are. |
549
+
550
+ Everything else — the frozen clock, the seed, the temporary folders — is set *by*
551
+ the tool, into the thing being checked, and is listed under `process` above.
552
+
553
+ ---
554
+
555
+ ## When you are not sure
556
+
557
+ ```
558
+ staysfixed doctor # what this machine can drive, and the exact command for each gap
559
+ staysfixed doctor --json # the same answer as one object, for an agent
560
+ staysfixed init --force # read the repository again and rewrite the settings
561
+ ```
562
+
563
+ Nothing `doctor` lists as missing got there without failing a real check first, so
564
+ you are never sent shopping for something that already works.
package/docs/watching.md CHANGED
@@ -76,13 +76,18 @@ and both windows open exactly where they would have anyway.
76
76
  | --- | --- |
77
77
  | `--watch` | Open the panel. |
78
78
  | `--watch-side left` | Push the app to the left edge and put the panel on its right. The default is `right`. |
79
- | `--watch-width 520` | How wide the panel is, in pixels. The default is 480; anything under 240 or over 900 is brought back to the nearest of the two. |
79
+ | `--watch-width 520` | How wide the panel is, in pixels. The default is 480 on `check` and 460 on `walk` — two panels, two defaults. Anything under 240 is **refused** with a message, because a panel narrower than that cannot show a before and an after side by side — which is the only reason to open one. Anything over 900 is brought down to 900. |
80
80
  | `--no-snap` | Leave both windows exactly where they are. Nothing is moved. |
81
81
  | `--no-keep-open` | Close the panel the moment the run ends. By default it stays up so you can look at what changed. |
82
82
  | `--watch-front` | Bring the panel to the front. See below for why that is not the default. |
83
83
  | `--profile` | Nothing to do with the panel: prints where the seconds went after the run. |
84
84
 
85
- Both `staysfixed check` and `staysfixed walk` take all of them.
85
+ Both `staysfixed check` and `staysfixed walk` take all of them, with one gap that
86
+ is worth knowing about rather than finding out: **`--profile` prints nothing on a
87
+ difference-engine check.** It works on `staysfixed walk`, and on the version 1
88
+ picture check — `staysfixed check --pictures --profile` or `--guards --profile`.
89
+ The difference engine accepts the flag and does not yet keep the timings, so a
90
+ plain `staysfixed check --profile` runs the check and prints no timing table.
86
91
 
87
92
  `--watch` and `--json` ask for opposite things — a window to look at, and output
88
93
  for a script to read. Ask for both and the tool says so in one line and carries
@@ -167,10 +172,11 @@ gets compared.
167
172
 
168
173
  ## Where the time went
169
174
 
170
- `--profile` is the other half of this work and needs no window:
175
+ `--profile` is the other half of this work and needs no window. On `walk`, and on
176
+ the version 1 picture check:
171
177
 
172
178
  ```
173
- $ staysfixed check --profile
179
+ $ staysfixed check --pictures --profile
174
180
 
175
181
  ...
176
182
 
@@ -1,12 +1,23 @@
1
1
  /**
2
- * An Electron desktop app, checked end to end.
2
+ * A desktop app, photographed screen by screen. THIS IS VERSION 1'S SHAPE.
3
3
  *
4
- * The difference from a web app is small on purpose: instead of an address, you
5
- * give it the executable to launch. Everything after that screens, freezing,
6
- * masks, guards works exactly the same way, because an Electron window is a
7
- * Chromium window with a different frame around it.
4
+ * Read this before copying it anywhere. Stays Fixed has two halves, and this file
5
+ * belongs to the older one the picture check, reached with `staysfixed check
6
+ * --pictures`. It is still supported and still works.
7
+ *
8
+ * **It is not what `staysfixed check` runs.** The everyday command is the
9
+ * difference engine, whose settings are a different shape: `product`, `source`,
10
+ * `process`, `http`, `web`, `electron`, `android`, `ios`, `windows`. For a desktop
11
+ * app there it is `electron: { binary: '...' }`, not `app: { binary: '...' }` —
12
+ * one of the two most common ways to write settings the adapter never reads.
13
+ *
14
+ * **For the difference engine, do not copy an example.** Run `staysfixed init`:
15
+ * it reads your project and writes every option, filled in from your own code,
16
+ * with the ones that do not apply commented out rather than left out. Every key
17
+ * is documented in `docs/settings.md`.
18
+ *
19
+ * What follows is the picture check's settings, fully commented.
8
20
  */
9
-
10
21
  import path from 'node:path';
11
22
 
12
23
  /**
@@ -1,10 +1,27 @@
1
1
  /**
2
- * A web app, checked end to end.
2
+ * A web app, photographed screen by screen. THIS IS VERSION 1'S SHAPE.
3
3
  *
4
- * Copy this to the root of your project as `staysfixed.config.js`, point it at
5
- * your dev server, and delete the parts you do not need. Every option here is
6
- * optional except `app` and `screens` the defaults are chosen so that a config
7
- * of five lines already works.
4
+ * Read this before copying it anywhere. Stays Fixed has two halves, and this file
5
+ * belongs to the older one the picture check, reached with `staysfixed check
6
+ * --pictures`. It is still supported and still works, and this is what its
7
+ * settings look like: an `app` to open and `screens` to photograph.
8
+ *
9
+ * **It is not what `staysfixed check` runs.** The everyday command is the
10
+ * difference engine, and its settings are a different shape entirely — `product`,
11
+ * `source`, `process`, `http`, `web`, `electron`, `android`, `ios`, `windows`.
12
+ * Copy this file into a project as `staysfixed.config.js` and a plain
13
+ * `staysfixed check` will tell you the settings do not name anything to open.
14
+ *
15
+ * **For the difference engine, do not copy an example at all.** Run `staysfixed
16
+ * init` in your project: it reads what is actually there and writes a settings
17
+ * file with every option in it, the ones that do not apply commented out rather
18
+ * than left out, filled in from your own code. That is better than any example
19
+ * here could be, because it is about your project. Every key is documented in
20
+ * `docs/settings.md`.
21
+ *
22
+ * With that said — for a picture check, copy this to the root of your project,
23
+ * point it at your dev server, and delete the parts you do not need. Every option
24
+ * here is optional except `app` and `screens`.
8
25
  */
9
26
 
10
27
  /** @type {import('../src/types.js').StaysFixedConfig} */