state-scenarios-cli 0.3.0 → 0.5.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 (45) 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 +264 -44
  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-VLZQEJRD.js → init-MRK4F5SV.js} +2 -1
  18. package/dist/init-MRK4F5SV.js.map +1 -0
  19. package/dist/init.d.ts +5 -1
  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/record-HXIYGMWA.js +65 -0
  28. package/dist/record-HXIYGMWA.js.map +1 -0
  29. package/dist/record.d.ts +45 -0
  30. package/dist/skills-NQRJHKHG.js +80 -0
  31. package/dist/skills-NQRJHKHG.js.map +1 -0
  32. package/dist/skills.d.ts +13 -0
  33. package/package.json +5 -4
  34. package/skills/create-scenario-map/SKILL.md +88 -0
  35. package/skills/state-scenarios/SKILL.md +91 -0
  36. package/skills/state-scenarios/references/auth.md +52 -0
  37. package/skills/state-scenarios/references/capture.md +45 -0
  38. package/skills/state-scenarios/references/debugging.md +52 -0
  39. package/skills/state-scenarios/references/mcp.md +39 -0
  40. package/skills/state-scenarios/references/media.md +60 -0
  41. package/skills/state-scenarios/references/network.md +67 -0
  42. package/skills/state-scenarios/references/setup.md +94 -0
  43. package/skills/state-scenarios/references/verify.md +47 -0
  44. package/skills/state-scenarios/references/writing-scenarios.md +55 -0
  45. package/dist/init-VLZQEJRD.js.map +0 -1
