@asrunjs/asrun 0.6.1 → 0.8.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 (74) hide show
  1. package/.claude/settings.json +5 -5
  2. package/.claude/skills/exploring-cases/references/assertion-targets.md +103 -103
  3. package/.claude/skills/exploring-cases/references/handoff-card.js +128 -128
  4. package/.claude/skills/making-cases/references/gaps.md +36 -36
  5. package/.claude/skills/making-cases/references/splitting.md +28 -28
  6. package/AGENTS.md +6 -0
  7. package/README.md +33 -9
  8. package/bin/asrun.mjs +19 -2
  9. package/dist/lib/ffmpeg.js +21 -10
  10. package/dist/lib/migrations.d.ts +106 -0
  11. package/dist/lib/migrations.js +106 -0
  12. package/dist/lib/paths.d.ts +9 -0
  13. package/dist/lib/paths.js +12 -1
  14. package/dist/lib/registry.d.ts +31 -0
  15. package/dist/lib/registry.js +66 -0
  16. package/dist/lib/runtime-deps.d.ts +59 -0
  17. package/dist/lib/runtime-deps.js +161 -0
  18. package/dist/lib/tar.d.ts +5 -0
  19. package/dist/lib/tar.js +79 -0
  20. package/dist/lib/update-check.d.ts +52 -0
  21. package/dist/lib/update-check.js +223 -0
  22. package/dist/migrations/index.d.ts +23 -0
  23. package/dist/migrations/index.js +22 -0
  24. package/dist/migrations/manifest.json +1 -0
  25. package/dist/scripts/build.js +7 -0
  26. package/dist/scripts/cli.js +33 -7
  27. package/dist/scripts/doctor.js +51 -9
  28. package/dist/scripts/go.mjs +43 -11
  29. package/dist/scripts/heal-evidence.mjs +149 -143
  30. package/dist/scripts/init.d.ts +9 -1
  31. package/dist/scripts/init.js +127 -23
  32. package/dist/scripts/migrate.d.ts +1 -0
  33. package/dist/scripts/migrate.js +259 -0
  34. package/dist/scripts/reset.d.ts +1 -0
  35. package/dist/scripts/reset.js +233 -0
  36. package/dist/scripts/run.js +25 -24
  37. package/dist/scripts/upgrade.d.ts +1 -0
  38. package/dist/scripts/upgrade.js +256 -0
  39. package/dist/ui/server/app.js +18 -0
  40. package/dist/ui/server/index.js +3 -0
  41. package/dist/ui/server/update.d.ts +18 -0
  42. package/dist/ui/server/update.js +87 -0
  43. package/dist/ui/shared/types.d.ts +24 -0
  44. package/docs/agents/framework-dev.md +99 -1
  45. package/docs/commands.md +96 -9
  46. package/docs/install.md +92 -19
  47. package/docs/troubleshooting.md +43 -14
  48. package/package.json +35 -33
  49. package/templates/.claude/settings.json +5 -5
  50. package/templates/AGENTS.md +1 -0
  51. package/templates/CLAUDE.md +1 -1
  52. package/templates/gitignore +13 -13
  53. package/templates/package.json +33 -32
  54. package/templates/playwright.config.ts +3 -3
  55. package/templates/pnpm-workspace.yaml +8 -5
  56. package/templates/seed.spec.ts +9 -9
  57. package/templates/tsconfig.json +14 -14
  58. package/ui/dist/assets/{dist-vmf3fnkM.js → dist-BC_P1Gnf.js} +1 -1
  59. package/ui/dist/assets/{dist-O_RVi4tP.js → dist-BgPbx2GZ.js} +1 -1
  60. package/ui/dist/assets/{dist-jPS6HCJ5.js → dist-BpkTvixI.js} +1 -1
  61. package/ui/dist/assets/{dist-DEPJZ3Me.js → dist-CJiwxr1r.js} +1 -1
  62. package/ui/dist/assets/{dist-CPLhRzom.js → dist-CavmZIv7.js} +1 -1
  63. package/ui/dist/assets/{dist-DZ7RiqmA.js → dist-ChBHX4rh.js} +1 -1
  64. package/ui/dist/assets/{dist-BudNAjOv.js → dist-D6jmnDr-.js} +1 -1
  65. package/ui/dist/assets/{dist-C-eCSFja.js → dist-DoChb6QQ.js} +1 -1
  66. package/ui/dist/assets/{dist-LixzGniF.js → dist-Do_MhHKF.js} +1 -1
  67. package/ui/dist/assets/{dist-CO4heSzh.js → dist-N-o4QIaZ.js} +1 -1
  68. package/ui/dist/assets/index-Cy2l91qV.css +2 -0
  69. package/ui/dist/assets/{index-BIGt8Hfz.js → index-DGQ5logJ.js} +129 -129
  70. package/ui/dist/favicon.svg +11 -11
  71. package/ui/dist/index.html +2 -2
  72. package/ui/dist/logos/claude.svg +1 -1
  73. package/ui/dist/logos/opencode.svg +1 -1
  74. package/ui/dist/assets/index-CTmnF0Wp.css +0 -2
