state-scenarios-cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +27 -6
- package/dist/cli.js.map +1 -1
- package/dist/init-VLZQEJRD.js +95 -0
- package/dist/init-VLZQEJRD.js.map +1 -0
- package/dist/init.d.ts +2 -0
- package/dist/promote-GSF5UXJL.js +78 -0
- package/dist/promote-GSF5UXJL.js.map +1 -0
- package/dist/promote.d.ts +12 -0
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -8,6 +8,8 @@ import { parseArgs } from "util";
|
|
|
8
8
|
import { createCatalog, scenarioJsonSchema } from "state-scenarios";
|
|
9
9
|
var HELP = `Usage: state-scenarios <command> <scenarios-dir> [options]
|
|
10
10
|
|
|
11
|
+
init Create <dir> (default src/scenarios) with default.json, a sibling scenario.schema.json,
|
|
12
|
+
and print the wiring snippet to paste into your entry file
|
|
11
13
|
validate Check every scenario parses, resolves and passes --rules
|
|
12
14
|
list Print scenarios with links (--base-url) for people and agents; --json for machines
|
|
13
15
|
shoot Open each scenario in Chromium, wait for its ready selector, screenshot it,
|
|
@@ -16,6 +18,7 @@ var HELP = `Usage: state-scenarios <command> <scenarios-dir> [options]
|
|
|
16
18
|
--src dir[,dir] (required), --schema file, --max n, --model (default $STATE_SCENARIOS_MODEL or claude-opus-5-5).
|
|
17
19
|
Needs ANTHROPIC_API_KEY or \`ant auth login\`; ANTHROPIC_BASE_URL points it at a compatible proxy.
|
|
18
20
|
--env-file file loads those variables first.
|
|
21
|
+
promote Move <dir>/proposed/*.json into <dir> after validate. --dry-run lists moves only.
|
|
19
22
|
|
|
20
23
|
Common: --rules file module exporting \`rules\` (ScenarioRule[])`;
|
|
21
24
|
var MAX_SOURCE_CHARS = 8e5;
|
|
@@ -33,16 +36,27 @@ var { positionals, values } = parseArgs({
|
|
|
33
36
|
max: { type: "string" },
|
|
34
37
|
model: { type: "string" },
|
|
35
38
|
"env-file": { type: "string" },
|
|
39
|
+
"dry-run": { type: "boolean" },
|
|
36
40
|
help: { type: "boolean", short: "h" }
|
|
37
41
|
}
|
|
38
42
|
});
|
|
39
|
-
var [command, dir] = positionals;
|
|
43
|
+
var [command, dir = command === "init" ? "src/scenarios" : void 0] = positionals;
|
|
40
44
|
if (values.help || !command || !dir) {
|
|
41
45
|
console.log(HELP);
|
|
42
46
|
process.exit(command ? 1 : 0);
|
|
43
47
|
}
|
|
44
48
|
try {
|
|
49
|
+
if (command === "init") {
|
|
50
|
+
const { initScenarios } = await import("./init-VLZQEJRD.js");
|
|
51
|
+
initScenarios(dir);
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
45
54
|
const rules = values.rules ? (await import(pathToFileURL(resolve(values.rules)).href)).rules : [];
|
|
55
|
+
if (command === "promote") {
|
|
56
|
+
const { promoteScenarios } = await import("./promote-GSF5UXJL.js");
|
|
57
|
+
const result = promoteScenarios(dir, { rules, dryRun: !!values["dry-run"] });
|
|
58
|
+
process.exit(result.skipped.length ? 1 : 0);
|
|
59
|
+
}
|
|
46
60
|
const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
47
61
|
const catalog = createCatalog(
|
|
48
62
|
files.map((f) => JSON.parse(readFileSync(join(dir, f), "utf8"))),
|
|
@@ -68,8 +82,10 @@ try {
|
|
|
68
82
|
if (values.json) console.log(JSON.stringify(rows, null, 2));
|
|
69
83
|
else {
|
|
70
84
|
for (const r of rows) {
|
|
71
|
-
console.log(
|
|
72
|
-
|
|
85
|
+
console.log(
|
|
86
|
+
`${r.name.padEnd(32)} ${r.description}${r.url ? `
|
|
87
|
+
${" ".repeat(33)}${r.url}` : ""}`
|
|
88
|
+
);
|
|
73
89
|
}
|
|
74
90
|
}
|
|
75
91
|
break;
|
|
@@ -98,7 +114,10 @@ ${" ".repeat(33)}${r.url}` : ""}`);
|
|
|
98
114
|
page.on("pageerror", (err) => pageErrors.push(err.message));
|
|
99
115
|
let error = null;
|
|
100
116
|
try {
|
|
101
|
-
await openScenario(page, s.name, {
|
|
117
|
+
await openScenario(page, s.name, {
|
|
118
|
+
path: catalog.resolve(s).path,
|
|
119
|
+
strict: !values["no-strict"]
|
|
120
|
+
});
|
|
102
121
|
} catch (err) {
|
|
103
122
|
error = err instanceof Error ? err.message.split("\n")[0] : String(err);
|
|
104
123
|
}
|
|
@@ -175,8 +194,10 @@ ${result.states.length} states detected, ${covered} have scenarios, ${result.sta
|
|
|
175
194
|
}
|
|
176
195
|
for (const r of result.rejected) console.log(`- rejected proposal for ${r.covers}: ${r.error}`);
|
|
177
196
|
if (result.accepted.length) {
|
|
178
|
-
console.log(
|
|
179
|
-
|
|
197
|
+
console.log(
|
|
198
|
+
`
|
|
199
|
+
Review the drafts, then: state-scenarios promote ${dir}`
|
|
200
|
+
);
|
|
180
201
|
}
|
|
181
202
|
break;
|
|
182
203
|
}
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';\nimport { join, relative, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { parseArgs } from 'node:util';\nimport { createCatalog, scenarioJsonSchema, type ScenarioRule } from 'state-scenarios';\n\nconst HELP = `Usage: state-scenarios <command> <scenarios-dir> [options]\n\n validate Check every scenario parses, resolves and passes --rules\n list Print scenarios with links (--base-url) for people and agents; --json for machines\n shoot Open each scenario in Chromium, wait for its ready selector, screenshot it,\n and report requests it didn't mock. --base-url (required), --out, --only a,b, --no-strict\n suggest Ask Claude which UI states lack scenarios and draft them into --out (default <dir>/proposed).\n --src dir[,dir] (required), --schema file, --max n, --model (default $STATE_SCENARIOS_MODEL or claude-opus-5-5).\n Needs ANTHROPIC_API_KEY or \\`ant auth login\\`; ANTHROPIC_BASE_URL points it at a compatible proxy.\n --env-file file loads those variables first.\n\nCommon: --rules file module exporting \\`rules\\` (ScenarioRule[])`;\n\nconst MAX_SOURCE_CHARS = 800_000;\n\nconst { positionals, values } = parseArgs({\n allowPositionals: true,\n options: {\n rules: { type: 'string' },\n 'base-url': { type: 'string' },\n out: { type: 'string' },\n only: { type: 'string' },\n 'no-strict': { type: 'boolean' },\n json: { type: 'boolean' },\n src: { type: 'string' },\n schema: { type: 'string' },\n max: { type: 'string' },\n model: { type: 'string' },\n 'env-file': { type: 'string' },\n help: { type: 'boolean', short: 'h' },\n },\n});\n\nconst [command, dir] = positionals;\n\nif (values.help || !command || !dir) {\n console.log(HELP);\n process.exit(command ? 1 : 0);\n}\n\ntry {\n const rules: ScenarioRule[] = values.rules\n ? (await import(pathToFileURL(resolve(values.rules)).href)).rules\n : [];\n\n const files = readdirSync(dir).filter((f) => f.endsWith('.json'));\n\n const catalog = createCatalog(\n files.map((f) => JSON.parse(readFileSync(join(dir, f), 'utf8'))),\n { rules },\n );\n\n switch (command) {\n case 'validate': {\n const problems = catalog.check();\n\n for (const p of problems) console.error(`✗ ${p}\\n`);\n console.log(`${catalog.list.length - problems.length}/${catalog.list.length} scenarios valid`);\n process.exit(problems.length ? 1 : 0);\n }\n\n case 'list': {\n const base = values['base-url'];\n\n const rows = catalog.list.map((s) => ({\n name: s.name,\n description: s.description ?? '',\n tags: s.tags ?? [],\n ready: s.ready ?? null,\n url: base\n ? `${base.replace(/\\/$/, '')}${catalog.resolve(s).path ?? '/'}?scenario=${\n encodeURIComponent(s.name)\n }`\n : null,\n }));\n\n if (values.json) console.log(JSON.stringify(rows, null, 2));\n else {for (const r of rows) {\n console.log(`${r.name.padEnd(32)} ${r.description}${r.url ? `\\n${' '.repeat(33)}${r.url}` : ''}`);\n }}\n\n break;\n }\n\n case 'shoot': {\n const baseURL = values['base-url'];\n\n if (!baseURL) throw new Error('shoot needs --base-url, e.g. http://localhost:5173');\n const out = values.out ?? 'scenario-shots';\n const only = values.only?.split(',');\n mkdirSync(out, { recursive: true });\n\n const { chromium } = await import('@playwright/test');\n const { openScenario, unhandledRequests } = await import('state-scenarios/playwright');\n const browser = await chromium.launch();\n const report = [];\n\n for (const s of catalog.list.filter((s) => !only || only.includes(s.name))) {\n const context = await browser.newContext({ baseURL });\n const page = await context.newPage();\n page.setDefaultTimeout(10_000);\n // Uncaught exceptions and console errors both fail the run. Browser network logs\n // (\"Failed to load resource\") are excluded: error scenarios cause them on purpose,\n // and requests the scenario didn't mock are reported as `unhandled` instead.\n const consoleErrors: string[] = [];\n const pageErrors: string[] = [];\n page.on('console', (m) => {\n if (m.type() === 'error' && !m.text().startsWith('Failed to load resource')) {\n consoleErrors.push(m.text());\n }\n });\n page.on('pageerror', (err) => pageErrors.push(err.message));\n let error: string | null = null;\n\n try {\n await openScenario(page, s.name, { path: catalog.resolve(s).path, strict: !values['no-strict'] });\n } catch (err) {\n error = err instanceof Error ? err.message.split('\\n')[0] : String(err);\n }\n\n const screenshot = join(out, `${s.name}.png`);\n await page.screenshot({ path: screenshot, fullPage: true });\n const unhandled = await unhandledRequests(page);\n const ok = !error && !unhandled.length && !consoleErrors.length && !pageErrors.length;\n report.push({ name: s.name, ok, screenshot, unhandled, consoleErrors, pageErrors, error });\n await context.close();\n }\n\n await browser.close();\n\n if (values.json) console.log(JSON.stringify(report, null, 2));\n else {\n for (const r of report) {\n console.log(`${r.ok ? '✓' : '✗'} ${r.name.padEnd(32)} ${r.screenshot}`);\n\n if (r.error) console.log(` error: ${r.error}`);\n\n for (const u of r.unhandled) console.log(` unmocked: ${u}`);\n\n for (const e of r.pageErrors) console.log(` uncaught: ${e}`);\n\n for (const e of r.consoleErrors) console.log(` console.error: ${e}`);\n }\n }\n\n process.exit(report.every((r) => r.ok) ? 0 : 1);\n }\n\n case 'suggest': {\n if (!values.src) throw new Error('suggest needs --src, e.g. --src src/pages,src/components');\n const sources = values.src.split(',').flatMap((d) => walk(d));\n const size = sources.reduce((n, f) => n + f.content.length, 0);\n\n if (size > MAX_SOURCE_CHARS) {\n throw new Error(\n `--src is ${size.toLocaleString()} characters; narrow it to the screens you want covered`,\n );\n }\n\n const jsonSchema = values.schema\n ? JSON.parse(readFileSync(values.schema, 'utf8'))\n : scenarioJsonSchema();\n\n if (values['env-file']) process.loadEnvFile(values['env-file']);\n const { default: Anthropic } = await import('@anthropic-ai/sdk');\n const model = values.model ?? process.env.STATE_SCENARIOS_MODEL ?? undefined;\n const { suggestScenarios } = await import('./suggest.js');\n console.error(\n `Reading ${sources.length} files (${size.toLocaleString()} chars) and ${catalog.list.length} scenarios with ${\n model ?? 'claude-opus-5-5'\n }…`,\n );\n\n const result = await suggestScenarios(\n { sources, catalog, jsonSchema, max: Number(values.max ?? 8), model },\n {\n client: new Anthropic({\n baseURL: process.env.ANTHROPIC_BASE_URL || undefined,\n // Some Anthropic-compatible proxies require a session id and a user agent.\n defaultHeaders: {\n 'x-opencode-session': `state-scenarios-${Date.now()}`,\n 'User-Agent': 'state-scenarios/0.0 (suggest)',\n },\n }),\n },\n );\n\n console.log('\\nCandidate UI states (not exhaustive)\\n');\n\n for (const s of result.states) {\n const mark = s.covered_by ? '✓' : '✗';\n console.log(\n `${mark} ${`${s.area} / ${s.state}`.padEnd(56)} ${s.covered_by ?? `uncovered (${s.priority})`}`,\n );\n }\n\n const covered = result.states.filter((s) => s.covered_by).length;\n console.log(\n `\\n${result.states.length} states detected, ${covered} have scenarios, ${\n result.states.length - covered\n } uncovered`,\n );\n\n const out = values.out ?? join(dir, 'proposed');\n\n if (result.accepted.length) mkdirSync(out, { recursive: true });\n\n for (const { scenario, covers } of result.accepted) {\n const file = join(out, `${scenario.name}.json`);\n const $schema = values.schema ? relative(out, values.schema) : undefined;\n writeFileSync(file, `${JSON.stringify({ $schema, ...scenario }, null, 2)}\\n`);\n console.log(`+ ${file} (${covers})`);\n }\n\n for (const r of result.rejected) console.log(`- rejected proposal for ${r.covers}: ${r.error}`);\n\n if (result.accepted.length) {\n console.log(`\\nReview the drafts, then move the ones you want into ${dir}.`);\n }\n\n break;\n }\n\n default:\n console.error(`Unknown command \"${command}\"\\n\\n${HELP}`);\n process.exit(1);\n }\n} catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const hint = /authentication/i.test(message) ? '\\nSet ANTHROPIC_API_KEY, or run `ant auth login`.' : '';\n console.error(`state-scenarios ${command}: ${message}${hint}`);\n process.exit(1);\n}\n\nfunction walk(path: string): { path: string; content: string; }[] {\n if (statSync(path).isDirectory()) {\n return readdirSync(path)\n .filter((f) => !['node_modules', 'dist', 'scenarios'].includes(f))\n .flatMap((f) => walk(join(path, f)));\n }\n\n const isSource = /\\.(tsx?|jsx?|vue|svelte)$/.test(path) && !/\\.(d|test|spec)\\.\\w+$/.test(path);\n\n return isSource ? [{ path, content: readFileSync(path, 'utf8') }] : [];\n}\n"],"mappings":"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","names":["s"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';\nimport { join, relative, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { parseArgs } from 'node:util';\nimport { createCatalog, scenarioJsonSchema, type ScenarioRule } from 'state-scenarios';\n\nconst HELP = `Usage: state-scenarios <command> <scenarios-dir> [options]\n\n init Create <dir> (default src/scenarios) with default.json, a sibling scenario.schema.json,\n and print the wiring snippet to paste into your entry file\n validate Check every scenario parses, resolves and passes --rules\n list Print scenarios with links (--base-url) for people and agents; --json for machines\n shoot Open each scenario in Chromium, wait for its ready selector, screenshot it,\n and report requests it didn't mock. --base-url (required), --out, --only a,b, --no-strict\n suggest Ask Claude which UI states lack scenarios and draft them into --out (default <dir>/proposed).\n --src dir[,dir] (required), --schema file, --max n, --model (default $STATE_SCENARIOS_MODEL or claude-opus-5-5).\n Needs ANTHROPIC_API_KEY or \\`ant auth login\\`; ANTHROPIC_BASE_URL points it at a compatible proxy.\n --env-file file loads those variables first.\n promote Move <dir>/proposed/*.json into <dir> after validate. --dry-run lists moves only.\n\nCommon: --rules file module exporting \\`rules\\` (ScenarioRule[])`;\n\nconst MAX_SOURCE_CHARS = 800_000;\n\nconst { positionals, values } = parseArgs({\n allowPositionals: true,\n options: {\n rules: { type: 'string' },\n 'base-url': { type: 'string' },\n out: { type: 'string' },\n only: { type: 'string' },\n 'no-strict': { type: 'boolean' },\n json: { type: 'boolean' },\n src: { type: 'string' },\n schema: { type: 'string' },\n max: { type: 'string' },\n model: { type: 'string' },\n 'env-file': { type: 'string' },\n 'dry-run': { type: 'boolean' },\n help: { type: 'boolean', short: 'h' },\n },\n});\n\nconst [command, dir = command === 'init' ? 'src/scenarios' : undefined] = positionals;\n\nif (values.help || !command || !dir) {\n console.log(HELP);\n process.exit(command ? 1 : 0);\n}\n\ntry {\n if (command === 'init') {\n const { initScenarios } = await import('./init.js');\n initScenarios(dir);\n process.exit(0);\n }\n\n const rules: ScenarioRule[] = values.rules\n ? (await import(pathToFileURL(resolve(values.rules)).href)).rules\n : [];\n\n if (command === 'promote') {\n const { promoteScenarios } = await import('./promote.js');\n const result = promoteScenarios(dir, { rules, dryRun: !!values['dry-run'] });\n process.exit(result.skipped.length ? 1 : 0);\n }\n\n const files = readdirSync(dir).filter((f) => f.endsWith('.json'));\n\n const catalog = createCatalog(\n files.map((f) => JSON.parse(readFileSync(join(dir, f), 'utf8'))),\n { rules },\n );\n\n switch (command) {\n case 'validate': {\n const problems = catalog.check();\n\n for (const p of problems) console.error(`✗ ${p}\\n`);\n console.log(`${catalog.list.length - problems.length}/${catalog.list.length} scenarios valid`);\n process.exit(problems.length ? 1 : 0);\n }\n\n case 'list': {\n const base = values['base-url'];\n\n const rows = catalog.list.map((s) => ({\n name: s.name,\n description: s.description ?? '',\n tags: s.tags ?? [],\n ready: s.ready ?? null,\n url: base\n ? `${base.replace(/\\/$/, '')}${catalog.resolve(s).path ?? '/'}?scenario=${\n encodeURIComponent(s.name)\n }`\n : null,\n }));\n\n if (values.json) console.log(JSON.stringify(rows, null, 2));\n else {\n for (const r of rows) {\n console.log(\n `${r.name.padEnd(32)} ${r.description}${r.url ? `\\n${' '.repeat(33)}${r.url}` : ''}`,\n );\n }\n }\n\n break;\n }\n\n case 'shoot': {\n const baseURL = values['base-url'];\n\n if (!baseURL) throw new Error('shoot needs --base-url, e.g. http://localhost:5173');\n const out = values.out ?? 'scenario-shots';\n const only = values.only?.split(',');\n mkdirSync(out, { recursive: true });\n\n const { chromium } = await import('@playwright/test');\n const { openScenario, unhandledRequests } = await import('state-scenarios/playwright');\n const browser = await chromium.launch();\n const report = [];\n\n for (const s of catalog.list.filter((s) => !only || only.includes(s.name))) {\n const context = await browser.newContext({ baseURL });\n const page = await context.newPage();\n page.setDefaultTimeout(10_000);\n // Uncaught exceptions and console errors both fail the run. Browser network logs\n // (\"Failed to load resource\") are excluded: error scenarios cause them on purpose,\n // and requests the scenario didn't mock are reported as `unhandled` instead.\n const consoleErrors: string[] = [];\n const pageErrors: string[] = [];\n page.on('console', (m) => {\n if (m.type() === 'error' && !m.text().startsWith('Failed to load resource')) {\n consoleErrors.push(m.text());\n }\n });\n page.on('pageerror', (err) => pageErrors.push(err.message));\n let error: string | null = null;\n\n try {\n await openScenario(page, s.name, {\n path: catalog.resolve(s).path,\n strict: !values['no-strict'],\n });\n } catch (err) {\n error = err instanceof Error ? err.message.split('\\n')[0] : String(err);\n }\n\n const screenshot = join(out, `${s.name}.png`);\n await page.screenshot({ path: screenshot, fullPage: true });\n const unhandled = await unhandledRequests(page);\n const ok = !error && !unhandled.length && !consoleErrors.length && !pageErrors.length;\n report.push({ name: s.name, ok, screenshot, unhandled, consoleErrors, pageErrors, error });\n await context.close();\n }\n\n await browser.close();\n\n if (values.json) console.log(JSON.stringify(report, null, 2));\n else {\n for (const r of report) {\n console.log(`${r.ok ? '✓' : '✗'} ${r.name.padEnd(32)} ${r.screenshot}`);\n\n if (r.error) console.log(` error: ${r.error}`);\n\n for (const u of r.unhandled) console.log(` unmocked: ${u}`);\n\n for (const e of r.pageErrors) console.log(` uncaught: ${e}`);\n\n for (const e of r.consoleErrors) console.log(` console.error: ${e}`);\n }\n }\n\n process.exit(report.every((r) => r.ok) ? 0 : 1);\n }\n\n case 'suggest': {\n if (!values.src) throw new Error('suggest needs --src, e.g. --src src/pages,src/components');\n const sources = values.src.split(',').flatMap((d) => walk(d));\n const size = sources.reduce((n, f) => n + f.content.length, 0);\n\n if (size > MAX_SOURCE_CHARS) {\n throw new Error(\n `--src is ${size.toLocaleString()} characters; narrow it to the screens you want covered`,\n );\n }\n\n const jsonSchema = values.schema\n ? JSON.parse(readFileSync(values.schema, 'utf8'))\n : scenarioJsonSchema();\n\n if (values['env-file']) process.loadEnvFile(values['env-file']);\n const { default: Anthropic } = await import('@anthropic-ai/sdk');\n const model = values.model ?? process.env.STATE_SCENARIOS_MODEL ?? undefined;\n const { suggestScenarios } = await import('./suggest.js');\n console.error(\n `Reading ${sources.length} files (${size.toLocaleString()} chars) and ${catalog.list.length} scenarios with ${\n model ?? 'claude-opus-5-5'\n }…`,\n );\n\n const result = await suggestScenarios(\n { sources, catalog, jsonSchema, max: Number(values.max ?? 8), model },\n {\n client: new Anthropic({\n baseURL: process.env.ANTHROPIC_BASE_URL || undefined,\n // Some Anthropic-compatible proxies require a session id and a user agent.\n defaultHeaders: {\n 'x-opencode-session': `state-scenarios-${Date.now()}`,\n 'User-Agent': 'state-scenarios/0.0 (suggest)',\n },\n }),\n },\n );\n\n console.log('\\nCandidate UI states (not exhaustive)\\n');\n\n for (const s of result.states) {\n const mark = s.covered_by ? '✓' : '✗';\n console.log(\n `${mark} ${`${s.area} / ${s.state}`.padEnd(56)} ${s.covered_by ?? `uncovered (${s.priority})`}`,\n );\n }\n\n const covered = result.states.filter((s) => s.covered_by).length;\n console.log(\n `\\n${result.states.length} states detected, ${covered} have scenarios, ${\n result.states.length - covered\n } uncovered`,\n );\n\n const out = values.out ?? join(dir, 'proposed');\n\n if (result.accepted.length) mkdirSync(out, { recursive: true });\n\n for (const { scenario, covers } of result.accepted) {\n const file = join(out, `${scenario.name}.json`);\n const $schema = values.schema ? relative(out, values.schema) : undefined;\n writeFileSync(file, `${JSON.stringify({ $schema, ...scenario }, null, 2)}\\n`);\n console.log(`+ ${file} (${covers})`);\n }\n\n for (const r of result.rejected) console.log(`- rejected proposal for ${r.covers}: ${r.error}`);\n\n if (result.accepted.length) {\n console.log(\n `\\nReview the drafts, then: state-scenarios promote ${dir}`,\n );\n }\n\n break;\n }\n\n default:\n console.error(`Unknown command \"${command}\"\\n\\n${HELP}`);\n process.exit(1);\n }\n} catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const hint = /authentication/i.test(message) ? '\\nSet ANTHROPIC_API_KEY, or run `ant auth login`.' : '';\n console.error(`state-scenarios ${command}: ${message}${hint}`);\n process.exit(1);\n}\n\nfunction walk(path: string): { path: string; content: string; }[] {\n if (statSync(path).isDirectory()) {\n return readdirSync(path)\n .filter((f) => !['node_modules', 'dist', 'scenarios'].includes(f))\n .flatMap((f) => walk(join(path, f)));\n }\n\n const isSource = /\\.(tsx?|jsx?|vue|svelte)$/.test(path) && !/\\.(d|test|spec)\\.\\w+$/.test(path);\n\n return isSource ? [{ path, content: readFileSync(path, 'utf8') }] : [];\n}\n"],"mappings":";;;AACA,SAAS,WAAW,aAAa,cAAc,UAAU,qBAAqB;AAC9E,SAAS,MAAM,UAAU,eAAe;AACxC,SAAS,qBAAqB;AAC9B,SAAS,iBAAiB;AAC1B,SAAS,eAAe,0BAA6C;AAErE,IAAM,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBb,IAAM,mBAAmB;AAEzB,IAAM,EAAE,aAAa,OAAO,IAAI,UAAU;AAAA,EACxC,kBAAkB;AAAA,EAClB,SAAS;AAAA,IACP,OAAO,EAAE,MAAM,SAAS;AAAA,IACxB,YAAY,EAAE,MAAM,SAAS;AAAA,IAC7B,KAAK,EAAE,MAAM,SAAS;AAAA,IACtB,MAAM,EAAE,MAAM,SAAS;AAAA,IACvB,aAAa,EAAE,MAAM,UAAU;AAAA,IAC/B,MAAM,EAAE,MAAM,UAAU;AAAA,IACxB,KAAK,EAAE,MAAM,SAAS;AAAA,IACtB,QAAQ,EAAE,MAAM,SAAS;AAAA,IACzB,KAAK,EAAE,MAAM,SAAS;AAAA,IACtB,OAAO,EAAE,MAAM,SAAS;AAAA,IACxB,YAAY,EAAE,MAAM,SAAS;AAAA,IAC7B,WAAW,EAAE,MAAM,UAAU;AAAA,IAC7B,MAAM,EAAE,MAAM,WAAW,OAAO,IAAI;AAAA,EACtC;AACF,CAAC;AAED,IAAM,CAAC,SAAS,MAAM,YAAY,SAAS,kBAAkB,MAAS,IAAI;AAE1E,IAAI,OAAO,QAAQ,CAAC,WAAW,CAAC,KAAK;AACnC,UAAQ,IAAI,IAAI;AAChB,UAAQ,KAAK,UAAU,IAAI,CAAC;AAC9B;AAEA,IAAI;AACF,MAAI,YAAY,QAAQ;AACtB,UAAM,EAAE,cAAc,IAAI,MAAM,OAAO,oBAAW;AAClD,kBAAc,GAAG;AACjB,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,QAAwB,OAAO,SAChC,MAAM,OAAO,cAAc,QAAQ,OAAO,KAAK,CAAC,EAAE,OAAO,QAC1D,CAAC;AAEL,MAAI,YAAY,WAAW;AACzB,UAAM,EAAE,iBAAiB,IAAI,MAAM,OAAO,uBAAc;AACxD,UAAM,SAAS,iBAAiB,KAAK,EAAE,OAAO,QAAQ,CAAC,CAAC,OAAO,SAAS,EAAE,CAAC;AAC3E,YAAQ,KAAK,OAAO,QAAQ,SAAS,IAAI,CAAC;AAAA,EAC5C;AAEA,QAAM,QAAQ,YAAY,GAAG,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,OAAO,CAAC;AAEhE,QAAM,UAAU;AAAA,IACd,MAAM,IAAI,CAAC,MAAM,KAAK,MAAM,aAAa,KAAK,KAAK,CAAC,GAAG,MAAM,CAAC,CAAC;AAAA,IAC/D,EAAE,MAAM;AAAA,EACV;AAEA,UAAQ,SAAS;AAAA,IACf,KAAK,YAAY;AACf,YAAM,WAAW,QAAQ,MAAM;AAE/B,iBAAW,KAAK,SAAU,SAAQ,MAAM,UAAK,CAAC;AAAA,CAAI;AAClD,cAAQ,IAAI,GAAG,QAAQ,KAAK,SAAS,SAAS,MAAM,IAAI,QAAQ,KAAK,MAAM,kBAAkB;AAC7F,cAAQ,KAAK,SAAS,SAAS,IAAI,CAAC;AAAA,IACtC;AAAA,IAEA,KAAK,QAAQ;AACX,YAAM,OAAO,OAAO,UAAU;AAE9B,YAAM,OAAO,QAAQ,KAAK,IAAI,CAAC,OAAO;AAAA,QACpC,MAAM,EAAE;AAAA,QACR,aAAa,EAAE,eAAe;AAAA,QAC9B,MAAM,EAAE,QAAQ,CAAC;AAAA,QACjB,OAAO,EAAE,SAAS;AAAA,QAClB,KAAK,OACD,GAAG,KAAK,QAAQ,OAAO,EAAE,CAAC,GAAG,QAAQ,QAAQ,CAAC,EAAE,QAAQ,GAAG,aAC3D,mBAAmB,EAAE,IAAI,CAC3B,KACE;AAAA,MACN,EAAE;AAEF,UAAI,OAAO,KAAM,SAAQ,IAAI,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;AAAA,WACrD;AACH,mBAAW,KAAK,MAAM;AACpB,kBAAQ;AAAA,YACN,GAAG,EAAE,KAAK,OAAO,EAAE,CAAC,IAAI,EAAE,WAAW,GAAG,EAAE,MAAM;AAAA,EAAK,IAAI,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE;AAAA,UACpF;AAAA,QACF;AAAA,MACF;AAEA;AAAA,IACF;AAAA,IAEA,KAAK,SAAS;AACZ,YAAM,UAAU,OAAO,UAAU;AAEjC,UAAI,CAAC,QAAS,OAAM,IAAI,MAAM,oDAAoD;AAClF,YAAM,MAAM,OAAO,OAAO;AAC1B,YAAM,OAAO,OAAO,MAAM,MAAM,GAAG;AACnC,gBAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAElC,YAAM,EAAE,SAAS,IAAI,MAAM,OAAO,kBAAkB;AACpD,YAAM,EAAE,cAAc,kBAAkB,IAAI,MAAM,OAAO,4BAA4B;AACrF,YAAM,UAAU,MAAM,SAAS,OAAO;AACtC,YAAM,SAAS,CAAC;AAEhB,iBAAW,KAAK,QAAQ,KAAK,OAAO,CAACA,OAAM,CAAC,QAAQ,KAAK,SAASA,GAAE,IAAI,CAAC,GAAG;AAC1E,cAAM,UAAU,MAAM,QAAQ,WAAW,EAAE,QAAQ,CAAC;AACpD,cAAM,OAAO,MAAM,QAAQ,QAAQ;AACnC,aAAK,kBAAkB,GAAM;AAI7B,cAAM,gBAA0B,CAAC;AACjC,cAAM,aAAuB,CAAC;AAC9B,aAAK,GAAG,WAAW,CAAC,MAAM;AACxB,cAAI,EAAE,KAAK,MAAM,WAAW,CAAC,EAAE,KAAK,EAAE,WAAW,yBAAyB,GAAG;AAC3E,0BAAc,KAAK,EAAE,KAAK,CAAC;AAAA,UAC7B;AAAA,QACF,CAAC;AACD,aAAK,GAAG,aAAa,CAAC,QAAQ,WAAW,KAAK,IAAI,OAAO,CAAC;AAC1D,YAAI,QAAuB;AAE3B,YAAI;AACF,gBAAM,aAAa,MAAM,EAAE,MAAM;AAAA,YAC/B,MAAM,QAAQ,QAAQ,CAAC,EAAE;AAAA,YACzB,QAAQ,CAAC,OAAO,WAAW;AAAA,UAC7B,CAAC;AAAA,QACH,SAAS,KAAK;AACZ,kBAAQ,eAAe,QAAQ,IAAI,QAAQ,MAAM,IAAI,EAAE,CAAC,IAAI,OAAO,GAAG;AAAA,QACxE;AAEA,cAAM,aAAa,KAAK,KAAK,GAAG,EAAE,IAAI,MAAM;AAC5C,cAAM,KAAK,WAAW,EAAE,MAAM,YAAY,UAAU,KAAK,CAAC;AAC1D,cAAM,YAAY,MAAM,kBAAkB,IAAI;AAC9C,cAAM,KAAK,CAAC,SAAS,CAAC,UAAU,UAAU,CAAC,cAAc,UAAU,CAAC,WAAW;AAC/E,eAAO,KAAK,EAAE,MAAM,EAAE,MAAM,IAAI,YAAY,WAAW,eAAe,YAAY,MAAM,CAAC;AACzF,cAAM,QAAQ,MAAM;AAAA,MACtB;AAEA,YAAM,QAAQ,MAAM;AAEpB,UAAI,OAAO,KAAM,SAAQ,IAAI,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AAAA,WACvD;AACH,mBAAW,KAAK,QAAQ;AACtB,kBAAQ,IAAI,GAAG,EAAE,KAAK,WAAM,QAAG,IAAI,EAAE,KAAK,OAAO,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE;AAEtE,cAAI,EAAE,MAAO,SAAQ,IAAI,cAAc,EAAE,KAAK,EAAE;AAEhD,qBAAW,KAAK,EAAE,UAAW,SAAQ,IAAI,iBAAiB,CAAC,EAAE;AAE7D,qBAAW,KAAK,EAAE,WAAY,SAAQ,IAAI,iBAAiB,CAAC,EAAE;AAE9D,qBAAW,KAAK,EAAE,cAAe,SAAQ,IAAI,sBAAsB,CAAC,EAAE;AAAA,QACxE;AAAA,MACF;AAEA,cAAQ,KAAK,OAAO,MAAM,CAAC,MAAM,EAAE,EAAE,IAAI,IAAI,CAAC;AAAA,IAChD;AAAA,IAEA,KAAK,WAAW;AACd,UAAI,CAAC,OAAO,IAAK,OAAM,IAAI,MAAM,0DAA0D;AAC3F,YAAM,UAAU,OAAO,IAAI,MAAM,GAAG,EAAE,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC;AAC5D,YAAM,OAAO,QAAQ,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,QAAQ,QAAQ,CAAC;AAE7D,UAAI,OAAO,kBAAkB;AAC3B,cAAM,IAAI;AAAA,UACR,YAAY,KAAK,eAAe,CAAC;AAAA,QACnC;AAAA,MACF;AAEA,YAAM,aAAa,OAAO,SACtB,KAAK,MAAM,aAAa,OAAO,QAAQ,MAAM,CAAC,IAC9C,mBAAmB;AAEvB,UAAI,OAAO,UAAU,EAAG,SAAQ,YAAY,OAAO,UAAU,CAAC;AAC9D,YAAM,EAAE,SAAS,UAAU,IAAI,MAAM,OAAO,mBAAmB;AAC/D,YAAM,QAAQ,OAAO,SAAS,QAAQ,IAAI,yBAAyB;AACnE,YAAM,EAAE,iBAAiB,IAAI,MAAM,OAAO,cAAc;AACxD,cAAQ;AAAA,QACN,WAAW,QAAQ,MAAM,WAAW,KAAK,eAAe,CAAC,eAAe,QAAQ,KAAK,MAAM,mBACzF,SAAS,iBACX;AAAA,MACF;AAEA,YAAM,SAAS,MAAM;AAAA,QACnB,EAAE,SAAS,SAAS,YAAY,KAAK,OAAO,OAAO,OAAO,CAAC,GAAG,MAAM;AAAA,QACpE;AAAA,UACE,QAAQ,IAAI,UAAU;AAAA,YACpB,SAAS,QAAQ,IAAI,sBAAsB;AAAA;AAAA,YAE3C,gBAAgB;AAAA,cACd,sBAAsB,mBAAmB,KAAK,IAAI,CAAC;AAAA,cACnD,cAAc;AAAA,YAChB;AAAA,UACF,CAAC;AAAA,QACH;AAAA,MACF;AAEA,cAAQ,IAAI,0CAA0C;AAEtD,iBAAW,KAAK,OAAO,QAAQ;AAC7B,cAAM,OAAO,EAAE,aAAa,WAAM;AAClC,gBAAQ;AAAA,UACN,GAAG,IAAI,IAAI,GAAG,EAAE,IAAI,MAAM,EAAE,KAAK,GAAG,OAAO,EAAE,CAAC,IAAI,EAAE,cAAc,cAAc,EAAE,QAAQ,GAAG;AAAA,QAC/F;AAAA,MACF;AAEA,YAAM,UAAU,OAAO,OAAO,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE;AAC1D,cAAQ;AAAA,QACN;AAAA,EAAK,OAAO,OAAO,MAAM,qBAAqB,OAAO,oBACnD,OAAO,OAAO,SAAS,OACzB;AAAA,MACF;AAEA,YAAM,MAAM,OAAO,OAAO,KAAK,KAAK,UAAU;AAE9C,UAAI,OAAO,SAAS,OAAQ,WAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAE9D,iBAAW,EAAE,UAAU,OAAO,KAAK,OAAO,UAAU;AAClD,cAAM,OAAO,KAAK,KAAK,GAAG,SAAS,IAAI,OAAO;AAC9C,cAAM,UAAU,OAAO,SAAS,SAAS,KAAK,OAAO,MAAM,IAAI;AAC/D,sBAAc,MAAM,GAAG,KAAK,UAAU,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC,CAAC;AAAA,CAAI;AAC5E,gBAAQ,IAAI,KAAK,IAAI,MAAM,MAAM,GAAG;AAAA,MACtC;AAEA,iBAAW,KAAK,OAAO,SAAU,SAAQ,IAAI,2BAA2B,EAAE,MAAM,KAAK,EAAE,KAAK,EAAE;AAE9F,UAAI,OAAO,SAAS,QAAQ;AAC1B,gBAAQ;AAAA,UACN;AAAA,mDAAsD,GAAG;AAAA,QAC3D;AAAA,MACF;AAEA;AAAA,IACF;AAAA,IAEA;AACE,cAAQ,MAAM,oBAAoB,OAAO;AAAA;AAAA,EAAQ,IAAI,EAAE;AACvD,cAAQ,KAAK,CAAC;AAAA,EAClB;AACF,SAAS,KAAK;AACZ,QAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,QAAM,OAAO,kBAAkB,KAAK,OAAO,IAAI,sDAAsD;AACrG,UAAQ,MAAM,mBAAmB,OAAO,KAAK,OAAO,GAAG,IAAI,EAAE;AAC7D,UAAQ,KAAK,CAAC;AAChB;AAEA,SAAS,KAAK,MAAoD;AAChE,MAAI,SAAS,IAAI,EAAE,YAAY,GAAG;AAChC,WAAO,YAAY,IAAI,EACpB,OAAO,CAAC,MAAM,CAAC,CAAC,gBAAgB,QAAQ,WAAW,EAAE,SAAS,CAAC,CAAC,EAChE,QAAQ,CAAC,MAAM,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC;AAAA,EACvC;AAEA,QAAM,WAAW,4BAA4B,KAAK,IAAI,KAAK,CAAC,wBAAwB,KAAK,IAAI;AAE7F,SAAO,WAAW,CAAC,EAAE,MAAM,SAAS,aAAa,MAAM,MAAM,EAAE,CAAC,IAAI,CAAC;AACvE;","names":["s"]}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// src/init.ts
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
3
|
+
import { dirname, join, relative, resolve } from "path";
|
|
4
|
+
import { scenarioJsonSchema } from "state-scenarios";
|
|
5
|
+
function initScenarios(scenariosDir, cwd = process.cwd()) {
|
|
6
|
+
const dir = resolve(cwd, scenariosDir);
|
|
7
|
+
mkdirSync(dir, { recursive: true });
|
|
8
|
+
const schemaPath = join(dirname(dir), "scenario.schema.json");
|
|
9
|
+
const defaultPath = join(dir, "default.json");
|
|
10
|
+
const schemaRel = relative(dir, schemaPath).replaceAll("\\", "/");
|
|
11
|
+
const schemaExisted = existsSync(schemaPath);
|
|
12
|
+
const defaultExisted = existsSync(defaultPath);
|
|
13
|
+
if (!schemaExisted) {
|
|
14
|
+
writeFileSync(schemaPath, `${JSON.stringify(scenarioJsonSchema(), null, 2)}
|
|
15
|
+
`);
|
|
16
|
+
}
|
|
17
|
+
if (!defaultExisted) {
|
|
18
|
+
writeFileSync(
|
|
19
|
+
defaultPath,
|
|
20
|
+
`${JSON.stringify(
|
|
21
|
+
{
|
|
22
|
+
$schema: schemaRel,
|
|
23
|
+
name: "default",
|
|
24
|
+
description: "Happy path: record the real network, then Save as scenario from the panel"
|
|
25
|
+
},
|
|
26
|
+
null,
|
|
27
|
+
2
|
|
28
|
+
)}
|
|
29
|
+
`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const stack = detectStack(cwd);
|
|
33
|
+
const scenariosImport = relative(cwd, dir).replaceAll("\\", "/");
|
|
34
|
+
console.log(`${defaultExisted ? "Kept existing" : "Created"} ${relative(cwd, defaultPath)}`);
|
|
35
|
+
console.log(`${schemaExisted ? "Kept existing" : "Created"} ${relative(cwd, schemaPath)}`);
|
|
36
|
+
console.log("");
|
|
37
|
+
console.log("Wire the runtime (DEV only), then open the app with ?scenario-record,");
|
|
38
|
+
console.log("click through the flow, Mark ready, and Download JSON into this folder.");
|
|
39
|
+
console.log("");
|
|
40
|
+
console.log(wiringSnippet(stack, scenariosImport));
|
|
41
|
+
}
|
|
42
|
+
function detectStack(cwd) {
|
|
43
|
+
if (hasFile(cwd, ["next.config.ts", "next.config.js", "next.config.mjs", "next.config.cjs"])) {
|
|
44
|
+
return "next";
|
|
45
|
+
}
|
|
46
|
+
if (hasFile(cwd, ["vite.config.ts", "vite.config.js", "vite.config.mjs", "vite.config.cjs"])) {
|
|
47
|
+
return "vite";
|
|
48
|
+
}
|
|
49
|
+
return "unknown";
|
|
50
|
+
}
|
|
51
|
+
function hasFile(cwd, names) {
|
|
52
|
+
return names.some((name) => existsSync(join(cwd, name)));
|
|
53
|
+
}
|
|
54
|
+
function wiringSnippet(stack, scenariosImport) {
|
|
55
|
+
const glob = `import.meta.glob<ScenarioInput>('${scenariosImport ? `/${scenariosImport}` : ""}/*.json', { eager: true, import: 'default' })`;
|
|
56
|
+
if (stack === "next") {
|
|
57
|
+
return `// Next.js (App Router): wrap children in a client <Scenarios> gate (see README).
|
|
58
|
+
// List scenario JSON imports explicitly (Next has no import.meta.glob).
|
|
59
|
+
// Serve the MSW worker with: npx msw init public
|
|
60
|
+
//
|
|
61
|
+
// mountPanel(session, { schemaPath: '../scenario.schema.json' });
|
|
62
|
+
//
|
|
63
|
+
// Let the panel save into ${scenariosImport || "."}: create app/%5F%5Fstate-scenarios/save/route.ts with
|
|
64
|
+
// import { createSaveRoute } from 'state-scenarios/next';
|
|
65
|
+
// export const { GET } = createSaveRoute({ dir: '${scenariosImport || "."}' });`;
|
|
66
|
+
}
|
|
67
|
+
const plugins = `import { msw } from 'msw/vite';
|
|
68
|
+
// import { stateScenarios } from 'state-scenarios/vite';
|
|
69
|
+
// plugins: [msw({ mode: 'worker-only' }), stateScenarios({ dir: '${scenariosImport || "."}' })]
|
|
70
|
+
// stateScenarios lets the panel save scenarios straight into this folder.`;
|
|
71
|
+
const viteNote = stack === "vite" ? `// In vite.config.ts:
|
|
72
|
+
// ${plugins}` : `// Vite, in vite.config.ts:
|
|
73
|
+
// ${plugins}
|
|
74
|
+
// Next: see README "Next.js (App Router)"`;
|
|
75
|
+
return `${viteNote}
|
|
76
|
+
|
|
77
|
+
import { type ScenarioInput, startScenarios } from 'state-scenarios';
|
|
78
|
+
import { mountPanel } from 'state-scenarios/panel';
|
|
79
|
+
// import { tanstackQuery } from 'state-scenarios/tanstack-query'; // if you use it
|
|
80
|
+
|
|
81
|
+
if (import.meta.env.DEV) {
|
|
82
|
+
const session = await startScenarios({
|
|
83
|
+
scenarios: Object.values(${glob}),
|
|
84
|
+
// adapters: [tanstackQuery(queryClient)],
|
|
85
|
+
// refresh: () => queryClient.resetQueries(),
|
|
86
|
+
});
|
|
87
|
+
mountPanel(session, { schemaPath: '../scenario.schema.json' });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Then: open /?scenario-record \u2192 use the app \u2192 panel \u2192 Mark ready \u2192 Download JSON`;
|
|
91
|
+
}
|
|
92
|
+
export {
|
|
93
|
+
initScenarios
|
|
94
|
+
};
|
|
95
|
+
//# sourceMappingURL=init-VLZQEJRD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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":[]}
|
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// src/promote.ts
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, renameSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { createCatalog, parseScenario } from "state-scenarios";
|
|
5
|
+
function promoteScenarios(dir, { rules = [], dryRun = false } = {}) {
|
|
6
|
+
const proposedDir = join(dir, "proposed");
|
|
7
|
+
const moved = [];
|
|
8
|
+
const skipped = [];
|
|
9
|
+
if (!existsSync(proposedDir)) {
|
|
10
|
+
console.log(`No ${proposedDir}; nothing to promote.`);
|
|
11
|
+
return { moved, skipped };
|
|
12
|
+
}
|
|
13
|
+
const existingFiles = readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
14
|
+
const existing = existingFiles.map((f) => readJson(join(dir, f)));
|
|
15
|
+
const proposed = readdirSync(proposedDir).filter((f) => f.endsWith(".json"));
|
|
16
|
+
if (!proposed.length) {
|
|
17
|
+
console.log(`No drafts in ${proposedDir}.`);
|
|
18
|
+
return { moved, skipped };
|
|
19
|
+
}
|
|
20
|
+
for (const file of proposed) {
|
|
21
|
+
const from = join(proposedDir, file);
|
|
22
|
+
let draft;
|
|
23
|
+
try {
|
|
24
|
+
draft = parseScenario(readJson(from), file);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
skipped.push(file);
|
|
27
|
+
console.error(`\u2717 ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const { name } = draft;
|
|
31
|
+
if (existing.some((s) => s.name === name)) {
|
|
32
|
+
skipped.push(file);
|
|
33
|
+
console.error(`\u2717 ${file}: scenario "${name}" already exists in ${dir}`);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (existingFiles.includes(file)) {
|
|
37
|
+
skipped.push(file);
|
|
38
|
+
console.error(`\u2717 ${file}: ${join(dir, file)} already exists`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const next = createCatalog([...existing, draft], { rules });
|
|
43
|
+
next.resolve(next.get(name));
|
|
44
|
+
} catch (err) {
|
|
45
|
+
skipped.push(file);
|
|
46
|
+
console.error(`\u2717 ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const to = join(dir, file);
|
|
50
|
+
if (dryRun) console.log(`~ ${from} \u2192 ${to}`);
|
|
51
|
+
else {
|
|
52
|
+
renameSync(from, to);
|
|
53
|
+
console.log(`+ ${to}`);
|
|
54
|
+
}
|
|
55
|
+
moved.push(file);
|
|
56
|
+
existing.push(draft);
|
|
57
|
+
existingFiles.push(file);
|
|
58
|
+
}
|
|
59
|
+
if (dryRun && moved.length) {
|
|
60
|
+
console.log(`
|
|
61
|
+
Dry run: ${moved.length} would move, ${skipped.length} skipped.`);
|
|
62
|
+
} else if (moved.length) {
|
|
63
|
+
console.log(`
|
|
64
|
+
Promoted ${moved.length}; review with validate, then shoot.`);
|
|
65
|
+
}
|
|
66
|
+
return { moved, skipped };
|
|
67
|
+
}
|
|
68
|
+
function readJson(file) {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
71
|
+
} catch (err) {
|
|
72
|
+
throw new Error(`${file}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
promoteScenarios
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=promote-GSF5UXJL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/promote.ts"],"sourcesContent":["import { existsSync, readdirSync, readFileSync, renameSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { createCatalog, parseScenario, type Scenario, type ScenarioRule } from 'state-scenarios';\n\n/**\n * Move `proposed/*.json` into the catalog dir after each draft resolves against\n * the existing catalog. Refuses duplicate names.\n */\nexport function promoteScenarios(\n dir: string,\n { rules = [], dryRun = false }: { rules?: ScenarioRule[]; dryRun?: boolean; } = {},\n) {\n const proposedDir = join(dir, 'proposed');\n\n const moved: string[] = [];\n const skipped: string[] = [];\n\n if (!existsSync(proposedDir)) {\n console.log(`No ${proposedDir}; nothing to promote.`);\n\n return { moved, skipped };\n }\n\n const existingFiles = readdirSync(dir).filter((f) => f.endsWith('.json'));\n const existing = existingFiles.map((f) => readJson(join(dir, f)));\n const proposed = readdirSync(proposedDir).filter((f) => f.endsWith('.json'));\n\n if (!proposed.length) {\n console.log(`No drafts in ${proposedDir}.`);\n\n return { moved, skipped };\n }\n\n for (const file of proposed) {\n const from = join(proposedDir, file);\n let draft: Scenario;\n\n try {\n draft = parseScenario(readJson(from), file);\n } catch (err) {\n skipped.push(file);\n console.error(`✗ ${file}: ${err instanceof Error ? err.message : String(err)}`);\n continue;\n }\n\n const { name } = draft;\n\n if (existing.some((s) => s.name === name)) {\n skipped.push(file);\n console.error(`✗ ${file}: scenario \"${name}\" already exists in ${dir}`);\n continue;\n }\n\n if (existingFiles.includes(file)) {\n skipped.push(file);\n console.error(`✗ ${file}: ${join(dir, file)} already exists`);\n continue;\n }\n\n try {\n const next = createCatalog([...existing, draft], { rules });\n next.resolve(next.get(name)!);\n } catch (err) {\n skipped.push(file);\n console.error(`✗ ${file}: ${err instanceof Error ? err.message : String(err)}`);\n continue;\n }\n\n const to = join(dir, file);\n\n if (dryRun) console.log(`~ ${from} → ${to}`);\n else {\n renameSync(from, to);\n console.log(`+ ${to}`);\n }\n\n moved.push(file);\n existing.push(draft);\n existingFiles.push(file);\n }\n\n if (dryRun && moved.length) {\n console.log(`\\nDry run: ${moved.length} would move, ${skipped.length} skipped.`);\n } else if (moved.length) {\n console.log(`\\nPromoted ${moved.length}; review with validate, then shoot.`);\n }\n\n return { moved, skipped };\n}\n\nfunction readJson(file: string) {\n try {\n return JSON.parse(readFileSync(file, 'utf8'));\n } catch (err) {\n throw new Error(`${file}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });\n }\n}\n"],"mappings":";AAAA,SAAS,YAAY,aAAa,cAAc,kBAAkB;AAClE,SAAS,YAAY;AACrB,SAAS,eAAe,qBAAuD;AAMxE,SAAS,iBACd,KACA,EAAE,QAAQ,CAAC,GAAG,SAAS,MAAM,IAAmD,CAAC,GACjF;AACA,QAAM,cAAc,KAAK,KAAK,UAAU;AAExC,QAAM,QAAkB,CAAC;AACzB,QAAM,UAAoB,CAAC;AAE3B,MAAI,CAAC,WAAW,WAAW,GAAG;AAC5B,YAAQ,IAAI,MAAM,WAAW,uBAAuB;AAEpD,WAAO,EAAE,OAAO,QAAQ;AAAA,EAC1B;AAEA,QAAM,gBAAgB,YAAY,GAAG,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,OAAO,CAAC;AACxE,QAAM,WAAW,cAAc,IAAI,CAAC,MAAM,SAAS,KAAK,KAAK,CAAC,CAAC,CAAC;AAChE,QAAM,WAAW,YAAY,WAAW,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,OAAO,CAAC;AAE3E,MAAI,CAAC,SAAS,QAAQ;AACpB,YAAQ,IAAI,gBAAgB,WAAW,GAAG;AAE1C,WAAO,EAAE,OAAO,QAAQ;AAAA,EAC1B;AAEA,aAAW,QAAQ,UAAU;AAC3B,UAAM,OAAO,KAAK,aAAa,IAAI;AACnC,QAAI;AAEJ,QAAI;AACF,cAAQ,cAAc,SAAS,IAAI,GAAG,IAAI;AAAA,IAC5C,SAAS,KAAK;AACZ,cAAQ,KAAK,IAAI;AACjB,cAAQ,MAAM,UAAK,IAAI,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAC9E;AAAA,IACF;AAEA,UAAM,EAAE,KAAK,IAAI;AAEjB,QAAI,SAAS,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI,GAAG;AACzC,cAAQ,KAAK,IAAI;AACjB,cAAQ,MAAM,UAAK,IAAI,eAAe,IAAI,uBAAuB,GAAG,EAAE;AACtE;AAAA,IACF;AAEA,QAAI,cAAc,SAAS,IAAI,GAAG;AAChC,cAAQ,KAAK,IAAI;AACjB,cAAQ,MAAM,UAAK,IAAI,KAAK,KAAK,KAAK,IAAI,CAAC,iBAAiB;AAC5D;AAAA,IACF;AAEA,QAAI;AACF,YAAM,OAAO,cAAc,CAAC,GAAG,UAAU,KAAK,GAAG,EAAE,MAAM,CAAC;AAC1D,WAAK,QAAQ,KAAK,IAAI,IAAI,CAAE;AAAA,IAC9B,SAAS,KAAK;AACZ,cAAQ,KAAK,IAAI;AACjB,cAAQ,MAAM,UAAK,IAAI,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAC9E;AAAA,IACF;AAEA,UAAM,KAAK,KAAK,KAAK,IAAI;AAEzB,QAAI,OAAQ,SAAQ,IAAI,KAAK,IAAI,WAAM,EAAE,EAAE;AAAA,SACtC;AACH,iBAAW,MAAM,EAAE;AACnB,cAAQ,IAAI,KAAK,EAAE,EAAE;AAAA,IACvB;AAEA,UAAM,KAAK,IAAI;AACf,aAAS,KAAK,KAAK;AACnB,kBAAc,KAAK,IAAI;AAAA,EACzB;AAEA,MAAI,UAAU,MAAM,QAAQ;AAC1B,YAAQ,IAAI;AAAA,WAAc,MAAM,MAAM,gBAAgB,QAAQ,MAAM,WAAW;AAAA,EACjF,WAAW,MAAM,QAAQ;AACvB,YAAQ,IAAI;AAAA,WAAc,MAAM,MAAM,qCAAqC;AAAA,EAC7E;AAEA,SAAO,EAAE,OAAO,QAAQ;AAC1B;AAEA,SAAS,SAAS,MAAc;AAC9B,MAAI;AACF,WAAO,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC;AAAA,EAC9C,SAAS,KAAK;AACZ,UAAM,IAAI,MAAM,GAAG,IAAI,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,IAAI,EAAE,OAAO,IAAI,CAAC;AAAA,EAChG;AACF;","names":[]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ScenarioRule } from 'state-scenarios';
|
|
2
|
+
/**
|
|
3
|
+
* Move `proposed/*.json` into the catalog dir after each draft resolves against
|
|
4
|
+
* the existing catalog. Refuses duplicate names.
|
|
5
|
+
*/
|
|
6
|
+
export declare function promoteScenarios(dir: string, { rules, dryRun }?: {
|
|
7
|
+
rules?: ScenarioRule[];
|
|
8
|
+
dryRun?: boolean;
|
|
9
|
+
}): {
|
|
10
|
+
moved: string[];
|
|
11
|
+
skipped: string[];
|
|
12
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "state-scenarios-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "state-scenarios CLI: validate, list,
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "state-scenarios CLI: init, validate, list, shoot, suggest and promote scenarios.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@anthropic-ai/sdk": "^0.131.0",
|
|
30
30
|
"ajv": "^8.20.0",
|
|
31
|
-
"state-scenarios": "^0.
|
|
31
|
+
"state-scenarios": "^0.3.0",
|
|
32
32
|
"zod": "4.6.5"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|