@@ -0,0 +1,94 @@
1
+ # Setup
2
+
3
+ ## Install and init
4
+
5
+ ```bash
6
+ npm i state-scenarios zod msw # msw only when scenarios mock the network
7
+ npm i -D state-scenarios-cli @playwright/test
8
+ npm i state-scenarios-react # useScenarioState, for component state (React, Preact)
9
+
10
+ npx state-scenarios init src/scenarios
11
+ ```
12
+
13
+ `init` writes `src/scenarios/default.json` and `src/scenario.schema.json`, prints a wiring snippet,
14
+ installs these skills into `.agents/skills/` (and `.claude/skills/`), and registers the MCP server
15
+ in `.mcp.json`. It never edits your source files. `--no-skills` skips the skills and the MCP server.
16
+
17
+ Vite: serve the MSW worker and let the panel save into the project.
18
+
19
+ ```ts
20
+ // vite.config.ts
21
+ import { msw } from 'msw/vite';
22
+ import { stateScenarios } from 'state-scenarios/vite';
23
+
24
+ export default { plugins: [msw({ mode: 'worker-only' }), stateScenarios({ dir: 'src/scenarios' })] };
25
+ ```
26
+
27
+ Without the MSW Vite plugin: `npx msw init public`.
28
+
29
+ ## Wire the runtime before render
30
+
31
+ ```ts
32
+ import { type ScenarioInput, startScenarios } from 'state-scenarios';
33
+ import { mountPanel } from 'state-scenarios/panel';
34
+
35
+ if (import.meta.env.DEV) {
36
+ const session = await startScenarios({
37
+ scenarios: Object.values(
38
+ import.meta.glob<ScenarioInput>('/src/scenarios/*.json', { eager: true, import: 'default' }),
39
+ ),
40
+ adapters: [], // see below
41
+ refresh: () => queryClient.resetQueries(), // when scenarios mock the network
42
+ });
43
+ mountPanel(session, { schemaPath: '../scenario.schema.json' });
44
+ }
45
+ // then create the router and render
46
+ ```
47
+
48
+ - `await` it before rendering. The first fetches must already hit the scenario's mocks.
49
+ - Routers read the URL once, when they're created. If a module creates the router as soon as it
50
+ is imported (`export const router = createBrowserRouter(...)`), a static import runs before the
51
+ session. Import it after:
52
+
53
+ ```ts
54
+ // main.ts: start the session first, then import the router
55
+ import type { ScenarioInput } from 'state-scenarios';
56
+
57
+ if (import.meta.env.DEV) {
58
+ const { startScenarios } = await import('state-scenarios');
59
+ await startScenarios({
60
+ scenarios: Object.values(
61
+ import.meta.glob<ScenarioInput>('/src/scenarios/*.json', { eager: true, import: 'default' }),
62
+ ),
63
+ });
64
+ }
65
+
66
+ const { router } = await import('./routing/router'); // created now, with the scenario's URL
67
+ ```
68
+
69
+ - Next.js (App Router): start it from a `'use client'` wrapper in the root layout and render
70
+ children once it resolves. The package README has the component and the save route.
71
+ - Apps that already run MSW: start your worker and pass it as `worker`. See topic `network`.
72
+
73
+ ## Connect app state
74
+
75
+ Mock the network first. Add adapters for state no response can produce, such as a panel left open
76
+ or cached data during a refetch.
77
+
78
+ | State lives in | Connect with |
79
+ | -------------- | ------------------------------------------------------------------------------------------------------------ |
80
+ | TanStack Query | `tanstackQuery(queryClient)` from `state-scenarios/tanstack-query` |
81
+ | Zustand | `zustandAdapter(store, { schema: StoreSchema.partial() })` from `state-scenarios/zustand` |
82
+ | Redux | `withScenarioState(rootReducer, StateSchema)`, then `reduxAdapter(store)` |
83
+ | XState | `xstateAdapter(machine, { context: ContextSchema })`, then `createActor(machine, { snapshot: xs.snapshot })` |
84
+ | Cookies | `cookiesAdapter()` from `state-scenarios/cookies` |
85
+ | A component | `useScenarioState('page', state, (s) => dispatch({ type: 'set', state: s }), Schema)` |
86
+
87
+ Pass a Zod schema wherever an adapter accepts one: invalid scenario state is then rejected before
88
+ anything changes. `scenarioJsonSchema({ state: { key: Schema } })` regenerates the editor schema
89
+ with typed `state`.
90
+
91
+ ## Check
92
+
93
+ `npx state-scenarios doctor src/scenarios --base-url http://localhost:5173` passes. Opening
94
+ `/?scenario=default` sets `<html data-scenario="default">`.
@@ -0,0 +1,47 @@
1
+ # Verify a UI change across every state
2
+
3
+ Start the dev server, then:
4
+
5
+ ```bash
6
+ npx state-scenarios shoot src/scenarios --base-url http://localhost:5173 --out .scenario-shots
7
+ ```
8
+
9
+ For each scenario `shoot` opens the link in strict mode, waits for `ready`, saves `<name>.png`, and
10
+ checks for unmocked requests, uncaught exceptions and `console.error` calls. `--only a,b` limits
11
+ the run; `--json` prints the report for scripts.
12
+
13
+ ## When a scenario fails
14
+
15
+ The terminal names a page for it: `❯ details .scenario-shots/<name>.md`. Read that page. It has:
16
+
17
+ - the codes (`READY_TIMEOUT`, `UNMOCKED_REQUEST`, `SESSION_NOT_STARTED`, `SCENARIO_NOT_IN_APP`, `PAGE_ERROR`,
18
+ `CONSOLE_ERROR`, `ELEMENT_NOT_FOUND`) and the link that reopens the state;
19
+ - for a ready timeout, whether the selector matched anything, plus distinctive selectors on the
20
+ page now;
21
+ - for unmocked requests, `network` entries ready to paste (fill in real bodies, or record them);
22
+ - uncaught exceptions and console errors.
23
+
24
+ Look at the screenshot before changing the scenario: a ready timeout can mean the selector is
25
+ wrong, or that the UI change broke the state. Fix the app when the screenshot shows a bug.
26
+
27
+ ## Evidence
28
+
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.
32
+
33
+ ## In Playwright tests
34
+
35
+ ```ts
36
+ import { openScenario, unhandledRequests } from 'state-scenarios/playwright';
37
+
38
+ await openScenario(page, 'orders-empty');
39
+ expect(await unhandledRequests(page)).toEqual([]);
40
+ ```
41
+
42
+ ## Missing states
43
+
44
+ Ask which states lack scenarios: loading, empty, error, partial data, long content, permissions,
45
+ signed out. Capture them (topic `capture`). `npx state-scenarios suggest` asks a model to list
46
+ candidate states from the source and drafts them into `proposed/`; treat drafts as ideas, then
47
+ `promote` the reviewed ones.
@@ -0,0 +1,55 @@
1
+ # Writing scenarios
2
+
3
+ | Field | Meaning | Through `extends` |
4
+ | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
5
+ | `name` | kebab-case, unique | – |
6
+ | `extends` | Scenario name(s) to build on; later parents win | – |
7
+ | `path` | Route it opens at, e.g. `/orders/42` | inherited |
8
+ | `url` | Query params the app starts with | merged; `null` removes |
9
+ | `network[]` | `method`, `path` (MSW syntax: `/api/users/:id`, absolute URLs, `*`), optional `query`, then one of `response`, `sequence`, `remove: true` | a child entry replaces the parent's with the same method, path and query |
10
+ | `handlers` | Names of MSW handlers passed to `startScenarios({ handlers })` | combined |
11
+ | `state` | Per-adapter state, keyed by adapter key | deep-merged; `null` removes |
12
+ | `ready` | Playwright selector visible once this state shows | not inherited |
13
+ | `viewport` | `{ "width": 390, "height": 844 }` for `shoot` and `record` | inherited |
14
+ | `description`, `tags` | For people and the panel | not inherited |
15
+
16
+ Responses: `body` is sent as JSON, `text` as text (an HTML error page). `status` defaults to 200.
17
+ `headers` sets headers. `delay` is milliseconds, or `"infinite"` to hold the request open for a
18
+ loading state. `sequence` answers successive calls in order and repeats the last one, e.g. fail
19
+ then succeed on retry:
20
+
21
+ ```json
22
+ {
23
+ "name": "flaky-then-recovers",
24
+ "extends": "default",
25
+ "network": [{
26
+ "path": "/api/orders",
27
+ "sequence": [{ "status": 503, "body": { "error": "busy" } }, { "body": [{ "id": 1 }] }]
28
+ }],
29
+ "ready": "text=Order #1"
30
+ }
31
+ ```
32
+
33
+ Loading state: `"response": { "delay": "infinite" }` and `"ready": "text=Loading orders…"`.
34
+
35
+ ## Ready selectors
36
+
37
+ `text=No orders yet`, `role=heading[name="Orders"]`, `[data-testid=empty-orders]`, or CSS. Pick
38
+ something that is visible only in this state: a count like "0" or a heading every state shares is
39
+ too generic. Check it against the running app (panel, or MCP `check_ready`) before saving.
40
+
41
+ ## Combining
42
+
43
+ `?scenario=signed-in,server-error` combines scenarios like `extends`, later names winning.
44
+
45
+ ## Rules and validation
46
+
47
+ `defineScenarioRules` checks business rules on every resolved scenario. Export `rules` from a module
48
+ and pass it to `startScenarios({ rules })` and `--rules` on the CLI.
49
+
50
+ ```bash
51
+ npx state-scenarios validate src/scenarios --rules src/scenario-rules.ts
52
+ ```
53
+
54
+ Schema shape, `extends` lookups and rules are checked. Whether production can actually reach a
55
+ state is your call: a valid scenario can describe a response the backend never sends.
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/init.ts"],"sourcesContent":["import { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname, join, relative, resolve } from 'node:path';\nimport { scenarioJsonSchema } from 'state-scenarios';\n\n/** Create a scenarios dir, schema, default stub, and print a wiring snippet. */\nexport function initScenarios(scenariosDir: string, cwd = process.cwd()) {\n const dir = resolve(cwd, scenariosDir);\n mkdirSync(dir, { recursive: true });\n\n const schemaPath = join(dirname(dir), 'scenario.schema.json');\n const defaultPath = join(dir, 'default.json');\n const schemaRel = relative(dir, schemaPath).replaceAll('\\\\', '/');\n\n const schemaExisted = existsSync(schemaPath);\n const defaultExisted = existsSync(defaultPath);\n\n if (!schemaExisted) {\n writeFileSync(schemaPath, `${JSON.stringify(scenarioJsonSchema(), null, 2)}\\n`);\n }\n\n if (!defaultExisted) {\n writeFileSync(\n defaultPath,\n `${\n JSON.stringify(\n {\n $schema: schemaRel,\n name: 'default',\n description: 'Happy path: record the real network, then Save as scenario from the panel',\n },\n null,\n 2,\n )\n }\\n`,\n );\n }\n\n const stack = detectStack(cwd);\n const scenariosImport = relative(cwd, dir).replaceAll('\\\\', '/');\n\n console.log(`${defaultExisted ? 'Kept existing' : 'Created'} ${relative(cwd, defaultPath)}`);\n console.log(`${schemaExisted ? 'Kept existing' : 'Created'} ${relative(cwd, schemaPath)}`);\n console.log('');\n console.log('Wire the runtime (DEV only), then open the app with ?scenario-record,');\n console.log('click through the flow, Mark ready, and Download JSON into this folder.');\n console.log('');\n console.log(wiringSnippet(stack, scenariosImport));\n}\n\ntype Stack = 'vite' | 'next' | 'unknown';\n\nfunction detectStack(cwd: string): Stack {\n if (hasFile(cwd, ['next.config.ts', 'next.config.js', 'next.config.mjs', 'next.config.cjs'])) {\n return 'next';\n }\n\n if (hasFile(cwd, ['vite.config.ts', 'vite.config.js', 'vite.config.mjs', 'vite.config.cjs'])) {\n return 'vite';\n }\n\n return 'unknown';\n}\n\nfunction hasFile(cwd: string, names: string[]): boolean {\n return names.some((name) => existsSync(join(cwd, name)));\n}\n\nfunction wiringSnippet(stack: Stack, scenariosImport: string): string {\n // Root-relative: Vite resolves '/…' globs from the project root, wherever the snippet is pasted.\n const glob = `import.meta.glob<ScenarioInput>('${\n scenariosImport ? `/${scenariosImport}` : ''\n }/*.json', { eager: true, import: 'default' })`;\n\n if (stack === 'next') {\n return `// Next.js (App Router): wrap children in a client <Scenarios> gate (see README).\n// List scenario JSON imports explicitly (Next has no import.meta.glob).\n// Serve the MSW worker with: npx msw init public\n//\n// mountPanel(session, { schemaPath: '../scenario.schema.json' });\n//\n// Let the panel save into ${scenariosImport || '.'}: create app/%5F%5Fstate-scenarios/save/route.ts with\n// import { createSaveRoute } from 'state-scenarios/next';\n// export const { GET } = createSaveRoute({ dir: '${scenariosImport || '.'}' });`;\n }\n\n const plugins = `import { msw } from 'msw/vite';\n// import { stateScenarios } from 'state-scenarios/vite';\n// plugins: [msw({ mode: 'worker-only' }), stateScenarios({ dir: '${scenariosImport || '.'}' })]\n// stateScenarios lets the panel save scenarios straight into this folder.`;\n\n const viteNote = stack === 'vite'\n ? `// In vite.config.ts:\\n// ${plugins}`\n : `// Vite, in vite.config.ts:\\n// ${plugins}\n// Next: see README \"Next.js (App Router)\"`;\n\n return `${viteNote}\n\nimport { type ScenarioInput, startScenarios } from 'state-scenarios';\nimport { mountPanel } from 'state-scenarios/panel';\n// import { tanstackQuery } from 'state-scenarios/tanstack-query'; // if you use it\n\nif (import.meta.env.DEV) {\n const session = await startScenarios({\n scenarios: Object.values(${glob}),\n // adapters: [tanstackQuery(queryClient)],\n // refresh: () => queryClient.resetQueries(),\n });\n mountPanel(session, { schemaPath: '../scenario.schema.json' });\n}\n\n// Then: open /?scenario-record → use the app → panel → Mark ready → Download JSON`;\n}\n"],"mappings":";AAAA,SAAS,YAAY,WAAW,qBAAqB;AACrD,SAAS,SAAS,MAAM,UAAU,eAAe;AACjD,SAAS,0BAA0B;AAG5B,SAAS,cAAc,cAAsB,MAAM,QAAQ,IAAI,GAAG;AACvE,QAAM,MAAM,QAAQ,KAAK,YAAY;AACrC,YAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAElC,QAAM,aAAa,KAAK,QAAQ,GAAG,GAAG,sBAAsB;AAC5D,QAAM,cAAc,KAAK,KAAK,cAAc;AAC5C,QAAM,YAAY,SAAS,KAAK,UAAU,EAAE,WAAW,MAAM,GAAG;AAEhE,QAAM,gBAAgB,WAAW,UAAU;AAC3C,QAAM,iBAAiB,WAAW,WAAW;AAE7C,MAAI,CAAC,eAAe;AAClB,kBAAc,YAAY,GAAG,KAAK,UAAU,mBAAmB,GAAG,MAAM,CAAC,CAAC;AAAA,CAAI;AAAA,EAChF;AAEA,MAAI,CAAC,gBAAgB;AACnB;AAAA,MACE;AAAA,MACA,GACE,KAAK;AAAA,QACH;AAAA,UACE,SAAS;AAAA,UACT,MAAM;AAAA,UACN,aAAa;AAAA,QACf;AAAA,QACA;AAAA,QACA;AAAA,MACF,CACF;AAAA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,QAAQ,YAAY,GAAG;AAC7B,QAAM,kBAAkB,SAAS,KAAK,GAAG,EAAE,WAAW,MAAM,GAAG;AAE/D,UAAQ,IAAI,GAAG,iBAAiB,kBAAkB,SAAS,IAAI,SAAS,KAAK,WAAW,CAAC,EAAE;AAC3F,UAAQ,IAAI,GAAG,gBAAgB,kBAAkB,SAAS,IAAI,SAAS,KAAK,UAAU,CAAC,EAAE;AACzF,UAAQ,IAAI,EAAE;AACd,UAAQ,IAAI,uEAAuE;AACnF,UAAQ,IAAI,yEAAyE;AACrF,UAAQ,IAAI,EAAE;AACd,UAAQ,IAAI,cAAc,OAAO,eAAe,CAAC;AACnD;AAIA,SAAS,YAAY,KAAoB;AACvC,MAAI,QAAQ,KAAK,CAAC,kBAAkB,kBAAkB,mBAAmB,iBAAiB,CAAC,GAAG;AAC5F,WAAO;AAAA,EACT;AAEA,MAAI,QAAQ,KAAK,CAAC,kBAAkB,kBAAkB,mBAAmB,iBAAiB,CAAC,GAAG;AAC5F,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAEA,SAAS,QAAQ,KAAa,OAA0B;AACtD,SAAO,MAAM,KAAK,CAAC,SAAS,WAAW,KAAK,KAAK,IAAI,CAAC,CAAC;AACzD;AAEA,SAAS,cAAc,OAAc,iBAAiC;AAEpE,QAAM,OAAO,oCACX,kBAAkB,IAAI,eAAe,KAAK,EAC5C;AAEA,MAAI,UAAU,QAAQ;AACpB,WAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,6BAMkB,mBAAmB,GAAG;AAAA;AAAA,sDAEG,mBAAmB,GAAG;AAAA,EAC1E;AAEA,QAAM,UAAU;AAAA;AAAA,sEAEoD,mBAAmB,GAAG;AAAA;AAG1F,QAAM,WAAW,UAAU,SACvB;AAAA,OAA+B,OAAO,KACtC;AAAA,OAAqC,OAAO;AAAA;AAGhD,SAAO,GAAG,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,+BAQW,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQnC;","names":[]}