state-scenarios-cli 0.6.0 → 0.7.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 (32) hide show
  1. package/README.md +6 -1
  2. package/dist/cli.js +194 -10
  3. package/dist/cli.js.map +1 -1
  4. package/dist/compare-TBRNNO4A.js +69 -0
  5. package/dist/compare-TBRNNO4A.js.map +1 -0
  6. package/dist/compare.d.ts +14 -0
  7. package/dist/map-NAV75ZEE.js +159 -0
  8. package/dist/map-NAV75ZEE.js.map +1 -0
  9. package/dist/map.d.ts +73 -0
  10. package/dist/publish-K6YWPOCO.js +80 -0
  11. package/dist/publish-K6YWPOCO.js.map +1 -0
  12. package/dist/publish.d.ts +18 -0
  13. package/dist/{record-SNQF63Y7.js → record-QUE3LDEG.js} +24 -1
  14. package/dist/record-QUE3LDEG.js.map +1 -0
  15. package/dist/record.d.ts +8 -1
  16. package/dist/{skills-NQRJHKHG.js → skills-P5SE3XSV.js} +7 -2
  17. package/dist/skills-P5SE3XSV.js.map +1 -0
  18. package/package.json +3 -3
  19. package/skills/create-scenario-map/SKILL.md +24 -4
  20. package/skills/scenario-coverage-loop/SKILL.md +122 -0
  21. package/skills/scenario-coverage-loop/references/controller.mjs +38 -0
  22. package/skills/scenario-coverage-loop/references/memory.md +13 -0
  23. package/skills/scenario-coverage-loop/references/pr-check.yml +50 -0
  24. package/skills/scenario-coverage-loop/references/prompt.md +58 -0
  25. package/skills/scenario-coverage-loop/references/workflow.yml +232 -0
  26. package/skills/scenario-pr/SKILL.md +114 -0
  27. package/skills/scenario-pr/references/pr-template.md +61 -0
  28. package/skills/state-scenarios/SKILL.md +12 -4
  29. package/skills/state-scenarios/references/media.md +7 -4
  30. package/skills/state-scenarios/references/verify.md +55 -3
  31. package/dist/record-SNQF63Y7.js.map +0 -1
  32. package/dist/skills-NQRJHKHG.js.map +0 -1
@@ -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 `locator.click()` 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.
@@ -56,8 +58,9 @@ npx state-scenarios record src/scenarios --base-url http://localhost:5173 --step
56
58
  - MP4, GIF, `--element`, `--fps` and `--width` need ffmpeg on `PATH`, or `FFMPEG_PATH`. WebM without them doesn't.
57
59
  - Each video gets a `<name>.video.json` manifest: size, length, description and clicks. See
58
60
  [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 })`.
61
+ - In your own Playwright scripts, `showCursor(page, { color, svg })` from `state-scenarios/playwright`
62
+ draws the cursor; call it before `openScenario(page, name, { panel: false })`. Hover before a
63
+ click there, so the cursor arrives first.
61
64
 
62
65
  ## Videos in Remotion
63
66
 
@@ -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":"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","names":[]}