spec-snake 0.0.1-beta.13 → 0.0.1-beta.14

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 CHANGED
@@ -4,7 +4,7 @@
4
4
  import { defineCommand as defineCommand3, runMain } from "citty";
5
5
 
6
6
  // package.json
7
- var version = "0.0.1-beta.13";
7
+ var version = "0.0.1-beta.14";
8
8
 
9
9
  // src/cli/commands/init.ts
10
10
  import * as fs from "node:fs";
package/dist/cli.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/cli/index.ts", "../package.json", "../src/cli/commands/init.ts", "../src/cli/commands/start.ts", "../src/types.ts", "../src/schema.ts", "../src/server/api.ts", "../src/server/apps/docs.ts", "../src/server/repositories/document.ts", "../src/server/helpers/docs/metadata.ts", "../src/server/usecases/docs/generate-doc.ts", "../src/server/apps/scenarios.ts", "../src/server/helpers/scenarios/build-step-info.ts"],
4
- "sourcesContent": ["import { defineCommand, runMain } from 'citty';\n\nimport { version } from '../../package.json';\nimport { initCommand } from './commands/init';\nimport { startCommand } from './commands/start';\n\nconst main = defineCommand({\n meta: {\n name: 'spec-snake',\n version,\n description: 'AI-powered design document generator CLI',\n },\n subCommands: {\n init: initCommand,\n start: startCommand,\n },\n});\n\nrunMain(main);\n", "{\n \"name\": \"spec-snake\",\n \"version\": \"0.0.1-beta.13\",\n \"type\": \"module\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/cut0/spec-snake.git\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"types\": \"./dist/types.d.ts\",\n \"main\": \"./dist/types.js\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/types.d.ts\",\n \"import\": \"./dist/types.js\",\n \"default\": \"./dist/types.js\"\n }\n },\n \"bin\": {\n \"spec-snake\": \"./dist/cli.js\"\n },\n \"files\": [\n \"dist\"\n ],\n \"scripts\": {\n \"build\": \"pnpm build:types && pnpm build:cli && pnpm build:client\",\n \"build:cli\": \"tsx scripts/build-cli.ts\",\n \"build:client\": \"vite build\",\n \"build:types\": \"tsc src/types.ts --declaration --outDir dist --skipLibCheck --module ESNext --moduleResolution bundler --target ES2022\",\n \"dev\": \"NODE_ENV=development SPEC_SNAKE_CONFIG=examples/local/spec-snake-ja.config.ts vite\",\n \"prd\": \"pnpm build && pnpm build:client && node dist/cli.js\",\n \"i18n\": \"lingui extract && lingui compile\",\n \"i18n:compile\": \"lingui compile\",\n \"i18n:extract\": \"lingui extract\",\n \"lint:check\": \"biome check .\",\n \"lint:fix\": \"biome check --write .\",\n \"typecheck\": \"tsc --noEmit\",\n \"prepublishOnly\": \"pnpm build\",\n \"release\": \"pnpm build && changeset publish\"\n },\n \"dependencies\": {\n \"@anthropic-ai/claude-agent-sdk\": \"0.1.75\",\n \"@hono/node-server\": \"1.19.7\",\n \"citty\": \"0.1.6\",\n \"consola\": \"3.4.2\",\n \"hono\": \"4.11.1\",\n \"jiti\": \"2.6.1\",\n \"valibot\": \"1.2.0\"\n },\n \"devDependencies\": {\n \"@base-ui/react\": \"1.0.0\",\n \"@biomejs/biome\": \"1.9.4\",\n \"@changesets/cli\": \"2.28.1\",\n \"@hono/vite-dev-server\": \"0.23.0\",\n \"@hookform/resolvers\": \"5.2.2\",\n \"@lingui/babel-plugin-lingui-macro\": \"5.7.0\",\n \"@lingui/cli\": \"5.7.0\",\n \"@lingui/conf\": \"5.7.0\",\n \"@lingui/core\": \"5.7.0\",\n \"@lingui/react\": \"5.7.0\",\n \"@lingui/vite-plugin\": \"5.7.0\",\n \"@tailwindcss/typography\": \"0.5.19\",\n \"@tailwindcss/vite\": \"4.1.18\",\n \"@tanstack/react-query\": \"5.35.1\",\n \"@tanstack/react-router\": \"1.141.6\",\n \"@tanstack/router-plugin\": \"1.141.7\",\n \"@types/node\": \"^25.0.3\",\n \"@types/react\": \"19.1.8\",\n \"@types/react-dom\": \"19.1.6\",\n \"@vitejs/plugin-react\": \"4.5.2\",\n \"esbuild\": \"0.27.2\",\n \"motion\": \"12.23.26\",\n \"react\": \"19.2.3\",\n \"react-dom\": \"19.2.3\",\n \"react-hook-form\": \"7.69.0\",\n \"react-markdown\": \"10.1.0\",\n \"remark-gfm\": \"4.0.1\",\n \"tailwindcss\": \"4.1.18\",\n \"tsx\": \"4.20.6\",\n \"typescript\": \"5.8.3\",\n \"vite\": \"7.3.0\",\n \"zustand\": \"5.0.9\"\n },\n \"packageManager\": \"pnpm@10.26.2\"\n}\n", "import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { defineCommand } from 'citty';\nimport { consola } from 'consola';\n\nconst CONFIG_TEMPLATE = `// For more detailed configuration examples, see:\n// https://github.com/cut0/spec-snake/blob/main/examples\n\nimport { defineConfig, defineScenario } from 'spec-snake';\n\nexport default defineConfig({\n scenarios: [\n defineScenario({\n id: 'default',\n name: 'Design Doc Generator',\n steps: [\n {\n slug: 'overview',\n title: 'Overview',\n description: 'Basic information about the feature',\n name: 'overview',\n fields: [\n {\n type: 'input',\n id: 'title',\n label: 'Title',\n description: 'Feature title',\n placeholder: 'Enter feature title',\n required: true,\n },\n {\n type: 'textarea',\n id: 'description',\n label: 'Description',\n description: 'Detailed description of the feature',\n placeholder: 'Describe the feature...',\n rows: 4,\n },\n {\n type: 'select',\n id: 'priority',\n label: 'Priority',\n description: 'Feature priority level',\n placeholder: 'Select priority',\n options: [\n { value: 'high', label: 'High' },\n { value: 'medium', label: 'Medium' },\n { value: 'low', label: 'Low' },\n ],\n },\n ],\n },\n ],\n filename: ({ timestamp }) => \\`\\${timestamp}.md\\`,\n prompt: ({ formData, aiContext }) =>\n \\`Generate a design doc based on the following input:\\n\\${JSON.stringify({ formData, aiContext }, null, 2)}\\`,\n }),\n ],\n permissions: {\n allowSave: true,\n },\n});\n`;\n\nexport const initCommand = defineCommand({\n meta: {\n name: 'init',\n description:\n 'Initialize a new spec-snake.config.ts file in the current directory',\n },\n args: {\n output: {\n type: 'string',\n description: 'Output file path',\n alias: 'o',\n default: 'spec-snake.config.ts',\n },\n force: {\n type: 'boolean',\n description: 'Overwrite existing file',\n alias: 'f',\n default: false,\n },\n },\n async run({ args }) {\n const outputPath = path.resolve(process.cwd(), args.output);\n\n if (fs.existsSync(outputPath) && !args.force) {\n consola.error(`File already exists: ${outputPath}`);\n consola.info('Use --force (-f) to overwrite');\n process.exit(1);\n }\n\n try {\n fs.writeFileSync(outputPath, CONFIG_TEMPLATE, 'utf-8');\n consola.success(`Config file created: ${outputPath}`);\n } catch (error) {\n consola.error('Failed to create config file:', error);\n process.exit(1);\n }\n },\n});\n", "import * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport * as url from 'node:url';\n\nimport { serve } from '@hono/node-server';\nimport { serveStatic } from '@hono/node-server/serve-static';\nimport { defineCommand } from 'citty';\nimport { consola } from 'consola';\nimport { createJiti } from 'jiti';\n\nimport { type Config, safeParseConfig } from '../../definitions';\nimport { createApiServer } from '../../server/api';\n\nconst getDistClientDir = (): string => {\n const __filename = url.fileURLToPath(import.meta.url);\n const __dirname = path.dirname(__filename);\n\n // After build: dist/cli.js \u2192 dist/client (same dist directory)\n // During development: src/cli/commands/start.ts \u2192 dist/client (3 levels up in dist)\n const isBuilt = __dirname.endsWith('/dist') || __dirname.includes('/dist/');\n return isBuilt\n ? path.resolve(__dirname, 'client')\n : path.resolve(__dirname, '../../../dist/client');\n};\n\nconst loadConfig = async (configPath: string): Promise<Config> => {\n const absolutePath = path.resolve(process.cwd(), configPath);\n\n if (!fs.existsSync(absolutePath)) {\n throw new Error(`Config file not found: ${absolutePath}`);\n }\n\n const jiti = createJiti(import.meta.url);\n const configModule = await jiti.import(absolutePath);\n const config = (configModule as { default: Config }).default;\n\n const result = safeParseConfig(config);\n if (!result.success) {\n const issues = result.issues.map((issue) => {\n const pathStr = issue.path?.map((p) => p.key).join('.') ?? '';\n return ` - ${pathStr}: ${issue.message}`;\n });\n throw new Error(`Invalid config:\\n${issues.join('\\n')}`);\n }\n\n // hooks cannot be validated with valibot, so preserve from original config\n return config;\n};\n\nconst runServer = async (\n config: Config,\n options: { distDir: string; port: number; host: string },\n) => {\n consola.start('Starting server...');\n\n const app = createApiServer(config);\n\n app.use(\n '/*',\n serveStatic({\n root: options.distDir,\n rewriteRequestPath: (requestPath) => {\n const fullPath = path.join(options.distDir, requestPath);\n if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {\n return requestPath;\n }\n return '/index.html';\n },\n }),\n );\n\n const server = serve(\n {\n fetch: app.fetch,\n port: options.port,\n hostname: options.host,\n },\n (info) => {\n const displayHost =\n info.address === '::1' || info.address === '127.0.0.1'\n ? 'localhost'\n : info.address;\n consola.success('Server started');\n consola.info(` \u279C Local: http://${displayHost}:${info.port}/`);\n },\n );\n\n const cleanup = () => {\n consola.info('Shutting down server...');\n server.close(() => {\n process.exit(0);\n });\n };\n\n process.on('SIGINT', cleanup);\n process.on('SIGTERM', cleanup);\n};\n\nexport const startCommand = defineCommand({\n meta: {\n name: 'start',\n description: 'Start the server with the specified config',\n },\n args: {\n config: {\n type: 'string',\n description: 'Path to config file',\n alias: 'c',\n default: 'spec-snake.config.ts',\n },\n port: {\n type: 'string',\n description: 'Port to run the server on',\n alias: 'p',\n default: '3000',\n },\n host: {\n type: 'string',\n description: 'Host to bind the server to',\n default: 'localhost',\n },\n },\n async run({ args }) {\n const configPath = args.config;\n const port = Number.parseInt(args.port, 10);\n\n consola.start(`Loading config from: ${configPath}`);\n\n try {\n const config = await loadConfig(configPath);\n consola.success(\n `Config loaded successfully (${config.scenarios.length} scenarios)`,\n );\n\n await runServer(config, {\n distDir: getDistClientDir(),\n port,\n host: args.host,\n });\n } catch (error) {\n if (error instanceof Error) {\n consola.error(error.message);\n } else {\n consola.error('Failed to start server:', error);\n }\n process.exit(1);\n }\n },\n});\n", "/**\n * Core type definitions for spec-snake\n *\n * This module defines all TypeScript types used throughout the application,\n * including form fields, sections, steps, scenarios, and configuration.\n *\n * @module types\n */\n\nimport type {\n McpHttpServerConfig,\n McpSSEServerConfig,\n McpStdioServerConfig,\n Options,\n PermissionMode,\n} from '@anthropic-ai/claude-agent-sdk';\n\n// =============================================================================\n// Form Field Types\n// =============================================================================\n\n/**\n * Option for select/dropdown fields\n *\n * @example\n * ```ts\n * const option: SelectOption = {\n * value: 'high',\n * label: 'High Priority'\n * };\n * ```\n */\nexport type SelectOption = {\n /** Internal value used in form data */\n value: string;\n /** Display label shown to users */\n label: string;\n};\n\n// =============================================================================\n// Field Condition Types\n// =============================================================================\n\n/**\n * Single field condition for visibility\n *\n * The `field` property supports dot notation for nested paths (e.g., 'overview.priority').\n *\n * @example\n * ```ts\n * // Show when priority is 'high'\n * when: { field: 'priority', is: 'high' }\n *\n * // Show when priority is 'high' or 'medium'\n * when: { field: 'priority', is: ['high', 'medium'] }\n *\n * // Show when priority is NOT 'low'\n * when: { field: 'priority', isNot: 'low' }\n *\n * // Show when checkbox is checked\n * when: { field: 'has_deadline', is: true }\n *\n * // Show when field is not empty\n * when: { field: 'title', isNotEmpty: true }\n *\n * // Cross-section reference using dot notation\n * when: { field: 'overview.priority', is: 'high' }\n * ```\n */\nexport type FieldConditionSingle =\n | { field: string; is: string | boolean | (string | boolean)[] }\n | { field: string; isNot: string | boolean | (string | boolean)[] }\n | { field: string; isEmpty: true }\n | { field: string; isNotEmpty: true };\n\n/**\n * Composite condition using AND logic\n *\n * All conditions must be true for the field to be visible.\n *\n * @example\n * ```ts\n * // Show when priority is 'high' AND type is 'feature'\n * when: {\n * and: [\n * { field: 'priority', is: 'high' },\n * { field: 'type', is: 'feature' }\n * ]\n * }\n * ```\n */\nexport type FieldConditionAnd = {\n and: FieldConditionObject[];\n};\n\n/**\n * Composite condition using OR logic\n *\n * At least one condition must be true for the field to be visible.\n *\n * @example\n * ```ts\n * // Show when priority is 'high' OR type is 'urgent'\n * when: {\n * or: [\n * { field: 'priority', is: 'high' },\n * { field: 'type', is: 'urgent' }\n * ]\n * }\n * ```\n */\nexport type FieldConditionOr = {\n or: FieldConditionObject[];\n};\n\n/**\n * Declarative condition for field visibility\n *\n * Supports single field conditions, AND/OR composite conditions,\n * and dot notation for cross-section field references.\n *\n * @example\n * ```ts\n * // Simple condition\n * when: { field: 'priority', is: 'high' }\n *\n * // AND condition\n * when: {\n * and: [\n * { field: 'priority', is: 'high' },\n * { field: 'type', is: 'feature' }\n * ]\n * }\n *\n * // OR condition\n * when: {\n * or: [\n * { field: 'priority', is: 'high' },\n * { field: 'overview.urgent', is: true }\n * ]\n * }\n *\n * // Nested AND/OR\n * when: {\n * or: [\n * { field: 'priority', is: 'high' },\n * {\n * and: [\n * { field: 'type', is: 'feature' },\n * { field: 'status', is: 'approved' }\n * ]\n * }\n * ]\n * }\n * ```\n */\nexport type FieldConditionObject =\n | FieldConditionSingle\n | FieldConditionAnd\n | FieldConditionOr;\n\n/**\n * Field visibility condition (object-based only)\n *\n * Use declarative object conditions for field visibility.\n * Supports single conditions, AND/OR logic, and dot notation for nested fields.\n *\n * Note: Function-based conditions are not supported as they cannot be\n * serialized when sending scenario data from server to client.\n */\nexport type FieldCondition = FieldConditionObject;\n\n/**\n * Text input field configuration\n *\n * Supports various input types including text, date, and URL.\n * Can include autocomplete suggestions.\n *\n * @example\n * ```ts\n * const field: InputField = {\n * id: 'project_name',\n * type: 'input',\n * label: 'Project Name',\n * description: 'Enter the name of your project',\n * placeholder: 'My Awesome Project',\n * required: true,\n * inputType: 'text',\n * suggestions: ['Project A', 'Project B']\n * };\n * ```\n */\nexport type InputField = {\n /** Unique identifier for the field (used as form data key) */\n id: string;\n /** Display label shown above the input */\n label: string;\n /** Help text describing the field's purpose */\n description: string;\n /** Placeholder text shown when input is empty */\n placeholder?: string;\n /** Whether the field must be filled before submission */\n required?: boolean;\n /** Field type discriminator */\n type: 'input';\n /** HTML input type - affects keyboard and validation */\n inputType?: 'text' | 'date' | 'url';\n /** Autocomplete suggestions shown as datalist options */\n suggestions?: string[];\n /** Condition for when this field should be visible */\n when?: FieldCondition;\n /** Default value for the field */\n default?: string;\n};\n\n/**\n * Multi-line text area field configuration\n *\n * @example\n * ```ts\n * const field: TextareaField = {\n * id: 'description',\n * type: 'textarea',\n * label: 'Description',\n * description: 'Provide a detailed description',\n * rows: 5\n * };\n * ```\n */\nexport type TextareaField = {\n /** Unique identifier for the field */\n id: string;\n /** Display label shown above the textarea */\n label: string;\n /** Help text describing the field's purpose */\n description: string;\n /** Placeholder text shown when textarea is empty */\n placeholder?: string;\n /** Whether the field must be filled before submission */\n required?: boolean;\n /** Field type discriminator */\n type: 'textarea';\n /** Number of visible text rows (affects initial height) */\n rows?: number;\n /** Condition for when this field should be visible */\n when?: FieldCondition;\n /** Default value for the field */\n default?: string;\n};\n\n/**\n * Dropdown select field configuration\n *\n * @example\n * ```ts\n * const field: SelectField = {\n * id: 'priority',\n * type: 'select',\n * label: 'Priority',\n * description: 'Select the priority level',\n * options: [\n * { value: 'low', label: 'Low' },\n * { value: 'medium', label: 'Medium' },\n * { value: 'high', label: 'High' }\n * ]\n * };\n * ```\n */\nexport type SelectField = {\n /** Unique identifier for the field */\n id: string;\n /** Display label shown above the select */\n label: string;\n /** Help text describing the field's purpose */\n description: string;\n /** Placeholder text (shown as first disabled option) */\n placeholder?: string;\n /** Whether a selection must be made before submission */\n required?: boolean;\n /** Field type discriminator */\n type: 'select';\n /** Available options for selection */\n options: SelectOption[];\n /** Condition for when this field should be visible */\n when?: FieldCondition;\n /** Default value for the field (must match one of the option values) */\n default?: string;\n};\n\n/**\n * Boolean checkbox field configuration\n *\n * @example\n * ```ts\n * const field: CheckboxField = {\n * id: 'agree_terms',\n * type: 'checkbox',\n * label: 'I agree to the terms',\n * description: 'You must agree to continue',\n * required: true\n * };\n * ```\n */\nexport type CheckboxField = {\n /** Unique identifier for the field */\n id: string;\n /** Display label shown next to the checkbox */\n label: string;\n /** Help text describing the field's purpose */\n description: string;\n /** Placeholder (not typically used for checkboxes) */\n placeholder?: string;\n /** Whether the checkbox must be checked before submission */\n required?: boolean;\n /** Field type discriminator */\n type: 'checkbox';\n /** Condition for when this field should be visible */\n when?: FieldCondition;\n /** Default value for the field */\n default?: boolean;\n};\n\nexport type FormField =\n | InputField\n | TextareaField\n | SelectField\n | CheckboxField;\n\n// =============================================================================\n// Layout Types\n// =============================================================================\n\n/**\n * Grid layout for arranging multiple fields in columns\n *\n * Allows organizing form fields horizontally within a step.\n * Supports nested fields including other grids (recursive).\n *\n * @example\n * ```ts\n * const layout: GridLayout = {\n * type: 'grid',\n * columns: 2,\n * fields: [\n * { id: 'first_name', type: 'input', ... },\n * { id: 'last_name', type: 'input', ... }\n * ]\n * };\n * ```\n */\nexport type GridLayout = {\n /** Layout type discriminator */\n type: 'grid';\n /** Number of columns (fields per row) */\n columns: number;\n /** Fields to arrange in the grid (can include nested layouts) */\n fields: Field[];\n};\n\n/**\n * Group layout for visually grouping multiple fields together\n *\n * Used for visual grouping only. Does not create nested structure in formData.\n * To repeat a group of fields, wrap this in a RepeatableLayout.\n *\n * @example\n * ```ts\n * // Standalone group (visual grouping only)\n * const layout: GroupLayout = {\n * type: 'group',\n * fields: [\n * { id: 'firstName', type: 'input', label: 'First Name', description: '' },\n * { id: 'lastName', type: 'input', label: 'Last Name', description: '' }\n * ]\n * };\n * // formData: { firstName: '...', lastName: '...' }\n * ```\n */\nexport type GroupLayout = {\n /** Layout type discriminator */\n type: 'group';\n /** Fields contained in the group */\n fields: Field[];\n};\n\n/**\n * Repeatable layout for fields that can be repeated\n *\n * Allows users to add multiple instances of a field or group.\n * The id is used as the key in form data, with values stored as an array.\n *\n * @example\n * ```ts\n * // Single field repeatable\n * const tagsLayout: RepeatableLayout = {\n * type: 'repeatable',\n * id: 'tags',\n * label: 'Tags',\n * minCount: 1,\n * field: { id: 'name', type: 'input', label: 'Tag', description: '' }\n * };\n * // formData: { tags: [{ name: 'tag1' }, { name: 'tag2' }] }\n *\n * // Group repeatable (multiple fields)\n * const librariesLayout: RepeatableLayout = {\n * type: 'repeatable',\n * id: 'libraries',\n * label: 'Libraries',\n * minCount: 1,\n * field: {\n * type: 'group',\n * fields: [\n * { id: 'name', type: 'input', label: 'Name', description: '' },\n * { id: 'url', type: 'input', label: 'URL', description: '' }\n * ]\n * }\n * };\n * // formData: { libraries: [{ name: 'React', url: '...' }, ...] }\n * ```\n */\nexport type RepeatableLayout = {\n /** Layout type discriminator */\n type: 'repeatable';\n /** Unique identifier (used as key in form data) */\n id: string;\n /** Display label shown above the repeatable field */\n label: string;\n /** Minimum number of entries required (default: 0) */\n minCount?: number;\n /** Default number of entries to create initially (defaults to minCount if not specified) */\n defaultCount?: number;\n /** The field or group to repeat */\n field: FormField | GroupLayout;\n};\n\nexport type LayoutField = GridLayout | RepeatableLayout | GroupLayout;\nexport type Field = FormField | LayoutField;\n\n// =============================================================================\n// Step Types\n// =============================================================================\n\n/**\n * Step definition for multi-step form wizard\n *\n * Each step represents one page in the form wizard, containing\n * fields for the user to fill out.\n *\n * @example\n * ```ts\n * const step: Step = {\n * slug: 'basic-info',\n * title: 'Basic Information',\n * description: 'Enter the basic details of your project',\n * name: 'basic',\n * fields: [\n * { id: 'title', type: 'input', label: 'Title', description: '...' },\n * { id: 'priority', type: 'select', label: 'Priority', description: '...', options: [...] }\n * ]\n * };\n *\n * // For repeatable fields (single field):\n * const stepWithRepeatable: Step = {\n * slug: 'tags',\n * title: 'Tags',\n * description: 'Add tags',\n * name: 'tags',\n * fields: [\n * {\n * type: 'repeatable',\n * id: 'items',\n * minCount: 1,\n * field: { id: 'name', type: 'input', label: 'Tag', description: '' }\n * }\n * ]\n * };\n * // formData: { tags: { items: [{ name: 'tag1' }, { name: 'tag2' }] } }\n *\n * // For repeatable group (multiple fields):\n * const stepWithRepeatableGroup: Step = {\n * slug: 'libraries',\n * title: 'Libraries',\n * description: 'Add libraries',\n * name: 'libraries',\n * fields: [\n * {\n * type: 'repeatable',\n * id: 'items',\n * minCount: 1,\n * field: {\n * type: 'group',\n * fields: [\n * { id: 'name', type: 'input', label: 'Name', description: '...' },\n * { id: 'url', type: 'input', label: 'URL', description: '...' }\n * ]\n * }\n * }\n * ]\n * };\n * // formData: { libraries: { items: [{ name: '...', url: '...' }, ...] } }\n * ```\n */\nexport type Step = {\n /** URL-friendly identifier (used in routing) */\n slug: string;\n /** Display title shown in step header */\n title: string;\n /** Description text shown below the title */\n description: string;\n /** Step name (used as key in form data output) */\n name: string;\n /** Fields contained in this step */\n fields: Field[];\n};\n\n// =============================================================================\n// AI Settings Types (Claude Agent SDK)\n// =============================================================================\n\n/**\n * MCP (Model Context Protocol) Server Configuration\n *\n * Defines how to connect to an MCP server. Supports three transport types:\n * - stdio: Local process started with a command\n * - sse: Remote server via Server-Sent Events\n * - http: Remote server via HTTP\n *\n * Re-exported from `@anthropic-ai/claude-agent-sdk` for convenience.\n * Note: The SDK also supports an 'sdk' type for in-memory servers,\n * but that requires runtime instances and is not serializable to config files.\n *\n * @see https://docs.anthropic.com/en/docs/claude-code/mcp\n *\n * @example\n * ```ts\n * // stdio server (local process)\n * const stdioConfig: McpServerConfig = {\n * command: 'npx',\n * args: ['@modelcontextprotocol/server-filesystem'],\n * env: { ALLOWED_PATHS: '/home/user/projects' }\n * };\n *\n * // SSE server (remote)\n * const sseConfig: McpServerConfig = {\n * type: 'sse',\n * url: 'https://api.example.com/mcp/sse',\n * headers: { 'Authorization': 'Bearer token' }\n * };\n *\n * // HTTP server (remote)\n * const httpConfig: McpServerConfig = {\n * type: 'http',\n * url: 'https://api.example.com/mcp',\n * headers: { 'Authorization': 'Bearer token' }\n * };\n * ```\n */\nexport type McpServerConfig =\n | McpStdioServerConfig\n | McpSSEServerConfig\n | McpHttpServerConfig;\n\n// Re-export SDK types for convenience\nexport type {\n PermissionMode,\n McpStdioServerConfig,\n McpSSEServerConfig,\n McpHttpServerConfig,\n};\n\n/**\n * AI Settings for Claude Agent SDK query options\n *\n * Derived from the SDK's `Options` type using `Pick` to select only the\n * fields relevant for config file serialization. This ensures type safety\n * and automatic updates when the SDK changes.\n *\n * This is a simplified subset focused on model selection, tools, MCP, and permissions.\n * Other SDK options (session, environment, output format, etc.) are managed by the server.\n *\n * @see https://docs.anthropic.com/en/docs/claude-code/sdk\n * @see https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk\n *\n * @example\n * ```ts\n * const aiSettings: AiSettings = {\n * model: 'claude-sonnet-4-5-20250929',\n * maxTurns: 10,\n * maxBudgetUsd: 1.0,\n * permissionMode: 'bypassPermissions',\n * allowDangerouslySkipPermissions: true,\n * mcpServers: {\n * filesystem: {\n * command: 'npx',\n * args: ['@modelcontextprotocol/server-filesystem']\n * }\n * }\n * };\n * ```\n *\n * **Available Tools:**\n *\n * File Operations:\n * - `Read` - Read file contents\n * - `Write` - Write/create files\n * - `Edit` - Edit existing files\n * - `Glob` - Find files by pattern\n * - `Grep` - Search file contents\n * - `NotebookEdit` - Edit Jupyter notebooks\n *\n * Command Execution:\n * - `Bash` - Execute shell commands\n *\n * Web:\n * - `WebSearch` - Search the web\n * - `WebFetch` - Fetch content from URLs\n *\n * Agent & Task:\n * - `Task` - Launch sub-agents\n * - `TodoWrite` - Manage task lists\n *\n * Code Intelligence:\n * - `LSP` - Language Server Protocol operations\n *\n * MCP Tools:\n * - Format: `mcp__<server>__<tool>` (e.g., `mcp__filesystem__read_file`)\n */\nexport type AiSettings = Omit<\n Pick<\n Options,\n | 'model'\n | 'fallbackModel'\n | 'maxThinkingTokens'\n | 'maxTurns'\n | 'maxBudgetUsd'\n | 'allowedTools'\n | 'disallowedTools'\n | 'tools'\n | 'permissionMode'\n | 'allowDangerouslySkipPermissions'\n | 'mcpServers'\n | 'strictMcpConfig'\n >,\n 'mcpServers'\n> & {\n /**\n * MCP server configurations keyed by server name\n *\n * Each server provides additional tools and capabilities to the model.\n * Only serializable transport types (stdio, sse, http) are supported in config files.\n *\n * @see https://docs.anthropic.com/en/docs/claude-code/mcp\n */\n mcpServers?: Record<string, McpServerConfig>;\n};\n\n// =============================================================================\n// Scenario Types\n// =============================================================================\n\n/**\n * Base scenario definition (serializable to JSON)\n *\n * Contains the core fields that can be validated by Valibot schema.\n * Extended by `Scenario` with function-based fields.\n */\nexport type ScenarioBase = {\n /** Unique identifier for the scenario (used in URLs) */\n id: string;\n /** Display name shown in the scenario list */\n name: string;\n /** Steps that make up the scenario's form wizard */\n steps: Step[];\n /**\n * AI settings for Claude Agent SDK\n * @see AiSettings\n */\n aiSettings?: AiSettings;\n};\n\n/**\n * Step metadata in AiContext\n */\nexport type AiContextStepMeta = {\n title: string;\n description: string;\n};\n\n/**\n * Field metadata in AiContext\n */\nexport type AiContextFieldMeta = {\n label: string;\n description: string;\n};\n\n/**\n * AiContext for a repeatable field (nested structure)\n */\nexport type AiContextRepeatable = {\n [fieldId: string]: AiContextFieldMeta | AiContextRepeatable;\n};\n\n/**\n * AiContext for a step\n */\nexport type AiContextStep = {\n _step: AiContextStepMeta;\n [fieldId: string]:\n | AiContextStepMeta\n | AiContextFieldMeta\n | AiContextRepeatable;\n};\n\n/**\n * AiContext type - metadata for form fields to help AI understand the data\n *\n * This is included in formData as `ai_context` property.\n * Contains labels and descriptions for each field, organized by step name.\n *\n * @example\n * ```ts\n * {\n * overview: {\n * _step: { title: \"Overview\", description: \"Basic information\" },\n * title: { label: \"Title\", description: \"Feature title\" },\n * priority: { label: \"Priority\", description: \"Priority level\" }\n * },\n * modules: {\n * _step: { title: \"Modules\", description: \"Module structure\" },\n * items: {\n * name: { label: \"Module Name\", description: \"Name of the module\" },\n * features: {\n * feature_name: { label: \"Feature Name\", description: \"Name of feature\" }\n * }\n * }\n * }\n * }\n * ```\n */\nexport type AiContext = {\n [stepName: string]: AiContextStep;\n};\n\n/**\n * Lifecycle hooks for scenario events\n *\n * Allows custom behavior during preview and save operations.\n *\n * @typeParam TFormData - Type of the raw form data from UI\n */\nexport type ScenarioHooks<\n TFormData extends Record<string, unknown> = Record<string, unknown>,\n> = {\n /**\n * Called after preview is generated but before displaying to user\n *\n * Use for logging, analytics, or transforming the preview content.\n *\n * @param params.formData - The raw form data from the UI\n * @param params.aiContext - Field metadata (labels, descriptions) for AI\n * @param params.content - The generated markdown content\n */\n onPreview?: (params: {\n formData: TFormData;\n aiContext: AiContext;\n content: string;\n }) => Promise<void>;\n\n /**\n * Called after document is saved to disk\n *\n * Use for post-processing, notifications, or integrations.\n *\n * @param params.content - The saved markdown content\n * @param params.filename - The filename that was used\n * @param params.outputPath - Full path to the saved file\n * @param params.formData - The raw form data from the UI\n * @param params.aiContext - Field metadata (labels, descriptions) for AI\n */\n onSave?: (params: {\n content: string;\n filename: string;\n outputPath: string;\n formData: TFormData;\n aiContext: AiContext;\n }) => Promise<void>;\n};\n\n/**\n * Filename function type for custom document naming\n *\n * Can be a static string or a function for dynamic naming.\n * Default format: `design-doc-{scenarioId}-{timestamp}.md`\n *\n * @typeParam TFormData - Type of the raw form data from UI\n *\n * @example\n * ```ts\n * // Static filename\n * filename: 'design-doc.md'\n *\n * // Dynamic filename based on form data\n * filename: ({ formData, timestamp }) =>\n * `${formData.project_name}-${timestamp}.md`\n * ```\n */\nexport type ScenarioFilename<\n TFormData extends Record<string, unknown> = Record<string, unknown>,\n> =\n | string\n | ((params: {\n scenarioId: string;\n timestamp: string;\n content: string;\n formData: TFormData;\n aiContext: AiContext;\n }) => string);\n\n/**\n * Prompt function type\n *\n * A function that generates the prompt string.\n * Use `formData` for raw values and `aiContext` for field metadata.\n *\n * @typeParam TFormData - Type of the raw form data from UI\n *\n * @example\n * ```ts\n * prompt: ({ formData, aiContext }) =>\n * `Create a document based on:\\n${JSON.stringify({ formData, aiContext }, null, 2)}`\n * ```\n */\nexport type ScenarioPrompt<\n TFormData extends Record<string, unknown> = Record<string, unknown>,\n> = (params: { formData: TFormData; aiContext: AiContext }) => string;\n\n/**\n * Complete scenario definition\n *\n * Extends ScenarioBase with function-based fields that cannot be\n * serialized to JSON (prompt and filename can be functions, hooks).\n *\n * @typeParam TFormData - Type of the raw form data from UI (inferred from steps)\n *\n * @example\n * ```ts\n * const scenario: Scenario = {\n * id: 'design-doc',\n * name: 'Design Document',\n * steps: [...],\n * prompt: ({ formData, aiContext }) =>\n * `Generate a design doc based on:\\n${JSON.stringify({ formData, aiContext }, null, 2)}`,\n * outputDir: './docs/designs',\n * filename: ({ formData, timestamp }) =>\n * `${formData.overview?.title ?? 'untitled'}-${timestamp}.md`,\n * aiSettings: {\n * model: 'claude-sonnet-4-5'\n * },\n * hooks: {\n * onSave: async ({ filename }) => {\n * console.log(`Saved: ${filename}`);\n * }\n * }\n * };\n * ```\n */\nexport type Scenario<\n TFormData extends Record<string, unknown> = Record<string, unknown>,\n> = ScenarioBase & {\n /** Prompt template function */\n prompt: ScenarioPrompt<TFormData>;\n /** Directory where generated documents are saved */\n outputDir?: string;\n /** Custom filename for saved documents */\n filename?: ScenarioFilename<TFormData>;\n /** Lifecycle hooks for custom behavior */\n hooks?: ScenarioHooks<TFormData>;\n};\n\n// =============================================================================\n// Type Inference Utilities\n// =============================================================================\n\n/**\n * Helper type to get the value type for a form field\n */\ntype FieldValueType<F> = F extends { type: 'checkbox' }\n ? boolean\n : F extends { type: 'select'; options: readonly { value: infer V }[] }\n ? V\n : string;\n\n/**\n * Helper type to create an object type from FormField array (not supporting nested GridLayout)\n */\ntype FormFieldsToObject<Fields extends readonly FormField[]> = {\n [F in Fields[number] as F['id']]: FieldValueType<F>;\n};\n\n/**\n * Helper type to merge union to intersection\n */\ntype UnionToIntersection<U> = (\n U extends unknown\n ? (k: U) => void\n : never\n) extends (k: infer I) => void\n ? I\n : never;\n\n/**\n * Infer the formData type from a Scenario's steps\n *\n * Use this utility type to get type-safe access to raw form data\n * in hooks, prompts, and filename.\n *\n * **Note**: This utility works with FormField arrays only (not nested layouts).\n * Define fields with `as const` for literal type inference.\n *\n * @example\n * ```ts\n * const scenario = {\n * id: 'design-doc',\n * name: 'Design Document',\n * steps: [\n * {\n * slug: 'overview',\n * title: 'Overview',\n * description: 'Project overview',\n * name: 'overview',\n * fields: [\n * { id: 'title', type: 'input', label: 'Title', description: '' },\n * { id: 'description', type: 'textarea', label: 'Description', description: '' },\n * ] as const,\n * },\n * ],\n * prompt: ({ formData, aiContext }) => '...',\n * } as const satisfies Scenario;\n *\n * type MyFormData = InferFormData<typeof scenario>;\n * // Result:\n * // {\n * // overview: { title: string; description: string };\n * // }\n * ```\n */\nexport type InferFormData<T extends Scenario> =\n T['steps'] extends readonly (infer S)[]\n ? S extends { name: infer N extends string; fields: readonly FormField[] }\n ? { [K in N]: FormFieldsToObject<S['fields']> }\n : never\n : never;\n\n/**\n * Infer the merged formData type from a Scenario\n *\n * This flattens all steps into a single object type.\n *\n * @example\n * ```ts\n * type MyFormData = InferFormDataMerged<typeof scenario>;\n * // {\n * // overview: { title: string; description: string };\n * // }\n * ```\n */\nexport type InferFormDataMerged<T extends Scenario> = UnionToIntersection<\n InferFormData<T>\n>;\n\n/**\n * Helper type to extract FormField from Field (handling grid, repeatable, group layouts)\n */\ntype ExtractFormFields<F> = F extends FormField\n ? F\n : F extends { type: 'grid'; fields: readonly (infer GF)[] }\n ? GF extends FormField\n ? GF\n : never\n : F extends { type: 'repeatable'; field: infer RF }\n ? RF extends FormField\n ? RF\n : never\n : F extends { type: 'group'; fields: readonly (infer GF)[] }\n ? GF extends FormField\n ? GF\n : never\n : never;\n\n/**\n * Helper type to create an object type from Field array (supporting layouts)\n */\ntype FieldsToObject<Fields extends readonly Field[]> = {\n [F in ExtractFormFields<Fields[number]> as F extends {\n id: infer ID extends string;\n }\n ? ID\n : never]?: F extends FormField ? FieldValueType<F> : unknown;\n};\n\n/**\n * Infer the formData type directly from a steps array\n *\n * Use this utility type when defining scenarios with `defineScenario`\n * for type-safe access to formData in hooks, prompts, and filename.\n *\n * **Note**: For best results, define steps with `as const satisfies Step[]`\n * to preserve literal types for step names and field IDs.\n *\n * @example\n * ```ts\n * const steps = [\n * {\n * slug: 'basic-info',\n * title: 'Basic Info',\n * description: 'Enter basic information',\n * name: 'basicInfo',\n * fields: [\n * { id: 'title', type: 'input', label: 'Title', description: '' },\n * ],\n * },\n * {\n * slug: 'libraries',\n * title: 'Libraries',\n * description: 'Add libraries',\n * name: 'libraries',\n * fields: [\n * {\n * type: 'group',\n * id: 'items',\n * minCount: 1,\n * fields: [\n * { id: 'name', type: 'input', label: 'Name', description: '' },\n * ],\n * },\n * ],\n * },\n * ] as const satisfies Step[];\n *\n * type FormData = InferFormDataFromSteps<typeof steps>;\n * // {\n * // basicInfo?: { title?: string };\n * // libraries?: { items?: Array<{ name?: string }> };\n * // }\n * ```\n */\nexport type InferFormDataFromSteps<TSteps extends readonly Step[]> =\n UnionToIntersection<\n TSteps[number] extends infer S\n ? S extends { name: infer N extends string; fields: readonly Field[] }\n ? { [K in N]?: FieldsToObject<S['fields']> }\n : never\n : never\n > &\n Record<string, unknown>;\n\n// =============================================================================\n// AI Mode Types\n// =============================================================================\n\n/**\n * AI mode for document generation\n *\n * - `'stream'` - AI enabled with SSE streaming (default)\n * - `'sync'` - AI enabled, returns full response at once\n * - `'mock'` - AI disabled, returns fixed mock response\n *\n * @example\n * ```ts\n * // Streaming mode (default)\n * ai: 'stream'\n *\n * // Non-streaming mode\n * ai: 'sync'\n *\n * // Mock mode for development/testing\n * ai: 'mock'\n * ```\n */\nexport type AiMode = 'stream' | 'sync' | 'mock';\n\n/**\n * Default mock response when AI is disabled\n */\nexport const DEFAULT_MOCK_RESPONSE =\n '# Mock Document\\n\\nAI is disabled. This is a mock response.';\n\n// =============================================================================\n// Configuration Types\n// =============================================================================\n\n/**\n * Permission settings for the application\n */\nexport type Permissions = {\n /** Whether users can save generated documents to disk */\n allowSave: boolean;\n};\n\n/**\n * Root configuration object\n *\n * This is the type for the config file exported from `config.ts`.\n *\n * @example\n * ```ts\n * // config.ts\n * import type { Config } from '@anthropic-ai/claude-agent-sdk';\n *\n * export default {\n * scenarios: [\n * { id: 'design-doc', name: 'Design Doc', ... }\n * ],\n * permissions: {\n * allowSave: true\n * }\n * } satisfies Config;\n * ```\n */\nexport type Config = {\n /** Available scenarios (each represents a document type) */\n scenarios: Scenario[];\n /** Global permission settings */\n permissions: Permissions;\n /** When true, runs in hosted mode where saving is disabled regardless of permissions.allowSave */\n hosted?: boolean;\n /**\n * AI mode for document generation\n * @default 'stream'\n */\n ai?: AiMode;\n};\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\n/**\n * Helper function to define a scenario with type-safe formData\n *\n * This function uses TypeScript's const type parameters to infer\n * literal types from the steps array, enabling type-safe access\n * to formData in hooks, prompts, and filename.\n *\n * **Usage**: Define your steps with `as const satisfies Step[]` for best results.\n *\n * @example\n * ```ts\n * import { defineScenario, type Step, type Config } from 'spec-snake';\n *\n * const steps = [\n * {\n * slug: 'basic-info',\n * title: 'Basic Info',\n * description: 'Enter basic information',\n * name: 'basicInfo',\n * fields: [\n * { id: 'projectName', type: 'input', label: 'Project Name', description: '' },\n * { id: 'overview', type: 'textarea', label: 'Overview', description: '' },\n * ],\n * },\n * {\n * slug: 'features',\n * title: 'Features',\n * description: 'List features',\n * name: 'features',\n * fields: [\n * {\n * type: 'repeatable',\n * id: 'items',\n * minCount: 1,\n * field: {\n * type: 'group',\n * fields: [\n * { id: 'name', type: 'input', label: 'Feature Name', description: '' },\n * ],\n * },\n * },\n * ],\n * },\n * ] as const satisfies Step[];\n *\n * const scenario = defineScenario({\n * id: 'my-scenario',\n * name: 'My Scenario',\n * steps,\n * prompt: ({ formData, aiContext }) =>\n * `Generate document based on:\\n${JSON.stringify({ formData, aiContext }, null, 2)}`,\n * outputDir: 'docs',\n * filename: ({ formData }) => {\n * // formData is now type-safe!\n * // formData.basicInfo?.projectName is typed as string | undefined\n * return `${formData.basicInfo?.projectName ?? 'untitled'}.md`;\n * },\n * });\n *\n * const config: Config = {\n * scenarios: [scenario],\n * permissions: { allowSave: true },\n * };\n * ```\n */\nexport function defineScenario<const TSteps extends readonly Step[]>(\n scenario: Omit<ScenarioBase, 'steps' | 'prompt'> & {\n steps: TSteps;\n prompt: ScenarioPrompt<InferFormDataFromSteps<TSteps>>;\n outputDir?: string;\n filename?: ScenarioFilename<InferFormDataFromSteps<TSteps>>;\n hooks?: ScenarioHooks<InferFormDataFromSteps<TSteps>>;\n },\n): Scenario {\n return scenario as unknown as Scenario;\n}\n\n/**\n * Helper function to define a config object\n *\n * This is a simple identity function that provides better type inference\n * and editor support when defining config files.\n *\n * @example\n * ```ts\n * import { defineConfig, defineScenario } from 'spec-snake';\n *\n * export default defineConfig({\n * scenarios: [\n * defineScenario({ ... }),\n * ],\n * permissions: {\n * allowSave: true,\n * },\n * });\n * ```\n */\nexport function defineConfig(config: Config): Config {\n return config;\n}\n", "import * as v from 'valibot';\nimport type {\n Field,\n FieldConditionObject,\n GridLayout,\n GroupLayout,\n LayoutField,\n RepeatableLayout,\n} from './types';\n\n// =============================================================================\n// Type Guards\n// =============================================================================\n\nexport const isLayoutField = (field: Field): field is LayoutField => {\n return (\n field.type === 'grid' ||\n field.type === 'repeatable' ||\n field.type === 'group'\n );\n};\n\n// =============================================================================\n// Form Field Schemas\n// =============================================================================\n\n// =============================================================================\n// Field Condition Schema\n// =============================================================================\n\n// Single field condition (supports dot notation for nested paths)\nconst FieldConditionSingleSchema = v.union([\n v.object({\n field: v.string(),\n is: v.union([\n v.string(),\n v.boolean(),\n v.array(v.union([v.string(), v.boolean()])),\n ]),\n }),\n v.object({\n field: v.string(),\n isNot: v.union([\n v.string(),\n v.boolean(),\n v.array(v.union([v.string(), v.boolean()])),\n ]),\n }),\n v.object({\n field: v.string(),\n isEmpty: v.literal(true),\n }),\n v.object({\n field: v.string(),\n isNotEmpty: v.literal(true),\n }),\n]);\n\n// Full condition schema with and/or support (recursive)\nconst FieldConditionSchema: v.GenericSchema<FieldConditionObject> = v.union([\n FieldConditionSingleSchema,\n v.object({\n and: v.array(v.lazy(() => FieldConditionSchema)),\n }),\n v.object({\n or: v.array(v.lazy(() => FieldConditionSchema)),\n }),\n]) as v.GenericSchema<FieldConditionObject>;\n\nconst FieldBaseSchema = v.object({\n id: v.string(),\n label: v.string(),\n description: v.string(),\n placeholder: v.optional(v.string()),\n required: v.optional(v.boolean()),\n when: v.optional(FieldConditionSchema),\n});\n\nexport const SelectOptionSchema = v.object({\n value: v.string(),\n label: v.string(),\n});\n\nexport const InputFieldSchema = v.object({\n ...FieldBaseSchema.entries,\n type: v.literal('input'),\n inputType: v.optional(v.picklist(['text', 'date', 'url'])),\n suggestions: v.optional(v.array(v.string())),\n default: v.optional(v.string()),\n});\n\nexport const TextareaFieldSchema = v.object({\n ...FieldBaseSchema.entries,\n type: v.literal('textarea'),\n rows: v.optional(v.number()),\n default: v.optional(v.string()),\n});\n\nexport const SelectFieldSchema = v.object({\n ...FieldBaseSchema.entries,\n type: v.literal('select'),\n options: v.array(SelectOptionSchema),\n default: v.optional(v.string()),\n});\n\nexport const CheckboxFieldSchema = v.object({\n ...FieldBaseSchema.entries,\n type: v.literal('checkbox'),\n default: v.optional(v.boolean()),\n});\n\nexport const FormFieldSchema = v.union([\n InputFieldSchema,\n TextareaFieldSchema,\n SelectFieldSchema,\n CheckboxFieldSchema,\n]);\n\n// =============================================================================\n// Layout Schemas\n// =============================================================================\n\nexport const GridLayoutSchema: v.GenericSchema<GridLayout> = v.object({\n type: v.literal('grid'),\n columns: v.number(),\n fields: v.array(v.lazy(() => FieldSchema)),\n});\n\nexport const GroupLayoutSchema: v.GenericSchema<GroupLayout> = v.object({\n type: v.literal('group'),\n fields: v.array(v.lazy(() => FieldSchema)),\n});\n\nexport const RepeatableLayoutSchema: v.GenericSchema<RepeatableLayout> =\n v.object({\n type: v.literal('repeatable'),\n id: v.string(),\n label: v.string(),\n minCount: v.optional(v.number()),\n defaultCount: v.optional(v.number()),\n field: v.union([FormFieldSchema, GroupLayoutSchema]),\n });\n\nexport const FieldSchema: v.GenericSchema<Field> = v.union([\n FormFieldSchema,\n GridLayoutSchema,\n RepeatableLayoutSchema,\n GroupLayoutSchema,\n]);\n\n// =============================================================================\n// Step Schema\n// =============================================================================\n\nexport const StepSchema = v.object({\n slug: v.string(),\n title: v.string(),\n description: v.string(),\n name: v.string(),\n fields: v.array(FieldSchema),\n});\n\n// =============================================================================\n// AI Settings Schemas (Claude Agent SDK)\n// =============================================================================\n\nexport const McpServerConfigSchema = v.union([\n v.object({\n type: v.optional(v.literal('stdio')),\n command: v.string(),\n args: v.optional(v.array(v.string())),\n env: v.optional(v.record(v.string(), v.string())),\n }),\n v.object({\n type: v.literal('sse'),\n url: v.string(),\n headers: v.optional(v.record(v.string(), v.string())),\n }),\n v.object({\n type: v.literal('http'),\n url: v.string(),\n headers: v.optional(v.record(v.string(), v.string())),\n }),\n]);\n\nexport const AiSettingsSchema = v.optional(\n v.object({\n model: v.optional(v.string()),\n fallbackModel: v.optional(v.string()),\n maxThinkingTokens: v.optional(v.number()),\n maxTurns: v.optional(v.number()),\n maxBudgetUsd: v.optional(v.number()),\n allowedTools: v.optional(v.array(v.string())),\n disallowedTools: v.optional(v.array(v.string())),\n tools: v.optional(\n v.union([\n v.array(v.string()),\n v.object({\n type: v.literal('preset'),\n preset: v.literal('claude_code'),\n }),\n ]),\n ),\n permissionMode: v.optional(\n v.picklist([\n 'default',\n 'acceptEdits',\n 'bypassPermissions',\n 'plan',\n 'delegate',\n 'dontAsk',\n ]),\n ),\n allowDangerouslySkipPermissions: v.optional(v.boolean()),\n mcpServers: v.optional(v.record(v.string(), McpServerConfigSchema)),\n strictMcpConfig: v.optional(v.boolean()),\n }),\n);\n\n// =============================================================================\n// Scenario Schema\n// =============================================================================\n\nexport const ScenarioBaseSchema = v.object({\n id: v.string(),\n name: v.string(),\n steps: v.array(StepSchema),\n prompt: v.custom<\n (params: { formData: unknown; aiContext: unknown }) => string\n >((value) => typeof value === 'function'),\n aiSettings: AiSettingsSchema,\n});\n\nexport const ScenarioSchema = ScenarioBaseSchema;\n\n// =============================================================================\n// Configuration Schemas\n// =============================================================================\n\nexport const PermissionsSchema = v.object({\n allowSave: v.boolean(),\n});\n\nexport const AiModeSchema = v.optional(\n v.picklist(['stream', 'sync', 'mock']),\n 'stream',\n);\n\nexport const ConfigSchema = v.object({\n scenarios: v.array(ScenarioSchema),\n permissions: PermissionsSchema,\n hosted: v.optional(v.boolean(), false),\n ai: AiModeSchema,\n});\n\n// =============================================================================\n// Parser Functions\n// =============================================================================\n\nexport const parseConfig = (data: unknown) => {\n return v.parse(ConfigSchema, data);\n};\n\nexport const safeParseConfig = (data: unknown) => {\n return v.safeParse(ConfigSchema, data);\n};\n", "import { Hono } from 'hono';\n\nimport type { Config } from '../definitions';\n\nimport { createDocsApp } from './apps/docs';\nimport { createScenariosApp } from './apps/scenarios';\nimport { buildStepInfoMap } from './helpers/scenarios/build-step-info';\n\nexport const createApiServer = (rawConfig: Config) => {\n const app = new Hono();\n\n // In hosted mode, allowSave is always false\n const config: Config = rawConfig.hosted\n ? {\n ...rawConfig,\n permissions: { ...rawConfig.permissions, allowSave: false },\n }\n : rawConfig;\n\n // Pre-build scenario info map\n const scenarioInfoMap = new Map(\n config.scenarios.map((scenario) => [\n scenario.id,\n {\n scenario,\n stepInfoMap: buildStepInfoMap(scenario.steps),\n },\n ]),\n );\n\n // Scenarios App\n const scenariosApp = createScenariosApp(config, scenarioInfoMap);\n\n // Docs App\n const docsApp = createDocsApp(config, scenarioInfoMap);\n\n // Mount sub-apps\n app.route('/', scenariosApp);\n app.route('/', docsApp);\n\n return app;\n};\n", "import { Hono } from 'hono';\nimport { createMiddleware } from 'hono/factory';\nimport { streamSSE } from 'hono/streaming';\n\nimport type { AiContext, AiMode, Config } from '../../definitions';\nimport {\n getDocumentsForScenario,\n getFilename,\n readDocument,\n saveDocument,\n} from '../repositories/document';\nimport {\n generateDesignDocStream,\n generateMockDocStream,\n} from '../usecases/docs/generate-doc';\n\nimport type { ScenarioInfoMapEntry } from './scenarios';\n\n/**\n * Get the document generator based on AI mode\n */\nfunction getDocGenerator(\n aiMode: AiMode,\n scenario: ScenarioInfoMapEntry['scenario'],\n formData: Record<string, unknown>,\n aiContext: AiContext,\n) {\n if (aiMode === 'mock') {\n return generateMockDocStream();\n }\n\n return generateDesignDocStream({\n scenario,\n formData,\n aiContext,\n });\n}\n\ntype Variables = {\n scenarioInfo: ScenarioInfoMapEntry;\n};\n\ntype PreviewDocBody = {\n formData: Record<string, unknown>;\n aiContext: AiContext;\n};\n\ntype CreateDocBody = {\n content: string;\n formData: Record<string, unknown>;\n aiContext: AiContext;\n};\n\ntype UpdateDocBody = {\n content: string;\n formData: Record<string, unknown>;\n aiContext: AiContext;\n};\n\nconst createScenarioMiddleware = (\n scenarioInfoMap: Map<string, ScenarioInfoMapEntry>,\n) =>\n createMiddleware<{ Variables: Variables }>(async (c, next) => {\n const scenarioId = c.req.param('scenarioId');\n\n if (scenarioId == null) {\n return c.json({ error: 'Scenario ID is required' }, 400);\n }\n\n const scenarioInfo = scenarioInfoMap.get(scenarioId);\n\n if (scenarioInfo == null) {\n return c.json({ error: 'Scenario not found' }, 404);\n }\n\n c.set('scenarioInfo', scenarioInfo);\n await next();\n });\n\nexport const createDocsApp = (\n config: Config,\n scenarioInfoMap: Map<string, ScenarioInfoMapEntry>,\n) => {\n const app = new Hono<{ Variables: Variables }>();\n\n app.use(\n '/api/scenarios/:scenarioId/*',\n createScenarioMiddleware(scenarioInfoMap),\n );\n\n app.get('/api/scenarios/:scenarioId/docs', async (c) => {\n const { scenario } = c.get('scenarioInfo');\n const docs = await getDocumentsForScenario(scenario);\n\n return c.json({ docs });\n });\n\n app.post('/api/scenarios/:scenarioId/docs/preview', async (c) => {\n const { scenario } = c.get('scenarioInfo');\n\n const { formData, aiContext } = (await c.req.json()) as PreviewDocBody;\n\n const aiMode = config.ai ?? 'stream';\n const generator = getDocGenerator(aiMode, scenario, formData, aiContext);\n\n // Non-streaming mode: return JSON response directly\n if (aiMode === 'sync') {\n let fullContent = '';\n for await (const chunk of generator) {\n fullContent += chunk.text;\n }\n\n if (scenario.hooks?.onPreview != null) {\n await scenario.hooks.onPreview({\n formData,\n aiContext,\n content: fullContent,\n });\n }\n\n return c.json({ content: fullContent });\n }\n\n // Streaming mode: return SSE response\n return streamSSE(c, async (stream) => {\n let fullContent = '';\n\n for await (const chunk of generator) {\n fullContent += chunk.text;\n await stream.writeSSE({\n data: JSON.stringify({ type: 'text_delta', text: chunk.text }),\n event: 'message',\n });\n }\n\n await stream.writeSSE({\n data: JSON.stringify({ type: 'done', content: fullContent }),\n event: 'message',\n });\n\n if (scenario.hooks?.onPreview != null) {\n await scenario.hooks.onPreview({\n formData,\n aiContext,\n content: fullContent,\n });\n }\n });\n });\n\n app.post('/api/scenarios/:scenarioId/docs', async (c) => {\n const scenarioId = c.req.param('scenarioId');\n const { scenario } = c.get('scenarioInfo');\n\n if (!config.permissions.allowSave) {\n return c.json({ error: 'Save is not allowed' }, 403);\n }\n\n const { content, formData, aiContext } =\n (await c.req.json()) as CreateDocBody;\n const filename = getFilename(\n scenario,\n scenarioId,\n content,\n formData,\n aiContext,\n );\n const { outputPath } = await saveDocument({\n scenario,\n scenarioId,\n filename,\n content,\n formData,\n });\n\n if (scenario.hooks?.onSave != null) {\n await scenario.hooks.onSave({\n content,\n filename,\n outputPath,\n formData,\n aiContext,\n });\n }\n\n return c.json({ success: true, filename });\n });\n\n app.get('/api/scenarios/:scenarioId/docs/:filename', async (c) => {\n const filename = c.req.param('filename');\n const { scenario } = c.get('scenarioInfo');\n\n const result = await readDocument(scenario, filename);\n\n if (!result.success) {\n return c.json({ error: 'Document not found' }, 404);\n }\n\n return c.json({ doc: result.doc });\n });\n\n app.put('/api/scenarios/:scenarioId/docs/:filename', async (c) => {\n const scenarioId = c.req.param('scenarioId');\n const filename = c.req.param('filename');\n const { scenario } = c.get('scenarioInfo');\n\n if (!config.permissions.allowSave) {\n return c.json({ error: 'Save is not allowed' }, 403);\n }\n\n const { content, formData, aiContext } =\n (await c.req.json()) as UpdateDocBody;\n const { outputPath } = await saveDocument({\n scenario,\n scenarioId,\n filename,\n content,\n formData,\n });\n\n if (scenario.hooks?.onSave != null) {\n await scenario.hooks.onSave({\n content,\n filename,\n outputPath,\n formData,\n aiContext,\n });\n }\n\n return c.json({ success: true, filename });\n });\n\n return app;\n};\n", "import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nimport type { AiContext, Scenario } from '../../definitions';\nimport {\n type DocumentWithMetadata,\n addMetadataToContent,\n parseMetadata,\n} from '../helpers/docs/metadata';\n\nexport const getOutputDir = (scenario: Scenario): string => {\n return scenario.outputDir ?? join(process.cwd(), 'output');\n};\n\ntype ReadDocumentResult =\n | { success: true; doc: DocumentWithMetadata }\n | { success: false; error: 'not_found' | 'scenario_mismatch' };\n\nexport const readDocument = async (\n scenario: Scenario,\n filename: string,\n): Promise<ReadDocumentResult> => {\n const outputDir = getOutputDir(scenario);\n const filePath = join(outputDir, filename);\n\n try {\n const rawContent = await readFile(filePath, 'utf-8');\n const { metadata, content } = parseMetadata(rawContent);\n\n if (metadata?.scenarioId !== scenario.id) {\n return { success: false, error: 'scenario_mismatch' };\n }\n\n return {\n success: true,\n doc: { filename, content, metadata },\n };\n } catch {\n return { success: false, error: 'not_found' };\n }\n};\n\nexport const getDocumentsForScenario = async (\n scenario: Scenario,\n): Promise<DocumentWithMetadata[]> => {\n const outputDir = getOutputDir(scenario);\n\n try {\n const files = await readdir(outputDir);\n const mdFiles = files.filter((file) => file.endsWith('.md'));\n\n const docs = await Promise.all(\n mdFiles.map(async (filename) => {\n const result = await readDocument(scenario, filename);\n return result.success ? result.doc : null;\n }),\n );\n\n return docs.filter((doc) => doc != null);\n } catch {\n return [];\n }\n};\n\ntype SaveDocumentOptions = {\n scenario: Scenario;\n scenarioId: string;\n filename: string;\n content: string;\n formData: Record<string, unknown>;\n};\n\nexport const saveDocument = async ({\n scenario,\n scenarioId,\n filename,\n content,\n formData,\n}: SaveDocumentOptions): Promise<{ outputPath: string }> => {\n const outputDir = getOutputDir(scenario);\n const outputPath = join(outputDir, filename);\n\n const contentWithMetadata = addMetadataToContent(content, {\n scenarioId,\n formData,\n });\n\n await mkdir(outputDir, { recursive: true });\n await writeFile(outputPath, contentWithMetadata, 'utf-8');\n\n return { outputPath };\n};\n\nexport const getFilename = (\n scenario: Scenario,\n scenarioId: string,\n content: string,\n formData: Record<string, unknown>,\n aiContext: AiContext,\n): string => {\n const timestamp = new Date().toISOString().replace(/[:.]/g, '-');\n\n const filename = scenario.filename;\n if (filename != null) {\n return typeof filename === 'function'\n ? filename({\n scenarioId,\n timestamp,\n content,\n formData,\n aiContext,\n })\n : filename;\n }\n\n return `design-doc-${scenarioId}-${timestamp}.md`;\n};\n", "export type DocumentMetadata = {\n scenarioId: string;\n formData: Record<string, unknown>;\n};\n\nexport type DocumentWithMetadata = {\n filename: string;\n content: string;\n metadata: DocumentMetadata | null;\n};\n\nconst METADATA_START = '<!-- design-docs-metadata';\nconst METADATA_END = '-->';\n\nexport const serializeMetadata = (metadata: DocumentMetadata): string => {\n return `${METADATA_START}\\n${JSON.stringify(metadata, null, 2)}\\n${METADATA_END}`;\n};\n\nexport const parseMetadata = (\n content: string,\n): { metadata: DocumentMetadata | null; content: string } => {\n const metadataStartIndex = content.lastIndexOf(METADATA_START);\n if (metadataStartIndex === -1) {\n return { metadata: null, content };\n }\n\n const metadataEndIndex = content.indexOf(METADATA_END, metadataStartIndex);\n if (metadataEndIndex === -1) {\n return { metadata: null, content };\n }\n\n try {\n const metadataJson = content\n .slice(metadataStartIndex + METADATA_START.length, metadataEndIndex)\n .trim();\n const metadata = JSON.parse(metadataJson) as DocumentMetadata;\n const cleanContent = content.slice(0, metadataStartIndex).trim();\n return { metadata, content: cleanContent };\n } catch {\n return { metadata: null, content };\n }\n};\n\nexport const addMetadataToContent = (\n content: string,\n metadata: DocumentMetadata,\n): string => {\n return `${content}\\n\\n${serializeMetadata(metadata)}`;\n};\n", "import {\n type Options,\n type SDKResultMessage,\n query,\n} from '@anthropic-ai/claude-agent-sdk';\n\nimport {\n type AiContext,\n DEFAULT_MOCK_RESPONSE,\n type Scenario,\n} from '../../../definitions';\n\ntype GenerateDesignDocParams = {\n scenario: Scenario;\n formData: Record<string, unknown>;\n aiContext: AiContext;\n};\n\nexport const generateDesignDoc = async ({\n scenario,\n formData,\n aiContext,\n}: GenerateDesignDocParams): Promise<string> => {\n const prompt = scenario.prompt({ formData, aiContext });\n\n let message: SDKResultMessage | null = null;\n\n for await (const msg of query({\n prompt,\n options: scenario.aiSettings as Options,\n })) {\n if (msg.type === 'result' && msg.subtype === 'success') {\n message = msg;\n }\n }\n\n if (message == null) {\n throw new Error('Query failed');\n }\n\n return message.result;\n};\n\ntype StreamChunk = {\n type: 'text_delta';\n text: string;\n};\n\nexport async function* generateDesignDocStream({\n scenario,\n formData,\n aiContext,\n}: GenerateDesignDocParams): AsyncGenerator<StreamChunk> {\n const prompt = scenario.prompt({ formData, aiContext });\n\n for await (const msg of query({\n prompt,\n options: {\n ...(scenario.aiSettings as Options),\n includePartialMessages: true,\n },\n })) {\n if (msg.type === 'stream_event') {\n const event = msg.event as {\n type: string;\n delta?: { type: string; text?: string };\n };\n if (\n event.type === 'content_block_delta' &&\n event.delta?.type === 'text_delta' &&\n event.delta.text != null\n ) {\n yield { type: 'text_delta', text: event.delta.text };\n }\n }\n }\n}\n\n/**\n * Generate a mock response when AI is disabled\n */\nexport async function* generateMockDocStream(): AsyncGenerator<StreamChunk> {\n yield { type: 'text_delta', text: DEFAULT_MOCK_RESPONSE };\n}\n", "import { Hono } from 'hono';\n\nimport type { Config, Scenario } from '../../definitions';\nimport type { buildStepInfoMap } from '../helpers/scenarios/build-step-info';\n\nexport type ScenarioInfo = {\n stepInfoMap: ReturnType<typeof buildStepInfoMap>;\n};\n\nexport type ScenarioInfoMapEntry = {\n scenario: Scenario;\n stepInfoMap: ReturnType<typeof buildStepInfoMap>;\n};\n\nexport const createScenariosApp = (\n config: Config,\n scenarioInfoMap: Map<string, ScenarioInfoMapEntry>,\n) => {\n const app = new Hono();\n\n // Get all scenarios\n app.get('/api/scenarios', (c) => {\n return c.json({\n scenarios: config.scenarios,\n });\n });\n\n // Get single scenario\n app.get('/api/scenarios/:scenarioId', (c) => {\n const scenarioId = c.req.param('scenarioId');\n const scenarioInfo = scenarioInfoMap.get(scenarioId);\n\n if (scenarioInfo == null) {\n return c.json({ error: 'Scenario not found' }, 404);\n }\n\n return c.json({\n scenario: scenarioInfo.scenario,\n permissions: config.permissions,\n });\n });\n\n return app;\n};\n", "import { type Field, type Step, isLayoutField } from '../../../definitions';\n\nexport type FieldInfo = {\n id: string;\n label: string;\n description: string;\n};\n\nexport type StepInfo = {\n name: string;\n title: string;\n description: string;\n fields: FieldInfo[];\n};\n\n/**\n * Get fields from a layout field (handles grid, repeatable, group)\n */\nconst getLayoutFields = (field: Field): Field[] => {\n if (field.type === 'grid' || field.type === 'group') {\n return field.fields;\n }\n if (field.type === 'repeatable') {\n return [field.field];\n }\n return [];\n};\n\nexport const extractFieldInfos = (fields: Field[]): FieldInfo[] => {\n const result: FieldInfo[] = [];\n for (const field of fields) {\n if (isLayoutField(field)) {\n result.push(...extractFieldInfos(getLayoutFields(field)));\n } else {\n result.push({\n id: field.id,\n label: field.label,\n description: field.description,\n });\n }\n }\n return result;\n};\n\nexport const buildStepInfoMap = (steps: Step[]): Map<string, StepInfo> => {\n const stepMap = new Map<string, StepInfo>(\n steps.map((step) => [\n step.name,\n {\n name: step.name,\n title: step.title,\n description: step.description,\n fields: extractFieldInfos(step.fields),\n },\n ]),\n );\n\n return stepMap;\n};\n"],
4
+ "sourcesContent": ["import { defineCommand, runMain } from 'citty';\n\nimport { version } from '../../package.json';\nimport { initCommand } from './commands/init';\nimport { startCommand } from './commands/start';\n\nconst main = defineCommand({\n meta: {\n name: 'spec-snake',\n version,\n description: 'AI-powered design document generator CLI',\n },\n subCommands: {\n init: initCommand,\n start: startCommand,\n },\n});\n\nrunMain(main);\n", "{\n \"name\": \"spec-snake\",\n \"version\": \"0.0.1-beta.14\",\n \"type\": \"module\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/cut0/spec-snake.git\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"types\": \"./dist/types.d.ts\",\n \"main\": \"./dist/types.js\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/types.d.ts\",\n \"import\": \"./dist/types.js\",\n \"default\": \"./dist/types.js\"\n }\n },\n \"bin\": {\n \"spec-snake\": \"./dist/cli.js\"\n },\n \"files\": [\n \"dist\"\n ],\n \"scripts\": {\n \"build\": \"pnpm build:types && pnpm build:cli && pnpm build:client\",\n \"build:cli\": \"tsx scripts/build-cli.ts\",\n \"build:client\": \"vite build\",\n \"build:types\": \"tsc src/types.ts --declaration --outDir dist --skipLibCheck --module ESNext --moduleResolution bundler --target ES2022\",\n \"dev\": \"NODE_ENV=development SPEC_SNAKE_CONFIG=examples/local/spec-snake-ja.config.ts vite\",\n \"prd\": \"pnpm build && pnpm build:client && node dist/cli.js\",\n \"i18n\": \"lingui extract && lingui compile\",\n \"i18n:compile\": \"lingui compile\",\n \"i18n:extract\": \"lingui extract\",\n \"lint:check\": \"biome check .\",\n \"lint:fix\": \"biome check --write .\",\n \"typecheck\": \"tsc --noEmit\",\n \"prepublishOnly\": \"pnpm build\",\n \"release\": \"pnpm build && changeset publish\"\n },\n \"dependencies\": {\n \"@anthropic-ai/claude-agent-sdk\": \"0.1.75\",\n \"@hono/node-server\": \"1.19.7\",\n \"citty\": \"0.1.6\",\n \"consola\": \"3.4.2\",\n \"hono\": \"4.11.1\",\n \"jiti\": \"2.6.1\",\n \"valibot\": \"1.2.0\"\n },\n \"devDependencies\": {\n \"@base-ui/react\": \"1.0.0\",\n \"@biomejs/biome\": \"1.9.4\",\n \"@changesets/cli\": \"2.28.1\",\n \"@hono/vite-dev-server\": \"0.23.0\",\n \"@hookform/resolvers\": \"5.2.2\",\n \"@lingui/babel-plugin-lingui-macro\": \"5.7.0\",\n \"@lingui/cli\": \"5.7.0\",\n \"@lingui/conf\": \"5.7.0\",\n \"@lingui/core\": \"5.7.0\",\n \"@lingui/react\": \"5.7.0\",\n \"@lingui/vite-plugin\": \"5.7.0\",\n \"@tailwindcss/typography\": \"0.5.19\",\n \"@tailwindcss/vite\": \"4.1.18\",\n \"@tanstack/react-query\": \"5.35.1\",\n \"@tanstack/react-router\": \"1.141.6\",\n \"@tanstack/router-plugin\": \"1.141.7\",\n \"@types/node\": \"^25.0.3\",\n \"@types/react\": \"19.1.8\",\n \"@types/react-dom\": \"19.1.6\",\n \"@vitejs/plugin-react\": \"4.5.2\",\n \"esbuild\": \"0.27.2\",\n \"motion\": \"12.23.26\",\n \"react\": \"19.2.3\",\n \"react-dom\": \"19.2.3\",\n \"react-hook-form\": \"7.69.0\",\n \"react-markdown\": \"10.1.0\",\n \"remark-gfm\": \"4.0.1\",\n \"tailwindcss\": \"4.1.18\",\n \"tsx\": \"4.20.6\",\n \"typescript\": \"5.8.3\",\n \"vite\": \"7.3.0\",\n \"zustand\": \"5.0.9\"\n },\n \"packageManager\": \"pnpm@10.26.2\"\n}\n", "import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { defineCommand } from 'citty';\nimport { consola } from 'consola';\n\nconst CONFIG_TEMPLATE = `// For more detailed configuration examples, see:\n// https://github.com/cut0/spec-snake/blob/main/examples\n\nimport { defineConfig, defineScenario } from 'spec-snake';\n\nexport default defineConfig({\n scenarios: [\n defineScenario({\n id: 'default',\n name: 'Design Doc Generator',\n steps: [\n {\n slug: 'overview',\n title: 'Overview',\n description: 'Basic information about the feature',\n name: 'overview',\n fields: [\n {\n type: 'input',\n id: 'title',\n label: 'Title',\n description: 'Feature title',\n placeholder: 'Enter feature title',\n required: true,\n },\n {\n type: 'textarea',\n id: 'description',\n label: 'Description',\n description: 'Detailed description of the feature',\n placeholder: 'Describe the feature...',\n rows: 4,\n },\n {\n type: 'select',\n id: 'priority',\n label: 'Priority',\n description: 'Feature priority level',\n placeholder: 'Select priority',\n options: [\n { value: 'high', label: 'High' },\n { value: 'medium', label: 'Medium' },\n { value: 'low', label: 'Low' },\n ],\n },\n ],\n },\n ],\n filename: ({ timestamp }) => \\`\\${timestamp}.md\\`,\n prompt: ({ formData, aiContext }) =>\n \\`Generate a design doc based on the following input:\\n\\${JSON.stringify({ formData, aiContext }, null, 2)}\\`,\n }),\n ],\n permissions: {\n allowSave: true,\n },\n});\n`;\n\nexport const initCommand = defineCommand({\n meta: {\n name: 'init',\n description:\n 'Initialize a new spec-snake.config.ts file in the current directory',\n },\n args: {\n output: {\n type: 'string',\n description: 'Output file path',\n alias: 'o',\n default: 'spec-snake.config.ts',\n },\n force: {\n type: 'boolean',\n description: 'Overwrite existing file',\n alias: 'f',\n default: false,\n },\n },\n async run({ args }) {\n const outputPath = path.resolve(process.cwd(), args.output);\n\n if (fs.existsSync(outputPath) && !args.force) {\n consola.error(`File already exists: ${outputPath}`);\n consola.info('Use --force (-f) to overwrite');\n process.exit(1);\n }\n\n try {\n fs.writeFileSync(outputPath, CONFIG_TEMPLATE, 'utf-8');\n consola.success(`Config file created: ${outputPath}`);\n } catch (error) {\n consola.error('Failed to create config file:', error);\n process.exit(1);\n }\n },\n});\n", "import * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport * as url from 'node:url';\n\nimport { serve } from '@hono/node-server';\nimport { serveStatic } from '@hono/node-server/serve-static';\nimport { defineCommand } from 'citty';\nimport { consola } from 'consola';\nimport { createJiti } from 'jiti';\n\nimport { type Config, safeParseConfig } from '../../definitions';\nimport { createApiServer } from '../../server/api';\n\nconst getDistClientDir = (): string => {\n const __filename = url.fileURLToPath(import.meta.url);\n const __dirname = path.dirname(__filename);\n\n // After build: dist/cli.js \u2192 dist/client (same dist directory)\n // During development: src/cli/commands/start.ts \u2192 dist/client (3 levels up in dist)\n const isBuilt = __dirname.endsWith('/dist') || __dirname.includes('/dist/');\n return isBuilt\n ? path.resolve(__dirname, 'client')\n : path.resolve(__dirname, '../../../dist/client');\n};\n\nconst loadConfig = async (configPath: string): Promise<Config> => {\n const absolutePath = path.resolve(process.cwd(), configPath);\n\n if (!fs.existsSync(absolutePath)) {\n throw new Error(`Config file not found: ${absolutePath}`);\n }\n\n const jiti = createJiti(import.meta.url);\n const configModule = await jiti.import(absolutePath);\n const config = (configModule as { default: Config }).default;\n\n const result = safeParseConfig(config);\n if (!result.success) {\n const issues = result.issues.map((issue) => {\n const pathStr = issue.path?.map((p) => p.key).join('.') ?? '';\n return ` - ${pathStr}: ${issue.message}`;\n });\n throw new Error(`Invalid config:\\n${issues.join('\\n')}`);\n }\n\n // hooks cannot be validated with valibot, so preserve from original config\n return config;\n};\n\nconst runServer = async (\n config: Config,\n options: { distDir: string; port: number; host: string },\n) => {\n consola.start('Starting server...');\n\n const app = createApiServer(config);\n\n app.use(\n '/*',\n serveStatic({\n root: options.distDir,\n rewriteRequestPath: (requestPath) => {\n const fullPath = path.join(options.distDir, requestPath);\n if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {\n return requestPath;\n }\n return '/index.html';\n },\n }),\n );\n\n const server = serve(\n {\n fetch: app.fetch,\n port: options.port,\n hostname: options.host,\n },\n (info) => {\n const displayHost =\n info.address === '::1' || info.address === '127.0.0.1'\n ? 'localhost'\n : info.address;\n consola.success('Server started');\n consola.info(` \u279C Local: http://${displayHost}:${info.port}/`);\n },\n );\n\n const cleanup = () => {\n consola.info('Shutting down server...');\n server.close(() => {\n process.exit(0);\n });\n };\n\n process.on('SIGINT', cleanup);\n process.on('SIGTERM', cleanup);\n};\n\nexport const startCommand = defineCommand({\n meta: {\n name: 'start',\n description: 'Start the server with the specified config',\n },\n args: {\n config: {\n type: 'string',\n description: 'Path to config file',\n alias: 'c',\n default: 'spec-snake.config.ts',\n },\n port: {\n type: 'string',\n description: 'Port to run the server on',\n alias: 'p',\n default: '3000',\n },\n host: {\n type: 'string',\n description: 'Host to bind the server to',\n default: 'localhost',\n },\n },\n async run({ args }) {\n const configPath = args.config;\n const port = Number.parseInt(args.port, 10);\n\n consola.start(`Loading config from: ${configPath}`);\n\n try {\n const config = await loadConfig(configPath);\n consola.success(\n `Config loaded successfully (${config.scenarios.length} scenarios)`,\n );\n\n await runServer(config, {\n distDir: getDistClientDir(),\n port,\n host: args.host,\n });\n } catch (error) {\n if (error instanceof Error) {\n consola.error(error.message);\n } else {\n consola.error('Failed to start server:', error);\n }\n process.exit(1);\n }\n },\n});\n", "/**\n * Core type definitions for spec-snake\n *\n * This module defines all TypeScript types used throughout the application,\n * including form fields, sections, steps, scenarios, and configuration.\n *\n * @module types\n */\n\nimport type {\n McpHttpServerConfig,\n McpSSEServerConfig,\n McpStdioServerConfig,\n Options,\n PermissionMode,\n} from '@anthropic-ai/claude-agent-sdk';\n\n// =============================================================================\n// Form Field Types\n// =============================================================================\n\n/**\n * Option for select/dropdown fields\n *\n * @example\n * ```ts\n * const option: SelectOption = {\n * value: 'high',\n * label: 'High Priority'\n * };\n * ```\n */\nexport type SelectOption = {\n /** Internal value used in form data */\n value: string;\n /** Display label shown to users */\n label: string;\n};\n\n// =============================================================================\n// Field Condition Types\n// =============================================================================\n\n/**\n * Single field condition for visibility\n *\n * The `field` property supports dot notation for nested paths (e.g., 'overview.priority').\n *\n * @example\n * ```ts\n * // Show when priority is 'high'\n * when: { field: 'priority', is: 'high' }\n *\n * // Show when priority is 'high' or 'medium'\n * when: { field: 'priority', is: ['high', 'medium'] }\n *\n * // Show when priority is NOT 'low'\n * when: { field: 'priority', isNot: 'low' }\n *\n * // Show when checkbox is checked\n * when: { field: 'has_deadline', is: true }\n *\n * // Show when field is not empty\n * when: { field: 'title', isNotEmpty: true }\n *\n * // Cross-section reference using dot notation\n * when: { field: 'overview.priority', is: 'high' }\n * ```\n */\nexport type FieldConditionSingle =\n | { field: string; is: string | boolean | (string | boolean)[] }\n | { field: string; isNot: string | boolean | (string | boolean)[] }\n | { field: string; isEmpty: true }\n | { field: string; isNotEmpty: true };\n\n/**\n * Composite condition using AND logic\n *\n * All conditions must be true for the field to be visible.\n *\n * @example\n * ```ts\n * // Show when priority is 'high' AND type is 'feature'\n * when: {\n * and: [\n * { field: 'priority', is: 'high' },\n * { field: 'type', is: 'feature' }\n * ]\n * }\n * ```\n */\nexport type FieldConditionAnd = {\n and: FieldConditionObject[];\n};\n\n/**\n * Composite condition using OR logic\n *\n * At least one condition must be true for the field to be visible.\n *\n * @example\n * ```ts\n * // Show when priority is 'high' OR type is 'urgent'\n * when: {\n * or: [\n * { field: 'priority', is: 'high' },\n * { field: 'type', is: 'urgent' }\n * ]\n * }\n * ```\n */\nexport type FieldConditionOr = {\n or: FieldConditionObject[];\n};\n\n/**\n * Declarative condition for field visibility\n *\n * Supports single field conditions, AND/OR composite conditions,\n * and dot notation for cross-section field references.\n *\n * @example\n * ```ts\n * // Simple condition\n * when: { field: 'priority', is: 'high' }\n *\n * // AND condition\n * when: {\n * and: [\n * { field: 'priority', is: 'high' },\n * { field: 'type', is: 'feature' }\n * ]\n * }\n *\n * // OR condition\n * when: {\n * or: [\n * { field: 'priority', is: 'high' },\n * { field: 'overview.urgent', is: true }\n * ]\n * }\n *\n * // Nested AND/OR\n * when: {\n * or: [\n * { field: 'priority', is: 'high' },\n * {\n * and: [\n * { field: 'type', is: 'feature' },\n * { field: 'status', is: 'approved' }\n * ]\n * }\n * ]\n * }\n * ```\n */\nexport type FieldConditionObject =\n | FieldConditionSingle\n | FieldConditionAnd\n | FieldConditionOr;\n\n/**\n * Field visibility condition (object-based only)\n *\n * Use declarative object conditions for field visibility.\n * Supports single conditions, AND/OR logic, and dot notation for nested fields.\n *\n * Note: Function-based conditions are not supported as they cannot be\n * serialized when sending scenario data from server to client.\n */\nexport type FieldCondition = FieldConditionObject;\n\n/**\n * Text input field configuration\n *\n * Supports various input types including text, date, and URL.\n * Can include autocomplete suggestions.\n *\n * @example\n * ```ts\n * const field: InputField = {\n * id: 'project_name',\n * type: 'input',\n * label: 'Project Name',\n * description: 'Enter the name of your project',\n * placeholder: 'My Awesome Project',\n * required: true,\n * inputType: 'text',\n * suggestions: ['Project A', 'Project B']\n * };\n * ```\n */\nexport type InputField = {\n /** Unique identifier for the field (used as form data key) */\n id: string;\n /** Display label shown above the input */\n label: string;\n /** Help text describing the field's purpose */\n description: string;\n /** Placeholder text shown when input is empty */\n placeholder?: string;\n /** Whether the field must be filled before submission */\n required?: boolean;\n /** Field type discriminator */\n type: 'input';\n /** HTML input type - affects keyboard and validation */\n inputType?: 'text' | 'date' | 'url';\n /** Autocomplete suggestions shown as datalist options */\n suggestions?: string[];\n /** Condition for when this field should be visible */\n when?: FieldCondition;\n /** Default value for the field */\n default?: string;\n};\n\n/**\n * Multi-line text area field configuration\n *\n * @example\n * ```ts\n * const field: TextareaField = {\n * id: 'description',\n * type: 'textarea',\n * label: 'Description',\n * description: 'Provide a detailed description',\n * rows: 5\n * };\n * ```\n */\nexport type TextareaField = {\n /** Unique identifier for the field */\n id: string;\n /** Display label shown above the textarea */\n label: string;\n /** Help text describing the field's purpose */\n description: string;\n /** Placeholder text shown when textarea is empty */\n placeholder?: string;\n /** Whether the field must be filled before submission */\n required?: boolean;\n /** Field type discriminator */\n type: 'textarea';\n /** Number of visible text rows (affects initial height) */\n rows?: number;\n /** Condition for when this field should be visible */\n when?: FieldCondition;\n /** Default value for the field */\n default?: string;\n};\n\n/**\n * Dropdown select field configuration\n *\n * @example\n * ```ts\n * const field: SelectField = {\n * id: 'priority',\n * type: 'select',\n * label: 'Priority',\n * description: 'Select the priority level',\n * options: [\n * { value: 'low', label: 'Low' },\n * { value: 'medium', label: 'Medium' },\n * { value: 'high', label: 'High' }\n * ]\n * };\n * ```\n */\nexport type SelectField = {\n /** Unique identifier for the field */\n id: string;\n /** Display label shown above the select */\n label: string;\n /** Help text describing the field's purpose */\n description: string;\n /** Placeholder text (shown as first disabled option) */\n placeholder?: string;\n /** Whether a selection must be made before submission */\n required?: boolean;\n /** Field type discriminator */\n type: 'select';\n /** Available options for selection */\n options: SelectOption[];\n /** Condition for when this field should be visible */\n when?: FieldCondition;\n /** Default value for the field (must match one of the option values) */\n default?: string;\n};\n\n/**\n * Boolean checkbox field configuration\n *\n * @example\n * ```ts\n * const field: CheckboxField = {\n * id: 'agree_terms',\n * type: 'checkbox',\n * label: 'I agree to the terms',\n * description: 'You must agree to continue',\n * required: true\n * };\n * ```\n */\nexport type CheckboxField = {\n /** Unique identifier for the field */\n id: string;\n /** Display label shown next to the checkbox */\n label: string;\n /** Help text describing the field's purpose */\n description: string;\n /** Placeholder (not typically used for checkboxes) */\n placeholder?: string;\n /** Whether the checkbox must be checked before submission */\n required?: boolean;\n /** Field type discriminator */\n type: 'checkbox';\n /** Condition for when this field should be visible */\n when?: FieldCondition;\n /** Default value for the field */\n default?: boolean;\n};\n\nexport type FormField =\n | InputField\n | TextareaField\n | SelectField\n | CheckboxField;\n\n// =============================================================================\n// Layout Types\n// =============================================================================\n\n/**\n * Grid layout for arranging multiple fields in columns\n *\n * Allows organizing form fields horizontally within a step.\n * Supports nested fields including other grids (recursive).\n *\n * @example\n * ```ts\n * const layout: GridLayout = {\n * type: 'grid',\n * columns: 2,\n * fields: [\n * { id: 'first_name', type: 'input', ... },\n * { id: 'last_name', type: 'input', ... }\n * ]\n * };\n * ```\n */\nexport type GridLayout = {\n /** Layout type discriminator */\n type: 'grid';\n /** Number of columns (fields per row) */\n columns: number;\n /** Fields to arrange in the grid (can include nested layouts) */\n fields: Field[];\n};\n\n/**\n * Group layout for visually grouping multiple fields together\n *\n * Used for visual grouping only. Does not create nested structure in formData.\n * To repeat a group of fields, wrap this in a RepeatableLayout.\n *\n * @example\n * ```ts\n * // Standalone group (visual grouping only)\n * const layout: GroupLayout = {\n * type: 'group',\n * fields: [\n * { id: 'firstName', type: 'input', label: 'First Name', description: '' },\n * { id: 'lastName', type: 'input', label: 'Last Name', description: '' }\n * ]\n * };\n * // formData: { firstName: '...', lastName: '...' }\n * ```\n */\nexport type GroupLayout = {\n /** Layout type discriminator */\n type: 'group';\n /** Fields contained in the group */\n fields: Field[];\n};\n\n/**\n * Repeatable layout for fields that can be repeated\n *\n * Allows users to add multiple instances of a field or group.\n * The id is used as the key in form data, with values stored as an array.\n *\n * @example\n * ```ts\n * // Single field repeatable\n * const tagsLayout: RepeatableLayout = {\n * type: 'repeatable',\n * id: 'tags',\n * label: 'Tags',\n * minCount: 1,\n * field: { id: 'name', type: 'input', label: 'Tag', description: '' }\n * };\n * // formData: { tags: [{ name: 'tag1' }, { name: 'tag2' }] }\n *\n * // Group repeatable (multiple fields)\n * const librariesLayout: RepeatableLayout = {\n * type: 'repeatable',\n * id: 'libraries',\n * label: 'Libraries',\n * minCount: 1,\n * field: {\n * type: 'group',\n * fields: [\n * { id: 'name', type: 'input', label: 'Name', description: '' },\n * { id: 'url', type: 'input', label: 'URL', description: '' }\n * ]\n * }\n * };\n * // formData: { libraries: [{ name: 'React', url: '...' }, ...] }\n * ```\n */\nexport type RepeatableLayout = {\n /** Layout type discriminator */\n type: 'repeatable';\n /** Unique identifier (used as key in form data) */\n id: string;\n /** Display label shown above the repeatable field */\n label: string;\n /** Minimum number of entries required (default: 0) */\n minCount?: number;\n /** Default number of entries to create initially (defaults to minCount if not specified) */\n defaultCount?: number;\n /** The field or group to repeat */\n field: FormField | GroupLayout;\n};\n\nexport type LayoutField = GridLayout | RepeatableLayout | GroupLayout;\nexport type Field = FormField | LayoutField;\n\n// =============================================================================\n// Step Types\n// =============================================================================\n\n/**\n * Step definition for multi-step form wizard\n *\n * Each step represents one page in the form wizard, containing\n * fields for the user to fill out.\n *\n * @example\n * ```ts\n * const step: Step = {\n * slug: 'basic-info',\n * title: 'Basic Information',\n * description: 'Enter the basic details of your project',\n * name: 'basic',\n * fields: [\n * { id: 'title', type: 'input', label: 'Title', description: '...' },\n * { id: 'priority', type: 'select', label: 'Priority', description: '...', options: [...] }\n * ]\n * };\n *\n * // For repeatable fields (single field):\n * const stepWithRepeatable: Step = {\n * slug: 'tags',\n * title: 'Tags',\n * description: 'Add tags',\n * name: 'tags',\n * fields: [\n * {\n * type: 'repeatable',\n * id: 'items',\n * minCount: 1,\n * field: { id: 'name', type: 'input', label: 'Tag', description: '' }\n * }\n * ]\n * };\n * // formData: { tags: { items: [{ name: 'tag1' }, { name: 'tag2' }] } }\n *\n * // For repeatable group (multiple fields):\n * const stepWithRepeatableGroup: Step = {\n * slug: 'libraries',\n * title: 'Libraries',\n * description: 'Add libraries',\n * name: 'libraries',\n * fields: [\n * {\n * type: 'repeatable',\n * id: 'items',\n * minCount: 1,\n * field: {\n * type: 'group',\n * fields: [\n * { id: 'name', type: 'input', label: 'Name', description: '...' },\n * { id: 'url', type: 'input', label: 'URL', description: '...' }\n * ]\n * }\n * }\n * ]\n * };\n * // formData: { libraries: { items: [{ name: '...', url: '...' }, ...] } }\n * ```\n */\nexport type Step = {\n /** URL-friendly identifier (used in routing) */\n slug: string;\n /** Display title shown in step header */\n title: string;\n /** Description text shown below the title */\n description: string;\n /** Step name (used as key in form data output) */\n name: string;\n /** Fields contained in this step */\n fields: Field[];\n};\n\n// =============================================================================\n// AI Settings Types (Claude Agent SDK)\n// =============================================================================\n\n/**\n * MCP (Model Context Protocol) Server Configuration\n *\n * Defines how to connect to an MCP server. Supports three transport types:\n * - stdio: Local process started with a command\n * - sse: Remote server via Server-Sent Events\n * - http: Remote server via HTTP\n *\n * Re-exported from `@anthropic-ai/claude-agent-sdk` for convenience.\n * Note: The SDK also supports an 'sdk' type for in-memory servers,\n * but that requires runtime instances and is not serializable to config files.\n *\n * @see https://docs.anthropic.com/en/docs/claude-code/mcp\n *\n * @example\n * ```ts\n * // stdio server (local process)\n * const stdioConfig: McpServerConfig = {\n * command: 'npx',\n * args: ['@modelcontextprotocol/server-filesystem'],\n * env: { ALLOWED_PATHS: '/home/user/projects' }\n * };\n *\n * // SSE server (remote)\n * const sseConfig: McpServerConfig = {\n * type: 'sse',\n * url: 'https://api.example.com/mcp/sse',\n * headers: { 'Authorization': 'Bearer token' }\n * };\n *\n * // HTTP server (remote)\n * const httpConfig: McpServerConfig = {\n * type: 'http',\n * url: 'https://api.example.com/mcp',\n * headers: { 'Authorization': 'Bearer token' }\n * };\n * ```\n */\nexport type McpServerConfig =\n | McpStdioServerConfig\n | McpSSEServerConfig\n | McpHttpServerConfig;\n\n// Re-export SDK types for convenience\nexport type {\n PermissionMode,\n McpStdioServerConfig,\n McpSSEServerConfig,\n McpHttpServerConfig,\n};\n\n/**\n * AI Settings for Claude Agent SDK query options\n *\n * Derived from the SDK's `Options` type using `Pick` to select only the\n * fields relevant for config file serialization. This ensures type safety\n * and automatic updates when the SDK changes.\n *\n * This is a simplified subset focused on model selection, tools, MCP, and permissions.\n * Other SDK options (session, environment, output format, etc.) are managed by the server.\n *\n * @see https://docs.anthropic.com/en/docs/claude-code/sdk\n * @see https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk\n *\n * @example\n * ```ts\n * const aiSettings: AiSettings = {\n * model: 'claude-sonnet-4-5-20250929',\n * maxTurns: 10,\n * maxBudgetUsd: 1.0,\n * permissionMode: 'bypassPermissions',\n * allowDangerouslySkipPermissions: true,\n * mcpServers: {\n * filesystem: {\n * command: 'npx',\n * args: ['@modelcontextprotocol/server-filesystem']\n * }\n * }\n * };\n * ```\n *\n * **Available Tools:**\n *\n * File Operations:\n * - `Read` - Read file contents\n * - `Write` - Write/create files\n * - `Edit` - Edit existing files\n * - `Glob` - Find files by pattern\n * - `Grep` - Search file contents\n * - `NotebookEdit` - Edit Jupyter notebooks\n *\n * Command Execution:\n * - `Bash` - Execute shell commands\n *\n * Web:\n * - `WebSearch` - Search the web\n * - `WebFetch` - Fetch content from URLs\n *\n * Agent & Task:\n * - `Task` - Launch sub-agents\n * - `TodoWrite` - Manage task lists\n *\n * Code Intelligence:\n * - `LSP` - Language Server Protocol operations\n *\n * MCP Tools:\n * - Format: `mcp__<server>__<tool>` (e.g., `mcp__filesystem__read_file`)\n */\nexport type AiSettings = Omit<\n Pick<\n Options,\n | 'model'\n | 'fallbackModel'\n | 'maxThinkingTokens'\n | 'maxTurns'\n | 'maxBudgetUsd'\n | 'allowedTools'\n | 'disallowedTools'\n | 'tools'\n | 'permissionMode'\n | 'allowDangerouslySkipPermissions'\n | 'mcpServers'\n | 'strictMcpConfig'\n >,\n 'mcpServers'\n> & {\n /**\n * MCP server configurations keyed by server name\n *\n * Each server provides additional tools and capabilities to the model.\n * Only serializable transport types (stdio, sse, http) are supported in config files.\n *\n * @see https://docs.anthropic.com/en/docs/claude-code/mcp\n */\n mcpServers?: Record<string, McpServerConfig>;\n};\n\n// =============================================================================\n// Scenario Types\n// =============================================================================\n\n/**\n * Base scenario definition (serializable to JSON)\n *\n * Contains the core fields that can be validated by Valibot schema.\n * Extended by `Scenario` with function-based fields.\n */\nexport type ScenarioBase = {\n /** Unique identifier for the scenario (used in URLs) */\n id: string;\n /** Display name shown in the scenario list */\n name: string;\n /** Steps that make up the scenario's form wizard */\n steps: Step[];\n /**\n * AI settings for Claude Agent SDK\n * @see AiSettings\n */\n aiSettings?: AiSettings;\n};\n\n/**\n * Step metadata in AiContext\n */\nexport type AiContextStepMeta = {\n title: string;\n description: string;\n};\n\n/**\n * Field metadata in AiContext\n */\nexport type AiContextFieldMeta = {\n label: string;\n description: string;\n};\n\n/**\n * AiContext for a repeatable field (nested structure)\n */\nexport type AiContextRepeatable = {\n [fieldId: string]: AiContextFieldMeta | AiContextRepeatable;\n};\n\n/**\n * AiContext for a step\n */\nexport type AiContextStep = {\n _step: AiContextStepMeta;\n [fieldId: string]:\n | AiContextStepMeta\n | AiContextFieldMeta\n | AiContextRepeatable;\n};\n\n/**\n * AiContext type - metadata for form fields to help AI understand the data\n *\n * This is included in formData as `ai_context` property.\n * Contains labels and descriptions for each field, organized by step name.\n *\n * @example\n * ```ts\n * {\n * overview: {\n * _step: { title: \"Overview\", description: \"Basic information\" },\n * title: { label: \"Title\", description: \"Feature title\" },\n * priority: { label: \"Priority\", description: \"Priority level\" }\n * },\n * modules: {\n * _step: { title: \"Modules\", description: \"Module structure\" },\n * items: {\n * name: { label: \"Module Name\", description: \"Name of the module\" },\n * features: {\n * feature_name: { label: \"Feature Name\", description: \"Name of feature\" }\n * }\n * }\n * }\n * }\n * ```\n */\nexport type AiContext = {\n [stepName: string]: AiContextStep;\n};\n\n/**\n * Lifecycle hooks for scenario events\n *\n * Allows custom behavior during preview and save operations.\n *\n * @typeParam TFormData - Type of the raw form data from UI\n */\nexport type ScenarioHooks<\n TFormData extends Record<string, unknown> = Record<string, unknown>,\n> = {\n /**\n * Called after preview is generated but before displaying to user\n *\n * Use for logging, analytics, or transforming the preview content.\n *\n * @param params.formData - The raw form data from the UI\n * @param params.aiContext - Field metadata (labels, descriptions) for AI\n * @param params.content - The generated markdown content\n */\n onPreview?: (params: {\n formData: TFormData;\n aiContext: AiContext;\n content: string;\n }) => Promise<void>;\n\n /**\n * Called after document is saved to disk\n *\n * Use for post-processing, notifications, or integrations.\n *\n * @param params.content - The saved markdown content\n * @param params.filename - The filename that was used\n * @param params.outputPath - Full path to the saved file\n * @param params.formData - The raw form data from the UI\n * @param params.aiContext - Field metadata (labels, descriptions) for AI\n */\n onSave?: (params: {\n content: string;\n filename: string;\n outputPath: string;\n formData: TFormData;\n aiContext: AiContext;\n }) => Promise<void>;\n};\n\n/**\n * Filename function type for custom document naming\n *\n * Can be a static string or a function for dynamic naming.\n * Default format: `design-doc-{scenarioId}-{timestamp}.md`\n *\n * @typeParam TFormData - Type of the raw form data from UI\n *\n * @example\n * ```ts\n * // Static filename\n * filename: 'design-doc.md'\n *\n * // Dynamic filename based on form data\n * filename: ({ formData, timestamp }) =>\n * `${formData.project_name}-${timestamp}.md`\n * ```\n */\nexport type ScenarioFilename<\n TFormData extends Record<string, unknown> = Record<string, unknown>,\n> =\n | string\n | ((params: {\n scenarioId: string;\n timestamp: string;\n content: string;\n formData: TFormData;\n aiContext: AiContext;\n }) => string);\n\n/**\n * Prompt function type\n *\n * A function that generates the prompt string.\n * Use `formData` for raw values and `aiContext` for field metadata.\n *\n * @typeParam TFormData - Type of the raw form data from UI\n *\n * @example\n * ```ts\n * prompt: ({ formData, aiContext }) =>\n * `Create a document based on:\\n${JSON.stringify({ formData, aiContext }, null, 2)}`\n * ```\n */\nexport type ScenarioPrompt<\n TFormData extends Record<string, unknown> = Record<string, unknown>,\n> = (params: { formData: TFormData; aiContext: AiContext }) => string;\n\n/**\n * Complete scenario definition\n *\n * Extends ScenarioBase with function-based fields that cannot be\n * serialized to JSON (prompt and filename can be functions, hooks).\n *\n * @typeParam TFormData - Type of the raw form data from UI (inferred from steps)\n *\n * @example\n * ```ts\n * const scenario: Scenario = {\n * id: 'design-doc',\n * name: 'Design Document',\n * steps: [...],\n * prompt: ({ formData, aiContext }) =>\n * `Generate a design doc based on:\\n${JSON.stringify({ formData, aiContext }, null, 2)}`,\n * outputDir: './docs/designs',\n * filename: ({ formData, timestamp }) =>\n * `${formData.overview?.title ?? 'untitled'}-${timestamp}.md`,\n * aiSettings: {\n * model: 'claude-sonnet-4-5'\n * },\n * hooks: {\n * onSave: async ({ filename }) => {\n * console.log(`Saved: ${filename}`);\n * }\n * }\n * };\n * ```\n */\nexport type Scenario<\n TFormData extends Record<string, unknown> = Record<string, unknown>,\n> = ScenarioBase & {\n /** Prompt template function */\n prompt: ScenarioPrompt<TFormData>;\n /** Directory where generated documents are saved */\n outputDir?: string;\n /** Custom filename for saved documents */\n filename?: ScenarioFilename<TFormData>;\n /** Lifecycle hooks for custom behavior */\n hooks?: ScenarioHooks<TFormData>;\n};\n\n// =============================================================================\n// Type Inference Utilities\n// =============================================================================\n\n/**\n * Helper type to get the value type for a form field\n */\ntype FieldValueType<F> = F extends { type: 'checkbox' }\n ? boolean\n : F extends { type: 'select'; options: readonly { value: infer V }[] }\n ? V\n : string;\n\n/**\n * Helper type to create an object type from FormField array (not supporting nested GridLayout)\n */\ntype FormFieldsToObject<Fields extends readonly FormField[]> = {\n [F in Fields[number] as F['id']]: FieldValueType<F>;\n};\n\n/**\n * Helper type to merge union to intersection\n */\ntype UnionToIntersection<U> = (\n U extends unknown\n ? (k: U) => void\n : never\n) extends (k: infer I) => void\n ? I\n : never;\n\n/**\n * Infer the formData type from a Scenario's steps\n *\n * Use this utility type to get type-safe access to raw form data\n * in hooks, prompts, and filename.\n *\n * **Note**: This utility works with FormField arrays only (not nested layouts).\n * Define fields with `as const` for literal type inference.\n *\n * @example\n * ```ts\n * const scenario = {\n * id: 'design-doc',\n * name: 'Design Document',\n * steps: [\n * {\n * slug: 'overview',\n * title: 'Overview',\n * description: 'Project overview',\n * name: 'overview',\n * fields: [\n * { id: 'title', type: 'input', label: 'Title', description: '' },\n * { id: 'description', type: 'textarea', label: 'Description', description: '' },\n * ] as const,\n * },\n * ],\n * prompt: ({ formData, aiContext }) => '...',\n * } as const satisfies Scenario;\n *\n * type MyFormData = InferFormData<typeof scenario>;\n * // Result:\n * // {\n * // overview: { title: string; description: string };\n * // }\n * ```\n */\nexport type InferFormData<T extends Scenario> =\n T['steps'] extends readonly (infer S)[]\n ? S extends { name: infer N extends string; fields: readonly FormField[] }\n ? { [K in N]: FormFieldsToObject<S['fields']> }\n : never\n : never;\n\n/**\n * Infer the merged formData type from a Scenario\n *\n * This flattens all steps into a single object type.\n *\n * @example\n * ```ts\n * type MyFormData = InferFormDataMerged<typeof scenario>;\n * // {\n * // overview: { title: string; description: string };\n * // }\n * ```\n */\nexport type InferFormDataMerged<T extends Scenario> = UnionToIntersection<\n InferFormData<T>\n>;\n\n/**\n * Helper type to extract FormField from Field (handling grid, repeatable, group layouts)\n */\ntype ExtractFormFields<F> = F extends FormField\n ? F\n : F extends { type: 'grid'; fields: readonly (infer GF)[] }\n ? GF extends FormField\n ? GF\n : never\n : F extends { type: 'repeatable'; field: infer RF }\n ? RF extends FormField\n ? RF\n : never\n : F extends { type: 'group'; fields: readonly (infer GF)[] }\n ? GF extends FormField\n ? GF\n : never\n : never;\n\n/**\n * Helper type to create an object type from Field array (supporting layouts)\n */\ntype FieldsToObject<Fields extends readonly Field[]> = {\n [F in ExtractFormFields<Fields[number]> as F extends {\n id: infer ID extends string;\n }\n ? ID\n : never]?: F extends FormField ? FieldValueType<F> : unknown;\n};\n\n/**\n * Infer the formData type directly from a steps array\n *\n * Use this utility type when defining scenarios with `defineScenario`\n * for type-safe access to formData in hooks, prompts, and filename.\n *\n * **Note**: For best results, define steps with `as const satisfies Step[]`\n * to preserve literal types for step names and field IDs.\n *\n * @example\n * ```ts\n * const steps = [\n * {\n * slug: 'basic-info',\n * title: 'Basic Info',\n * description: 'Enter basic information',\n * name: 'basicInfo',\n * fields: [\n * { id: 'title', type: 'input', label: 'Title', description: '' },\n * ],\n * },\n * {\n * slug: 'libraries',\n * title: 'Libraries',\n * description: 'Add libraries',\n * name: 'libraries',\n * fields: [\n * {\n * type: 'group',\n * id: 'items',\n * minCount: 1,\n * fields: [\n * { id: 'name', type: 'input', label: 'Name', description: '' },\n * ],\n * },\n * ],\n * },\n * ] as const satisfies Step[];\n *\n * type FormData = InferFormDataFromSteps<typeof steps>;\n * // {\n * // basicInfo?: { title?: string };\n * // libraries?: { items?: Array<{ name?: string }> };\n * // }\n * ```\n */\nexport type InferFormDataFromSteps<TSteps extends readonly Step[]> =\n UnionToIntersection<\n TSteps[number] extends infer S\n ? S extends { name: infer N extends string; fields: readonly Field[] }\n ? { [K in N]?: FieldsToObject<S['fields']> }\n : never\n : never\n > &\n Record<string, unknown>;\n\n// =============================================================================\n// AI Mode Types\n// =============================================================================\n\n/**\n * AI mode for document generation\n *\n * - `'stream'` - AI enabled with SSE streaming (default)\n * - `'sync'` - AI enabled, returns full response at once\n * - `'mock'` - AI disabled, returns fixed mock response\n *\n * @example\n * ```ts\n * // Streaming mode (default)\n * ai: 'stream'\n *\n * // Non-streaming mode\n * ai: 'sync'\n *\n * // Mock mode for development/testing\n * ai: 'mock'\n * ```\n */\nexport type AiMode = 'stream' | 'sync' | 'mock';\n\n/**\n * Default mock response when AI is disabled\n */\nexport const DEFAULT_MOCK_RESPONSE =\n '# Mock Document\\n\\nAI is disabled. This is a mock response.';\n\n// =============================================================================\n// Configuration Types\n// =============================================================================\n\n/**\n * Permission settings for the application\n */\nexport type Permissions = {\n /** Whether users can save generated documents to disk */\n allowSave: boolean;\n};\n\n/**\n * Root configuration object\n *\n * This is the type for the config file exported from `config.ts`.\n *\n * @example\n * ```ts\n * // config.ts\n * import type { Config } from '@anthropic-ai/claude-agent-sdk';\n *\n * export default {\n * scenarios: [\n * { id: 'design-doc', name: 'Design Doc', ... }\n * ],\n * permissions: {\n * allowSave: true\n * }\n * } satisfies Config;\n * ```\n */\nexport type Config = {\n /** Available scenarios (each represents a document type) */\n scenarios: Scenario[];\n /** Global permission settings */\n permissions: Permissions;\n /** When true, runs in hosted mode where saving is disabled regardless of permissions.allowSave */\n hosted?: boolean;\n /**\n * AI mode for document generation\n * @default 'stream'\n */\n ai?: AiMode;\n};\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\n/**\n * Helper function to define a scenario with type-safe formData\n *\n * This function uses TypeScript's const type parameters to infer\n * literal types from the steps array, enabling type-safe access\n * to formData in hooks, prompts, and filename.\n *\n * **Usage**: Define your steps with `as const satisfies Step[]` for best results.\n *\n * @example\n * ```ts\n * import { defineScenario, type Step, type Config } from 'spec-snake';\n *\n * const steps = [\n * {\n * slug: 'basic-info',\n * title: 'Basic Info',\n * description: 'Enter basic information',\n * name: 'basicInfo',\n * fields: [\n * { id: 'projectName', type: 'input', label: 'Project Name', description: '' },\n * { id: 'overview', type: 'textarea', label: 'Overview', description: '' },\n * ],\n * },\n * {\n * slug: 'features',\n * title: 'Features',\n * description: 'List features',\n * name: 'features',\n * fields: [\n * {\n * type: 'repeatable',\n * id: 'items',\n * minCount: 1,\n * field: {\n * type: 'group',\n * fields: [\n * { id: 'name', type: 'input', label: 'Feature Name', description: '' },\n * ],\n * },\n * },\n * ],\n * },\n * ] as const satisfies Step[];\n *\n * const scenario = defineScenario({\n * id: 'my-scenario',\n * name: 'My Scenario',\n * steps,\n * prompt: ({ formData, aiContext }) =>\n * `Generate document based on:\\n${JSON.stringify({ formData, aiContext }, null, 2)}`,\n * outputDir: 'docs',\n * filename: ({ formData }) => {\n * // formData is now type-safe!\n * // formData.basicInfo?.projectName is typed as string | undefined\n * return `${formData.basicInfo?.projectName ?? 'untitled'}.md`;\n * },\n * });\n *\n * const config: Config = {\n * scenarios: [scenario],\n * permissions: { allowSave: true },\n * };\n * ```\n */\nexport function defineScenario<const TSteps extends readonly Step[]>(\n scenario: Omit<ScenarioBase, 'steps' | 'prompt'> & {\n steps: TSteps;\n prompt: ScenarioPrompt<InferFormDataFromSteps<TSteps>>;\n outputDir?: string;\n filename?: ScenarioFilename<InferFormDataFromSteps<TSteps>>;\n hooks?: ScenarioHooks<InferFormDataFromSteps<TSteps>>;\n },\n): Scenario {\n return scenario as unknown as Scenario;\n}\n\n/**\n * Helper function to define a config object\n *\n * This is a simple identity function that provides better type inference\n * and editor support when defining config files.\n *\n * @example\n * ```ts\n * import { defineConfig, defineScenario } from 'spec-snake';\n *\n * export default defineConfig({\n * scenarios: [\n * defineScenario({ ... }),\n * ],\n * permissions: {\n * allowSave: true,\n * },\n * });\n * ```\n */\nexport function defineConfig(config: Config): Config {\n return config;\n}\n", "import * as v from 'valibot';\nimport type {\n Field,\n FieldConditionObject,\n GridLayout,\n GroupLayout,\n LayoutField,\n RepeatableLayout,\n} from './types';\n\n// =============================================================================\n// Type Guards\n// =============================================================================\n\nexport const isLayoutField = (field: Field): field is LayoutField => {\n return (\n field.type === 'grid' ||\n field.type === 'repeatable' ||\n field.type === 'group'\n );\n};\n\n// =============================================================================\n// Form Field Schemas\n// =============================================================================\n\n// =============================================================================\n// Field Condition Schema\n// =============================================================================\n\n// Single field condition (supports dot notation for nested paths)\nconst FieldConditionSingleSchema = v.union([\n v.object({\n field: v.string(),\n is: v.union([\n v.string(),\n v.boolean(),\n v.array(v.union([v.string(), v.boolean()])),\n ]),\n }),\n v.object({\n field: v.string(),\n isNot: v.union([\n v.string(),\n v.boolean(),\n v.array(v.union([v.string(), v.boolean()])),\n ]),\n }),\n v.object({\n field: v.string(),\n isEmpty: v.literal(true),\n }),\n v.object({\n field: v.string(),\n isNotEmpty: v.literal(true),\n }),\n]);\n\n// Full condition schema with and/or support (recursive)\nconst FieldConditionSchema: v.GenericSchema<FieldConditionObject> = v.union([\n FieldConditionSingleSchema,\n v.object({\n and: v.array(v.lazy(() => FieldConditionSchema)),\n }),\n v.object({\n or: v.array(v.lazy(() => FieldConditionSchema)),\n }),\n]) as v.GenericSchema<FieldConditionObject>;\n\nconst FieldBaseSchema = v.object({\n id: v.string(),\n label: v.string(),\n description: v.string(),\n placeholder: v.optional(v.string()),\n required: v.optional(v.boolean()),\n when: v.optional(FieldConditionSchema),\n});\n\nexport const SelectOptionSchema = v.object({\n value: v.string(),\n label: v.string(),\n});\n\nexport const InputFieldSchema = v.object({\n ...FieldBaseSchema.entries,\n type: v.literal('input'),\n inputType: v.optional(v.picklist(['text', 'date', 'url'])),\n suggestions: v.optional(v.array(v.string())),\n default: v.optional(v.string()),\n});\n\nexport const TextareaFieldSchema = v.object({\n ...FieldBaseSchema.entries,\n type: v.literal('textarea'),\n rows: v.optional(v.number()),\n default: v.optional(v.string()),\n});\n\nexport const SelectFieldSchema = v.object({\n ...FieldBaseSchema.entries,\n type: v.literal('select'),\n options: v.array(SelectOptionSchema),\n default: v.optional(v.string()),\n});\n\nexport const CheckboxFieldSchema = v.object({\n ...FieldBaseSchema.entries,\n type: v.literal('checkbox'),\n default: v.optional(v.boolean()),\n});\n\nexport const FormFieldSchema = v.union([\n InputFieldSchema,\n TextareaFieldSchema,\n SelectFieldSchema,\n CheckboxFieldSchema,\n]);\n\n// =============================================================================\n// Layout Schemas\n// =============================================================================\n\nexport const GridLayoutSchema: v.GenericSchema<GridLayout> = v.object({\n type: v.literal('grid'),\n columns: v.number(),\n fields: v.array(v.lazy(() => FieldSchema)),\n});\n\nexport const GroupLayoutSchema: v.GenericSchema<GroupLayout> = v.object({\n type: v.literal('group'),\n fields: v.array(v.lazy(() => FieldSchema)),\n});\n\nexport const RepeatableLayoutSchema: v.GenericSchema<RepeatableLayout> =\n v.object({\n type: v.literal('repeatable'),\n id: v.string(),\n label: v.string(),\n minCount: v.optional(v.number()),\n defaultCount: v.optional(v.number()),\n field: v.union([FormFieldSchema, GroupLayoutSchema]),\n });\n\nexport const FieldSchema: v.GenericSchema<Field> = v.union([\n FormFieldSchema,\n GridLayoutSchema,\n RepeatableLayoutSchema,\n GroupLayoutSchema,\n]);\n\n// =============================================================================\n// Step Schema\n// =============================================================================\n\nexport const StepSchema = v.object({\n slug: v.string(),\n title: v.string(),\n description: v.string(),\n name: v.string(),\n fields: v.array(FieldSchema),\n});\n\n// =============================================================================\n// AI Settings Schemas (Claude Agent SDK)\n// =============================================================================\n\nexport const McpServerConfigSchema = v.union([\n v.object({\n type: v.optional(v.literal('stdio')),\n command: v.string(),\n args: v.optional(v.array(v.string())),\n env: v.optional(v.record(v.string(), v.string())),\n }),\n v.object({\n type: v.literal('sse'),\n url: v.string(),\n headers: v.optional(v.record(v.string(), v.string())),\n }),\n v.object({\n type: v.literal('http'),\n url: v.string(),\n headers: v.optional(v.record(v.string(), v.string())),\n }),\n]);\n\nexport const AiSettingsSchema = v.optional(\n v.object({\n model: v.optional(v.string()),\n fallbackModel: v.optional(v.string()),\n maxThinkingTokens: v.optional(v.number()),\n maxTurns: v.optional(v.number()),\n maxBudgetUsd: v.optional(v.number()),\n allowedTools: v.optional(v.array(v.string())),\n disallowedTools: v.optional(v.array(v.string())),\n tools: v.optional(\n v.union([\n v.array(v.string()),\n v.object({\n type: v.literal('preset'),\n preset: v.literal('claude_code'),\n }),\n ]),\n ),\n permissionMode: v.optional(\n v.picklist([\n 'default',\n 'acceptEdits',\n 'bypassPermissions',\n 'plan',\n 'delegate',\n 'dontAsk',\n ]),\n ),\n allowDangerouslySkipPermissions: v.optional(v.boolean()),\n mcpServers: v.optional(v.record(v.string(), McpServerConfigSchema)),\n strictMcpConfig: v.optional(v.boolean()),\n }),\n);\n\n// =============================================================================\n// Scenario Schema\n// =============================================================================\n\nexport const ScenarioBaseSchema = v.object({\n id: v.string(),\n name: v.string(),\n steps: v.array(StepSchema),\n prompt: v.custom<\n (params: { formData: unknown; aiContext: unknown }) => string\n >((value) => typeof value === 'function'),\n aiSettings: AiSettingsSchema,\n});\n\nexport const ScenarioSchema = ScenarioBaseSchema;\n\n// =============================================================================\n// Configuration Schemas\n// =============================================================================\n\nexport const PermissionsSchema = v.object({\n allowSave: v.boolean(),\n});\n\nexport const AiModeSchema = v.optional(\n v.picklist(['stream', 'sync', 'mock']),\n 'stream',\n);\n\nexport const ConfigSchema = v.object({\n scenarios: v.array(ScenarioSchema),\n permissions: PermissionsSchema,\n hosted: v.optional(v.boolean(), false),\n ai: AiModeSchema,\n});\n\n// =============================================================================\n// Parser Functions\n// =============================================================================\n\nexport const parseConfig = (data: unknown) => {\n return v.parse(ConfigSchema, data);\n};\n\nexport const safeParseConfig = (data: unknown) => {\n return v.safeParse(ConfigSchema, data);\n};\n", "import { Hono } from 'hono';\n\nimport type { Config } from '../definitions';\n\nimport { createDocsApp } from './apps/docs';\nimport { createScenariosApp } from './apps/scenarios';\nimport { buildStepInfoMap } from './helpers/scenarios/build-step-info';\n\nexport const createApiServer = (rawConfig: Config) => {\n const app = new Hono();\n\n // In hosted mode, allowSave is always false\n const config: Config = rawConfig.hosted\n ? {\n ...rawConfig,\n permissions: { ...rawConfig.permissions, allowSave: false },\n }\n : rawConfig;\n\n // Pre-build scenario info map\n const scenarioInfoMap = new Map(\n config.scenarios.map((scenario) => [\n scenario.id,\n {\n scenario,\n stepInfoMap: buildStepInfoMap(scenario.steps),\n },\n ]),\n );\n\n // Scenarios App\n const scenariosApp = createScenariosApp(config, scenarioInfoMap);\n\n // Docs App\n const docsApp = createDocsApp(config, scenarioInfoMap);\n\n // Mount sub-apps\n app.route('/', scenariosApp);\n app.route('/', docsApp);\n\n return app;\n};\n", "import { Hono } from 'hono';\nimport { createMiddleware } from 'hono/factory';\nimport { streamSSE } from 'hono/streaming';\n\nimport type { AiContext, AiMode, Config } from '../../definitions';\nimport {\n getDocumentsForScenario,\n getFilename,\n readDocument,\n saveDocument,\n} from '../repositories/document';\nimport {\n generateDesignDocStream,\n generateMockDocStream,\n} from '../usecases/docs/generate-doc';\n\nimport type { ScenarioInfoMapEntry } from './scenarios';\n\n/**\n * Get the document generator based on AI mode\n */\nfunction getDocGenerator(\n aiMode: AiMode,\n scenario: ScenarioInfoMapEntry['scenario'],\n formData: Record<string, unknown>,\n aiContext: AiContext,\n) {\n if (aiMode === 'mock') {\n return generateMockDocStream();\n }\n\n return generateDesignDocStream({\n scenario,\n formData,\n aiContext,\n });\n}\n\ntype Variables = {\n scenarioInfo: ScenarioInfoMapEntry;\n};\n\ntype PreviewDocBody = {\n formData: Record<string, unknown>;\n aiContext: AiContext;\n};\n\ntype CreateDocBody = {\n content: string;\n formData: Record<string, unknown>;\n aiContext: AiContext;\n};\n\ntype UpdateDocBody = {\n content: string;\n formData: Record<string, unknown>;\n aiContext: AiContext;\n};\n\nconst createScenarioMiddleware = (\n scenarioInfoMap: Map<string, ScenarioInfoMapEntry>,\n) =>\n createMiddleware<{ Variables: Variables }>(async (c, next) => {\n const scenarioId = c.req.param('scenarioId');\n\n if (scenarioId == null) {\n return c.json({ error: 'Scenario ID is required' }, 400);\n }\n\n const scenarioInfo = scenarioInfoMap.get(scenarioId);\n\n if (scenarioInfo == null) {\n return c.json({ error: 'Scenario not found' }, 404);\n }\n\n c.set('scenarioInfo', scenarioInfo);\n await next();\n });\n\nexport const createDocsApp = (\n config: Config,\n scenarioInfoMap: Map<string, ScenarioInfoMapEntry>,\n) => {\n const app = new Hono<{ Variables: Variables }>();\n\n app.use(\n '/api/scenarios/:scenarioId/*',\n createScenarioMiddleware(scenarioInfoMap),\n );\n\n app.get('/api/scenarios/:scenarioId/docs', async (c) => {\n const { scenario } = c.get('scenarioInfo');\n const docs = await getDocumentsForScenario(scenario);\n\n return c.json({ docs });\n });\n\n app.post('/api/scenarios/:scenarioId/docs/preview', async (c) => {\n const { scenario } = c.get('scenarioInfo');\n\n const { formData, aiContext } = (await c.req.json()) as PreviewDocBody;\n\n const aiMode = config.ai ?? 'stream';\n const generator = getDocGenerator(aiMode, scenario, formData, aiContext);\n\n // Non-streaming mode: return JSON response directly\n if (aiMode === 'sync') {\n let fullContent = '';\n for await (const chunk of generator) {\n fullContent += chunk.text;\n }\n\n if (scenario.hooks?.onPreview != null) {\n await scenario.hooks.onPreview({\n formData,\n aiContext,\n content: fullContent,\n });\n }\n\n return c.json({ content: fullContent });\n }\n\n // Streaming mode: return SSE response\n return streamSSE(c, async (stream) => {\n let fullContent = '';\n\n for await (const chunk of generator) {\n fullContent += chunk.text;\n await stream.writeSSE({\n data: JSON.stringify({ type: 'text_delta', text: chunk.text }),\n event: 'message',\n });\n }\n\n await stream.writeSSE({\n data: JSON.stringify({ type: 'done', content: fullContent }),\n event: 'message',\n });\n\n if (scenario.hooks?.onPreview != null) {\n await scenario.hooks.onPreview({\n formData,\n aiContext,\n content: fullContent,\n });\n }\n });\n });\n\n app.post('/api/scenarios/:scenarioId/docs', async (c) => {\n const scenarioId = c.req.param('scenarioId');\n const { scenario } = c.get('scenarioInfo');\n\n if (!config.permissions.allowSave) {\n return c.json({ error: 'Save is not allowed' }, 403);\n }\n\n const { content, formData, aiContext } =\n (await c.req.json()) as CreateDocBody;\n const filename = getFilename(\n scenario,\n scenarioId,\n content,\n formData,\n aiContext,\n );\n const { outputPath } = await saveDocument({\n scenario,\n scenarioId,\n filename,\n content,\n formData,\n });\n\n if (scenario.hooks?.onSave != null) {\n await scenario.hooks.onSave({\n content,\n filename,\n outputPath,\n formData,\n aiContext,\n });\n }\n\n return c.json({ success: true, filename });\n });\n\n app.get('/api/scenarios/:scenarioId/docs/:filename', async (c) => {\n const filename = c.req.param('filename');\n const { scenario } = c.get('scenarioInfo');\n\n const result = await readDocument(scenario, filename);\n\n if (!result.success) {\n return c.json({ error: 'Document not found' }, 404);\n }\n\n return c.json({ doc: result.doc });\n });\n\n app.put('/api/scenarios/:scenarioId/docs/:filename', async (c) => {\n const scenarioId = c.req.param('scenarioId');\n const filename = c.req.param('filename');\n const { scenario } = c.get('scenarioInfo');\n\n if (!config.permissions.allowSave) {\n return c.json({ error: 'Save is not allowed' }, 403);\n }\n\n const { content, formData, aiContext } =\n (await c.req.json()) as UpdateDocBody;\n const { outputPath } = await saveDocument({\n scenario,\n scenarioId,\n filename,\n content,\n formData,\n });\n\n if (scenario.hooks?.onSave != null) {\n await scenario.hooks.onSave({\n content,\n filename,\n outputPath,\n formData,\n aiContext,\n });\n }\n\n return c.json({ success: true, filename });\n });\n\n return app;\n};\n", "import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nimport type { AiContext, Scenario } from '../../definitions';\nimport {\n type DocumentWithMetadata,\n addMetadataToContent,\n parseMetadata,\n} from '../helpers/docs/metadata';\n\nexport const getOutputDir = (scenario: Scenario): string => {\n return scenario.outputDir ?? join(process.cwd(), 'output');\n};\n\ntype ReadDocumentResult =\n | { success: true; doc: DocumentWithMetadata }\n | { success: false; error: 'not_found' | 'scenario_mismatch' };\n\nexport const readDocument = async (\n scenario: Scenario,\n filename: string,\n): Promise<ReadDocumentResult> => {\n const outputDir = getOutputDir(scenario);\n const filePath = join(outputDir, filename);\n\n try {\n const rawContent = await readFile(filePath, 'utf-8');\n const { metadata, content } = parseMetadata(rawContent);\n\n if (metadata?.scenarioId !== scenario.id) {\n return { success: false, error: 'scenario_mismatch' };\n }\n\n return {\n success: true,\n doc: { filename, content, metadata },\n };\n } catch {\n return { success: false, error: 'not_found' };\n }\n};\n\nexport const getDocumentsForScenario = async (\n scenario: Scenario,\n): Promise<DocumentWithMetadata[]> => {\n const outputDir = getOutputDir(scenario);\n\n try {\n const files = await readdir(outputDir);\n const mdFiles = files.filter((file) => file.endsWith('.md'));\n\n const docs = await Promise.all(\n mdFiles.map(async (filename) => {\n const result = await readDocument(scenario, filename);\n return result.success ? result.doc : null;\n }),\n );\n\n return docs.filter((doc) => doc != null);\n } catch {\n return [];\n }\n};\n\ntype SaveDocumentOptions = {\n scenario: Scenario;\n scenarioId: string;\n filename: string;\n content: string;\n formData: Record<string, unknown>;\n};\n\nexport const saveDocument = async ({\n scenario,\n scenarioId,\n filename,\n content,\n formData,\n}: SaveDocumentOptions): Promise<{ outputPath: string }> => {\n const outputDir = getOutputDir(scenario);\n const outputPath = join(outputDir, filename);\n\n const contentWithMetadata = addMetadataToContent(content, {\n scenarioId,\n formData,\n });\n\n await mkdir(outputDir, { recursive: true });\n await writeFile(outputPath, contentWithMetadata, 'utf-8');\n\n return { outputPath };\n};\n\nexport const getFilename = (\n scenario: Scenario,\n scenarioId: string,\n content: string,\n formData: Record<string, unknown>,\n aiContext: AiContext,\n): string => {\n const timestamp = new Date().toISOString().replace(/[:.]/g, '-');\n\n const filename = scenario.filename;\n if (filename != null) {\n return typeof filename === 'function'\n ? filename({\n scenarioId,\n timestamp,\n content,\n formData,\n aiContext,\n })\n : filename;\n }\n\n return `design-doc-${scenarioId}-${timestamp}.md`;\n};\n", "export type DocumentMetadata = {\n scenarioId: string;\n formData: Record<string, unknown>;\n};\n\nexport type DocumentWithMetadata = {\n filename: string;\n content: string;\n metadata: DocumentMetadata | null;\n};\n\nconst METADATA_START = '<!-- design-docs-metadata';\nconst METADATA_END = '-->';\n\nexport const serializeMetadata = (metadata: DocumentMetadata): string => {\n return `${METADATA_START}\\n${JSON.stringify(metadata, null, 2)}\\n${METADATA_END}`;\n};\n\nexport const parseMetadata = (\n content: string,\n): { metadata: DocumentMetadata | null; content: string } => {\n const metadataStartIndex = content.lastIndexOf(METADATA_START);\n if (metadataStartIndex === -1) {\n return { metadata: null, content };\n }\n\n const metadataEndIndex = content.indexOf(METADATA_END, metadataStartIndex);\n if (metadataEndIndex === -1) {\n return { metadata: null, content };\n }\n\n try {\n const metadataJson = content\n .slice(metadataStartIndex + METADATA_START.length, metadataEndIndex)\n .trim();\n const metadata = JSON.parse(metadataJson) as DocumentMetadata;\n const cleanContent = content.slice(0, metadataStartIndex).trim();\n return { metadata, content: cleanContent };\n } catch {\n return { metadata: null, content };\n }\n};\n\nexport const addMetadataToContent = (\n content: string,\n metadata: DocumentMetadata,\n): string => {\n return `${content}\\n\\n${serializeMetadata(metadata)}`;\n};\n", "import {\n type Options,\n type SDKResultMessage,\n query,\n} from '@anthropic-ai/claude-agent-sdk';\n\nimport {\n type AiContext,\n DEFAULT_MOCK_RESPONSE,\n type Scenario,\n} from '../../../definitions';\n\ntype GenerateDesignDocParams = {\n scenario: Scenario;\n formData: Record<string, unknown>;\n aiContext: AiContext;\n};\n\nexport const generateDesignDoc = async ({\n scenario,\n formData,\n aiContext,\n}: GenerateDesignDocParams): Promise<string> => {\n const prompt = scenario.prompt({ formData, aiContext });\n\n let message: SDKResultMessage | null = null;\n\n for await (const msg of query({\n prompt,\n options: scenario.aiSettings as Options,\n })) {\n if (msg.type === 'result' && msg.subtype === 'success') {\n message = msg;\n }\n }\n\n if (message == null) {\n throw new Error('Query failed');\n }\n\n return message.result;\n};\n\ntype StreamChunk = {\n type: 'text_delta';\n text: string;\n};\n\nexport async function* generateDesignDocStream({\n scenario,\n formData,\n aiContext,\n}: GenerateDesignDocParams): AsyncGenerator<StreamChunk> {\n const prompt = scenario.prompt({ formData, aiContext });\n\n for await (const msg of query({\n prompt,\n options: {\n ...(scenario.aiSettings as Options),\n includePartialMessages: true,\n },\n })) {\n if (msg.type === 'stream_event') {\n const event = msg.event as {\n type: string;\n delta?: { type: string; text?: string };\n };\n if (\n event.type === 'content_block_delta' &&\n event.delta?.type === 'text_delta' &&\n event.delta.text != null\n ) {\n yield { type: 'text_delta', text: event.delta.text };\n }\n }\n }\n}\n\n/**\n * Generate a mock response when AI is disabled\n */\nexport async function* generateMockDocStream(): AsyncGenerator<StreamChunk> {\n yield { type: 'text_delta', text: DEFAULT_MOCK_RESPONSE };\n}\n", "import { Hono } from 'hono';\n\nimport type { Config, Scenario } from '../../definitions';\nimport type { buildStepInfoMap } from '../helpers/scenarios/build-step-info';\n\nexport type ScenarioInfo = {\n stepInfoMap: ReturnType<typeof buildStepInfoMap>;\n};\n\nexport type ScenarioInfoMapEntry = {\n scenario: Scenario;\n stepInfoMap: ReturnType<typeof buildStepInfoMap>;\n};\n\nexport const createScenariosApp = (\n config: Config,\n scenarioInfoMap: Map<string, ScenarioInfoMapEntry>,\n) => {\n const app = new Hono();\n\n // Get all scenarios\n app.get('/api/scenarios', (c) => {\n return c.json({\n scenarios: config.scenarios,\n });\n });\n\n // Get single scenario\n app.get('/api/scenarios/:scenarioId', (c) => {\n const scenarioId = c.req.param('scenarioId');\n const scenarioInfo = scenarioInfoMap.get(scenarioId);\n\n if (scenarioInfo == null) {\n return c.json({ error: 'Scenario not found' }, 404);\n }\n\n return c.json({\n scenario: scenarioInfo.scenario,\n permissions: config.permissions,\n });\n });\n\n return app;\n};\n", "import { type Field, type Step, isLayoutField } from '../../../definitions';\n\nexport type FieldInfo = {\n id: string;\n label: string;\n description: string;\n};\n\nexport type StepInfo = {\n name: string;\n title: string;\n description: string;\n fields: FieldInfo[];\n};\n\n/**\n * Get fields from a layout field (handles grid, repeatable, group)\n */\nconst getLayoutFields = (field: Field): Field[] => {\n if (field.type === 'grid' || field.type === 'group') {\n return field.fields;\n }\n if (field.type === 'repeatable') {\n return [field.field];\n }\n return [];\n};\n\nexport const extractFieldInfos = (fields: Field[]): FieldInfo[] => {\n const result: FieldInfo[] = [];\n for (const field of fields) {\n if (isLayoutField(field)) {\n result.push(...extractFieldInfos(getLayoutFields(field)));\n } else {\n result.push({\n id: field.id,\n label: field.label,\n description: field.description,\n });\n }\n }\n return result;\n};\n\nexport const buildStepInfoMap = (steps: Step[]): Map<string, StepInfo> => {\n const stepMap = new Map<string, StepInfo>(\n steps.map((step) => [\n step.name,\n {\n name: step.name,\n title: step.title,\n description: step.description,\n fields: extractFieldInfos(step.fields),\n },\n ]),\n );\n\n return stepMap;\n};\n"],
5
5
  "mappings": "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",
