state-scenarios-cli 0.6.0 → 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/README.md +6 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +224 -17
- package/dist/cli.js.map +1 -1
- package/dist/compare-TBRNNO4A.js +69 -0
- package/dist/compare-TBRNNO4A.js.map +1 -0
- package/dist/compare.d.ts +14 -0
- package/dist/map-NAV75ZEE.js +159 -0
- package/dist/map-NAV75ZEE.js.map +1 -0
- package/dist/map.d.ts +73 -0
- package/dist/publish-K6YWPOCO.js +80 -0
- package/dist/publish-K6YWPOCO.js.map +1 -0
- package/dist/publish.d.ts +18 -0
- package/dist/{record-SNQF63Y7.js → record-BO2NJ4Q2.js} +3 -2
- package/dist/record-BO2NJ4Q2.js.map +1 -0
- package/dist/record.d.ts +3 -1
- package/dist/{skills-NQRJHKHG.js → skills-P5SE3XSV.js} +7 -2
- package/dist/skills-P5SE3XSV.js.map +1 -0
- package/package.json +3 -3
- package/skills/create-scenario-map/SKILL.md +24 -4
- package/skills/scenario-coverage-loop/SKILL.md +122 -0
- package/skills/scenario-coverage-loop/references/controller.mjs +38 -0
- package/skills/scenario-coverage-loop/references/memory.md +13 -0
- package/skills/scenario-coverage-loop/references/pr-check.yml +50 -0
- package/skills/scenario-coverage-loop/references/prompt.md +58 -0
- package/skills/scenario-coverage-loop/references/workflow.yml +232 -0
- package/skills/scenario-pr/SKILL.md +114 -0
- package/skills/scenario-pr/references/pr-template.md +61 -0
- package/skills/state-scenarios/SKILL.md +12 -4
- package/skills/state-scenarios/references/media.md +9 -5
- package/skills/state-scenarios/references/verify.md +55 -3
- package/dist/record-SNQF63Y7.js.map +0 -1
- package/dist/skills-NQRJHKHG.js.map +0 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: scenario-pr
|
|
3
|
+
description: >
|
|
4
|
+
Write or update a pull request description that shows the UI change: before and after
|
|
5
|
+
screenshots of every state the branch touches, taken with state-scenarios, published to the PR,
|
|
6
|
+
plus a short outline of the code's shape. Use for /scenario-pr, or when asked to describe a UI
|
|
7
|
+
PR "with screenshots", "show the before and after", or "make the PR visual".
|
|
8
|
+
disable-model-invocation: true
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Describe a UI pull request with its real states
|
|
12
|
+
|
|
13
|
+
A reviewer of a UI change needs to see it. This skill finds the states the branch touches, shoots
|
|
14
|
+
them before and after, publishes the images, and writes a short PR body around them. Write as one
|
|
15
|
+
person to another: plain words, no jargon, nothing a screenshot already says.
|
|
16
|
+
|
|
17
|
+
For the images alone on every PR, without an agent, the repository's GitHub Action
|
|
18
|
+
`jagreehal/state-scenarios/actions/pr-screenshots` posts them as a comment (see the README). This
|
|
19
|
+
skill adds the written description around them.
|
|
20
|
+
|
|
21
|
+
The `state-scenarios` skill is the reference for the library; its topic `verify` covers every
|
|
22
|
+
command used here. Read it first (`npx state-scenarios guide verify`).
|
|
23
|
+
|
|
24
|
+
## 1. Find the PR and the base
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
gh pr view --json number,url,baseRefName,headRefName,title 2>/dev/null
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
No PR yet: check `git status --short --branch` and the branch's commits. Commit work that belongs
|
|
31
|
+
to the task, push the branch, and `gh pr create --draft --fill`. Ask the user only when the branch
|
|
32
|
+
holds no relevant work.
|
|
33
|
+
|
|
34
|
+
Done when: you have the PR number and its base branch.
|
|
35
|
+
|
|
36
|
+
## 2. Understand the change
|
|
37
|
+
|
|
38
|
+
Read the full diff (`gh pr diff`) and enough of the code around it to explain what changed and why.
|
|
39
|
+
Read the ticket or task notes if they are linked.
|
|
40
|
+
|
|
41
|
+
Done when: you can say in one sentence why the change exists.
|
|
42
|
+
|
|
43
|
+
## 3. Pick the states
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx state-scenarios affected <dir> --base origin/<base> --src <app folders>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Treat the list as a starting point. Keep states the diff really changes; drop route or endpoint
|
|
50
|
+
matches the diff can't reach. When a shared file changed (layout, theme, global CSS), add the
|
|
51
|
+
main states of each feature, or all of them if there are few. Add loading, empty and error for
|
|
52
|
+
any feature the change touches. A change no scenario covers is worth saying in the PR; capturing
|
|
53
|
+
the state (topic `capture`) is better still.
|
|
54
|
+
|
|
55
|
+
Done when: you have the list of scenario names, each with a reason you could defend.
|
|
56
|
+
|
|
57
|
+
## 4. Shoot before and after
|
|
58
|
+
|
|
59
|
+
The dev server must be running (the project's scenario map skill, `scenarios-<app>`, says how).
|
|
60
|
+
|
|
61
|
+
1. Before: put the base's code in front of the dev server, e.g. `git switch --detach origin/<base>`
|
|
62
|
+
with a clean tree (or a worktree of the base on another port). Shoot the listed states that
|
|
63
|
+
exist on the base, with the same options you'll use after:
|
|
64
|
+
`npx state-scenarios shoot <dir> --base-url <url> --only <names> --out .shots/before`
|
|
65
|
+
2. After: switch back to the branch, let the dev server reload, then
|
|
66
|
+
`npx state-scenarios shoot <dir> --base-url <url> --only <names> --out .shots/after --compare .shots/before`
|
|
67
|
+
|
|
68
|
+
Look at every `.diff.png` and every `new` state. Tell the user about any diff the change didn't
|
|
69
|
+
intend before writing the PR. A scenario that fails (`✗`) needs fixing first; read its
|
|
70
|
+
`.md` page.
|
|
71
|
+
|
|
72
|
+
When the change is about an interaction (a flow, an animation, focus), record it too:
|
|
73
|
+
`npx state-scenarios record <dir> --base-url <url> --steps <file> --only <name> --format gif --width 900`
|
|
74
|
+
(topic `media`). GitHub plays GIFs inline; it doesn't play linked MP4s.
|
|
75
|
+
|
|
76
|
+
Done when: `.shots/after` holds a diff for each changed state, a PNG for each new one, and you
|
|
77
|
+
have looked at all of them.
|
|
78
|
+
|
|
79
|
+
## 5. Publish the images
|
|
80
|
+
|
|
81
|
+
If the repository is public, the images will be public: check them for real names, emails, tokens
|
|
82
|
+
or internal URLs first, and ask the user before the first publish.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npx state-scenarios publish .shots/after/*.diff.png <new states' PNGs> <GIFs> --folder pr-<number>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
It pushes to the `scenario-shots` branch without touching the checkout and prints one Markdown
|
|
89
|
+
image line per file. Publishing the same folder again replaces it.
|
|
90
|
+
|
|
91
|
+
Done when: you have an image line for each picture the PR body will show.
|
|
92
|
+
|
|
93
|
+
## 6. Write and save the description
|
|
94
|
+
|
|
95
|
+
Read [references/pr-template.md](references/pr-template.md) and follow it. Save the body to
|
|
96
|
+
`.shots/pr-body.md`, then:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
gh pr edit <number> --body-file .shots/pr-body.md
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Done when: `gh pr view <number>` shows the new body with its images.
|
|
103
|
+
|
|
104
|
+
## 7. Report
|
|
105
|
+
|
|
106
|
+
Tell the user, in a few lines: the PR URL, the states shown (changed, new, unchanged), anything
|
|
107
|
+
that looked wrong in the screenshots, and states the change touches that have no scenario.
|
|
108
|
+
|
|
109
|
+
## Rules
|
|
110
|
+
|
|
111
|
+
- Show only states the change touches. Unchanged states go in one line, not in pictures.
|
|
112
|
+
- Never publish a screenshot you haven't looked at.
|
|
113
|
+
- Keep `.shots/` out of commits (add it to `.gitignore` if it isn't).
|
|
114
|
+
- Don't restate the diff in prose. The outline shows shape; the screenshots show the result.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# PR description template
|
|
2
|
+
|
|
3
|
+
Fill this in. Leave out any section or view that doesn't apply; never add sections beyond these.
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
## Why
|
|
7
|
+
|
|
8
|
+
{One sentence: the problem this PR solves and what's possible once it ships.}
|
|
9
|
+
|
|
10
|
+
## What changes on screen
|
|
11
|
+
|
|
12
|
+
{One line per changed or new state, with its picture. Order by importance, not by name.}
|
|
13
|
+
|
|
14
|
+
**{State, e.g. "Orders: empty"}** · `{scenario-name}` · {changed | new}
|
|
15
|
+
{One short line on what changed, if the picture doesn't make it obvious.}
|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
{Unchanged states that were checked, in one line: "No change: orders-error, orders-loading."}
|
|
19
|
+
|
|
20
|
+
## Things to note
|
|
21
|
+
|
|
22
|
+
- {1-3 bullets: anything the reviewer could miss. A state the change touches with no scenario,
|
|
23
|
+
an intended diff elsewhere, a migration, a deliberate omission. "None." when there is nothing.}
|
|
24
|
+
|
|
25
|
+
## Change outline
|
|
26
|
+
|
|
27
|
+
{The smallest code-shape views that explain the implementation, each under one line of text.
|
|
28
|
+
Pick from: a component tree, a call tree, a file tree with responsibilities, a key type, or
|
|
29
|
+
pseudocode for the logic. Use a `diff` block when the shape already existed, a plain block when
|
|
30
|
+
it's new.}
|
|
31
|
+
|
|
32
|
+
## Try it
|
|
33
|
+
|
|
34
|
+
{The scenario links for the states above, so a reviewer can open them in their own dev server.}
|
|
35
|
+
|
|
36
|
+
- `/orders?scenario=orders-empty`
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Change outline examples
|
|
40
|
+
|
|
41
|
+
A component change:
|
|
42
|
+
|
|
43
|
+
```diff
|
|
44
|
+
<OrdersPage> (src/features/orders/OrdersPage.tsx)
|
|
45
|
+
useOrders()
|
|
46
|
+
- <OrdersTable />
|
|
47
|
+
+ {orders.length ? <OrdersTable /> : <EmptyOrders />}
|
|
48
|
+
+ <EmptyOrders> (src/features/orders/EmptyOrders.tsx)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A control-flow change:
|
|
52
|
+
|
|
53
|
+
```diff
|
|
54
|
+
loadOrders
|
|
55
|
+
fetch /api/orders
|
|
56
|
+
- on error: throw
|
|
57
|
+
+ on error: show <OrdersError> with Retry
|
|
58
|
+
+ retry: loadOrders
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Keep each view to what the reviewer needs. Leave out files and calls the change didn't touch.
|
|
@@ -44,6 +44,13 @@ http://localhost:5173/orders?scenario=orders-empty&scenario-strict
|
|
|
44
44
|
real backend. Once `<html data-scenario="orders-empty">` is set and the `ready` selector is
|
|
45
45
|
visible, the state is on screen.
|
|
46
46
|
|
|
47
|
+
## Show the real state, not a mock-up
|
|
48
|
+
|
|
49
|
+
When you explain a UI change or a design choice to a person, show the app: `shoot --only` the states
|
|
50
|
+
in question and put the PNGs (or `--compare` diffs) in front of them, instead of drawing the UI in
|
|
51
|
+
HTML or ASCII. Real screenshots show real data, real styles and real bugs. Describe in words only
|
|
52
|
+
what a screenshot can't show, such as the order things happen in.
|
|
53
|
+
|
|
47
54
|
## Topics
|
|
48
55
|
|
|
49
56
|
Read the topic for the job before you start. The files sit next to this one, and
|
|
@@ -56,7 +63,7 @@ Read the topic for the job before you start. The files sit next to this one, and
|
|
|
56
63
|
| `capture` | [references/capture.md](references/capture.md) | Making a scenario from the running app: the panel, `?scenario-record`, MCP `save_scenario` |
|
|
57
64
|
| `auth` | [references/auth.md](references/auth.md) | Signed-in and signed-out states, cookies, apps that validate a token on startup |
|
|
58
65
|
| `network` | [references/network.md](references/network.md) | Strict mode, `allow`, `ignore` for analytics, an existing MSW setup, named handlers |
|
|
59
|
-
| `verify` | [references/verify.md](references/verify.md) | Checking a UI change
|
|
66
|
+
| `verify` | [references/verify.md](references/verify.md) | Checking a UI change: `affected`, `shoot`, before/after `--compare`, failure pages, `publish` |
|
|
60
67
|
| `media` | [references/media.md](references/media.md) | Screenshots and videos for docs and websites: viewports, dark mode, locales, `record`, Remotion |
|
|
61
68
|
| `mcp` | [references/mcp.md](references/mcp.md) | Driving the live app from a coding agent: open, observe, act, check ready, save |
|
|
62
69
|
| `debugging` | [references/debugging.md](references/debugging.md) | Something fails: `doctor`, error codes and exit codes, common causes |
|
|
@@ -69,9 +76,10 @@ Read the topic for the job before you start. The files sit next to this one, and
|
|
|
69
76
|
the wiring one step at a time and prints the fix for each problem.
|
|
70
77
|
3. New state: capture it from the running app (topic `capture`) instead of writing fixtures by
|
|
71
78
|
hand. Set a `ready` selector and check it matches.
|
|
72
|
-
4. Changed UI: `npx state-scenarios
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
4. Changed UI: `npx state-scenarios affected <dir>` names the states your branch touches; shoot
|
|
80
|
+
them with `--only`, and with `--compare` against a run from before the change (topic `verify`).
|
|
81
|
+
Look at the screenshots. Each failed scenario gets `<out>/<name>.md` with the fix; read that
|
|
82
|
+
page, not the JSON.
|
|
75
83
|
5. `npx state-scenarios validate <dir>` before committing.
|
|
76
84
|
|
|
77
85
|
## Rules
|
|
@@ -40,8 +40,10 @@ export default {
|
|
|
40
40
|
npx state-scenarios record src/scenarios --base-url http://localhost:5173 --steps steps.ts --format mp4
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
- The panel is hidden and a cursor is drawn (Playwright videos don't show the mouse).
|
|
44
|
-
|
|
43
|
+
- The panel is hidden and a bright arrow cursor is drawn (Playwright videos don't show the mouse).
|
|
44
|
+
It pulses, glides to each target before a click, check or `fill` lands, and sends out a ripple on the
|
|
45
|
+
click. `--cursor-color <css>` (default `#ff2d55`) recolours it; `--cursor <file.svg>` draws your
|
|
46
|
+
own, with its top-left corner at the mouse.
|
|
45
47
|
- The video starts once the scenario is ready and holds the last frame for `--hold` ms (default
|
|
46
48
|
800), so there's no need for a `waitForTimeout` at the end of steps. Uncropped
|
|
47
49
|
`--format webm` is the exception: it keeps the page load, and needs no ffmpeg.
|
|
@@ -54,10 +56,12 @@ npx state-scenarios record src/scenarios --base-url http://localhost:5173 --step
|
|
|
54
56
|
- Video records at viewport size, so use a larger `--viewport` for sharper video; `--scale`
|
|
55
57
|
affects screenshots only.
|
|
56
58
|
- MP4, GIF, `--element`, `--fps` and `--width` need ffmpeg on `PATH`, or `FFMPEG_PATH`. WebM without them doesn't.
|
|
57
|
-
- Each video gets a `<name>.video.json` manifest: size, length, description and clicks
|
|
59
|
+
- Each video gets a `<name>.video.json` manifest: size, length, description and clicks; a field
|
|
60
|
+
taking focus is a point with `kind: 'focus'`, so editors can hold on it while it's typed into. See
|
|
58
61
|
[Videos in Remotion](#videos-in-remotion).
|
|
59
|
-
- In your own Playwright scripts, `showCursor(page)` from `state-scenarios/playwright`
|
|
60
|
-
cursor; call it before `openScenario(page, name, { panel: false })`.
|
|
62
|
+
- In your own Playwright scripts, `showCursor(page, { color, svg, size, halo })` from `state-scenarios/playwright`
|
|
63
|
+
draws the cursor; call it before `openScenario(page, name, { panel: false })`. `{ glide: true }`
|
|
64
|
+
makes actions wait for the cursor to arrive, as `record` does.
|
|
61
65
|
|
|
62
66
|
## Videos in Remotion
|
|
63
67
|
|
|
@@ -10,6 +10,47 @@ For each scenario `shoot` opens the link in strict mode, waits for `ready`, save
|
|
|
10
10
|
checks for unmocked requests, uncaught exceptions and `console.error` calls. `--only a,b` limits
|
|
11
11
|
the run; `--json` prints the report for scripts.
|
|
12
12
|
|
|
13
|
+
## Which states a change touches
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx state-scenarios affected src/scenarios --base main --src src
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
It lists each scenario the branch can affect (committed, uncommitted and new files) and why:
|
|
20
|
+
|
|
21
|
+
| Reason | Means |
|
|
22
|
+
| ---------- | ------------------------------------------------------------------ |
|
|
23
|
+
| `scenario` | The scenario's file changed |
|
|
24
|
+
| `extends` | It extends a scenario that changed |
|
|
25
|
+
| `map` | A scenario map row's Source column covers a changed file |
|
|
26
|
+
| `endpoint` | A changed file mentions an endpoint the scenario mocks |
|
|
27
|
+
| `route` | A changed file sits under a folder named like the scenario's route |
|
|
28
|
+
|
|
29
|
+
The last line is `--only a,b,c`, ready for `shoot`. Changed files nothing pointed at are listed
|
|
30
|
+
too: a shared file (layout, theme, global CSS) can change every state, so shoot them all then.
|
|
31
|
+
`--src` limits it to the app's folders in a monorepo; `--json` is for scripts. A scenario map with
|
|
32
|
+
Source columns (`/create-scenario-map`) makes the result much sharper than the endpoint and route
|
|
33
|
+
guesses.
|
|
34
|
+
|
|
35
|
+
## Before and after
|
|
36
|
+
|
|
37
|
+
Shoot the base branch first, then the change, and compare:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
git stash # or check out the base branch in a worktree on another port
|
|
41
|
+
npx state-scenarios shoot src/scenarios --base-url http://localhost:5173 --only <affected> --out .shots/before
|
|
42
|
+
git stash pop
|
|
43
|
+
npx state-scenarios shoot src/scenarios --base-url http://localhost:5173 --only <affected> --out .shots/after --compare .shots/before
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Each state that changed gets `.shots/after/<name>.diff.png`: before, after and the changed pixels
|
|
47
|
+
in red, side by side. States are reported `changed`, `same` or `new` (no earlier shot), and ones
|
|
48
|
+
that are gone as `removed`. A diff doesn't fail the run: in a UI change, diffs are the point. Look
|
|
49
|
+
at every `changed` state and ask whether the change meant to touch it.
|
|
50
|
+
|
|
51
|
+
Shoot both runs with the same viewport and options, and with the dev server warm, or fonts and
|
|
52
|
+
images still loading show as changes.
|
|
53
|
+
|
|
13
54
|
## When a scenario fails
|
|
14
55
|
|
|
15
56
|
The terminal names a page for it: `❯ details .scenario-shots/<name>.md`. Read that page. It has:
|
|
@@ -26,9 +67,20 @@ wrong, or that the UI change broke the state. Fix the app when the screenshot sh
|
|
|
26
67
|
|
|
27
68
|
## Evidence
|
|
28
69
|
|
|
29
|
-
Look at the PNGs for the states the change touches, plus loading, empty and error.
|
|
30
|
-
|
|
31
|
-
|
|
70
|
+
Look at the PNGs for the states the change touches, plus loading, empty and error. Pages for
|
|
71
|
+
passing scenarios are deleted on each run, so the `.md` files in the output folder are always the
|
|
72
|
+
current failures.
|
|
73
|
+
|
|
74
|
+
To show them in a PR, publish them; `gh` can't upload images:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx state-scenarios publish .shots/after/*.diff.png --folder pr-42
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
It commits the files to a `scenario-shots` branch with git plumbing (the checkout, index and
|
|
81
|
+
current branch stay as they are), pushes it, and prints a Markdown image line per file, linked to
|
|
82
|
+
that commit so the links don't change. Paste those lines into the PR body. `--branch` and
|
|
83
|
+
`--remote` change where they go. The `scenario-pr` skill writes the whole PR description this way.
|
|
32
84
|
|
|
33
85
|
## In Playwright tests
|
|
34
86
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/record.ts"],"sourcesContent":["import type { Frame } from '@playwright/test';\nimport { spawnSync } from 'node:child_process';\nimport { copyFileSync } from 'node:fs';\nimport { CliError } from './errors.js';\n\nexport type VideoFormat = 'mp4' | 'gif' | 'webm';\n\nexport interface Crop {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * The part of an element's box the video shows. Video holds only the viewport, so a crop must\n * stay inside it; `clipped` says the element was larger than the visible area.\n */\nexport function visibleCrop(box: Crop, viewport: { width: number; height: number; }) {\n const x = Math.max(0, box.x);\n const y = Math.max(0, box.y);\n const width = Math.min(viewport.width, box.x + box.width) - x;\n const height = Math.min(viewport.height, box.y + box.height) - y;\n\n if (width < 2 || height < 2) return undefined;\n\n return { crop: { x, y, width, height }, clipped: width < box.width || height < box.height };\n}\n\nconst even = (n: number) => Math.max(2, Math.floor(n / 2) * 2);\n\n/**\n * ffmpeg arguments that trim the page load off the start, crop to an element, and encode.\n * MP4 is H.264 in yuv420p so it plays everywhere, including Safari and phones; GIF uses a\n * generated palette, which keeps it sharp at a fraction of the naive size.\n */\nexport function ffmpegArgs(\n { input, output, format, start, crop, fps, width }: {\n input: string;\n output: string;\n format: VideoFormat;\n /** Seconds to skip, e.g. the page loading before the steps begin. */\n start: number;\n crop?: Crop;\n /** Frames per second; GIFs default to 12. */\n fps?: number;\n /** Maximum output width in pixels, never upscaled; height follows the aspect ratio. */\n width?: number;\n },\n): string[] {\n const rate = fps ?? (format === 'gif' ? 12 : undefined);\n\n // yuv420p needs even sizes.\n const filters = [\n crop && `crop=${even(crop.width)}:${even(crop.height)}:${Math.round(crop.x)}:${Math.round(crop.y)}`,\n rate && `fps=${rate}`,\n // Quoted so the comma in min() isn't read as a filter separator.\n width && `scale='trunc(min(${width},iw)/2)*2':-2:flags=lanczos`,\n ].filter(Boolean);\n\n const codec = {\n mp4: ['-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-crf', '23', '-movflags', '+faststart'],\n webm: ['-c:v', 'libvpx-vp9', '-b:v', '0', '-crf', '32'],\n gif: [],\n }[format];\n\n const vf = format === 'gif'\n ? `${filters.join(',')},split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=bayer`\n : filters.join(',');\n\n return [\n '-y',\n '-loglevel',\n 'error',\n '-i',\n input,\n '-ss',\n start.toFixed(2),\n ...(vf ? ['-vf', vf] : []),\n '-an',\n ...codec,\n output,\n ];\n}\n\n/**\n * Convert Playwright's WebM into the requested format. WebM without a crop, --fps or --width is\n * copied as recorded (page load included), so it works without ffmpeg.\n */\nexport function encodeVideo(options: Parameters<typeof ffmpegArgs>[0]) {\n if (options.format === 'webm' && !options.crop && !options.fps && !options.width) {\n copyFileSync(options.input, options.output);\n\n return { start: 0 };\n }\n\n // FFMPEG_PATH is the usual override when ffmpeg isn't on PATH (common on Windows).\n const ffmpeg = process.env.FFMPEG_PATH || 'ffmpeg';\n const result = spawnSync(ffmpeg, ffmpegArgs(options), { encoding: 'utf8' });\n\n if (result.error) {\n throw new CliError(\n 'FFMPEG_MISSING',\n `record needs ffmpeg for ${options.format}${\n options.format === 'webm' ? ' with --element, --fps or --width' : ''\n }: install it (https://ffmpeg.org/download.html) or set FFMPEG_PATH`,\n );\n }\n\n if (result.status !== 0) throw new Error(`ffmpeg failed: ${result.stderr.trim()}`);\n\n return { start: options.start };\n}\n\n/** The video size ffmpeg writes: the crop or viewport, capped at `width` as the scale filter does. */\nexport function outputSize(source: { width: number; height: number; }, width?: number) {\n if (!width) return source;\n\n const w = Math.trunc(Math.min(width, source.width) / 2) * 2;\n\n return { width: w, height: Math.round((w * source.height) / (source.width * 2)) * 2 };\n}\n\n/**\n * What `<name>.video.json` holds next to each video, so video editors such as Remotion can size\n * a composition, title it and zoom to clicks. Times are ms into the saved video; click positions\n * are pixels in it.\n */\nexport function videoManifest(\n { name, description, video, viewport, crop, width, start, end, clicks }: {\n name: string;\n description?: string;\n video: string;\n viewport: { width: number; height: number; };\n crop?: Crop;\n width?: number;\n /** Epoch ms where the saved video begins and ends. */\n start: number;\n end: number;\n /** Epoch ms and page coordinates of each pointerdown. */\n clicks: { at: number; x: number; y: number; }[];\n },\n) {\n const source = crop\n ? { ...crop, width: even(crop.width), height: even(crop.height) }\n : { x: 0, y: 0, ...viewport };\n\n const size = outputSize({ width: source.width, height: source.height }, width);\n const ratio = size.width / source.width;\n\n return {\n name,\n description,\n video,\n width: size.width,\n height: size.height,\n durationMs: Math.round(end - start),\n clicks: clicks\n .map((c) => ({\n t: Math.round(c.at - start),\n x: Math.round((c.x - Math.round(source.x)) * ratio),\n y: Math.round((c.y - Math.round(source.y)) * ratio),\n }))\n .filter((c) => c.t >= 0 && c.x >= 0 && c.y >= 0 && c.x < size.width && c.y < size.height),\n };\n}\n\n/**\n * A point in a frame, in page coordinates, so a click inside an iframe maps to the video.\n * Playwright measures the iframe element on the page, CSS transforms included; comparing that with\n * its layout size gives the scale for the border, padding and the point itself. Scales and\n * translations map exactly; a rotated or skewed iframe maps to its bounding box.\n */\nexport async function pagePoint(frame: Frame, x: number, y: number) {\n if (!frame.parentFrame()) return { x, y };\n\n const element = await frame.frameElement();\n\n const [box, layout] = await Promise.all([\n element.boundingBox(),\n element.evaluate((node) => {\n // frameElement() returns the <iframe> or <frame> element, so this always passes.\n if (!(node instanceof HTMLElement)) return undefined;\n\n const style = getComputedStyle(node);\n\n return {\n left: node.clientLeft + parseFloat(style.paddingLeft),\n top: node.clientTop + parseFloat(style.paddingTop),\n width: node.offsetWidth,\n height: node.offsetHeight,\n };\n }),\n ]);\n\n if (!box || !layout?.width || !layout.height) throw new Error('The iframe is not visible');\n\n const scaleX = box.width / layout.width;\n const scaleY = box.height / layout.height;\n\n return { x: box.x + (layout.left + x) * scaleX, y: box.y + (layout.top + y) * scaleY };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/skills.ts"],"sourcesContent":["import {\n cpSync,\n existsSync,\n lstatSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n rmSync,\n symlinkSync,\n writeFileSync,\n} from 'node:fs';\nimport { dirname, join, relative } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { z } from 'zod';\nimport { CliError } from './errors.js';\n\n/** Shipped in the package, so the guidance always matches the installed version. */\nconst SKILLS = fileURLToPath(new URL('../skills/', import.meta.url));\n\nexport const SKILL_NAMES = ['state-scenarios', 'create-scenario-map'];\n\nconst topics = () =>\n readdirSync(join(SKILLS, 'state-scenarios/references')).map((f) => f.replace(/\\.md$/, '')).toSorted();\n\n/** The skill's overview, or one topic of it. */\nexport function guide(topic?: string): string {\n if (!topic) return readFileSync(join(SKILLS, 'state-scenarios/SKILL.md'), 'utf8');\n\n if (!topics().includes(topic)) {\n throw new CliError('INVALID_ARGUMENT', `Unknown topic \"${topic}\". Topics: ${topics().join(', ')}`);\n }\n\n return readFileSync(join(SKILLS, `state-scenarios/references/${topic}.md`), 'utf8');\n}\n\n/**\n * Copy the skills into `.agents/skills/` (Codex, Cursor and others read it) and link them\n * from `.claude/skills/` for Claude Code. Rerun after an upgrade to update them.\n */\nexport function installSkills(cwd: string): string[] {\n return SKILL_NAMES.map((name) => {\n const target = join(cwd, '.agents/skills', name);\n rmSync(target, { recursive: true, force: true });\n mkdirSync(dirname(target), { recursive: true });\n cpSync(join(SKILLS, name), target, { recursive: true });\n\n const link = join(cwd, '.claude/skills', name);\n mkdirSync(dirname(link), { recursive: true });\n\n if (existsSync(link) || isLink(link)) rmSync(link, { recursive: true, force: true });\n\n try {\n symlinkSync(relative(dirname(link), target), link, 'dir');\n } catch {\n // Windows without Developer Mode can't create symlinks: copy instead.\n cpSync(target, link, { recursive: true });\n }\n\n return `Installed skill ${relative(cwd, target).replaceAll('\\\\', '/')} (and .claude/skills/${name})`;\n });\n}\n\nconst isLink = (path: string) => {\n try {\n return lstatSync(path).isSymbolicLink();\n } catch {\n return false;\n }\n};\n\nconst McpConfig = z.looseObject({ mcpServers: z.record(z.string(), z.unknown()).optional() });\n\n/**\n * Register `state-scenarios mcp` with Claude Code (`.mcp.json`) and, when the project uses it,\n * Cursor (`.cursor/mcp.json`). An existing entry is kept.\n */\nexport function registerMcp(cwd: string, args: string[]): string[] {\n // Windows can't spawn npx directly from an MCP client: go through cmd.\n const server = process.platform === 'win32'\n ? { command: 'cmd', args: ['/c', 'npx', 'state-scenarios', ...args] }\n : { command: 'npx', args: ['state-scenarios', ...args] };\n\n const files = ['.mcp.json', ...(existsSync(join(cwd, '.cursor')) ? ['.cursor/mcp.json'] : [])];\n\n return files.map((file) => {\n const path = join(cwd, file);\n const config = existsSync(path) ? McpConfig.parse(JSON.parse(readFileSync(path, 'utf8'))) : {};\n\n if (config.mcpServers?.['state-scenarios']) return `Kept the state-scenarios server in ${file}`;\n writeFileSync(\n path,\n `${\n JSON.stringify(\n { ...config, mcpServers: { ...config.mcpServers, 'state-scenarios': server } },\n null,\n 2,\n )\n }\\n`,\n );\n\n return `Registered the state-scenarios MCP server in ${file}`;\n });\n}\n"],"mappings":"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","names":[]}
|