state-scenarios-cli 0.2.0 → 0.4.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 (47) hide show
  1. package/README.md +10 -1
  2. package/dist/browser.d.ts +27 -0
  3. package/dist/chunk-GCYOLAFU.js +40 -0
  4. package/dist/chunk-GCYOLAFU.js.map +1 -0
  5. package/dist/chunk-O6QTNAAZ.js +67 -0
  6. package/dist/chunk-O6QTNAAZ.js.map +1 -0
  7. package/dist/chunk-YFNROT3R.js +115 -0
  8. package/dist/chunk-YFNROT3R.js.map +1 -0
  9. package/dist/cli.js +260 -43
  10. package/dist/cli.js.map +1 -1
  11. package/dist/doctor-URGIFA3W.js +200 -0
  12. package/dist/doctor-URGIFA3W.js.map +1 -0
  13. package/dist/doctor.d.ts +21 -0
  14. package/dist/errors-G7ODRBLT.js +11 -0
  15. package/dist/errors-G7ODRBLT.js.map +1 -0
  16. package/dist/errors.d.ts +33 -0
  17. package/dist/init-MRK4F5SV.js +96 -0
  18. package/dist/init-MRK4F5SV.js.map +1 -0
  19. package/dist/init.d.ts +6 -0
  20. package/dist/inspect-LN4EACWA.js +13 -0
  21. package/dist/inspect-LN4EACWA.js.map +1 -0
  22. package/dist/inspect.d.ts +63 -0
  23. package/dist/mcp-HRQIWAIR.js +342 -0
  24. package/dist/mcp-HRQIWAIR.js.map +1 -0
  25. package/dist/mcp-protocol.d.ts +51 -0
  26. package/dist/mcp.d.ts +19 -0
  27. package/dist/promote-GSF5UXJL.js +78 -0
  28. package/dist/promote-GSF5UXJL.js.map +1 -0
  29. package/dist/promote.d.ts +12 -0
  30. package/dist/record-WEZRD2VT.js +59 -0
  31. package/dist/record-WEZRD2VT.js.map +1 -0
  32. package/dist/record.d.ts +41 -0
  33. package/dist/skills-NQRJHKHG.js +80 -0
  34. package/dist/skills-NQRJHKHG.js.map +1 -0
  35. package/dist/skills.d.ts +13 -0
  36. package/package.json +5 -4
  37. package/skills/create-scenario-map/SKILL.md +88 -0
  38. package/skills/state-scenarios/SKILL.md +91 -0
  39. package/skills/state-scenarios/references/auth.md +52 -0
  40. package/skills/state-scenarios/references/capture.md +45 -0
  41. package/skills/state-scenarios/references/debugging.md +52 -0
  42. package/skills/state-scenarios/references/mcp.md +39 -0
  43. package/skills/state-scenarios/references/media.md +50 -0
  44. package/skills/state-scenarios/references/network.md +67 -0
  45. package/skills/state-scenarios/references/setup.md +94 -0
  46. package/skills/state-scenarios/references/verify.md +47 -0
  47. package/skills/state-scenarios/references/writing-scenarios.md +55 -0