6
6
  "names": ["defineCommand", "fs", "path", "defineCommand", "consola", "Hono", "Hono", "Hono", "consola", "defineCommand", "defineCommand"]
7
7
  }
@@ -1 +1 @@
1
- import{j as o}from"./index-D9YrThAm.js";const e=r=>o.jsx("svg",{"aria-hidden":"true",width:"16",height:"16",viewBox:"0 0 16 16",fill:"none",xmlns:"http://www.w3.org/2000/svg",...r,children:o.jsx("path",{d:"M6 4L10 8L6 12",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})});export{e as C};
1
+ import{j as o}from"./index-Dvs5Nn8L.js";const e=r=>o.jsx("svg",{"aria-hidden":"true",width:"16",height:"16",viewBox:"0 0 16 16",fill:"none",xmlns:"http://www.w3.org/2000/svg",...r,children:o.jsx("path",{d:"M6 4L10 8L6 12",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})});export{e as C};
@@ -1 +1 @@
1
- import{j as o}from"./index-D9YrThAm.js";const e=t=>o.jsx("svg",{"aria-hidden":"true",width:"16",height:"16",viewBox:"0 0 16 16",fill:"none",xmlns:"http://www.w3.org/2000/svg",...t,children:o.jsx("path",{d:"M9 1H4C3.44772 1 3 1.44772 3 2V14C3 14.5523 3.44772 15 4 15H12C12.5523 15 13 14.5523 13 14V5M9 1L13 5M9 1V5H13M5 8H11M5 11H11",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})});export{e as D};
1
+ import{j as o}from"./index-Dvs5Nn8L.js";const e=t=>o.jsx("svg",{"aria-hidden":"true",width:"16",height:"16",viewBox:"0 0 16 16",fill:"none",xmlns:"http://www.w3.org/2000/svg",...t,children:o.jsx("path",{d:"M9 1H4C3.44772 1 3 1.44772 3 2V14C3 14.5523 3.44772 15 4 15H12C12.5523 15 13 14.5523 13 14V5M9 1L13 5M9 1V5H13M5 8H11M5 11H11",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})});export{e as D};
@@ -0,0 +1 @@
1
+ import{S as D,h as m,M as N,N as E,O as L,P,Q as B,U as H,V as Q,n as U,r as d,l as A,j as r,W,X as _,u as z,Y as V,L as G}from"./index-Dvs5Nn8L.js";var Z=class extends D{constructor(t,e){super(),this.options=e,this.#s=t,this.#i=null,this.bindMethods(),this.setOptions(e)}#s;#t=void 0;#f=void 0;#e=void 0;#n;#u;#i;#p;#c;#l;#o;#a;#r;#d=new Set;bindMethods(){this.refetch=this.refetch.bind(this)}onSubscribe(){this.listeners.size===1&&(this.#t.addObserver(this),I(this.#t,this.options)?this.#h():this.updateResult(),this.#y())}onUnsubscribe(){this.hasListeners()||this.destroy()}shouldFetchOnReconnect(){return C(this.#t,this.options,this.options.refetchOnReconnect)}shouldFetchOnWindowFocus(){return C(this.#t,this.options,this.options.refetchOnWindowFocus)}destroy(){this.listeners=new Set,this.#x(),this.#m(),this.#t.removeObserver(this)}setOptions(t,e){const s=this.options,i=this.#t;if(this.options=this.#s.defaultQueryOptions(t),this.options.enabled!==void 0&&typeof this.options.enabled!="boolean")throw new Error("Expected enabled to be a boolean");this.#C(),this.#t.setOptions(this.options),s._defaulted&&!m(this.options,s)&&this.#s.getQueryCache().notify({type:"observerOptionsUpdated",query:this.#t,observer:this});const a=this.hasListeners();a&&M(this.#t,i,this.options,s)&&this.#h(),this.updateResult(e),a&&(this.#t!==i||this.options.enabled!==s.enabled||this.options.staleTime!==s.staleTime)&&this.#g();const u=this.#v();a&&(this.#t!==i||this.options.enabled!==s.enabled||u!==this.#r)&&this.#b(u)}getOptimisticResult(t){const e=this.#s.getQueryCache().build(this.#s,t),s=this.createResult(e,t);return K(this,s)&&(this.#e=s,this.#u=this.options,this.#n=this.#t.state),s}getCurrentResult(){return this.#e}trackResult(t,e){const s={};return Object.keys(t).forEach(i=>{Object.defineProperty(s,i,{configurable:!1,enumerable:!0,get:()=>(this.trackProp(i),e?.(i),t[i])})}),s}trackProp(t){this.#d.add(t)}getCurrentQuery(){return this.#t}refetch({...t}={}){return this.fetch({...t})}fetchOptimistic(t){const e=this.#s.defaultQueryOptions(t),s=this.#s.getQueryCache().build(this.#s,e);return s.isFetchingOptimistic=!0,s.fetch().then(()=>this.createResult(s,e))}fetch(t){return this.#h({...t,cancelRefetch:t.cancelRefetch??!0}).then(()=>(this.updateResult(),this.#e))}#h(t){this.#C();let e=this.#t.fetch(this.options,t);return t?.throwOnError||(e=e.catch(N)),e}#g(){if(this.#x(),E||this.#e.isStale||!L(this.options.staleTime))return;const e=P(this.#e.dataUpdatedAt,this.options.staleTime)+1;this.#o=setTimeout(()=>{this.#e.isStale||this.updateResult()},e)}#v(){return(typeof this.options.refetchInterval=="function"?this.options.refetchInterval(this.#t):this.options.refetchInterval)??!1}#b(t){this.#m(),this.#r=t,!(E||this.options.enabled===!1||!L(this.#r)||this.#r===0)&&(this.#a=setInterval(()=>{(this.options.refetchIntervalInBackground||B.isFocused())&&this.#h()},this.#r))}#y(){this.#g(),this.#b(this.#v())}#x(){this.#o&&(clearTimeout(this.#o),this.#o=void 0)}#m(){this.#a&&(clearInterval(this.#a),this.#a=void 0)}createResult(t,e){const s=this.#t,i=this.options,a=this.#e,u=this.#n,o=this.#u,f=t!==s?t.state:this.#f,{state:p}=t;let n={...p},w=!1,l;if(e._optimisticResults){const c=this.hasListeners(),x=!c&&I(t,e),T=c&&M(t,s,e,i);(x||T)&&(n={...n,...H(p.data,t.options)}),e._optimisticResults==="isRestoring"&&(n.fetchStatus="idle")}let{error:O,errorUpdatedAt:j,status:g}=n;if(e.select&&n.data!==void 0)if(a&&n.data===u?.data&&e.select===this.#p)l=this.#c;else try{this.#p=e.select,l=e.select(n.data),l=Q(a?.data,l,e),this.#c=l,this.#i=null}catch(c){this.#i=c}else l=n.data;if(e.placeholderData!==void 0&&l===void 0&&g==="pending"){let c;if(a?.isPlaceholderData&&e.placeholderData===o?.placeholderData)c=a.data;else if(c=typeof e.placeholderData=="function"?e.placeholderData(this.#l?.state.data,this.#l):e.placeholderData,e.select&&c!==void 0)try{c=e.select(c),this.#i=null}catch(x){this.#i=x}c!==void 0&&(g="success",l=Q(a?.data,c,e),w=!0)}this.#i&&(O=this.#i,l=this.#c,j=Date.now(),g="error");const v=n.fetchStatus==="fetching",b=g==="pending",y=g==="error",S=b&&v,k=l!==void 0;return{status:g,fetchStatus:n.fetchStatus,isPending:b,isSuccess:g==="success",isError:y,isInitialLoading:S,isLoading:S,data:l,dataUpdatedAt:n.dataUpdatedAt,error:O,errorUpdatedAt:j,failureCount:n.fetchFailureCount,failureReason:n.fetchFailureReason,errorUpdateCount:n.errorUpdateCount,isFetched:n.dataUpdateCount>0||n.errorUpdateCount>0,isFetchedAfterMount:n.dataUpdateCount>f.dataUpdateCount||n.errorUpdateCount>f.errorUpdateCount,isFetching:v,isRefetching:v&&!b,isLoadingError:y&&!k,isPaused:n.fetchStatus==="paused",isPlaceholderData:w,isRefetchError:y&&k,isStale:R(t,e),refetch:this.refetch}}updateResult(t){const e=this.#e,s=this.createResult(this.#t,this.options);if(this.#n=this.#t.state,this.#u=this.options,this.#n.data!==void 0&&(this.#l=this.#t),m(s,e))return;this.#e=s;const i={},a=()=>{if(!e)return!0;const{notifyOnChangeProps:u}=this.options,o=typeof u=="function"?u():u;if(o==="all"||!o&&!this.#d.size)return!0;const h=new Set(o??this.#d);return this.options.throwOnError&&h.add("error"),Object.keys(this.#e).some(f=>{const p=f;return this.#e[p]!==e[p]&&h.has(p)})};t?.listeners!==!1&&a()&&(i.listeners=!0),this.#R({...i,...t})}#C(){const t=this.#s.getQueryCache().build(this.#s,this.options);if(t===this.#t)return;const e=this.#t;this.#t=t,this.#f=t.state,this.hasListeners()&&(e?.removeObserver(this),t.addObserver(this))}onQueryUpdate(){this.updateResult(),this.hasListeners()&&this.#y()}#R(t){U.batch(()=>{t.listeners&&this.listeners.forEach(e=>{e(this.#e)}),this.#s.getQueryCache().notify({query:this.#t,type:"observerResultsUpdated"})})}};function $(t,e){return e.enabled!==!1&&t.state.data===void 0&&!(t.state.status==="error"&&e.retryOnMount===!1)}function I(t,e){return $(t,e)||t.state.data!==void 0&&C(t,e,e.refetchOnMount)}function C(t,e,s){if(e.enabled!==!1){const i=typeof s=="function"?s(t):s;return i==="always"||i!==!1&&R(t,e)}return!1}function M(t,e,s,i){return(t!==e||i.enabled===!1)&&(!s.suspense||t.state.status!=="error")&&R(t,s)}function R(t,e){return e.enabled!==!1&&t.isStaleByTime(e.staleTime)}function K(t,e){return!m(t.getCurrentResult(),e)}var F=d.createContext(!1),X=()=>d.useContext(F);F.Provider;function Y(){let t=!1;return{clearReset:()=>{t=!1},reset:()=>{t=!0},isReset:()=>t}}var q=d.createContext(Y()),J=()=>d.useContext(q);function tt(t,e){return typeof t=="function"?t(...e):!!t}function bt(){}var et=(t,e)=>{(t.suspense||t.throwOnError)&&(e.isReset()||(t.retryOnMount=!1))},st=t=>{d.useEffect(()=>{t.clearReset()},[t])},it=({result:t,errorResetBoundary:e,throwOnError:s,query:i})=>t.isError&&!e.isReset()&&!t.isFetching&&i&&tt(s,[t.error,i]),rt=(t,e)=>e.state.data===void 0,nt=t=>{t.suspense&&typeof t.staleTime!="number"&&(t.staleTime=1e3)},ot=(t,e)=>t?.suspense&&e.isPending,at=(t,e,s)=>e.fetchOptimistic(t).catch(()=>{s.clearReset()});function ht(t,e,s){const i=A(),a=X(),u=J(),o=i.defaultQueryOptions(t);o._optimisticResults=a?"isRestoring":"optimistic",nt(o),et(o,u),st(u);const[h]=d.useState(()=>new e(i,o)),f=h.getOptimisticResult(o);if(d.useSyncExternalStore(d.useCallback(p=>{const n=a?()=>{}:h.subscribe(U.batchCalls(p));return h.updateResult(),n},[h,a]),()=>h.getCurrentResult(),()=>h.getCurrentResult()),d.useEffect(()=>{h.setOptions(o,{listeners:!1})},[o,h]),ot(o,f))throw at(o,h,u);if(it({result:f,errorResetBoundary:u,throwOnError:o.throwOnError,query:i.getQueryCache().get(o.queryHash)}))throw f.error;return o.notifyOnChangeProps?f:h.trackResult(f)}function yt(t,e){return ht({...t,enabled:!0,suspense:!0,throwOnError:rt,placeholderData:void 0},Z)}const ut=()=>r.jsx("svg",{"aria-hidden":"true",width:"16",height:"16",viewBox:"0 0 16 16",fill:"currentColor",xmlns:"http://www.w3.org/2000/svg",children:r.jsx("path",{d:"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"})}),ct=()=>r.jsx("svg",{"aria-hidden":"true",width:"16",height:"16",viewBox:"0 0 16 16",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:r.jsx("path",{d:"M4 4L12 12M12 4L4 12",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})}),lt=()=>r.jsxs("svg",{"aria-hidden":"true",width:"16",height:"16",viewBox:"0 0 16 16",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[r.jsx("path",{d:"M0.666992 8.00004C0.666992 8.00004 3.33366 2.66671 8.00033 2.66671C12.667 2.66671 15.3337 8.00004 15.3337 8.00004C15.3337 8.00004 12.667 13.3334 8.00033 13.3334C3.33366 13.3334 0.666992 8.00004 0.666992 8.00004Z",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"}),r.jsx("path",{d:"M8.00033 10C9.10489 10 10.0003 9.10461 10.0003 8.00004C10.0003 6.89547 9.10489 6.00004 8.00033 6.00004C6.89576 6.00004 6.00033 6.89547 6.00033 8.00004C6.00033 9.10461 6.89576 10 8.00033 10Z",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})]}),dt=()=>r.jsx("svg",{"aria-hidden":"true",width:"16",height:"16",viewBox:"0 0 16 16",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:r.jsx("path",{d:"M2 4H14M2 8H14M2 12H14",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})}),ft=({locale:t,isActive:e,onClick:s})=>{const i=d.useCallback(()=>{s(t)},[t,s]);return r.jsx("button",{className:`px-2 py-1 text-xs rounded transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-300 focus-visible:ring-offset-2 ${e?"bg-indigo-500 text-white":"bg-gray-100 text-gray-600 hover:bg-gray-200"}`,type:"button",onClick:i,children:t.toUpperCase()})},pt=({currentLocale:t})=>{const e=d.useCallback(s=>{W(s),window.location.reload()},[]);return r.jsx("div",{className:"flex gap-1",children:Object.keys(_).map(s=>r.jsx(ft,{locale:s,isActive:t===s,onClick:e},s))})},xt=({isMenuOpen:t,onToggleMenu:e,onOpenPreview:s})=>{const{_:i}=z(),a=V();return r.jsxs("header",{className:"flex items-center justify-between h-14 px-4 xl:px-6 bg-white rounded-2xl border border-gray-100",children:[r.jsxs("div",{className:"flex items-center gap-2",children:[e!=null&&r.jsx("button",{"aria-label":i(t?{id:"vcpc5o"}:{id:"GSr0rF"}),className:"xl:hidden p-2 rounded-lg text-gray-500 hover:text-gray-700 hover:bg-gray-100 transition-colors cursor-pointer *:size-5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-300 focus-visible:ring-offset-2",type:"button",onClick:e,children:t?r.jsx(ct,{}):r.jsx(dt,{})}),r.jsx(G,{to:"/scenarios",className:"text-lg font-bold text-gray-900 hover:text-indigo-600 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-300 focus-visible:ring-offset-2 rounded",children:"Spec Snake Beta"})]}),r.jsxs("div",{className:"flex items-center gap-2",children:[r.jsx(pt,{currentLocale:a}),s!=null&&r.jsx("button",{"aria-label":i({id:"fsyAH8"}),className:"lg:hidden p-2 rounded-lg text-gray-500 hover:text-gray-700 hover:bg-gray-100 transition-colors cursor-pointer *:size-5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-300 focus-visible:ring-offset-2",type:"button",onClick:s,children:r.jsx(lt,{})}),r.jsxs("div",{className:"relative",children:[r.jsx("a",{href:"https://github.com/cut0/spec-snake",target:"_blank",rel:"noopener noreferrer","aria-label":"GitHub",className:"p-2 rounded-lg text-gray-700 hover:bg-gray-100 transition-colors *:size-5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-300 focus-visible:ring-offset-2 block",children:r.jsx(ut,{})}),r.jsx("div",{className:"absolute top-full right-1/2 translate-x-1/2 mt-1 whitespace-nowrap",children:r.jsxs("div",{className:"relative bg-gray-800 text-white text-xs px-2 py-1 rounded-md",children:[r.jsx("div",{className:"absolute -top-1 left-1/2 -translate-x-1/2 w-2 h-2 bg-gray-800 rotate-45"}),i({id:"hD+bsw"})]})})]})]})]})};export{ct as C,lt as E,xt as H,bt as n,tt as s,yt as u};
@@ -1 +1 @@
1
- import{j as o}from"./index-D9YrThAm.js";const e=r=>o.jsx("svg",{"aria-hidden":"true",width:"16",height:"16",viewBox:"0 0 16 16",fill:"none",xmlns:"http://www.w3.org/2000/svg",...r,children:o.jsx("path",{d:"M8 3V13M3 8H13",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})});export{e as P};
1
+ import{j as o}from"./index-Dvs5Nn8L.js";const e=r=>o.jsx("svg",{"aria-hidden":"true",width:"16",height:"16",viewBox:"0 0 16 16",fill:"none",xmlns:"http://www.w3.org/2000/svg",...r,children:o.jsx("path",{d:"M8 3V13M3 8H13",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})});export{e as P};
@@ -1 +1 @@
1
- import{u as E,c as $,q as B,d as U,e as _,a as W,r as t,f as A,j as o}from"./index-D9YrThAm.js";import{u as Y,H as Z}from"./Header-BjC1hR8m.js";import{u as ee,a as se,b as te,c as oe,d as ae,g as ne,f as ie,M as re,P as le,S as ce,e as ue,h as de,i as me,j as pe}from"./useStepFormStore-5Xw9vaEg.js";import"./PlusIcon-D0whcAeM.js";const ge=async e=>{if(!(await fetch(`/api/scenarios/${e.scenarioId}/docs`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({content:e.content,formData:e.formData,aiContext:e.aiContext})})).ok)throw new Error("Failed to submit");await B.invalidateQueries({queryKey:U(e.scenarioId).queryKey})},fe=()=>{const{_:e}=E(),l=$(d=>d.showSnackbar);return ee({mutationFn:ge,onSuccess:()=>{l(e({id:"d61rJy"}),"success")},onError:()=>{l(e({id:"Vw8l6h"}),"error")}})},Se=()=>{const{_:e}=E(),{stepIndex:l,prevStep:d,step:i,nextStep:b}=_.useLoaderData(),{scenarioId:a}=_.useParams(),{data:q}=Y(W(a)),{scenario:v,permissions:w}=q,{steps:n}=v,{formValues:c,setFormError:h}=se(),{previewContent:m,setPreviewContent:k}=te(),{navStatuses:D,updateNavStatus:r}=oe(),p=fe(),g=ae(),{showSnackbar:C}=$(),[Q,P]=t.useState(!1),[j,y]=t.useState(!1),[V,H]=t.useState(!1),N=t.useCallback(()=>{if(!p.isPending){if(m==null){C(e({id:"afF891"}),"error");return}p.mutate({scenarioId:a,content:m,formData:c,aiContext:A(n)})}},[p,m,a,c,n,C,e]),x=t.useCallback(()=>{if(g.isPending)return;let s=!1;for(const S of n){const u=S.name,X=c[u],T=ne(S,X)==="error";h(u,T),r(u),T&&(s=!0)}if(s){C(e({id:"T6PvRl"}),"error");return}const f={};for(const S of n){const u=S.name;f[u]=ie(S,c[u])}g.mutate({scenarioId:a,formData:f,aiContext:A(n)})},[g,n,c,a,h,r,C,e]),M=t.useCallback(s=>{k(s.target.value)},[k]),R=t.useCallback(()=>{P(!0)},[]),G=t.useCallback(()=>{P(!1)},[]),J=t.useCallback(()=>{y(s=>!s)},[]),K=t.useCallback(()=>{y(!1)},[]),L=t.useCallback(()=>{H(s=>!s)},[]),O=t.useMemo(()=>n.map((s,f)=>({to:`/scenarios/${a}/docs/new?step=${s.slug}`,title:s.title,description:s.description,step:f+1,sectionName:s.name,status:D[s.name]??"default",selected:f===l})),[n,a,D,l]),I=t.useCallback(()=>{r(i.name),requestAnimationFrame(()=>{document.querySelector('[role="tab"][aria-selected="true"]')?.focus()})},[i.name,r]),z=t.useMemo(()=>d!=null?{to:`/scenarios/${a}/docs/new?step=${d.slug}`,label:e({id:"iH8pgl"}),onClick:()=>r(i.name)}:void 0,[d,a,i.name,r,e]),F=t.useMemo(()=>b!=null?{to:`/scenarios/${a}/docs/new?step=${b.slug}`,label:e({id:"hXzOVo"}),onClick:()=>r(i.name)}:void 0,[b,a,i.name,r,e]);return o.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-[1fr_1fr] xl:grid-cols-[auto_1fr_1fr] grid-rows-[auto_1fr] gap-6 min-h-screen bg-gradient-to-br from-indigo-50 via-purple-50 to-pink-50 p-8",children:[o.jsx(re,{scenarioName:v.name,isOpen:j,items:O,onClose:K,onItemClick:I}),o.jsx("div",{className:"col-span-1 xl:col-span-2",children:o.jsx(Z,{isMenuOpen:j,onOpenPreview:R,onToggleMenu:J})}),o.jsx("aside",{className:"hidden lg:flex row-span-2 min-w-0 bg-white rounded-2xl border border-gray-100 flex-col overflow-hidden sticky top-8 h-[calc(100vh-64px)] self-start",children:o.jsx(le,{allowSave:w.allowSave,formData:c,isGenerating:g.isPending,isSubmitting:p.isPending,previewContent:m,steps:n,onContentChange:M,onCreate:N,onPreview:x})}),o.jsx(ce,{scenarioName:v.name,isCollapsed:V,items:O,onItemClick:I,onToggleCollapse:L}),o.jsxs("main",{className:"flex flex-col gap-6 min-w-0",children:[o.jsx("div",{className:"xl:hidden",children:o.jsx(ue,{current:l+1,description:i.description,title:i.title,total:n.length})}),o.jsx(de,{step:i},i.slug),o.jsx(me,{prev:z,next:F,onSubmit:F==null?x:void 0})]}),o.jsx(pe,{allowSave:w.allowSave,formData:c,isGenerating:g.isPending,isOpen:Q,isSubmitting:p.isPending,previewContent:m,steps:n,onClose:G,onContentChange:M,onCreate:N,onPreview:x})]})},we=Se;export{we as component};
1
+ import{u as E,c as $,q as B,d as U,e as _,a as W,r as t,f as A,j as o}from"./index-Dvs5Nn8L.js";import{u as Y,H as Z}from"./Header-BBhD0sYd.js";import{u as ee,a as se,b as te,c as oe,d as ae,g as ne,f as ie,M as re,P as le,S as ce,e as ue,h as de,i as me,j as pe}from"./useStepFormStore-CtbSQu3G.js";import"./PlusIcon-edwI8dET.js";const ge=async e=>{if(!(await fetch(`/api/scenarios/${e.scenarioId}/docs`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({content:e.content,formData:e.formData,aiContext:e.aiContext})})).ok)throw new Error("Failed to submit");await B.invalidateQueries({queryKey:U(e.scenarioId).queryKey})},fe=()=>{const{_:e}=E(),l=$(d=>d.showSnackbar);return ee({mutationFn:ge,onSuccess:()=>{l(e({id:"d61rJy"}),"success")},onError:()=>{l(e({id:"Vw8l6h"}),"error")}})},Se=()=>{const{_:e}=E(),{stepIndex:l,prevStep:d,step:i,nextStep:b}=_.useLoaderData(),{scenarioId:a}=_.useParams(),{data:q}=Y(W(a)),{scenario:v,permissions:w}=q,{steps:n}=v,{formValues:c,setFormError:h}=se(),{previewContent:m,setPreviewContent:k}=te(),{navStatuses:D,updateNavStatus:r}=oe(),p=fe(),g=ae(),{showSnackbar:C}=$(),[Q,P]=t.useState(!1),[j,y]=t.useState(!1),[V,H]=t.useState(!1),N=t.useCallback(()=>{if(!p.isPending){if(m==null){C(e({id:"afF891"}),"error");return}p.mutate({scenarioId:a,content:m,formData:c,aiContext:A(n)})}},[p,m,a,c,n,C,e]),x=t.useCallback(()=>{if(g.isPending)return;let s=!1;for(const S of n){const u=S.name,X=c[u],T=ne(S,X)==="error";h(u,T),r(u),T&&(s=!0)}if(s){C(e({id:"T6PvRl"}),"error");return}const f={};for(const S of n){const u=S.name;f[u]=ie(S,c[u])}g.mutate({scenarioId:a,formData:f,aiContext:A(n)})},[g,n,c,a,h,r,C,e]),M=t.useCallback(s=>{k(s.target.value)},[k]),R=t.useCallback(()=>{P(!0)},[]),G=t.useCallback(()=>{P(!1)},[]),J=t.useCallback(()=>{y(s=>!s)},[]),K=t.useCallback(()=>{y(!1)},[]),L=t.useCallback(()=>{H(s=>!s)},[]),O=t.useMemo(()=>n.map((s,f)=>({to:`/scenarios/${a}/docs/new?step=${s.slug}`,title:s.title,description:s.description,step:f+1,sectionName:s.name,status:D[s.name]??"default",selected:f===l})),[n,a,D,l]),I=t.useCallback(()=>{r(i.name),requestAnimationFrame(()=>{document.querySelector('[role="tab"][aria-selected="true"]')?.focus()})},[i.name,r]),z=t.useMemo(()=>d!=null?{to:`/scenarios/${a}/docs/new?step=${d.slug}`,label:e({id:"iH8pgl"}),onClick:()=>r(i.name)}:void 0,[d,a,i.name,r,e]),F=t.useMemo(()=>b!=null?{to:`/scenarios/${a}/docs/new?step=${b.slug}`,label:e({id:"hXzOVo"}),onClick:()=>r(i.name)}:void 0,[b,a,i.name,r,e]);return o.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-[1fr_1fr] xl:grid-cols-[auto_1fr_1fr] grid-rows-[auto_1fr] gap-6 min-h-screen bg-gradient-to-br from-indigo-50 via-purple-50 to-pink-50 p-8",children:[o.jsx(re,{scenarioName:v.name,isOpen:j,items:O,onClose:K,onItemClick:I}),o.jsx("div",{className:"col-span-1 xl:col-span-2",children:o.jsx(Z,{isMenuOpen:j,onOpenPreview:R,onToggleMenu:J})}),o.jsx("aside",{className:"hidden lg:flex row-span-2 min-w-0 bg-white rounded-2xl border border-gray-100 flex-col overflow-hidden sticky top-8 h-[calc(100vh-64px)] self-start",children:o.jsx(le,{allowSave:w.allowSave,formData:c,isGenerating:g.isPending,isSubmitting:p.isPending,previewContent:m,steps:n,onContentChange:M,onCreate:N,onPreview:x})}),o.jsx(ce,{scenarioName:v.name,isCollapsed:V,items:O,onItemClick:I,onToggleCollapse:L}),o.jsxs("main",{className:"flex flex-col gap-6 min-w-0",children:[o.jsx("div",{className:"xl:hidden",children:o.jsx(ue,{current:l+1,description:i.description,title:i.title,total:n.length})}),o.jsx(de,{step:i},i.slug),o.jsx(me,{prev:z,next:F,onSubmit:F==null?x:void 0})]}),o.jsx(pe,{allowSave:w.allowSave,formData:c,isGenerating:g.isPending,isOpen:Q,isSubmitting:p.isPending,previewContent:m,steps:n,onClose:G,onContentChange:M,onCreate:N,onPreview:x})]})},we=Se;export{we as component};