@@ -0,0 +1,87 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { checkForUpdate, formatNotice, upgradeCommand, useColor } from '../../lib/update-check.js';
3
+ /**
4
+ * The release check of the web UI: once when the server starts, then once a day at 00:00 UTC. The answer is cached in
5
+ * `.tmp/` (see lib/update-check.ts), so several tabs and the command line share one registry read. Pages learn about a
6
+ * change through the `update` event of `/api/events`.
7
+ */
8
+ export const updateEvents = new EventEmitter();
9
+ // One listener per open page; the default warning at ten tabs is noise.
10
+ updateEvents.setMaxListeners(0);
11
+ let status = null;
12
+ let inFlight = null;
13
+ /** The notice already printed, so a daily re-check of the same release does not repeat it. */
14
+ let printedNotice = null;
15
+ /** What the UI should show now; before the first answer every field that needs one is null. */
16
+ export function updateInfo() {
17
+ if (status === null) {
18
+ return {
19
+ current: null,
20
+ latest: null,
21
+ available: false,
22
+ checkedAt: null,
23
+ kind: null,
24
+ command: null,
25
+ disabled: false,
26
+ mocked: false,
27
+ error: null,
28
+ };
29
+ }
30
+ return {
31
+ current: status.current,
32
+ latest: status.latest,
33
+ available: status.available,
34
+ checkedAt: status.checkedAt,
35
+ kind: status.kind,
36
+ command: status.available ? upgradeCommand(status.kind) : null,
37
+ disabled: status.disabled,
38
+ mocked: status.mocked,
39
+ error: status.error,
40
+ };
41
+ }
42
+ /**
43
+ * Asks the registry again and tells the open pages. `force` goes past a fresh cache, which is what the startup and the
44
+ * daily checks do; the answer is written back to `.tmp/`, so the command line shares it. Overlapping calls share one
45
+ * read: the daily timer and the first page load can arrive together.
46
+ */
47
+ export function refreshUpdate(force = true) {
48
+ if (inFlight !== null)
49
+ return inFlight;
50
+ inFlight = (async () => {
51
+ try {
52
+ status = checkForUpdate({ force });
53
+ }
54
+ catch {
55
+ // checkForUpdate reports a failed read instead of throwing; if that ever changes, keep serving the last answer.
56
+ }
57
+ const answer = updateInfo();
58
+ const notice = status === null || status.disabled ? null : formatNotice(status, { color: useColor(process.stdout) });
59
+ // Printed on the terminal only when something changed: once at startup, and again for a newer release.
60
+ if (notice !== printedNotice && notice !== null) {
61
+ printedNotice = notice;
62
+ console.log(notice);
63
+ }
64
+ if (notice === null)
65
+ printedNotice = null;
66
+ updateEvents.emit('update', answer);
67
+ return answer;
68
+ })().finally(() => {
69
+ inFlight = null;
70
+ });
71
+ return inFlight;
72
+ }
73
+ /** Today's check, then one at every UTC midnight while the server runs. */
74
+ export function startUpdateWatcher() {
75
+ void refreshUpdate(true);
76
+ scheduleNextCheck();
77
+ }
78
+ function scheduleNextCheck() {
79
+ const now = new Date();
80
+ const nextMidnight = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1);
81
+ const timer = setTimeout(() => {
82
+ void refreshUpdate(true);
83
+ scheduleNextCheck();
84
+ }, Math.max(1_000, nextMidnight - now.getTime()));
85
+ // The check must never keep the server process alive on its own.
86
+ timer.unref();
87
+ }
@@ -332,6 +332,30 @@ export interface InfoResponse {
332
332
  suites: string[];
333
333
  caseIds: string[];
334
334
  }
335
+ /**
336
+ * What the web UI shows about a newer release. The first check of a server run may still be running, so every field
337
+ * that needs an answer is null until it finished.
338
+ */
339
+ export interface UpdateInfo {
340
+ /** The version of the framework this server runs; null until the first check finished. */
341
+ current: string | null;
342
+ /** The newest release on the registry; null when there is none (or the registry could not be read). */
343
+ latest: string | null;
344
+ /** True when `latest` is newer than `current`. */
345
+ available: boolean;
346
+ /** When the answer was fetched, ISO. A cached answer keeps the time of the fetch. */
347
+ checkedAt: string | null;
348
+ /** `installed` when the framework comes from the registry, `checkout` for the framework's own git repository. */
349
+ kind: 'installed' | 'checkout' | null;
350
+ /** The line to run to get the newest release; null when there is nothing to upgrade. */
351
+ command: string | null;
352
+ /** True when ASRUN_NO_UPDATE_CHECK turned the check off. */
353
+ disabled: boolean;
354
+ /** True when the answer is a development mock (ASRUN_UPDATE_MOCK_* or `.tmp/update-mock.json`). */
355
+ mocked: boolean;
356
+ /** Why the last check could not read the registry; the UI stays quiet then. */
357
+ error: string | null;
358
+ }
335
359
  /** Everything that belongs to one case. Shown before a delete or rename, and returned by them. */