@@ -0,0 +1,41 @@
1
+ export type VideoFormat = 'mp4' | 'gif' | 'webm';
2
+ export interface Crop {
3
+ x: number;
4
+ y: number;
5
+ width: number;
6
+ height: number;
7
+ }
8
+ /**
9
+ * The part of an element's box the video shows. Video holds only the viewport, so a crop must
10
+ * stay inside it; `clipped` says the element was larger than the visible area.
11
+ */
12
+ export declare function visibleCrop(box: Crop, viewport: {
13
+ width: number;
14
+ height: number;
15
+ }): {
16
+ crop: {
17
+ x: number;
18
+ y: number;
19
+ width: number;
20
+ height: number;
21
+ };
22
+ clipped: boolean;
23
+ } | undefined;
24
+ /**
25
+ * ffmpeg arguments that trim the page load off the start, crop to an element, and encode.
26
+ * MP4 is H.264 in yuv420p so it plays everywhere, including Safari and phones; GIF uses a
27
+ * generated palette, which keeps it sharp at a fraction of the naive size.
28
+ */
29
+ export declare function ffmpegArgs({ input, output, format, start, crop }: {
30
+ input: string;
31
+ output: string;
32
+ format: VideoFormat;
33
+ /** Seconds to skip, e.g. the page loading before the steps begin. */
34
+ start: number;
35
+ crop?: Crop;
36
+ }): string[];
37
+ /**
38
+ * Convert Playwright's WebM into the requested format. Uncropped WebM is copied as recorded
39
+ * (page load included), so it works without ffmpeg.
40
+ */
41
+ export declare function encodeVideo(options: Parameters<typeof ffmpegArgs>[0]): void;
@@ -0,0 +1,80 @@
1
+ import {
2
+ CliError
3
+ } from "./chunk-GCYOLAFU.js";
4
+
5
+ // src/skills.ts
6
+ import {
7
+ cpSync,
8
+ existsSync,
9
+ lstatSync,
10
+ mkdirSync,
11
+ readdirSync,
12
+ readFileSync,
13
+ rmSync,
14
+ symlinkSync,
15
+ writeFileSync
16
+ } from "fs";
17
+ import { dirname, join, relative } from "path";
18
+ import { fileURLToPath } from "url";
19
+ import { z } from "zod";
20
+ var SKILLS = fileURLToPath(new URL("../skills/", import.meta.url));
21
+ var SKILL_NAMES = ["state-scenarios", "create-scenario-map"];
22
+ var topics = () => readdirSync(join(SKILLS, "state-scenarios/references")).map((f) => f.replace(/\.md$/, "")).toSorted();
23
+ function guide(topic) {
24
+ if (!topic) return readFileSync(join(SKILLS, "state-scenarios/SKILL.md"), "utf8");
25
+ if (!topics().includes(topic)) {
26
+ throw new CliError("INVALID_ARGUMENT", `Unknown topic "${topic}". Topics: ${topics().join(", ")}`);
27
+ }
28
+ return readFileSync(join(SKILLS, `state-scenarios/references/${topic}.md`), "utf8");
29
+ }
30
+ function installSkills(cwd) {
31
+ return SKILL_NAMES.map((name) => {
32
+ const target = join(cwd, ".agents/skills", name);
33
+ rmSync(target, { recursive: true, force: true });
34
+ mkdirSync(dirname(target), { recursive: true });
35
+ cpSync(join(SKILLS, name), target, { recursive: true });
36
+ const link = join(cwd, ".claude/skills", name);
37
+ mkdirSync(dirname(link), { recursive: true });
38
+ if (existsSync(link) || isLink(link)) rmSync(link, { recursive: true, force: true });
39
+ try {
40
+ symlinkSync(relative(dirname(link), target), link, "dir");
41
+ } catch {
42
+ cpSync(target, link, { recursive: true });
43
+ }
44
+ return `Installed skill ${relative(cwd, target).replaceAll("\\", "/")} (and .claude/skills/${name})`;
45
+ });
46
+ }
47
+ var isLink = (path) => {
48
+ try {
49
+ return lstatSync(path).isSymbolicLink();
50
+ } catch {
51
+ return false;
52
+ }
53
+ };
54
+ var McpConfig = z.looseObject({ mcpServers: z.record(z.string(), z.unknown()).optional() });
55
+ function registerMcp(cwd, args) {
56
+ const server = process.platform === "win32" ? { command: "cmd", args: ["/c", "npx", "state-scenarios", ...args] } : { command: "npx", args: ["state-scenarios", ...args] };
57
+ const files = [".mcp.json", ...existsSync(join(cwd, ".cursor")) ? [".cursor/mcp.json"] : []];
58
+ return files.map((file) => {
59
+ const path = join(cwd, file);
60
+ const config = existsSync(path) ? McpConfig.parse(JSON.parse(readFileSync(path, "utf8"))) : {};
61
+ if (config.mcpServers?.["state-scenarios"]) return `Kept the state-scenarios server in ${file}`;
62
+ writeFileSync(
63
+ path,
64
+ `${JSON.stringify(
65
+ { ...config, mcpServers: { ...config.mcpServers, "state-scenarios": server } },
66
+ null,
67
+ 2
68
+ )}
69
+ `
70
+ );
71
+ return `Registered the state-scenarios MCP server in ${file}`;
72
+ });
73
+ }
74
+ export {
75
+ SKILL_NAMES,
76
+ guide,
77
+ installSkills,
78
+ registerMcp
79
+ };
80
+ //# sourceMappingURL=skills-NQRJHKHG.js.map
@@ -0,0 +1 @@
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":[]}
@@ -0,0 +1,13 @@
1
+ export declare const SKILL_NAMES: string[];
2
+ /** The skill's overview, or one topic of it. */
3
+ export declare function guide(topic?: string): string;
4
+ /**
5
+ * Copy the skills into `.agents/skills/` (Codex, Cursor and others read it) and link them
6
+ * from `.claude/skills/` for Claude Code. Rerun after an upgrade to update them.
7
+ */
8
+ export declare function installSkills(cwd: string): string[];
9
+ /**
10
+ * Register `state-scenarios mcp` with Claude Code (`.mcp.json`) and, when the project uses it,
11
+ * Cursor (`.cursor/mcp.json`). An existing entry is kept.
12
+ */
13
+ export declare function registerMcp(cwd: string, args: string[]): string[];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "state-scenarios-cli",
3
- "version": "0.2.0",
4
- "description": "state-scenarios CLI: validate, list, screenshot (shoot) and AI-suggest scenarios.",
3
+ "version": "0.4.0",
4
+ "description": "state-scenarios CLI: init, guide, doctor, validate, list, shoot, record, mcp, suggest and promote scenarios.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -10,7 +10,8 @@
10
10
  },
