@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.
- package/.claude/settings.json +5 -5
- package/.claude/skills/exploring-cases/references/assertion-targets.md +103 -103
- package/.claude/skills/exploring-cases/references/handoff-card.js +128 -128
- package/.claude/skills/making-cases/references/gaps.md +36 -36
- package/.claude/skills/making-cases/references/splitting.md +28 -28
- package/AGENTS.md +6 -0
- package/README.md +33 -9
- package/bin/asrun.mjs +19 -2
- package/dist/lib/ffmpeg.js +21 -10
- package/dist/lib/migrations.d.ts +106 -0
- package/dist/lib/migrations.js +106 -0
- package/dist/lib/paths.d.ts +9 -0
- package/dist/lib/paths.js +12 -1
- package/dist/lib/registry.d.ts +31 -0
- package/dist/lib/registry.js +66 -0
- package/dist/lib/runtime-deps.d.ts +59 -0
- package/dist/lib/runtime-deps.js +161 -0
- package/dist/lib/tar.d.ts +5 -0
- package/dist/lib/tar.js +79 -0
- package/dist/lib/update-check.d.ts +52 -0
- package/dist/lib/update-check.js +223 -0
- package/dist/migrations/index.d.ts +23 -0
- package/dist/migrations/index.js +22 -0
- package/dist/migrations/manifest.json +1 -0
- package/dist/scripts/build.js +7 -0
- package/dist/scripts/cli.js +33 -7
- package/dist/scripts/doctor.js +51 -9
- package/dist/scripts/go.mjs +43 -11
- package/dist/scripts/heal-evidence.mjs +149 -143
- package/dist/scripts/init.d.ts +9 -1
- package/dist/scripts/init.js +127 -23
- package/dist/scripts/migrate.d.ts +1 -0
- package/dist/scripts/migrate.js +259 -0
- package/dist/scripts/reset.d.ts +1 -0
- package/dist/scripts/reset.js +233 -0
- package/dist/scripts/run.js +25 -24
- package/dist/scripts/upgrade.d.ts +1 -0
- package/dist/scripts/upgrade.js +256 -0
- package/dist/ui/server/app.js +18 -0
- package/dist/ui/server/index.js +3 -0
- package/dist/ui/server/update.d.ts +18 -0
- package/dist/ui/server/update.js +87 -0
- package/dist/ui/shared/types.d.ts +24 -0
- package/docs/agents/framework-dev.md +99 -1
- package/docs/commands.md +96 -9
- package/docs/install.md +92 -19
- package/docs/troubleshooting.md +43 -14
- package/package.json +35 -33
- package/templates/.claude/settings.json +5 -5
- package/templates/AGENTS.md +1 -0
- package/templates/CLAUDE.md +1 -1
- package/templates/gitignore +13 -13
- package/templates/package.json +33 -32
- package/templates/playwright.config.ts +3 -3
- package/templates/pnpm-workspace.yaml +8 -5
- package/templates/seed.spec.ts +9 -9
- package/templates/tsconfig.json +14 -14
- package/ui/dist/assets/{dist-vmf3fnkM.js → dist-BC_P1Gnf.js} +1 -1
- package/ui/dist/assets/{dist-O_RVi4tP.js → dist-BgPbx2GZ.js} +1 -1
- package/ui/dist/assets/{dist-jPS6HCJ5.js → dist-BpkTvixI.js} +1 -1
- package/ui/dist/assets/{dist-DEPJZ3Me.js → dist-CJiwxr1r.js} +1 -1
- package/ui/dist/assets/{dist-CPLhRzom.js → dist-CavmZIv7.js} +1 -1
- package/ui/dist/assets/{dist-DZ7RiqmA.js → dist-ChBHX4rh.js} +1 -1
- package/ui/dist/assets/{dist-BudNAjOv.js → dist-D6jmnDr-.js} +1 -1
- package/ui/dist/assets/{dist-C-eCSFja.js → dist-DoChb6QQ.js} +1 -1
- package/ui/dist/assets/{dist-LixzGniF.js → dist-Do_MhHKF.js} +1 -1
- package/ui/dist/assets/{dist-CO4heSzh.js → dist-N-o4QIaZ.js} +1 -1
- package/ui/dist/assets/index-Cy2l91qV.css +2 -0
- package/ui/dist/assets/{index-BIGt8Hfz.js → index-DGQ5logJ.js} +129 -129
- package/ui/dist/favicon.svg +11 -11
- package/ui/dist/index.html +2 -2
- package/ui/dist/logos/claude.svg +1 -1
- package/ui/dist/logos/opencode.svg +1 -1
- 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
|
-
-
|
|
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
|
|
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
|
|
159
|
-
pnpm
|
|
160
|
-
pnpm ui
|
|
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
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
|
|
26
|
-
`
|
|
27
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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`
|
|
95
|
-
`--reset-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
|
-
`
|
|
106
|
-
|
|
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`).
|
|
252
|
+
configs, `.asrun.json`). `pnpm asrun reset` does most of that; keep the files if another tool still uses them.
|