336
360
  export interface CaseImpact {
337
361
  id: string;
@@ -44,10 +44,19 @@ is relevant to a project that uses asrun as a dependency.
44
44
 
45
45
  `pnpm demo:reset` deletes the database and seeds it again; logins must be redone afterwards. Demo accounts are in
46
46
  `examples/demo/README.md`.
47
+
48
+ `.env` is per machine and gitignored, so a fresh clone has none: `init --root examples/demo` writes it in the folder
49
+ the command runs in (`ASRUN_ROOT=examples/demo`), and `pnpm go` does the same in its third step, so no manual step is
50
+ needed. Without it `pnpm go` still finds the demo through its fallback, but the other commands (`pnpm asrun`,
51
+ `pnpm signin`, `pnpm explore`) resolve the workspace from `ASRUN_ROOT` and then from the current folder, and stop
52
+ with "No workspace found: this is the framework folder".
47
53
  - Everything the framework reads for a project goes through `ROOT` in `lib/paths.ts` (the workspace root); its own
48
54
  assets (templates, docs, skills, its `node_modules`) go through `FRAMEWORK_ROOT`. Never write into
49
55
  `FRAMEWORK_ROOT` at run time.
50
56
  - After changing `lib/`, run `pnpm build` before exercising the demo's specs.
57
+ - `pnpm build` copies `scripts/*.mjs` into `dist/scripts/`, where they sit one folder deeper than in this repository.
58
+ `pnpm verify:pack` (part of CI) checks that layout on a packed tarball; the checkout alone hides such bugs, because
59
+ there `scripts/go.mjs` is next to `bin/asrun.mjs`.
51
60
  - `templates/` is the source of the files a workspace gets. When a template changes, bump `version` in
52
61
  `package.json` so `pnpm asrun doctor` in a project shows that it is behind.
53
62
  - `init` owns two lists: `GENERATED` (rewritten by `--update`; the commands and the Playwright files) and `ONCE`
@@ -57,6 +66,74 @@ is relevant to a project that uses asrun as a dependency.
57
66
  - A project's rules never flow back here. The framework reads a workspace only through `ROOT` at run time; nothing
58
67
  under `templates/` or `.claude/skills/` is generated from a project.
59
68
 
69
+ ### Seeing the release notice without a release
70
+
71
+ `lib/update-check.ts` asks the registry for the newest version. To see the notice without publishing anything, make it
72
+ answer with a made-up version. The check is the same code the command line and the web UI run, so both show exactly
73
+ what a real release would produce.
74
+
75
+ ```powershell
76
+ # A framework checkout (the default in this repository): the notice says to update with git
77
+ $env:ASRUN_UPDATE_MOCK_LATEST = '9.9.9'
78
+ pnpm asrun doctor # stderr: ... Update the framework checkout: git pull
79
+ pnpm ui # top-bar badge, and the dialog offers git pull
80
+
81
+ # A registry install: the notice offers "pnpm asrun upgrade"
82
+ $env:ASRUN_UPDATE_MOCK_KIND = 'installed'
83
+ pnpm ui
84
+ ```
85
+
86
+ - `ASRUN_UPDATE_MOCK_LATEST=<version>` replaces the registry answer; `ASRUN_UPDATE_MOCK_KIND=installed|checkout`
87
+ replaces the install-kind detection. Each field falls back, in this order, to the environment, then to
88
+ `.tmp/update-mock.json` at the workspace root (in this repository that is `examples/demo/.tmp/update-mock.json`,
89
+ because `.env` points `ASRUN_ROOT` there; for example `{"latest": "9.9.9", "kind": "installed"}`). A mock is never
90
+ written to the `.tmp/update-check.json` cache, so deleting the variable or the file brings the real check straight
91
+ back.
92
+ - The environment is read when a process starts: a running `pnpm ui` must be restarted after changing it. The mock file
93
+ is read on every check, so editing it and pressing **Check again** (or `GET /api/update?refresh=1`) is enough.
94
+ - The UI marks a mocked answer with a **Dev mock** tag, and the API returns `mocked: true`.
95
+ - `ASRUN_NO_UPDATE_CHECK=1` still wins over both: it turns the notice off entirely.
96
+
97
+ `GET /api/update` answers with the state of the server's last check; `GET /api/update?refresh=1` checks again first
98
+ (it is what the dialog's **Check again** button calls).
99
+
100
+ ### Optional runtime packages
101
+
102
+ A dependency with an install script makes `pnpm dlx @asrunjs/asrun` stop at pnpm's build-approval picker: the dlx
103
+ sandbox does not read the workspace's `allowBuilds`, and in a pipeline nobody can answer the prompt. So a package that
104
+ is heavy and optional (the bundled `ffmpeg-static` today) is not a dependency at all:
105
+
106
+ - Add it to `RUNTIME_EXTRAS` in `lib/runtime-deps.ts`, with the line the person sees (`provides`), a `probe` command
107
+ that proves the machine already has the capability, and `builds: true` when the package runs an install script.
108
+ - `init` (and therefore `go`, whose third step runs it) writes the `allowBuilds` entry and installs the package as a
109
+ dependency of the workspace, only when the probe fails; `--no-extras` leaves it out. `BUILD_APPROVALS` is the one
110
+ list `doctor`, `go` and the template of `pnpm-workspace.yaml` follow.
111
+ - Resolve an extra from `ROOT` first, then from the framework's own install, the way `lib/ffmpeg.ts` does.
112
+ - `doctor` warns while an extra is missing, and `verify:pack` fails when one of them becomes a runtime dependency
113
+ again (that would bring the dlx prompt back). CI runs it on a packed tarball.
114
+
115
+ ### Workspace migrations
116
+
117
+ A release can require an *existing* workspace to change; `docs/commands.md` has the user's view. The registry is
118
+ `migrations/index.ts`, and the interface plus the helpers live in `lib/migrations.ts`:
119
+
120
+ - `version` is the release that introduces the change. `pnpm asrun migrate` runs an entry when
121
+ `migrations.since < version <= installed`, in version order, and moves `since` only after every entry of that
122
+ release has been processed, so a failed run repeats the release instead of skipping its other entries.
123
+ - `auto: true` with an `apply` runs after the install, when `detect` says the workspace is not migrated yet. Keep it
124
+ idempotent: a run that failed repeats the entry.
125
+ - Anything a person has to do is `manual: string[]`. The id is remembered in `.asrun.json` (`migrations.manual`) and
126
+ `doctor` keeps reminding until `pnpm asrun migrate --ack <id>`.
127
+ - `breaking: true` makes `pnpm asrun upgrade` ask before installing anything. The pre-install check reads
128
+ `dist/migrations/manifest.json` out of the target tarball, so `pnpm build` has to write it (`scripts/build.ts`
129
+ serialises `MIGRATIONS` with `toManifest`). `verify:pack` asserts that a packed tarball carries the manifest and that
130
+ the reader in `lib/tar.ts` finds it.
131
+ - Append only: never delete or renumber an entry, because a workspace on an older release may still need it. A change
132
+ that only concerns brand-new workspaces belongs in `templates/`, not in a migration.
133
+ - `scripts/migrate.ts` applies the *installed* package's registry, so it never runs an entry from a release that is not
134
+ installed. `scripts/upgrade.ts` installs first, then runs `init --update`, `migrate` and `doctor` through the newly
135
+ installed bin (`bin/asrun.mjs`), never through the process that started the upgrade.
136
+
60
137
  ### Installing this repository as a Git dependency (private)
61
138
 
62
139
  The npm package is the public channel for users. This repository is private, so a Git dependency works only on a
@@ -151,6 +228,27 @@ Every lead, label and tooltip is written for the person using the UI, never for
151
228
  - **Say what the user sees.** "The run appears in the Jobs bar", not "the command runs in a one-shot session"; "the
152
229
  browser shows a small card", not "the agent pauses on a `writes` step".
153
230
 
231
+ ## Releases
232
+
233
+ Nothing bumps the version by hand. A merge to `main` runs `.github/workflows/release.yml`, and Release Please opens
234
+ (or refreshes) one `chore(release): X.Y.Z` pull request holding the next `package.json` version and the matching
235
+ `CHANGELOG.md` entry. Merging that pull request is the release: the same run tags the commit and publishes to npm.
236
+
237
+ - Tags carry the package component, which manifest mode adds by default: `asrun-v0.7.0`, not `v0.7.0`. The publish check
238
+ accepts either, so a hand-made `v0.7.0` still works.
239
+
240
+ - The next version comes from the commit messages on `main`, and a squashed pull request title becomes that message.
241
+ `fix:` is a patch, `feat:` a minor, `feat!:` a major (the project is past 0.x, so a breaking change releases
242
+ `1.0.0`). `chore:`, `ci:` and the rest ship nothing and open no release at all.
243
+ `.github/workflows/pr-title.yml` enforces the prefixes.
244
+ - `templates/`, `docs/`, `ui/dist/` and `.claude/` are shipped files: a change to them needs `fix:` or `feat:`, not
245
+ `docs:`, or no release is made and no workspace ever sees the new version (`pnpm asrun doctor` compares it).
246
+ - `release.yml` is the only uploader. When an upload fails after the tag exists, run it by hand with the tag instead of
247
+ re-running the failed job: `gh workflow run release.yml -f tag=asrun-v1.0.0` (`-f dry_run=true` packs without
248
+ uploading). A re-run of the push run finds the release already made and skips publishing.
249
+ - npm authentication is trusted publishing (OIDC, no secret): the workflow filename in the package's trusted publisher
250
+ settings is `release.yml`. A private repository gets no provenance, so the package page shows no provenance badge.
251
+
154
252
  ## Before you open a pull request
155
253
 
156
254
  ```bash
@@ -162,6 +260,6 @@ pnpm ui:build # ui/dist/ must match ui/web and ui/server
162
260
  ```
163
261
 
164
262
  - `pnpm build` and `pnpm ui:build` also run from `prepare` and `prepack`, so a published package is complete.
165
- - Bump `version` when `templates/` or the shipped docs change; `pnpm asrun doctor` in a project compares it.
263
+ - The version is not bumped by hand (see Releases).
166
264
  - Keep the demo's committed expectations (`profiles.yaml`, `cases/`, `tests/`, baselines) in step with the demo site.
167
265
  Baselines are a person's decision: never let an agent regenerate them.
package/docs/commands.md CHANGED
@@ -7,8 +7,11 @@ thin wrappers, so `pnpm asrun`, `pnpm signin` and the rest work as below. The sa
7
7
 
8
8
  | I want to... | Command |
9
9
  |---|---|
10
- | Install / update the framework | `pnpm dlx @asrunjs/asrun init --install`, `pnpm add -D @asrunjs/asrun@latest` |
10
+ | Install the framework in a project | `pnpm dlx @asrunjs/asrun init --install` |
11
+ | Upgrade the framework and migrate the workspace | `pnpm asrun upgrade [--version <v>] [--dry-run]` |
11
12
  | Prepare or check the workspace | `pnpm asrun init [--update]`, `pnpm asrun doctor` |
13
+ | See or apply the workspace migrations after a manual install | `pnpm asrun migrate [--list] [--dry-run]` |
14
+ | Start over with fresh dependencies | `pnpm asrun reset [--yes] [--state] [--lockfile]` |
12
15
  | Start the web UI | `pnpm go`, `pnpm ui`, `pnpm ui:dev` |
13
16
  | See which profiles are logged in | `pnpm status [--env <env>]` |
14
17
  | Log in a profile (a person, with 2FA) | `pnpm signin <profile-id>` |
@@ -150,26 +153,110 @@ pnpm cleanup --env staging --forget
150
153
 
151
154
  A write case registers everything it creates (named `[asrun] ...`) and removes it after the test, also after a
152
155
  failure. Whatever could not be removed is listed in the report and by `cleanup`. Delete it by hand in the product,
153
- then run `--forget` so it is not listed again. `--env` is required.
156
+ then run `--forget` so it is not listed again. `--env` is required. `cleanup` is about test data in the product;
157
+ `pnpm asrun reset` (below) is the command that removes workspace files and dependencies.
154
158
 
155
159
  ## Web UI
156
160
 
157
161
  ```bash
158
- pnpm go # install + build + start + open the browser (the usual command)
159
- pnpm ui # build the web app and serve http://127.0.0.1:4173
160
- pnpm ui:dev # Vite on :5173 with hot reload, API in watch mode
162
+ pnpm go # install + build + start + open the browser (the usual command)
163
+ pnpm go --no-open # same, without opening a browser
164
+ pnpm go --no-ui # install and prepare the workspace, then stop (no server)
165
+ pnpm go --dry-run # only print the resolved workspace, framework and asrun command
166
+ pnpm ui # build the web app and serve http://127.0.0.1:4173
167
+ pnpm ui:dev # Vite on :5173 with hot reload, API in watch mode
161
168
  ```
162
169
 
163
170
  The UI only listens on `127.0.0.1`. Set `ASRUN_UI_PORT` to change the port (`ASRUN_UI_WEB_PORT` for the Vite dev
164
171
  server). See [web-ui.md](./web-ui.md).
165
172
 
173
+ The UI checks the registry for a newer release when it starts and then once a day at 00:00 UTC (the answer is cached in
174
+ `.tmp/`, so the command line shares it). A newer release is printed on the terminal at startup and shows up as a badge
175
+ in the top bar; click it to see the version and the command that upgrades this install. `pnpm asrun upgrade` is skipped
176
+ there (it reads the registry itself). Set `ASRUN_NO_UPDATE_CHECK=1` to switch the check off, for a pipeline or a machine
177
+ without a registry.
178
+
179
+ ## Upgrading the framework
180
+
181
+ ```bash
182
+ pnpm asrun upgrade
183
+ pnpm asrun upgrade --dry-run
184
+ pnpm asrun upgrade --version 0.9.0
185
+ ```
186
+
187
+ `upgrade` runs four steps, in this order:
188
+
189
+ 1. resolves the target release (`latest`, or the dist-tag/version `--version <v>` names) and prints
190
+ `installed -> target`;
191
+ 2. reads the **migration manifest of the target release out of its tarball, before installing anything**, and prints
192
+ the migrations and breaking changes between the workspace's baseline (`.asrun.json` → `migrations.since`) and that
193
+ release. A breaking change needs a person's confirmation (type `upgrade`); an unattended run passes `--yes`. A
194
+ release that ships no manifest (it predates the manifest) cannot be checked in advance: `upgrade` says so and asks
195
+ before it installs;
196
+ 3. installs `@asrunjs/asrun@<target>` (it first exempts a fresh release from pnpm's release age, or pnpm would install
197
+ the previous one);
198
+ 4. runs `init --update`, `migrate` and `doctor` through the package that was just installed.
199
+
200
+ `--dry-run` stops after step 2. `--version` pins a release; `upgrade` never downgrades.
201
+
202
+ Every `asrun` command also checks the registry for a newer release, at most once a day (the answer is cached in `.tmp/`
203
+ and shared with the web UI), and prints one line on stderr before it starts:
204
+ `@asrunjs/asrun 0.8.0 is available (installed 0.7.0). Upgrade: pnpm asrun upgrade`. On a terminal the line carries an
205
+ `UPDATE` badge and highlights the version and the command; a pipe or a log gets the plain sentence. Inside the
206
+ framework's own git checkout the line names `git pull` instead, because there is no installed package to upgrade. A
207
+ failed check is silent. Set `ASRUN_NO_UPDATE_CHECK=1` to switch it off; `NO_COLOR` and `FORCE_COLOR` decide the colors
208
+ as they do everywhere else.
209
+
210
+ ### Migrations
211
+
212
+ A migration is a change an existing workspace must make when the framework moves it forward: a config entry that
213
+ moved, a file whose shape changed, a dependency range that has to follow the peer dependency. Anything `init --update`
214
+ refreshes by itself is not a migration. The registry is `migrations/index.ts` in the framework package, and the state
215
+ lives in `.asrun.json`:
216
+
217
+ ```json
218
+ "migrations": {"since": "0.7.0", "manual": ["0.8.0-rename-..."]}
219
+ ```
220
+
221
+ `since` is the release the workspace matches. It only moves forward when a migration succeeds, so an interrupted run
222
+ can simply be repeated. A migration marked `auto` runs by itself; one with `manual` steps is printed and remembered
223
+ until a person does it and acknowledges it. `init --update` never advances the baseline, which is why a manual
224
+ `pnpm add -D @asrunjs/asrun@latest` followed by `pnpm asrun init --update` still leaves its migrations pending.
225
+
226
+ ```bash
227
+ pnpm asrun migrate # apply what the installed release brings
228
+ pnpm asrun migrate --dry-run # show it, change nothing
229
+ pnpm asrun migrate --list # every migration of the installed release, and its state
230
+ pnpm asrun migrate --ack <id> # a manual step is done (--ack-all for every one)
231
+ pnpm asrun migrate --to <v> # stop at that release
232
+ ```
233
+
234
+ `migrate` never installs anything, and refuses to migrate beyond the installed release: the code that applies a
235
+ migration is the installed package. `doctor` warns while migrations or manual steps are outstanding.
236
+
166
237
  ## Workspace
167
238
 
168
239
  ```bash
169
- pnpm asrun init [--update] [--force <file>] [--reset-rules] [--root <dir>] [--no-env] [--install [spec]]
240
+ pnpm asrun init [--update] [--force] [--force <file>] [--reset-rules] [--root <dir>] [--no-env] [--no-extras] [--install [spec]]
170
241
  pnpm asrun doctor
242
+ pnpm asrun upgrade [--version <v>] [--dry-run] [--yes]
243
+ pnpm asrun migrate [--list] [--dry-run] [--yes] [--ack <id>|--ack-all]
244
+ pnpm asrun reset [--root <dir>] [--dry-run] [--yes] [--state] [--lockfile]
171
245
  ```
172
246
 
173
- `init` writes the workspace files; `--update` refreshes only the generated ones. `doctor` checks profiles,
174
- `tsconfig.json`, `playwright.config.ts`, the generated files, the installed version, the project rule table and the
175
- skill overrides. Full details in [install.md](./install.md).
247
+ `init` writes the workspace files; `--update` refreshes only the generated ones, and `--force` rewrites every file
248
+ init writes (a single file with `--force <file>`). `--force` still keeps `profiles.yaml` and `package.json`; name
249
+ `--force profiles.yaml` to replace profiles, and add `--reset-rules` to replace `AGENTS.md` or `CLAUDE.md`. It also
250
+ adds the `allowBuilds` entries `pnpm-workspace.yaml` lacks, and installs the optional packages this machine lacks
251
+ (`ffmpeg-static`; `--no-extras` skips them).
252
+ `doctor` checks profiles, `tsconfig.json`, `playwright.config.ts`, the generated files, the installed version, the
253
+ project rule table, the skill overrides and the workspace migrations. `upgrade` and `migrate` are described above.
254
+
255
+ `reset` removes `node_modules` and the files `init` writes, for a clean reinstall. It deletes a once-only file only
256
+ when it is still identical to its template, so edited files stay; `cases/`, specs, baselines, `profiles.yaml`,
257
+ `package.json` and `.claude/skills/**` are never removed. `--state` also removes `.auth/`, `reports/`, `.tmp/`,
258
+ `.explore/`, `test-results/`, `playwright-report/` and `.playwright-mcp/`; `--lockfile` also removes
259
+ `pnpm-lock.yaml`. It lists what it would remove and asks for confirmation (`--yes` skips it, `--dry-run` only
260
+ lists). After it, run `pnpm dlx @asrunjs/asrun init --install` and `pnpm go`.
261
+
262
+ Full details in [install.md](./install.md).
package/docs/install.md CHANGED
@@ -8,9 +8,9 @@ and is never edited: a project commits only its own test content.
8
8
 
9
9
  - **Node.js 22 or newer** and **pnpm**.
10
10
  - **ffmpeg** joins the recordings of one test (a new browser tab has its own video) into one video for the report.
11
- `pnpm go` installs it with Homebrew on macOS or winget on Windows; on Linux run `sudo apt install ffmpeg`. When it
12
- is missing, the optional `ffmpeg-static` package is used instead; if neither works, the report lists the videos
13
- separately. Not fatal.
11
+ `pnpm go` installs it with Homebrew on macOS or winget on Windows; on Linux run `sudo apt install ffmpeg`. When it is
12
+ missing, `pnpm go` / `pnpm asrun init` install the bundled `ffmpeg-static` package into the workspace; if neither
13
+ works, the report lists the videos separately. Not fatal.
14
14
  - **Claude Code or OpenCode** for the AI steps (make cases, explore, generate, heal). Everything else works without
15
15
  them; `pnpm go` warns when neither command is found.
16
16
 
@@ -22,9 +22,16 @@ pnpm go
22
22
  ```
23
23
 
24
24
  `init --install` scaffolds the workspace and then adds the dependency. Running `init` first matters on pnpm 12: it
25
- writes `pnpm-workspace.yaml` with the `allowBuilds` entry (`esbuild` through `tsx`, `ffmpeg-static`), and without it
26
- `pnpm add` exits with `ERR_PNPM_IGNORED_BUILDS`. An existing `pnpm-workspace.yaml` that lacks the entry gets a note
27
- instead: add the block by hand (see "Manual install").
25
+ writes `pnpm-workspace.yaml` with the `allowBuilds` entry that `esbuild` needs, and without it `pnpm add` exits with
26
+ `ERR_PNPM_IGNORED_BUILDS`. The same file lists `@asrunjs/asrun` under `minimumReleaseAgeExclude`, so pnpm's 24-hour
27
+ release age does not hold a fresh release back (see
28
+ [troubleshooting.md](./troubleshooting.md#installing)). A `pnpm-workspace.yaml` that already exists gets the entries
29
+ it lacks: `init` adds them before it installs anything.
30
+
31
+ Optional packages are not part of the framework's dependency graph: a dependency with an install script would make
32
+ `pnpm dlx @asrunjs/asrun` stop at pnpm's build-approval picker, and the dlx sandbox does not read the workspace's
33
+ `allowBuilds`. So `init` installs `ffmpeg-static` into the workspace only when this machine has no ffmpeg, adding its
34
+ `allowBuilds` entry first. `--no-extras` skips that step.
28
35
 
29
36
  `init` also records `@asrunjs/asrun` (pinned to its own version) in the workspace `package.json`, so `pnpm install`
30
37
  installs the framework even without `--install`. When the install step itself fails — `pnpm` is not on `PATH`, the
@@ -35,11 +42,13 @@ registry is unreachable, or a build script is not approved — `init` names the
35
42
 
36
43
  1. checks Node (22+) and ffmpeg, and warns when no AI CLI is installed;
37
44
  2. installs the dependencies and the Chromium browser (`pnpm install` + `playwright install chromium`);
38
- 3. runs `asrun init` (workspace files and `.env`) and `asrun doctor`;
45
+ 3. runs `asrun init` (workspace files, `.env`, and the optional packages this machine lacks) and `asrun doctor`;
39
46
  4. starts the web UI at http://127.0.0.1:4173 and opens it.
40
47
 
41
48
  Run it again on any day: the install step then takes seconds, and if the UI is already running it only opens it.
42
- `pnpm go --no-open` does not open a browser. Set `ASRUN_UI_PORT` to change the port.
49
+ `pnpm go --no-open` does not open a browser; `pnpm go --no-ui` stops after step 3, with the workspace prepared and no
50
+ server running; `pnpm go --dry-run` only prints the workspace, the framework and the `asrun` command it resolved, then
51
+ exits. Set `ASRUN_UI_PORT` to change the port.
43
52
 
44
53
  The UI is optional: all commands in [commands.md](./commands.md) work without it.
45
54
 
@@ -53,22 +62,27 @@ pnpm asrun init
53
62
  pnpm go
54
63
  ```
55
64
 
56
- If pnpm refuses the build scripts of `esbuild` / `ffmpeg-static`, add this to `pnpm-workspace.yaml` and run the
57
- install again:
65
+ If pnpm refuses the build script of `esbuild`, or installs the release before `latest`, add this to
66
+ `pnpm-workspace.yaml` and run the install again:
58
67
 
59
68
  ```yaml
60
69
  allowBuilds:
61
70
  esbuild: true
62
- ffmpeg-static: true
71
+ minimumReleaseAgeExclude:
72
+ - '@asrunjs/asrun'
63
73
  ```
64
74
 
75
+ `pnpm asrun init` adds the `allowBuilds` entry of an optional package (for example `ffmpeg-static`) itself, right
76
+ before it installs that package, so this block only has to name what the framework's own dependencies need.
77
+
65
78
  To pin a version:
66
79
 
67
80
  ```bash
68
81
  pnpm add -D @asrunjs/asrun@<version>
69
82
  ```
70
83
 
71
- `pnpm asrun init` is safe to run many times: it never overwrites a file the project owns.
84
+ `pnpm asrun init` is safe to run many times: it never overwrites a file the project owns unless `--force` asks for
85
+ it (and `--reset-rules` for the project's rules).
72
86
 
73
87
  ## What `init` writes
74
88
 
@@ -78,7 +92,9 @@ pnpm add -D @asrunjs/asrun@<version>
78
92
  | Once | `AGENTS.md`, `CLAUDE.md`, `.mcp.json`, `opencode.jsonc`, `.claude/settings.json`, `tsconfig.json`, `pnpm-workspace.yaml`, `profiles.yaml`, `cases/_template.md`, `package.json` (scripts and devDependencies merged in) | kept |
79
93
 
80
94
  It also writes the asrun block in `.gitignore`, the `ASRUN_ROOT` line in `.env`, and `.asrun.json` (the marker with
81
- the framework version and the generated file list).
95
+ the framework version, the migration baseline and the generated file list). It appends the `allowBuilds` entries an
96
+ existing `pnpm-workspace.yaml` lacks, and installs the optional packages this machine lacks (`ffmpeg-static`, only
97
+ when there is no ffmpeg).
82
98
 
83
99
  Flags:
84
100
 
@@ -86,24 +102,54 @@ Flags:
86
102
  |---|---|
87
103
  | `--root <dir>` | initialize that folder instead of the current one |
88
104
  | `--update` | refresh only the generated files |
105
+ | `--force` | overwrite every file init writes, for a clean installation (keeps `profiles.yaml` and `package.json`) |
89
106
  | `--force <path>` | overwrite one file init owns (repeatable) |
90
107
  | `--reset-rules` | allow `--force` on `AGENTS.md`, `CLAUDE.md` or `.claude/skills/**` |
91
108
  | `--no-env` | do not write `.env` |
109
+ | `--no-extras` | do not install the optional packages this machine lacks (`ffmpeg-static`) |
92
110
  | `--install [spec]` | also add the dependency (default `@asrunjs/asrun`) |
93
111
 
94
- `AGENTS.md`, `CLAUDE.md` and `.claude/skills/**` belong to the project. `init --force` refuses them without
95
- `--reset-rules`, so a framework update can never replace a project's rules.
112
+ `AGENTS.md`, `CLAUDE.md` and `.claude/skills/**` belong to the project. `init --force` keeps them unless
113
+ `--reset-rules` is given, so a framework update can never replace a project's rules. `--force` also keeps
114
+ `profiles.yaml` and `package.json`: replace the profiles with `--force profiles.yaml`; `package.json` is only merged
115
+ into, so your scripts and dependencies stay.
96
116
 
97
117
  ## Update
98
118
 
119
+ ```bash
120
+ pnpm asrun upgrade
121
+ ```
122
+
123
+ One command installs the newest release, refreshes the generated files to its templates, applies the workspace
124
+ migrations and runs `doctor`. Before it installs anything it reads the migration manifest of the target release and
125
+ prints the breaking changes and migrations between the workspace's baseline and that release; a breaking change needs
126
+ a person's confirmation (type `upgrade`), and an unattended run passes `--yes`. `--dry-run` shows the same plan and
127
+ changes nothing; `--version <v>` pins a release; it never downgrades.
128
+
129
+ `upgrade` arrives with this release, so a workspace that predates it updates once by hand:
130
+
99
131
  ```bash
100
132
  pnpm add -D @asrunjs/asrun@latest
101
133
  pnpm asrun init --update
134
+ pnpm asrun migrate
102
135
  pnpm asrun doctor
103
136
  ```
104
137
 
105
- `init --update` rewrites only the generated files and leaves project rules, case files, values and baselines alone.
106
- `doctor` tells you when the workspace is behind the installed version.
138
+ After that, `pnpm asrun upgrade` is the command to remember. `init --update` alone only refreshes the generated files:
139
+ it never moves the migration baseline, so a manual install that skipped `migrate` still shows up in `doctor`, and
140
+ `pnpm asrun migrate` fixes it. `init --force` rewrites every file init writes (see the flags above). `doctor` tells
141
+ you when the workspace is behind the installed version, and what is still to migrate.
142
+
143
+ An upgrade never replaces what belongs to the project: `cases/`, `tests/` with their baselines, `profiles.yaml`,
144
+ `AGENTS.md`, `CLAUDE.md`, the skill overrides and `.claude/settings.json` stay as they are. A release that needs a
145
+ project-owned file to change says so as a migration with manual steps.
146
+
147
+ To go back to an older release, install it yourself and refresh:
148
+
149
+ ```bash
150
+ pnpm add -D @asrunjs/asrun@<version>
151
+ pnpm asrun init --update
152
+ ```
107
153
 
108
154
  There is no folder to move: the framework lives in `node_modules`, which the package manager owns. To change the
109
155
  framework itself, `pnpm patch @asrunjs/asrun` (a committed patch).
@@ -116,6 +162,7 @@ Run it after changing profiles, rules or overrides, and after an update. It chec
116
162
  - `tsconfig.json`, `playwright.config.ts` (its re-export of the framework config) and the generated files;
117
163
  - that `.gitignore` ignores `.auth/` and `reports/`;
118
164
  - whether the installed version is behind the package's templates, and whether `dist/` is out of date;
165
+ - the workspace migrations: the baseline in `.asrun.json`, what still applies, and the manual steps left open;
119
166
  - the project rule table in `AGENTS.md` §2 (unknown ids, a missing `Effect`, a non-negotiable rule);
120
167
  - the framework skills and the project overrides under `.claude/skills/<skill>.md`, and lists the ones it sees.
121
168
 
@@ -149,7 +196,7 @@ CLAUDE.md imports AGENTS.md for Claude Code
149
196
  .opencode/commands/ the same commands for OpenCode (it does not read .claude/commands)
150
197
  .mcp.json the playwright-test MCP server for Claude Code
151
198
  opencode.jsonc the same for OpenCode, plus an optional model
152
- .asrun.json marker: framework version and the generated file list
199
+ .asrun.json marker: framework version, migration baseline and the generated file list
153
200
  package.json the workspace scripts (`asrun`, `signin`, ...) and devDependencies
154
201
  .env ASRUN_ROOT (which workspace the commands use) and local settings
155
202
  node_modules/@asrunjs/asrun/ the framework (installed dependency; never edited)
@@ -169,6 +216,32 @@ and `pnpm-workspace.yaml`.
169
216
  Never commit or share `.auth/` (one saved login per profile, as sensitive as a password), `reports/`, `.tmp/`,
170
217
  `.explore/`, `.env` or `node_modules/`. The asrun block of `.gitignore` covers them.
171
218
 
219
+ ## Reset and reinstall
220
+
221
+ To get a clean environment without losing the project's test content:
222
+
223
+ ```bash
224
+ pnpm asrun reset # lists what it removes, then asks
225
+ pnpm dlx @asrunjs/asrun init --install
226
+ pnpm go
227
+ ```
228
+
229
+ `reset` removes `node_modules/` and the files `init` writes: the generated config and commands, `.asrun.json`, the
230
+ asrun block of `.gitignore` and the `ASRUN_ROOT` lines of `.env`. It deletes a once-only file only when it is still
231
+ identical to its template, so a file you edited is kept (and listed). `cases/`, specs and their baselines,
232
+ `profiles.yaml`, `package.json` and `.claude/skills/**` are never removed.
233
+
234
+ | Flag | Meaning |
235
+ |---|---|
236
+ | `--root <dir>` | reset that folder instead of the current workspace |
237
+ | `--dry-run` | list what would be removed, and change nothing |
238
+ | `--yes` | skip the confirmation (for unattended runs) |
239
+ | `--state` | also remove `.auth/`, `reports/`, `.tmp/`, `.explore/`, `test-results/`, `playwright-report/`, `.playwright-mcp/` |
240
+ | `--lockfile` | also remove `pnpm-lock.yaml`, so the next install resolves the dependencies again |
241
+
242
+ Without `--yes` it asks you to type `yes`; a run without a terminal refuses instead of removing anything. It refuses
243
+ to run on the framework's own checkout. `--state` removes saved logins, so every profile must be signed in again.
244
+
172
245
  ## Uninstall
173
246
 
174
247
  ```bash
@@ -176,4 +249,4 @@ pnpm remove @asrunjs/asrun
176
249
  ```
177
250
 
178
251
  Then delete the workspace files you no longer want (`profiles.yaml`, `cases/`, `tests/`, the generated commands and
179
- configs, `.asrun.json`). They are ordinary files: keep them if another tool still uses them.
252
+ configs, `.asrun.json`). `pnpm asrun reset` does most of that; keep the files if another tool still uses them.