11
11
  "type": "module",
12
12
  "files": [
13
- "dist"
13
+ "dist",
14
+ "skills"
14
15
  ],
15
16
  "bin": {
16
17
  "state-scenarios": "./dist/cli.js"
@@ -28,7 +29,7 @@
28
29
  "dependencies": {
29
30
  "@anthropic-ai/sdk": "^0.131.0",
30
31
  "ajv": "^8.20.0",
31
- "state-scenarios": "^0.2.0",
32
+ "state-scenarios": "^0.4.0",
32
33
  "zod": "4.6.5"
33
34
  },
34
35
  "peerDependencies": {
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: create-scenario-map
3
+ description: >
4
+ Map every user-facing feature of an app that uses state-scenarios to the UI states it can be in
5
+ (loading, empty, error, signed out, edge cases), mark which have scenarios, capture the missing
6
+ ones from the running app, and write it all as a project-local skill so later agents can show,
7
+ check or screenshot any state. Use for /create-scenario-map, "which states are we missing",
8
+ "map our UI states", or before relying on scenarios for QA or docs images.
9
+ disable-model-invocation: true
10
+ ---
11
+
12
+ # Create a scenario map
13
+
14
+ A coding agent that changes UI should be able to open every state that change touches, by name,
15
+ without clicking through the app. This skill writes that map for one project as a skill,
16
+ `.agents/skills/scenarios-<app>/`, built on state-scenarios, then captures the most important
17
+ missing states and proves the map once. Write it for the next agent, which reads it cold, mid-task.
18
+
19
+ The `state-scenarios` skill is the reference for the library. Read it first and link to its topics
20
+ (`setup`, `capture`, `verify`, `mcp`, `debugging`) rather than restating them. It is
21
+ `.agents/skills/state-scenarios/SKILL.md` once `npx state-scenarios init` ran, and
22
+ `npx state-scenarios guide <topic>` prints it from the installed package.
23
+
24
+ ## 1. Learn the app from the repo, not the user
25
+
26
+ Ask the user only what the code can't tell you.
27
+
28
+ | Question | Where the answer is | What it decides |
29
+ | ------------------ | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
30
+ | Wiring | `startScenarios` call, scenarios folder, `doctor` | Nothing wired: follow topic `setup` first. `doctor` must pass before you map anything |
31
+ | Run | Dev command, port, env vars, seed data | The launch line and `--base-url` |
32
+ | Features | Routes, navigation, page components, docs | One map row per user-facing feature |
33
+ | States | Each feature's data fetching, conditionals, error boundaries, empty branches, permission checks, feature flags | The states per feature: loading, empty, populated, long content, error, partial failure, signed out, no permission, and the feature's own edge cases |
34
+ | Endpoints | API clients, query hooks, existing MSW handlers and fixtures | Which `network` entries a state needs, and whether `handlers` can reuse test handlers |
35
+ | Auth | How the app decides who is signed in | The `signed-in` base scenario (topic `auth`) |
36
+ | Existing scenarios | `npx state-scenarios list <dir> --json` | Which states are covered already |
37
+
38
+ ## 2. Write the map
39
+
40
+ `.agents/skills/scenarios-<app>/SKILL.md`, with frontmatter (`name: scenarios-<app>`, a description
41
+ naming the app and when to use it), then:
42
+
43
+ | Section | Holds |
44
+ | -------------- | ---------------------------------------------------------------------------------------------- |
45
+ | Read first | One line sending the reader to the `state-scenarios` skill, and the scenarios folder |
46
+ | Launch | The exact dev command, port, env and seed data; the line that shows it's ready |
47
+ | Doctor | `npx state-scenarios doctor <dir> --base-url <url>`; run it first whenever something looks off |
48
+ | Open a state | The link pattern and the MCP server; that links beat clicking through |
49
+ | Base scenarios | What `default` and `signed-in` (or the app's equivalents) contain, and which to extend |
50
+ | Map | A table: feature, route, state, scenario name, status (`covered`, `missing`, `can't mock`) |
51
+ | Gotchas | What reads as broken but isn't; third parties in `ignore`; states only the server can produce |
52
+
53
+ For more than a handful of features, put one file per feature under `features/` with the same
54
+ columns and link them from the map.
55
+
56
+ Name scenarios `<feature>-<state>`, e.g. `orders-empty`, `orders-error`, `checkout-card-declined`.
57
+
58
+ ## 3. Fill the most important gaps
59
+
60
+ Capture the missing states users hit most (empty and error for each main feature first), using
61
+ topic `capture`: over MCP, `open_app` or `open_scenario`, act until the state shows, `check_ready`,
62
+ `save_scenario` with `extends`. Mark each `covered` as you go. Leave the rest `missing`; the map is
63
+ the to-do list.
64
+
65
+ A state the browser can't produce (server-rendered data, a native permission prompt) is
66
+ `can't mock`, with one line on why.
67
+
68
+ ## 4. Prove it before handing over
69
+
70
+ 1. `doctor` passes.
71
+ 2. `npx state-scenarios validate <dir>` passes.
72
+ 3. `npx state-scenarios shoot <dir> --base-url <url> --only <the scenarios you added>` passes. Look
73
+ at each screenshot: it must show the state the map says.
74
+ 4. Open one state through the link pattern the map gives.
75
+
76
+ Fix what fails. A map that was never run is a draft.
77
+
78
+ ## 5. Hand over
79
+
80
+ Tell the user: the skill path, features mapped, states covered and missing, the scenarios you added,
81
+ and the screenshot folder. Don't commit for them. When a feature gains a state, its map row changes
82
+ in the same commit as the scenario.
83
+
84
+ ## Rules
85
+
86
+ - Write from what you observed. A section that could apply to any app isn't finished.
87
+ - No secrets or real personal data in scenarios or the map. Use fictional people.
88
+ - Link the `state-scenarios` topics instead of repeating them.
@@ -0,0 +1,91 @@
1
+ ---
2
+ name: state-scenarios
3
+ description: >
4
+ Open any UI state of a web app from a URL with state-scenarios: JSON scenarios that mock API
5
+ responses, set URL params and store or cache state. Covers adding it to an app, writing and
6
+ capturing scenarios, signed-in states, strict network mode, checking a UI change across every
7
+ state with `shoot`, docs screenshots and videos with `record`, driving the app over the
8
+ `state-scenarios mcp` server, and fixing failures with `doctor` and error codes. Use when a
9
+ project depends on state-scenarios, when asked to show, test, demo, screenshot or record a
10
+ loading, empty, error, signed-in or edge-case state, after changing UI code, or when a
11
+ `?scenario=` link or `shoot` run fails.
12
+ ---
13
+
14
+ # state-scenarios: any UI state from a URL
15
+
16
+ A scenario is a JSON file describing one UI state: the route, the API responses (served by MSW in
17
+ the browser), URL params, and store or cache state. `startScenarios` runs before the app renders and
18
+ applies the scenario named in the URL. `?scenario=orders-empty` opens that state; nothing else in
19
+ the app changes.
20
+
21
+ ```json
22
+ {
23
+ "$schema": "../scenario.schema.json",
24
+ "name": "orders-empty",
25
+ "extends": "default",
26
+ "path": "/orders",
27
+ "network": [{ "method": "GET", "path": "/api/orders", "response": { "body": [] } }],
28
+ "ready": "text=No orders yet"
29
+ }
30
+ ```
31
+
32
+ ## Reach a state with a link, not by clicking
33
+
34
+ Any browser tool can open a state directly: the `state-scenarios mcp` server, Playwright,
35
+ agent-browser, Playwright MCP, or an AI testing agent. Use the link instead of clicking through the
36
+ app to reach a state:
37
+
38
+ ```
39
+ http://localhost:5173/orders?scenario=orders-empty&scenario-strict
40
+ ```
41
+
42
+ `npx state-scenarios list <dir> --base-url <url> --json` prints every scenario with its link.
43
+ `&scenario-strict` answers any request the scenario didn't mock with a 501, so nothing reaches a
44
+ real backend. Once `<html data-scenario="orders-empty">` is set and the `ready` selector is
45
+ visible, the state is on screen.
46
+
47
+ ## Topics
48
+
49
+ Read the topic for the job before you start. The files sit next to this one, and
50
+ `npx state-scenarios guide <topic>` prints the same text from the installed package.
51
+
52
+ | Topic | File | Read it when |
53
+ | ------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
54
+ | `setup` | [references/setup.md](references/setup.md) | Adding state-scenarios to an app: install, wiring before render, routers created on import, adapters |
55
+ | `writing-scenarios` | [references/writing-scenarios.md](references/writing-scenarios.md) | Writing or editing scenario JSON: fields, `extends`, sequences, loading states, rules |
56
+ | `capture` | [references/capture.md](references/capture.md) | Making a scenario from the running app: the panel, `?scenario-record`, MCP `save_scenario` |
57
+ | `auth` | [references/auth.md](references/auth.md) | Signed-in and signed-out states, cookies, apps that validate a token on startup |
58
+ | `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 |
60
+ | `media` | [references/media.md](references/media.md) | Screenshots and videos for docs and websites: viewports, dark mode, locales, `record` |
61
+ | `mcp` | [references/mcp.md](references/mcp.md) | Driving the live app from a coding agent: open, observe, act, check ready, save |
62
+ | `debugging` | [references/debugging.md](references/debugging.md) | Something fails: `doctor`, error codes and exit codes, common causes |
63
+
64
+ ## Workflow
65
+
66
+ 1. Look at what exists: `state-scenarios` in `package.json`, the scenarios folder (often
67
+ `src/scenarios/*.json`), and where `startScenarios` is called. Nothing there: follow `setup`.
68
+ 2. If anything looks off, run `npx state-scenarios doctor <dir> --base-url <url>` first. It checks
69
+ the wiring one step at a time and prints the fix for each problem.
70
+ 3. New state: capture it from the running app (topic `capture`) instead of writing fixtures by
71
+ 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.
75
+ 5. `npx state-scenarios validate <dir>` before committing.
76
+
77
+ ## Rules
78
+
79
+ - Every scenario gets a `ready` selector: text or a role that appears only once this state is on
80
+ screen. Without it, tools screenshot whatever rendered first.
81
+ - Run in strict mode (`&scenario-strict`, the default for `shoot`, `openScenario` and MCP). A
82
+ request that reaches a real backend makes the state depend on that backend.
83
+ - Prefer mocking the network over setting store state: a mocked response exercises the app's real
84
+ fetching and error handling. Use `state` only for what no response can produce.
85
+ - Prefer capturing over hand-writing fixtures, and `extends: "default"` so a scenario holds only
86
+ what differs.
87
+ - Never put real secrets or personal data in a scenario. Recording redacts secret-looking fields
88
+ and re-issues JWTs unsigned, but check what it saved before committing.
89
+ - Don't edit recorded fixtures by hand to make a test pass; record again or fix the app.
90
+ - Keep `startScenarios` behind the app's development check so production bundles skip it.
91
+ - Run the CLI as `npx state-scenarios ...` (or `pnpm exec state-scenarios ...`).
@@ -0,0 +1,52 @@
1
+ # Signed-in and signed-out states
2
+
3
+ ## Cookie sessions
4
+
5
+ `cookiesAdapter()` from `state-scenarios/cookies` sets cookies before the app renders, so a cookie
6
+ check lets the scenario in:
7
+
8
+ ```json
9
+ { "name": "signed-in", "state": { "cookies": { "session": "scenario" } } }
10
+ ```
11
+
12
+ It overwrites real cookies of the same name. It applies from catalog scenarios only, never from
13
+ inline `#scenario-data=` links, which anyone can write.
14
+
15
+ ## Apps that validate a token on startup
16
+
17
+ Many apps read a stored token, send it to a validate endpoint, decode the JWT that comes back and
18
+ fill stores from its claims. A signed-in scenario then needs the cookie (so the check runs) and a
19
+ mocked validate response carrying a token. `unsignedJwt(claims)` builds one: UTF-8 safe, so names
20
+ in any script decode, and unsigned (`alg: "none"`), so anything that verifies signatures rejects
21
+ it. Build that scenario in code and let JSON scenarios extend it:
22
+
23
+ ```ts
24
+ import { type ScenarioInput, startScenarios, unsignedJwt } from 'state-scenarios';
25
+ import { cookiesAdapter } from 'state-scenarios/cookies';
26
+
27
+ const signedIn: ScenarioInput = {
28
+ name: 'signed-in',
29
+ state: { cookies: { session: 'scenario' } }, // so the app's startup check runs
30
+ network: [{
31
+ method: 'POST',
32
+ path: '/api/auth/validate-token',
33
+ response: {
34
+ body: {
35
+ token: unsignedJwt({ sub: 'user-1', name: 'Amara Okafor', roles: ['admin'], exp: 4102444800 }),
36
+ },
37
+ },
38
+ }],
39
+ };
40
+
41
+ await startScenarios({ scenarios: [signedIn], adapters: [cookiesAdapter()] });
42
+ ```
43
+
44
+ Use the claim names and endpoint the app reads; find them where the app decodes the token. Set
45
+ `exp` in the future when the app checks expiry. `?scenario-record` captures the real exchange too;
46
+ recording keeps the claims and drops the signature.
47
+
48
+ ## Signed out and expired
49
+
50
+ Mock the validate endpoint with a 401 (or whatever the app treats as an expired session), and ready
51
+ on the sign-in screen's heading. A scenario without the cookie only shows signed out in a browser
52
+ that never had it, such as `shoot` and the MCP server, which start each run fresh.
@@ -0,0 +1,45 @@
1
+ # Capturing scenarios from the running app
2
+
3
+ Capturing beats writing fixtures by hand: the responses are real, and the scenario matches what the
4
+ app actually renders.
5
+
6
+ ## From a coding agent (MCP)
7
+
8
+ With the `state-scenarios` MCP server registered (topic `mcp`):
9
+
10
+ 1. `open_app { path: "/orders", record: true }`: requests reach the real backend and their JSON
11
+ responses are recorded. Or `open_scenario { name: "default" }` to start from a scenario.
12
+ 2. Drive the app into the state with `click`, `fill` and `press`; `observe` shows the page.
13
+ 3. `check_ready { selector: "text=No orders yet" }` until it reports `match` and `visible`. Its
14
+ `suggestions` list distinctive selectors on the page.
15
+ 4. `save_scenario { name: "orders-empty", ready: "text=No orders yet", extends: "default" }` writes
16
+ `<dir>/orders-empty.json` with only the differences from `default`, and reports problems.
17
+ 5. `open_scenario { name: "orders-empty" }` replays it offline in strict mode. It should come back
18
+ with `ok: true`.
19
+
20
+ ## From the panel (people)
21
+
22
+ 1. Open the app with `?scenario-record` and click through to the state.
23
+ 2. Panel → **Save as scenario** → **Mark ready** and click distinctive UI (or pick a suggestion).
24
+ The line under Ready says whether it matches.
25
+ 3. Choose **Extends** (`default` when present) → **Save to src/scenarios** (Vite plugin or Next.js
26
+ route), or **Download JSON**. **Copy link** shares a self-contained snapshot instead.
27
+
28
+ ## From code
29
+
30
+ `captureScenario(session, { name, ready, base })` returns `{ flat, forCatalog }`; write `forCatalog`
31
+ to the scenarios folder. `checkReadySelector` and `suggestReadySelectors` check and propose ready
32
+ selectors.
33
+
34
+ ## What recording keeps and removes
35
+
36
+ - JSON responses only. Repeated calls to one endpoint become a `sequence` in call order.
37
+ - Secrets: values under secret-like keys (`password`, `secret`, `apiKey`, `token`, `sessionId`, …)
38
+ become `"[redacted]"`, and signed JWTs are re-issued unsigned with the same claims, so the app
39
+ still decodes them but no server accepts them. The browser console lists what was redacted.
40
+ - Inside any string, `Bearer`/`Basic` credentials, `user:pass@` in URLs and signed URL params
41
+ are redacted too.
42
+ - Saving applies the same rules to adapter state, and leaves secret URL params (`key`,
43
+ `access_token`, …) out of `url` and recorded queries.
44
+ - Personal data (names, emails, addresses) is kept. Record with a test account, or replace it
45
+ before committing.
@@ -0,0 +1,52 @@
1
+ # Debugging
2
+
3
+ Start with doctor. It is read-only: it opens one scenario and writes nothing.
4
+
5
+ ```bash
6
+ npx state-scenarios doctor src/scenarios --base-url http://localhost:5173
7
+ ```
8
+
9
+ It checks, in order: the scenarios parse and resolve; ffmpeg; the skills and MCP server are
10
+ installed; the dev server answers; `/mockServiceWorker.js` is served when scenarios mock the
11
+ network; Chromium starts; a scenario becomes active and shows its ready selector; the panel can
12
+ save into the project. Each problem prints its code and fix. `--json` for scripts.
13
+
14
+ ## Exit codes
15
+
16
+ | Exit | Class | Meaning |
17
+ | ---: | ------ | ---------------------------------------------------------------------------- |
18
+ | 0 | | Everything passed |
19
+ | 1 | failed | A scenario ran and failed, or validation found problems |
20
+ | 2 | config | Bad arguments, invalid or unknown scenarios |
21
+ | 3 | infra | Something needed is missing or down: dev server, browser, ffmpeg, MSW worker |
22
+
23
+ ## Error codes
24
+
25
+ | Code | Exit | Cause | Fix |
26
+ | --------------------- | ---: | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
27
+ | `SESSION_NOT_STARTED` | 1 | `<html data-scenario>` never named the scenario | `startScenarios` must run and be awaited before render, outside production-only code; import routers created on import after it |
28
+ | `SCENARIO_NOT_IN_APP` | 1 | The session runs, but the app's catalog has no scenario by that name | Add the file to the folder or list the app loads; reload or restart the dev server for new files |
29
+ | `READY_TIMEOUT` | 1 | The ready selector never became visible | Read `<out>/<name>.md` and the screenshot: fix the selector or the app |
30
+ | `UNMOCKED_REQUEST` | 1 | The scenario didn't cover a request | Add the pasted `network` entries, `extends` a scenario that has them, or `ignore` third-party SDKs |
31
+ | `PAGE_ERROR` | 1 | Uncaught exception while rendering | Fix the app, or the fixture that crashes it |
32
+ | `CONSOLE_ERROR` | 1 | `console.error` during render | Same; browser "Failed to load resource" logs are not counted |
33
+ | `ELEMENT_NOT_FOUND` | 1 | `--element` matched nothing visible | Check the selector |
34
+ | `STEPS_FAILED` | 1 | A `record` step threw | Fix the step; selectors must match the scenario's UI |
35
+ | `INVALID_ARGUMENT` | 2 | A flag or argument is wrong | The message says which |
36
+ | `INVALID_SCENARIO` | 2 | A scenario file doesn't parse or resolve | `validate` lists them |
37
+ | `UNKNOWN_SCENARIO` | 2 | A name isn't in the catalog | The message lists known names |
38
+ | `DEV_SERVER_DOWN` | 3 | Nothing answers at `--base-url` | Start the dev server, or pass the URL it prints |
39
+ | `BROWSER_MISSING` | 3 | Playwright or Chromium isn't installed | `npm i -D @playwright/test && npx playwright install chromium` |
40
+ | `FFMPEG_MISSING` | 3 | `record` needs ffmpeg | Install it, or set `FFMPEG_PATH` |
41
+ | `WORKER_NOT_SERVED` | 3 | `/mockServiceWorker.js` 404s | `msw({ mode: 'worker-only' })` in Vite, or `npx msw init public` |
42
+
43
+ ## Common causes
44
+
45
+ - A scenario works in the panel but not from a link: the app reads the URL before
46
+ `startScenarios` finishes. Await it before render.
47
+ - A state flickers to real data: the scenario misses a request, so it went to the backend. Use
48
+ strict mode and read the unmocked list.
49
+ - Switching scenarios reloads the page: expected when URL params differ, state can't be un-applied,
50
+ or scenarios mock the network without `refresh`.
51
+ - A path with a file extension (`/api/data.json`) passes as an asset in strict mode; mock it
52
+ explicitly.
@@ -0,0 +1,39 @@
1
+ # Driving the app over MCP
2
+
3
+ `state-scenarios mcp <dir> --base-url <url>` serves the running app to a coding agent over MCP
4
+ (stdio). `init` registers it in `.mcp.json` (Claude Code) and `.cursor/mcp.json` (Cursor). By hand:
5
+
6
+ ```bash
7
+ claude mcp add state-scenarios -- npx state-scenarios mcp src/scenarios --base-url http://localhost:5173
8
+ codex mcp add state-scenarios -- npx state-scenarios mcp src/scenarios --base-url http://localhost:5173
9
+ ```
10
+
11
+ The dev server must already be running. Each agent's client starts its own server, and each server
12
+ has its own headless browser page. Browser flags (`--viewport`, `--locale`, `--timezone`,
13
+ `--color-scheme`) apply.
14
+
15
+ ## Tools
16
+
17
+ | Tool | Does |
18
+ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
19
+ | `list_scenarios` | Every scenario with description, ready selector and link |
20
+ | `validate` | Problems in the catalog; empty means valid |
21
+ | `open_scenario` | Opens a scenario in strict mode, waits for ready; returns codes, unmocked requests, errors and a screenshot |
22
+ | `open_app` | Opens a path without a scenario; `record: true` records real responses for `save_scenario` |
23
+ | `observe` | The page as an accessibility tree, plus URL, active scenario, unmocked requests and errors |
24
+ | `screenshot` | PNG of the page or of one element |
25
+ | `click`, `fill`, `press` | Act on the page; selectors are Playwright selectors (`text=Save`, `role=button[name="Save"]`, CSS) |
26
+ | `check_ready` | Whether a selector matches and is visible now, plus selectors that would work |
27
+ | `save_scenario` | Saves the current state as `<dir>/<name>.json`, `extends` writing only differences; refuses to overwrite unless `replace: true` |
28
+
29
+ Failed calls return `isError` with a code in brackets, e.g. `[UNKNOWN_SCENARIO]`.
30
+
31
+ ## Loops
32
+
33
+ Check a change: `list_scenarios`, then `open_scenario` for each affected state; read the codes and
34
+ look at the screenshot.
35
+
36
+ Add a state: `open_app` (with `record: true`) or `open_scenario`, act until the state shows,
37
+ `check_ready`, `save_scenario`, then `open_scenario` on the new name to prove it replays offline.
38
+
39
+ The panel is hidden in MCP sessions; agents read the page through `observe`.
@@ -0,0 +1,50 @@
1
+ # Screenshots and videos for docs and websites
2
+
3
+ ## Screenshots
4
+
5
+ ```bash
6
+ npx state-scenarios shoot src/scenarios --base-url http://localhost:5173 --out shots \
7
+ --no-panel --viewport 1440x900 --scale 2 --color-scheme dark --element main
8
+ ```
9
+
10
+ - `--no-panel` hides the dev panel. `--scale 2` gives sharp images on high-density screens.
11
+ - `--element <selector>` captures one component instead of the full page.
12
+ - A scenario's `viewport` overrides `--viewport`, so phone and desktop layouts shoot in one run.
13
+ - `--locale` (BCP 47: `en-GB`, `de-DE`, `ar-EG`, `hi-IN`, `ja-JP`, `pt-BR`) and `--timezone` (IANA:
14
+ `Europe/Berlin`, `Asia/Kolkata`, `America/Sao_Paulo`) set what `Intl`, `Date` and
15
+ `navigator.language` report. Set both, or shots change with each machine's settings. Run once
16
+ per market to show dates, numbers, currencies and right-to-left layouts as people there see them.
17
+
18
+ ## Videos
19
+
20
+ ```ts
21
+ // steps.ts: scenario name → what to do once it is ready
22
+ import type { Page } from '@playwright/test';
23
+
24
+ export default {
25
+ 'search-no-results': async (page: Page) => {
26
+ await page.getByRole('searchbox').click();
27
+ await page.keyboard.type('Atlantis', { delay: 100 });
28
+ },
29
+ };
30
+ ```
31
+
32
+ ```bash
33
+ npx state-scenarios record src/scenarios --base-url http://localhost:5173 --steps steps.ts --format mp4
34
+ ```
35
+
36
+ - The panel is hidden and a cursor is drawn (Playwright videos don't show the mouse). Actions are
37
+ slowed slightly so the cursor reaches each target before the click.
38
+ - The video starts once the scenario is ready and holds the last frame briefly. Uncropped
39
+ `--format webm` is the exception: it keeps the page load, and needs no ffmpeg.
40
+ - `--format mp4` (default) plays in every browser and phone at a fraction of a GIF's size; use it
41
+ on websites. `--format gif` for READMEs and other image-only places. `--element` crops: the
42
+ element is scrolled into view first, and one larger than the viewport is cut to the visible part
43
+ (use a larger `--viewport`). Steps that scroll the page move the element out of the crop.
44
+ - Video records at viewport size, so use a larger `--viewport` for sharper video; `--scale`
45
+ affects screenshots only.
46
+ - MP4, GIF and `--element` need ffmpeg on `PATH`, or `FFMPEG_PATH`. Uncropped WebM doesn't.
47
+ - In your own Playwright scripts, `showCursor(page)` from `state-scenarios/playwright` draws the
48
+ cursor; call it before `openScenario(page, name, { panel: false })`.
49
+
50
+ Use demo data in scenarios meant for public images: fictional names, no real customer records.