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.
Files changed (33) hide show
  1. package/README.md +6 -1
  2. package/dist/cli.d.ts +2 -2
  3. package/dist/cli.js +224 -17
  4. package/dist/cli.js.map +1 -1
  5. package/dist/compare-TBRNNO4A.js +69 -0
  6. package/dist/compare-TBRNNO4A.js.map +1 -0
  7. package/dist/compare.d.ts +14 -0
  8. package/dist/map-NAV75ZEE.js +159 -0
  9. package/dist/map-NAV75ZEE.js.map +1 -0
  10. package/dist/map.d.ts +73 -0
  11. package/dist/publish-K6YWPOCO.js +80 -0
  12. package/dist/publish-K6YWPOCO.js.map +1 -0
  13. package/dist/publish.d.ts +18 -0
  14. package/dist/{record-SNQF63Y7.js → record-BO2NJ4Q2.js} +3 -2
  15. package/dist/record-BO2NJ4Q2.js.map +1 -0
  16. package/dist/record.d.ts +3 -1
  17. package/dist/{skills-NQRJHKHG.js → skills-P5SE3XSV.js} +7 -2
  18. package/dist/skills-P5SE3XSV.js.map +1 -0
  19. package/package.json +3 -3
  20. package/skills/create-scenario-map/SKILL.md +24 -4
  21. package/skills/scenario-coverage-loop/SKILL.md +122 -0
  22. package/skills/scenario-coverage-loop/references/controller.mjs +38 -0
  23. package/skills/scenario-coverage-loop/references/memory.md +13 -0
  24. package/skills/scenario-coverage-loop/references/pr-check.yml +50 -0
  25. package/skills/scenario-coverage-loop/references/prompt.md +58 -0
  26. package/skills/scenario-coverage-loop/references/workflow.yml +232 -0
  27. package/skills/scenario-pr/SKILL.md +114 -0
  28. package/skills/scenario-pr/references/pr-template.md +61 -0
  29. package/skills/state-scenarios/SKILL.md +12 -4
  30. package/skills/state-scenarios/references/media.md +9 -5
  31. package/skills/state-scenarios/references/verify.md +55 -3
  32. package/dist/record-SNQF63Y7.js.map +0 -1
  33. 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
+ ![{scenario-name}]({published URL})
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 across every state with `shoot`, reading failure pages, PR evidence |
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 shoot <dir> --base-url <url>` and look at the screenshots of the
73
- states your change touches (topic `verify`). Each failed scenario gets `<out>/<name>.md` with the
74
- fix; read that page, not the JSON.
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). Actions are
44
- slowed slightly so the cursor reaches each target before the click.
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. See
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` draws the
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. Attach the
30
- relevant ones to the PR. Pages for passing scenarios are deleted on each run, so the `.md` files
31
- in the output folder are always the current failures.
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":";;;;;AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,SAAS,MAAM,gBAAgB;AACxC,SAAS,qBAAqB;AAC9B,SAAS,SAAS;AAIlB,IAAM,SAAS,cAAc,IAAI,IAAI,cAAc,YAAY,GAAG,CAAC;AAE5D,IAAM,cAAc,CAAC,mBAAmB,qBAAqB;AAEpE,IAAM,SAAS,MACb,YAAY,KAAK,QAAQ,4BAA4B,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,SAAS,EAAE,CAAC,EAAE,SAAS;AAG/F,SAAS,MAAM,OAAwB;AAC5C,MAAI,CAAC,MAAO,QAAO,aAAa,KAAK,QAAQ,0BAA0B,GAAG,MAAM;AAEhF,MAAI,CAAC,OAAO,EAAE,SAAS,KAAK,GAAG;AAC7B,UAAM,IAAI,SAAS,oBAAoB,kBAAkB,KAAK,cAAc,OAAO,EAAE,KAAK,IAAI,CAAC,EAAE;AAAA,EACnG;AAEA,SAAO,aAAa,KAAK,QAAQ,8BAA8B,KAAK,KAAK,GAAG,MAAM;AACpF;AAMO,SAAS,cAAc,KAAuB;AACnD,SAAO,YAAY,IAAI,CAAC,SAAS;AAC/B,UAAM,SAAS,KAAK,KAAK,kBAAkB,IAAI;AAC/C,WAAO,QAAQ,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAC/C,cAAU,QAAQ,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9C,WAAO,KAAK,QAAQ,IAAI,GAAG,QAAQ,EAAE,WAAW,KAAK,CAAC;AAEtD,UAAM,OAAO,KAAK,KAAK,kBAAkB,IAAI;AAC7C,cAAU,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAE5C,QAAI,WAAW,IAAI,KAAK,OAAO,IAAI,EAAG,QAAO,MAAM,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAEnF,QAAI;AACF,kBAAY,SAAS,QAAQ,IAAI,GAAG,MAAM,GAAG,MAAM,KAAK;AAAA,IAC1D,QAAQ;AAEN,aAAO,QAAQ,MAAM,EAAE,WAAW,KAAK,CAAC;AAAA,IAC1C;AAEA,WAAO,mBAAmB,SAAS,KAAK,MAAM,EAAE,WAAW,MAAM,GAAG,CAAC,wBAAwB,IAAI;AAAA,EACnG,CAAC;AACH;AAEA,IAAM,SAAS,CAAC,SAAiB;AAC/B,MAAI;AACF,WAAO,UAAU,IAAI,EAAE,eAAe;AAAA,EACxC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,IAAM,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS,EAAE,CAAC;AAMrF,SAAS,YAAY,KAAa,MAA0B;AAEjE,QAAM,SAAS,QAAQ,aAAa,UAChC,EAAE,SAAS,OAAO,MAAM,CAAC,MAAM,OAAO,mBAAmB,GAAG,IAAI,EAAE,IAClE,EAAE,SAAS,OAAO,MAAM,CAAC,mBAAmB,GAAG,IAAI,EAAE;AAEzD,QAAM,QAAQ,CAAC,aAAa,GAAI,WAAW,KAAK,KAAK,SAAS,CAAC,IAAI,CAAC,kBAAkB,IAAI,CAAC,CAAE;AAE7F,SAAO,MAAM,IAAI,CAAC,SAAS;AACzB,UAAM,OAAO,KAAK,KAAK,IAAI;AAC3B,UAAM,SAAS,WAAW,IAAI,IAAI,UAAU,MAAM,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC,CAAC,IAAI,CAAC;AAE7F,QAAI,OAAO,aAAa,iBAAiB,EAAG,QAAO,sCAAsC,IAAI;AAC7F;AAAA,MACE;AAAA,MACA,GACE,KAAK;AAAA,QACH,EAAE,GAAG,QAAQ,YAAY,EAAE,GAAG,OAAO,YAAY,mBAAmB,OAAO,EAAE;AAAA,QAC7E;AAAA,QACA;AAAA,MACF,CACF;AAAA;AAAA,IACF;AAEA,WAAO,gDAAgD,IAAI;AAAA,EAC7D,CAAC;AACH;","names":[]}