clawfire 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +10 -10
  2. package/dist/cli.js +76 -16
  3. package/dist/codegen.cjs.map +1 -1
  4. package/dist/codegen.d.cts +1 -1
  5. package/dist/codegen.d.ts +1 -1
  6. package/dist/codegen.js.map +1 -1
  7. package/dist/{dev-server-5ATZVQJT.js → dev-server-GD445Q6F.js} +247 -6
  8. package/dist/dev.cjs +247 -6
  9. package/dist/dev.cjs.map +1 -1
  10. package/dist/dev.js +247 -6
  11. package/dist/dev.js.map +1 -1
  12. package/dist/{discover-DYNqz_ym.d.cts → discover-8p9Mujyt.d.cts} +3 -3
  13. package/dist/{discover-DYNqz_ym.d.ts → discover-8p9Mujyt.d.ts} +3 -3
  14. package/dist/functions.cjs.map +1 -1
  15. package/dist/functions.d.cts +1 -1
  16. package/dist/functions.d.ts +1 -1
  17. package/dist/functions.js.map +1 -1
  18. package/package.json +1 -1
  19. package/templates/CLAUDE.md +22 -19
  20. package/templates/functions/index.ts +3 -3
  21. package/templates/starter/.claude/skills/clawfire-api/SKILL.md +8 -8
  22. package/templates/starter/.claude/skills/clawfire-diagnose/SKILL.md +6 -6
  23. package/templates/starter/.claude/skills/clawfire-model/SKILL.md +2 -2
  24. package/templates/starter/CLAUDE.md +33 -31
  25. package/templates/starter/app/pages/index.html +7 -6
  26. package/templates/starter/functions/index.ts +52 -0
  27. package/templates/starter/functions/package.json +22 -0
  28. package/templates/starter/functions/tsconfig.json +18 -0
  29. package/templates/starter/package.json +4 -2
  30. package/templates/starter/tsconfig.json +1 -1
  31. /package/templates/{app → functions}/routes/auth/login.ts +0 -0
  32. /package/templates/{app → functions}/routes/health.ts +0 -0
  33. /package/templates/{app → functions}/schemas/user.ts +0 -0
  34. /package/templates/starter/{app → functions}/routes/health.ts +0 -0
  35. /package/templates/starter/{app → functions}/routes/todos/create.ts +0 -0
  36. /package/templates/starter/{app → functions}/routes/todos/delete.ts +0 -0
  37. /package/templates/starter/{app → functions}/routes/todos/list.ts +0 -0
  38. /package/templates/starter/{app → functions}/routes/todos/update.ts +0 -0
  39. /package/templates/starter/{app → functions}/schemas/todo.ts +0 -0
  40. /package/templates/starter/{app → functions}/store.ts +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/codegen/client-gen.ts","../src/playground/html.ts","../src/core/schema.ts","../src/routing/discover.ts"],"sourcesContent":["/**\n * Clawfire Client Code Generator\n *\n * 라우트 계약에서 타입 안전한 api-client.ts 자동 생성\n * api.products.list(), api.auth.login() 형태로 호출 가능\n */\nimport type { Manifest, ManifestEntry } from \"../core/schema.js\";\n\n/**\n * 매니페스트에서 타입 안전한 API 클라이언트 코드 생성\n */\nexport function generateClientCode(manifest: Manifest, options?: {\n baseUrl?: string;\n importPath?: string;\n}): string {\n const baseUrl = options?.baseUrl || \"\";\n const lines: string[] = [];\n\n lines.push(\"// AUTO-GENERATED by Clawfire — DO NOT EDIT\");\n lines.push(\"// Regenerate: clawfire codegen\");\n lines.push(\"\");\n lines.push(\"/* eslint-disable */\");\n lines.push(\"\");\n\n // 타입 정의 생성\n lines.push(\"// ─── Types ───────────────────────────────────────────────\");\n lines.push(\"\");\n\n for (const api of manifest.apis) {\n const typeName = pathToTypeName(api.path);\n lines.push(`/** ${api.meta.description} */`);\n lines.push(`export interface ${typeName}Input ${jsonSchemaToTsType(api.inputSchema)}`);\n lines.push(\"\");\n lines.push(`export interface ${typeName}Output ${jsonSchemaToTsType(api.outputSchema)}`);\n lines.push(\"\");\n }\n\n // API 응답 래퍼\n lines.push(\"// ─── Response Wrapper ─────────────────────────────────────\");\n lines.push(\"\");\n lines.push(\"interface ClawfireResponse<T> {\");\n lines.push(\" data: T;\");\n lines.push(\"}\");\n lines.push(\"\");\n lines.push(\"interface ClawfireError {\");\n lines.push(\" error: {\");\n lines.push(\" code: string;\");\n lines.push(\" message: string;\");\n lines.push(\" details?: unknown;\");\n lines.push(\" };\");\n lines.push(\"}\");\n lines.push(\"\");\n\n // HTTP 클라이언트\n lines.push(\"// ─── HTTP Client ─────────────────────────────────────────\");\n lines.push(\"\");\n lines.push(\"type GetTokenFn = () => Promise<string | null>;\");\n lines.push(\"\");\n lines.push(\"let _baseUrl = \" + JSON.stringify(baseUrl) + \";\");\n lines.push(\"let _getToken: GetTokenFn = async () => null;\");\n lines.push(\"\");\n lines.push(\"/**\");\n lines.push(\" * API 클라이언트 설정\");\n lines.push(\" * @param baseUrl - API 기본 URL (예: https://us-central1-myproject.cloudfunctions.net/api)\");\n lines.push(\" * @param getToken - 인증 토큰 반환 함수\");\n lines.push(\" */\");\n lines.push(\"export function configureClient(baseUrl: string, getToken?: GetTokenFn) {\");\n lines.push(\" _baseUrl = baseUrl;\");\n lines.push(\" if (getToken) _getToken = getToken;\");\n lines.push(\"}\");\n lines.push(\"\");\n lines.push(\"async function call<TInput, TOutput>(path: string, input: TInput): Promise<TOutput> {\");\n lines.push(\" const token = await _getToken();\");\n lines.push(\" const headers: Record<string, string> = {\");\n lines.push(' \"Content-Type\": \"application/json\",');\n lines.push(\" };\");\n lines.push(' if (token) headers[\"Authorization\"] = `Bearer ${token}`;');\n lines.push(\"\");\n lines.push(\" const res = await fetch(`${_baseUrl}/api${path}`, {\");\n lines.push(' method: \"POST\",');\n lines.push(\" headers,\");\n lines.push(\" body: JSON.stringify(input),\");\n lines.push(\" });\");\n lines.push(\"\");\n lines.push(\" const json = await res.json();\");\n lines.push(\"\");\n lines.push(\" if (!res.ok) {\");\n lines.push(\" const err = json as ClawfireError;\");\n lines.push(\" throw new Error(err.error?.message || `API error: ${res.status}`);\");\n lines.push(\" }\");\n lines.push(\"\");\n lines.push(\" return (json as ClawfireResponse<TOutput>).data;\");\n lines.push(\"}\");\n lines.push(\"\");\n\n // API 네임스페이스 객체 생성\n lines.push(\"// ─── API Client ────────────────────────────────────────────\");\n lines.push(\"\");\n\n const tree = buildApiTree(manifest.apis);\n lines.push(generateApiObject(tree));\n\n return lines.join(\"\\n\");\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────\n\nfunction pathToTypeName(path: string): string {\n return path\n .split(\"/\")\n .filter(Boolean)\n .filter((p) => !p.startsWith(\":\"))\n .map((p) => p.charAt(0).toUpperCase() + p.slice(1))\n .join(\"\");\n}\n\ninterface ApiTreeNode {\n apis: Array<{ name: string; path: string; typeName: string; meta: ManifestEntry[\"meta\"] }>;\n children: Record<string, ApiTreeNode>;\n}\n\nfunction buildApiTree(apis: ManifestEntry[]): ApiTreeNode {\n const root: ApiTreeNode = { apis: [], children: {} };\n\n for (const api of apis) {\n const parts = api.path.split(\"/\").filter(Boolean).filter((p) => !p.startsWith(\":\"));\n let node = root;\n\n for (let i = 0; i < parts.length - 1; i++) {\n if (!node.children[parts[i]]) {\n node.children[parts[i]] = { apis: [], children: {} };\n }\n node = node.children[parts[i]];\n }\n\n const name = parts[parts.length - 1] || \"index\";\n const typeName = pathToTypeName(api.path);\n node.apis.push({ name, path: api.path, typeName, meta: api.meta });\n }\n\n return root;\n}\n\nfunction generateApiObject(tree: ApiTreeNode, indent = \"\"): string {\n const lines: string[] = [];\n lines.push(`${indent}export const api = {`);\n\n for (const [name, child] of Object.entries(tree.children)) {\n lines.push(`${indent} ${name}: {`);\n\n // 자식 API\n for (const api of child.apis) {\n lines.push(`${indent} /** ${api.meta.description}${api.meta.auth ? ` [${api.meta.auth}]` : \"\"} */`);\n lines.push(\n `${indent} ${api.name}: (input: ${api.typeName}Input): Promise<${api.typeName}Output> => call(\"${api.path}\", input),`,\n );\n }\n\n // 자식 네임스페이스\n for (const [subName, subChild] of Object.entries(child.children)) {\n lines.push(`${indent} ${subName}: {`);\n for (const subApi of subChild.apis) {\n lines.push(`${indent} /** ${subApi.meta.description}${subApi.meta.auth ? ` [${subApi.meta.auth}]` : \"\"} */`);\n lines.push(\n `${indent} ${subApi.name}: (input: ${subApi.typeName}Input): Promise<${subApi.typeName}Output> => call(\"${subApi.path}\", input),`,\n );\n }\n // 추가 깊이 지원\n for (const [deepName, deepChild] of Object.entries(subChild.children)) {\n lines.push(`${indent} ${deepName}: {`);\n for (const deepApi of deepChild.apis) {\n lines.push(`${indent} /** ${deepApi.meta.description} */`);\n lines.push(\n `${indent} ${deepApi.name}: (input: ${deepApi.typeName}Input): Promise<${deepApi.typeName}Output> => call(\"${deepApi.path}\", input),`,\n );\n }\n lines.push(`${indent} },`);\n }\n lines.push(`${indent} },`);\n }\n\n lines.push(`${indent} },`);\n }\n\n // 루트 API\n for (const api of tree.apis) {\n lines.push(`${indent} /** ${api.meta.description} */`);\n lines.push(\n `${indent} ${api.name}: (input: ${api.typeName}Input): Promise<${api.typeName}Output> => call(\"${api.path}\", input),`,\n );\n }\n\n lines.push(`${indent}};`);\n return lines.join(\"\\n\");\n}\n\nfunction jsonSchemaToTsType(schema: Record<string, unknown>): string {\n if (!schema) return \"{}\";\n\n const type = schema.type as string;\n\n switch (type) {\n case \"object\": {\n const props = schema.properties as Record<string, Record<string, unknown>> | undefined;\n if (!props) return \"Record<string, unknown>\";\n const required = (schema.required as string[]) || [];\n\n const fields: string[] = [];\n for (const [key, propSchema] of Object.entries(props)) {\n const isRequired = required.includes(key);\n const tsType = jsonSchemaToInlineType(propSchema);\n fields.push(` ${key}${isRequired ? \"\" : \"?\"}: ${tsType};`);\n }\n return `{\\n${fields.join(\"\\n\")}\\n}`;\n }\n default:\n return \"{}\";\n }\n}\n\nfunction jsonSchemaToInlineType(schema: Record<string, unknown>): string {\n if (!schema) return \"unknown\";\n\n // const 값\n if (\"const\" in schema) return JSON.stringify(schema.const);\n\n // enum\n if (schema.enum) return (schema.enum as string[]).map((v) => JSON.stringify(v)).join(\" | \");\n\n // nullable\n const nullable = schema.nullable ? \" | null\" : \"\";\n const optional = schema.optional ? \"\" : \"\";\n\n const type = schema.type as string;\n\n switch (type) {\n case \"string\": return `string${nullable}`;\n case \"number\": return `number${nullable}`;\n case \"boolean\": return `boolean${nullable}`;\n case \"array\": {\n const items = schema.items as Record<string, unknown> | undefined;\n const itemType = items ? jsonSchemaToInlineType(items) : \"unknown\";\n return `${itemType}[]${nullable}`;\n }\n case \"object\": {\n const props = schema.properties as Record<string, Record<string, unknown>> | undefined;\n if (!props) {\n const additionalProps = schema.additionalProperties as Record<string, unknown> | undefined;\n if (additionalProps) return `Record<string, ${jsonSchemaToInlineType(additionalProps)}>${nullable}`;\n return `Record<string, unknown>${nullable}`;\n }\n const required = (schema.required as string[]) || [];\n const fields = Object.entries(props)\n .map(([k, v]) => `${k}${required.includes(k) ? \"\" : \"?\"}: ${jsonSchemaToInlineType(v)}`)\n .join(\"; \");\n return `{ ${fields} }${nullable}`;\n }\n default:\n if (schema.oneOf) {\n return (schema.oneOf as Record<string, unknown>[])\n .map(jsonSchemaToInlineType)\n .join(\" | \");\n }\n return \"unknown\";\n }\n}\n\n/**\n * 매니페스트에서 manifest.json 파일 내용 생성\n */\nexport function generateManifestJson(manifest: Manifest): string {\n return JSON.stringify(manifest, null, 2);\n}\n","/**\n * Clawfire Playground\n *\n * 웹 기반 API 탐색기: API 목록, 인증 테스트, 요청/응답 뷰어\n * 단일 HTML 파일로 생성되어 Firebase Hosting에 배포됩니다.\n */\n\nexport function generatePlaygroundHtml(options?: {\n title?: string;\n apiBaseUrl?: string;\n}): string {\n const title = options?.title || \"Clawfire Playground\";\n const apiBaseUrl = options?.apiBaseUrl || \"\";\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${title}</title>\n <style>\n :root {\n --bg: #0a0a0a;\n --surface: #141414;\n --surface2: #1e1e1e;\n --border: #2a2a2a;\n --text: #e5e5e5;\n --text2: #a3a3a3;\n --accent: #f97316;\n --accent2: #fb923c;\n --green: #22c55e;\n --red: #ef4444;\n --blue: #3b82f6;\n --yellow: #eab308;\n --radius: 8px;\n --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n --mono: 'JetBrains Mono', 'Fira Code', monospace;\n }\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { font-family: var(--font); background: var(--bg); color: var(--text); min-height: 100vh; }\n\n .layout { display: grid; grid-template-columns: 320px 1fr; min-height: 100vh; }\n .sidebar { background: var(--surface); border-right: 1px solid var(--border); overflow-y: auto; }\n .main { padding: 24px; overflow-y: auto; }\n\n .logo { padding: 20px; border-bottom: 1px solid var(--border); }\n .logo h1 { font-size: 20px; font-weight: 700; color: var(--accent); }\n .logo p { font-size: 12px; color: var(--text2); margin-top: 4px; }\n\n .auth-section { padding: 16px; border-bottom: 1px solid var(--border); }\n .auth-section label { font-size: 12px; color: var(--text2); display: block; margin-bottom: 6px; }\n .auth-input { width: 100%; padding: 8px 12px; background: var(--surface2); border: 1px solid var(--border);\n border-radius: var(--radius); color: var(--text); font-family: var(--mono); font-size: 12px; }\n .auth-status { font-size: 11px; margin-top: 6px; }\n .auth-status.ok { color: var(--green); }\n .auth-status.no { color: var(--text2); }\n\n .search { padding: 12px 16px; border-bottom: 1px solid var(--border); }\n .search input { width: 100%; padding: 8px 12px; background: var(--surface2); border: 1px solid var(--border);\n border-radius: var(--radius); color: var(--text); font-size: 13px; }\n\n .api-list { padding: 8px 0; }\n .api-group { padding: 4px 0; }\n .api-group-title { padding: 8px 16px; font-size: 11px; color: var(--text2); text-transform: uppercase;\n letter-spacing: 0.5px; font-weight: 600; }\n .api-item { padding: 8px 16px; cursor: pointer; transition: background 0.15s; display: flex; align-items: center;\n gap: 8px; font-size: 13px; }\n .api-item:hover { background: var(--surface2); }\n .api-item.active { background: var(--surface2); border-left: 2px solid var(--accent); }\n .api-badge { font-size: 10px; padding: 2px 6px; border-radius: 4px; font-weight: 600; }\n .badge-public { background: #22c55e20; color: var(--green); }\n .badge-auth { background: #3b82f620; color: var(--blue); }\n .badge-role { background: #eab30820; color: var(--yellow); }\n .badge-reauth { background: #ef444420; color: var(--red); }\n\n .panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 16px; }\n .panel-header { padding: 16px; border-bottom: 1px solid var(--border); display: flex; align-items: center;\n justify-content: space-between; }\n .panel-header h2 { font-size: 16px; font-weight: 600; }\n .panel-body { padding: 16px; }\n\n .field { margin-bottom: 12px; }\n .field label { font-size: 12px; color: var(--text2); display: block; margin-bottom: 4px; }\n .field-type { font-size: 11px; color: var(--text2); font-family: var(--mono); }\n .field-required { color: var(--red); font-size: 11px; }\n\n textarea, input[type=\"text\"] { width: 100%; padding: 10px 14px; background: var(--surface2);\n border: 1px solid var(--border); border-radius: var(--radius); color: var(--text);\n font-family: var(--mono); font-size: 13px; resize: vertical; }\n textarea { min-height: 200px; }\n\n .btn { padding: 10px 20px; border: none; border-radius: var(--radius); font-size: 14px;\n font-weight: 600; cursor: pointer; transition: all 0.15s; }\n .btn-primary { background: var(--accent); color: white; }\n .btn-primary:hover { background: var(--accent2); }\n .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }\n\n .response-section { margin-top: 16px; }\n .status-badge { font-size: 12px; padding: 4px 8px; border-radius: 4px; font-weight: 600; }\n .status-ok { background: #22c55e20; color: var(--green); }\n .status-err { background: #ef444420; color: var(--red); }\n pre { background: var(--surface2); padding: 16px; border-radius: var(--radius); overflow-x: auto;\n font-family: var(--mono); font-size: 13px; line-height: 1.5; white-space: pre-wrap; }\n\n .schema-info { font-size: 12px; color: var(--text2); line-height: 1.6; }\n .schema-info code { background: var(--surface2); padding: 2px 6px; border-radius: 4px; font-family: var(--mono);\n font-size: 11px; }\n\n .empty-state { text-align: center; padding: 80px 40px; color: var(--text2); }\n .empty-state h2 { font-size: 24px; margin-bottom: 8px; color: var(--text); }\n\n .timer { font-size: 12px; color: var(--text2); font-family: var(--mono); }\n\n @media (max-width: 768px) {\n .layout { grid-template-columns: 1fr; }\n .sidebar { max-height: 40vh; }\n }\n </style>\n</head>\n<body>\n <div class=\"layout\">\n <div class=\"sidebar\">\n <div class=\"logo\">\n <h1>Clawfire</h1>\n <p>API Playground</p>\n </div>\n <div class=\"auth-section\">\n <label>Bearer Token</label>\n <input type=\"text\" class=\"auth-input\" id=\"token-input\" placeholder=\"Paste your ID token...\">\n <div class=\"auth-status no\" id=\"auth-status\">Not authenticated</div>\n </div>\n <div class=\"search\">\n <input type=\"text\" id=\"search-input\" placeholder=\"Search APIs...\">\n </div>\n <div class=\"api-list\" id=\"api-list\"></div>\n </div>\n <div class=\"main\" id=\"main-content\">\n <div class=\"empty-state\">\n <h2>Select an API</h2>\n <p>Choose an API from the sidebar to test it.</p>\n </div>\n </div>\n </div>\n\n <script>\n const BASE_URL = ${JSON.stringify(apiBaseUrl)} || window.location.origin;\n let manifest = null;\n let selectedApi = null;\n\n async function loadManifest() {\n try {\n const res = await fetch(BASE_URL + '/api/__manifest', { method: 'POST' });\n manifest = await res.json();\n renderApiList(manifest.apis);\n } catch (e) {\n document.getElementById('api-list').innerHTML =\n '<div style=\"padding:16px;color:var(--red);font-size:13px;\">Failed to load API manifest. Make sure your server is running.</div>';\n }\n }\n\n function renderApiList(apis) {\n const groups = {};\n apis.forEach(api => {\n const parts = api.path.split('/').filter(Boolean);\n const group = parts.length > 1 ? parts[0] : 'root';\n if (!groups[group]) groups[group] = [];\n groups[group].push(api);\n });\n\n const el = document.getElementById('api-list');\n el.innerHTML = Object.entries(groups).map(([group, items]) =>\n '<div class=\"api-group\">' +\n '<div class=\"api-group-title\">' + group + '</div>' +\n items.map(api => {\n const auth = api.meta.auth || 'public';\n const badgeClass = 'badge-' + auth;\n return '<div class=\"api-item\" onclick=\"selectApi(\\\\'' + api.path + '\\\\')\">' +\n '<span class=\"api-badge ' + badgeClass + '\">' + auth.toUpperCase() + '</span>' +\n '<span>' + api.path + '</span>' +\n '</div>';\n }).join('') +\n '</div>'\n ).join('');\n }\n\n function selectApi(path) {\n selectedApi = manifest.apis.find(a => a.path === path);\n if (!selectedApi) return;\n\n document.querySelectorAll('.api-item').forEach(el => el.classList.remove('active'));\n event.currentTarget?.classList.add('active');\n\n const main = document.getElementById('main-content');\n const exampleInput = selectedApi.meta.exampleInput\n ? JSON.stringify(selectedApi.meta.exampleInput, null, 2)\n : generateExampleFromSchema(selectedApi.inputSchema);\n\n main.innerHTML =\n '<div class=\"panel\">' +\n '<div class=\"panel-header\">' +\n '<h2>POST ' + selectedApi.path + '</h2>' +\n '<span class=\"api-badge badge-' + (selectedApi.meta.auth || 'public') + '\">' +\n (selectedApi.meta.auth || 'public').toUpperCase() + '</span>' +\n '</div>' +\n '<div class=\"panel-body\">' +\n '<p style=\"color:var(--text2);margin-bottom:16px;\">' + (selectedApi.meta.description || '') + '</p>' +\n (selectedApi.meta.tags ? '<div style=\"margin-bottom:12px;\">' + selectedApi.meta.tags.map(t =>\n '<span style=\"background:var(--surface2);padding:2px 8px;border-radius:4px;font-size:11px;margin-right:4px;\">' + t + '</span>'\n ).join('') + '</div>' : '') +\n '<div class=\"schema-info\" style=\"margin-bottom:16px;\">' +\n '<strong>Input Schema:</strong><br>' + renderSchemaInfo(selectedApi.inputSchema) +\n '</div>' +\n '<div class=\"schema-info\" style=\"margin-bottom:16px;\">' +\n '<strong>Output Schema:</strong><br>' + renderSchemaInfo(selectedApi.outputSchema) +\n '</div>' +\n '</div>' +\n '</div>' +\n '<div class=\"panel\">' +\n '<div class=\"panel-header\"><h2>Request</h2></div>' +\n '<div class=\"panel-body\">' +\n '<textarea id=\"req-body\" placeholder=\"Request JSON body\">' + exampleInput + '</textarea>' +\n '<div style=\"margin-top:12px;display:flex;align-items:center;gap:12px;\">' +\n '<button class=\"btn btn-primary\" onclick=\"sendRequest()\">Send Request</button>' +\n '<span class=\"timer\" id=\"timer\"></span>' +\n '</div>' +\n '</div>' +\n '</div>' +\n '<div class=\"response-section\" id=\"response-section\"></div>';\n }\n\n function renderSchemaInfo(schema) {\n if (!schema || !schema.properties) return '<code>void</code>';\n return Object.entries(schema.properties).map(([key, prop]) => {\n const required = schema.required?.includes(key);\n const type = prop.type || 'unknown';\n const enumVals = prop.enum ? ' (' + prop.enum.join(', ') + ')' : '';\n return '<code>' + key + '</code>: <span class=\"field-type\">' + type + enumVals + '</span>' +\n (required ? ' <span class=\"field-required\">required</span>' : ' <span style=\"color:var(--text2);font-size:11px;\">optional</span>');\n }).join('<br>');\n }\n\n function generateExampleFromSchema(schema) {\n if (!schema || !schema.properties) return '{}';\n const obj = {};\n for (const [key, prop] of Object.entries(schema.properties)) {\n if (prop.enum) { obj[key] = prop.enum[0]; continue; }\n switch (prop.type) {\n case 'string': obj[key] = prop.format === 'email' ? 'user@example.com' : 'string'; break;\n case 'number': obj[key] = 0; break;\n case 'boolean': obj[key] = false; break;\n case 'array': obj[key] = []; break;\n case 'object': obj[key] = {}; break;\n default: obj[key] = null;\n }\n }\n return JSON.stringify(obj, null, 2);\n }\n\n async function sendRequest() {\n if (!selectedApi) return;\n const body = document.getElementById('req-body').value;\n const token = document.getElementById('token-input').value;\n const timer = document.getElementById('timer');\n const section = document.getElementById('response-section');\n\n let parsed;\n try { parsed = JSON.parse(body); } catch {\n section.innerHTML = '<div class=\"panel\"><div class=\"panel-body\"><pre style=\"color:var(--red)\">Invalid JSON</pre></div></div>';\n return;\n }\n\n const start = performance.now();\n timer.textContent = 'Sending...';\n\n try {\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers['Authorization'] = 'Bearer ' + token;\n\n const res = await fetch(BASE_URL + '/api' + selectedApi.path, {\n method: 'POST', headers, body: JSON.stringify(parsed)\n });\n const elapsed = Math.round(performance.now() - start);\n timer.textContent = elapsed + 'ms';\n\n const json = await res.json();\n const isOk = res.ok;\n\n section.innerHTML =\n '<div class=\"panel\">' +\n '<div class=\"panel-header\">' +\n '<h2>Response</h2>' +\n '<span class=\"status-badge ' + (isOk ? 'status-ok' : 'status-err') + '\">' +\n res.status + ' ' + res.statusText + '</span>' +\n '</div>' +\n '<div class=\"panel-body\"><pre>' + syntaxHighlight(JSON.stringify(json, null, 2)) + '</pre></div>' +\n '</div>';\n } catch (e) {\n const elapsed = Math.round(performance.now() - start);\n timer.textContent = elapsed + 'ms';\n section.innerHTML =\n '<div class=\"panel\"><div class=\"panel-body\"><pre style=\"color:var(--red)\">Network error: ' + e.message + '</pre></div></div>';\n }\n }\n\n function syntaxHighlight(json) {\n return json.replace(/(\"(\\\\\\\\u[a-fA-F0-9]{4}|\\\\\\\\[^u]|[^\\\\\\\\\"])*\"(\\\\s*:)?)|\\\\b(true|false|null)\\\\b|-?\\\\d+(\\\\.\\\\d+)?([eE][+-]?\\\\d+)?/g,\n function(match) {\n let cls = 'color:#eab308';\n if (/^\"/.test(match)) {\n if (/:$/.test(match)) cls = 'color:#3b82f6';\n else cls = 'color:#22c55e';\n } else if (/true|false/.test(match)) cls = 'color:#f97316';\n else if (/null/.test(match)) cls = 'color:#ef4444';\n return '<span style=\"' + cls + '\">' + match + '</span>';\n }\n );\n }\n\n // Search\n document.getElementById('search-input')?.addEventListener('input', (e) => {\n if (!manifest) return;\n const q = e.target.value.toLowerCase();\n const filtered = manifest.apis.filter(a => a.path.toLowerCase().includes(q) || a.meta.description?.toLowerCase().includes(q));\n renderApiList(filtered);\n });\n\n // Token status\n document.getElementById('token-input')?.addEventListener('input', (e) => {\n const el = document.getElementById('auth-status');\n if (e.target.value) {\n el.textContent = 'Token set';\n el.className = 'auth-status ok';\n } else {\n el.textContent = 'Not authenticated';\n el.className = 'auth-status no';\n }\n });\n\n loadManifest();\n </script>\n</body>\n</html>`;\n}\n","/**\n * Clawfire Schema & Contract System\n *\n * 모든 API는 input/output schema + meta + handler로 구성된 \"계약(Contract)\"으로 정의됩니다.\n * Zod를 사용하여 타입 안전성과 런타임 검증을 동시에 보장합니다.\n */\nimport { z, type ZodType, type ZodObject, type ZodRawShape } from \"zod\";\n\n// ─── Types ───────────────────────────────────────────────────────────\n\n/** 인증 컨텍스트 */\nexport interface AuthContext {\n uid: string;\n email?: string;\n emailVerified?: boolean;\n role?: string;\n customClaims?: Record<string, unknown>;\n token?: string;\n}\n\n/** API 핸들러에 전달되는 컨텍스트 */\nexport interface HandlerContext {\n auth: AuthContext | null;\n /** 재인증 여부 (민감 작업용) */\n reauthenticated?: boolean;\n /** 원본 요청 헤더 */\n headers?: Record<string, string>;\n /** 요청 IP */\n ip?: string;\n}\n\n/** 권한 수준 */\nexport type AuthLevel = \"public\" | \"authenticated\" | \"role\" | \"reauth\";\n\n/** API 메타데이터 */\nexport interface APIMeta {\n /** API 설명 (AI/Playground용) */\n description: string;\n /** 태그 (그룹화용) */\n tags?: string[];\n /** 인증 요구 수준 */\n auth?: AuthLevel;\n /** 필요 역할 (auth가 'role'일 때) */\n roles?: string[];\n /** 재인증 필요 여부 */\n reauth?: boolean;\n /** Rate limit (초당 요청 수) */\n rateLimit?: number;\n /** 비활성화 여부 */\n deprecated?: boolean;\n /** 예시 입력값 */\n exampleInput?: unknown;\n /** 예시 출력값 */\n exampleOutput?: unknown;\n}\n\n/** API 계약 정의 */\nexport interface APIContract<\n TInput extends ZodType = ZodType,\n TOutput extends ZodType = ZodType,\n> {\n /** 입력 스키마 */\n input: TInput;\n /** 출력 스키마 */\n output: TOutput;\n /** 메타데이터 */\n meta: APIMeta;\n /** 핸들러 함수 */\n handler: (\n input: z.infer<TInput>,\n ctx: HandlerContext,\n ) => Promise<z.infer<TOutput>>;\n}\n\n/** 모델 필드 정의 */\nexport interface ModelField {\n type: \"string\" | \"number\" | \"boolean\" | \"timestamp\" | \"array\" | \"map\" | \"reference\" | \"geopoint\";\n required?: boolean;\n description?: string;\n default?: unknown;\n /** 배열 아이템 타입 */\n items?: ModelField;\n /** 맵 값 타입 */\n values?: ModelField;\n /** 참조 대상 컬렉션 */\n ref?: string;\n /** enum 값 리스트 */\n enum?: string[];\n}\n\n/** 모델 정의 */\nexport interface ModelDefinition {\n /** 컬렉션 이름 */\n collection: string;\n /** 필드 정의 */\n fields: Record<string, ModelField>;\n /** 서브컬렉션 */\n subcollections?: Record<string, ModelDefinition>;\n /** 인덱스 */\n indexes?: ModelIndex[];\n /** 보안 규칙 */\n rules?: ModelRules;\n /** 타임스탬프 자동 생성 */\n timestamps?: boolean;\n /** 소프트 삭제 */\n softDelete?: boolean;\n}\n\n/** Firestore 인덱스 */\nexport interface ModelIndex {\n fields: Array<{ field: string; order?: \"asc\" | \"desc\" }>;\n}\n\n/** 모델 보안 규칙 */\nexport interface ModelRules {\n read?: AuthLevel;\n create?: AuthLevel;\n update?: AuthLevel;\n delete?: AuthLevel;\n readRoles?: string[];\n createRoles?: string[];\n updateRoles?: string[];\n deleteRoles?: string[];\n /** 소유자만 읽기/쓰기 가능 필드 */\n ownerField?: string;\n}\n\n// ─── Builders ────────────────────────────────────────────────────────\n\n/**\n * API 계약 정의\n *\n * @example\n * ```ts\n * export default defineAPI({\n * input: z.object({ name: z.string() }),\n * output: z.object({ id: z.string(), name: z.string() }),\n * meta: { description: \"상품 생성\", auth: \"authenticated\" },\n * handler: async (input, ctx) => {\n * const id = await db.create(\"products\", input);\n * return { id, ...input };\n * }\n * });\n * ```\n */\nexport function defineAPI<\n TInput extends ZodType,\n TOutput extends ZodType,\n>(contract: APIContract<TInput, TOutput>): APIContract<TInput, TOutput> {\n return contract;\n}\n\n/**\n * 모델(Firestore 컬렉션) 정의\n *\n * @example\n * ```ts\n * export const Product = defineModel({\n * collection: \"products\",\n * fields: {\n * name: { type: \"string\", required: true },\n * price: { type: \"number\", required: true },\n * tags: { type: \"array\", items: { type: \"string\" } },\n * },\n * timestamps: true,\n * rules: { read: \"public\", create: \"authenticated\" }\n * });\n * ```\n */\nexport function defineModel(definition: ModelDefinition): ModelDefinition {\n return {\n timestamps: true,\n ...definition,\n };\n}\n\n// ─── Schema Utilities ────────────────────────────────────────────────\n\n/** Zod 스키마에서 JSON Schema 생성 (Playground/문서용) */\nexport function zodToJsonSchema(schema: ZodType): Record<string, unknown> {\n return extractZodShape(schema);\n}\n\nfunction extractZodShape(schema: ZodType): Record<string, unknown> {\n const def = (schema as any)._def;\n\n if (!def) return { type: \"unknown\" };\n\n switch (def.typeName) {\n case \"ZodObject\": {\n const shape = (schema as ZodObject<ZodRawShape>).shape;\n const properties: Record<string, unknown> = {};\n const required: string[] = [];\n\n for (const [key, value] of Object.entries(shape)) {\n properties[key] = extractZodShape(value as ZodType);\n if (!(value as any).isOptional?.()) {\n const innerDef = (value as any)._def;\n if (innerDef?.typeName !== \"ZodOptional\" && innerDef?.typeName !== \"ZodDefault\") {\n required.push(key);\n }\n }\n }\n\n return { type: \"object\", properties, ...(required.length > 0 ? { required } : {}) };\n }\n case \"ZodString\":\n return { type: \"string\", ...(def.checks?.length ? extractStringChecks(def.checks) : {}) };\n case \"ZodNumber\":\n return { type: \"number\" };\n case \"ZodBoolean\":\n return { type: \"boolean\" };\n case \"ZodArray\":\n return { type: \"array\", items: extractZodShape(def.type) };\n case \"ZodEnum\":\n return { type: \"string\", enum: def.values };\n case \"ZodOptional\":\n return { ...extractZodShape(def.innerType), optional: true };\n case \"ZodDefault\":\n return { ...extractZodShape(def.innerType), default: def.defaultValue() };\n case \"ZodNullable\":\n return { ...extractZodShape(def.innerType), nullable: true };\n case \"ZodLiteral\":\n return { type: typeof def.value, const: def.value };\n case \"ZodUnion\":\n return { oneOf: def.options.map((o: ZodType) => extractZodShape(o)) };\n case \"ZodRecord\":\n return { type: \"object\", additionalProperties: extractZodShape(def.valueType) };\n case \"ZodDate\":\n return { type: \"string\", format: \"date-time\" };\n default:\n return { type: \"unknown\" };\n }\n}\n\nfunction extractStringChecks(checks: Array<{ kind: string; value?: unknown }>): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const check of checks) {\n switch (check.kind) {\n case \"min\": result.minLength = check.value; break;\n case \"max\": result.maxLength = check.value; break;\n case \"email\": result.format = \"email\"; break;\n case \"url\": result.format = \"uri\"; break;\n case \"uuid\": result.format = \"uuid\"; break;\n }\n }\n return result;\n}\n\n/** 모델 정의에서 Zod 스키마 자동 생성 */\nexport function modelToZodSchema(model: ModelDefinition): ZodObject<ZodRawShape> {\n const shape: ZodRawShape = {};\n\n for (const [key, field] of Object.entries(model.fields)) {\n let fieldSchema: ZodType = fieldToZod(field);\n if (!field.required) {\n fieldSchema = fieldSchema.optional();\n }\n shape[key] = fieldSchema;\n }\n\n if (model.timestamps) {\n shape.createdAt = z.string().datetime().optional();\n shape.updatedAt = z.string().datetime().optional();\n }\n\n if (model.softDelete) {\n shape.deletedAt = z.string().datetime().nullable().optional();\n }\n\n return z.object(shape);\n}\n\nfunction fieldToZod(field: ModelField): ZodType {\n switch (field.type) {\n case \"string\":\n if (field.enum) return z.enum(field.enum as [string, ...string[]]);\n return z.string();\n case \"number\":\n return z.number();\n case \"boolean\":\n return z.boolean();\n case \"timestamp\":\n return z.string().datetime();\n case \"array\":\n if (field.items) return z.array(fieldToZod(field.items));\n return z.array(z.unknown());\n case \"map\":\n if (field.values) return z.record(z.string(), fieldToZod(field.values));\n return z.record(z.string(), z.unknown());\n case \"reference\":\n return z.string(); // 참조는 문서 경로 문자열\n case \"geopoint\":\n return z.object({ latitude: z.number(), longitude: z.number() });\n default:\n return z.unknown();\n }\n}\n\n// ─── Manifest ────────────────────────────────────────────────────────\n\n/** API 매니페스트 항목 */\nexport interface ManifestEntry {\n path: string;\n method: \"POST\"; // Clawfire는 모두 POST\n meta: APIMeta;\n inputSchema: Record<string, unknown>;\n outputSchema: Record<string, unknown>;\n}\n\n/** 전체 매니페스트 */\nexport interface Manifest {\n version: string;\n generatedAt: string;\n apis: ManifestEntry[];\n models: Record<string, ModelDefinition>;\n}\n\n/** 계약에서 매니페스트 항목 생성 */\nexport function contractToManifest(\n path: string,\n contract: APIContract,\n): ManifestEntry {\n return {\n path,\n method: \"POST\",\n meta: contract.meta,\n inputSchema: zodToJsonSchema(contract.input),\n outputSchema: zodToJsonSchema(contract.output),\n };\n}\n","/**\n * Clawfire Route Discovery\n *\n * 파일 시스템에서 라우트 자동 발견 (빌드 타임 & 런타임)\n */\nimport { resolve, relative, join } from \"path\";\nimport { existsSync, readdirSync, statSync } from \"fs\";\n\nexport interface DiscoveredRoute {\n /** 파일 경로 (상대) */\n filePath: string;\n /** API 경로 (/products/list) */\n apiPath: string;\n /** 동적 파라미터 이름 */\n params: string[];\n}\n\n/**\n * routes 디렉터리에서 라우트 파일 자동 발견\n *\n * @param routesDir - routes 디렉터리 절대 경로\n * @returns 발견된 라우트 목록\n *\n * @example\n * ```\n * app/routes/products/list.ts → { apiPath: \"/products/list\", params: [] }\n * app/routes/products/[id]/get.ts → { apiPath: \"/products/:id/get\", params: [\"id\"] }\n * app/routes/health.ts → { apiPath: \"/health\", params: [] }\n * ```\n */\nexport function discoverRoutes(routesDir: string): DiscoveredRoute[] {\n if (!existsSync(routesDir)) {\n return [];\n }\n\n const routes: DiscoveredRoute[] = [];\n scanDirectory(routesDir, routesDir, routes);\n return routes.sort((a, b) => a.apiPath.localeCompare(b.apiPath));\n}\n\nfunction scanDirectory(baseDir: string, currentDir: string, routes: DiscoveredRoute[]): void {\n const entries = readdirSync(currentDir);\n\n for (const entry of entries) {\n const fullPath = join(currentDir, entry);\n const stat = statSync(fullPath);\n\n if (stat.isDirectory()) {\n // 숨김 디렉터리, node_modules 무시\n if (entry.startsWith(\".\") || entry === \"node_modules\") continue;\n scanDirectory(baseDir, fullPath, routes);\n } else if (stat.isFile()) {\n // .ts, .js 파일만\n if (!entry.endsWith(\".ts\") && !entry.endsWith(\".js\")) continue;\n // index, _로 시작하는 파일 무시\n if (entry.startsWith(\"_\")) continue;\n // .d.ts 무시\n if (entry.endsWith(\".d.ts\")) continue;\n\n const relativePath = relative(baseDir, fullPath);\n const route = filePathToRoute(relativePath);\n routes.push(route);\n }\n }\n}\n\nfunction filePathToRoute(filePath: string): DiscoveredRoute {\n const params: string[] = [];\n\n // 확장자 제거\n let routePath = filePath.replace(/\\.(ts|js)$/, \"\");\n\n // Windows 경로 → POSIX\n routePath = routePath.replace(/\\\\/g, \"/\");\n\n // index 파일은 디렉터리 자체\n if (routePath.endsWith(\"/index\") || routePath === \"index\") {\n routePath = routePath.replace(/\\/?index$/, \"\");\n }\n\n // [param] → :param 변환\n routePath = routePath.replace(/\\[([^\\]]+)\\]/g, (_, param) => {\n params.push(param);\n return `:${param}`;\n });\n\n // 앞에 / 추가\n const apiPath = `/${routePath}`;\n\n return {\n filePath,\n apiPath,\n params,\n };\n}\n\n/**\n * 라우트 파일에서 import하여 라우터에 등록하는 코드 생성 (빌드 타임)\n */\nexport function generateRouteImports(routes: DiscoveredRoute[], routesDir: string): string {\n const lines: string[] = [\n '// AUTO-GENERATED by Clawfire — DO NOT EDIT',\n '// This file is regenerated whenever routes change.',\n '',\n 'import { createRouter } from \"clawfire/functions\";',\n '',\n ];\n\n routes.forEach((route, i) => {\n const importPath = `./${route.filePath.replace(/\\.(ts|js)$/, \".js\")}`;\n lines.push(`import route_${i} from \"${importPath}\";`);\n });\n\n lines.push('');\n lines.push('export function registerAllRoutes(router: ReturnType<typeof createRouter>) {');\n\n routes.forEach((route, i) => {\n lines.push(` router.register(\"${route.apiPath}\", route_${i});`);\n });\n\n lines.push(' return router;');\n lines.push('}');\n\n return lines.join('\\n');\n}\n"],"mappings":"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AAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uBA8HO,KAAK,UAAU,UAAU,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqMjD;;;AChVA,SAAS,SAAyD;;;ACDlE,SAAkB,UAAU,YAAY;AACxC,SAAS,YAAY,aAAa,gBAAgB;AAwB3C,SAAS,eAAe,WAAsC;AACnE,MAAI,CAAC,WAAW,SAAS,GAAG;AAC1B,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,SAA4B,CAAC;AACnC,gBAAc,WAAW,WAAW,MAAM;AAC1C,SAAO,OAAO,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,cAAc,EAAE,OAAO,CAAC;AACjE;AAEA,SAAS,cAAc,SAAiB,YAAoB,QAAiC;AAC3F,QAAM,UAAU,YAAY,UAAU;AAEtC,aAAW,SAAS,SAAS;AAC3B,UAAM,WAAW,KAAK,YAAY,KAAK;AACvC,UAAM,OAAO,SAAS,QAAQ;AAE9B,QAAI,KAAK,YAAY,GAAG;AAEtB,UAAI,MAAM,WAAW,GAAG,KAAK,UAAU,eAAgB;AACvD,oBAAc,SAAS,UAAU,MAAM;AAAA,IACzC,WAAW,KAAK,OAAO,GAAG;AAExB,UAAI,CAAC,MAAM,SAAS,KAAK,KAAK,CAAC,MAAM,SAAS,KAAK,EAAG;AAEtD,UAAI,MAAM,WAAW,GAAG,EAAG;AAE3B,UAAI,MAAM,SAAS,OAAO,EAAG;AAE7B,YAAM,eAAe,SAAS,SAAS,QAAQ;AAC/C,YAAM,QAAQ,gBAAgB,YAAY;AAC1C,aAAO,KAAK,KAAK;AAAA,IACnB;AAAA,EACF;AACF;AAEA,SAAS,gBAAgB,UAAmC;AAC1D,QAAM,SAAmB,CAAC;AAG1B,MAAI,YAAY,SAAS,QAAQ,cAAc,EAAE;AAGjD,cAAY,UAAU,QAAQ,OAAO,GAAG;AAGxC,MAAI,UAAU,SAAS,QAAQ,KAAK,cAAc,SAAS;AACzD,gBAAY,UAAU,QAAQ,aAAa,EAAE;AAAA,EAC/C;AAGA,cAAY,UAAU,QAAQ,iBAAiB,CAAC,GAAG,UAAU;AAC3D,WAAO,KAAK,KAAK;AACjB,WAAO,IAAI,KAAK;AAAA,EAClB,CAAC;AAGD,QAAM,UAAU,IAAI,SAAS;AAE7B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAKO,SAAS,qBAAqB,QAA2B,WAA2B;AACzF,QAAM,QAAkB;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO,QAAQ,CAAC,OAAO,MAAM;AAC3B,UAAM,aAAa,KAAK,MAAM,SAAS,QAAQ,cAAc,KAAK,CAAC;AACnE,UAAM,KAAK,gBAAgB,CAAC,UAAU,UAAU,IAAI;AAAA,EACtD,CAAC;AAED,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,8EAA8E;AAEzF,SAAO,QAAQ,CAAC,OAAO,MAAM;AAC3B,UAAM,KAAK,sBAAsB,MAAM,OAAO,YAAY,CAAC,IAAI;AAAA,EACjE,CAAC;AAED,QAAM,KAAK,kBAAkB;AAC7B,QAAM,KAAK,GAAG;AAEd,SAAO,MAAM,KAAK,IAAI;AACxB;","names":[]}
1
+ {"version":3,"sources":["../src/codegen/client-gen.ts","../src/playground/html.ts","../src/core/schema.ts","../src/routing/discover.ts"],"sourcesContent":["/**\n * Clawfire Client Code Generator\n *\n * 라우트 계약에서 타입 안전한 api-client.ts 자동 생성\n * api.products.list(), api.auth.login() 형태로 호출 가능\n */\nimport type { Manifest, ManifestEntry } from \"../core/schema.js\";\n\n/**\n * 매니페스트에서 타입 안전한 API 클라이언트 코드 생성\n */\nexport function generateClientCode(manifest: Manifest, options?: {\n baseUrl?: string;\n importPath?: string;\n}): string {\n const baseUrl = options?.baseUrl || \"\";\n const lines: string[] = [];\n\n lines.push(\"// AUTO-GENERATED by Clawfire — DO NOT EDIT\");\n lines.push(\"// Regenerate: clawfire codegen\");\n lines.push(\"\");\n lines.push(\"/* eslint-disable */\");\n lines.push(\"\");\n\n // 타입 정의 생성\n lines.push(\"// ─── Types ───────────────────────────────────────────────\");\n lines.push(\"\");\n\n for (const api of manifest.apis) {\n const typeName = pathToTypeName(api.path);\n lines.push(`/** ${api.meta.description} */`);\n lines.push(`export interface ${typeName}Input ${jsonSchemaToTsType(api.inputSchema)}`);\n lines.push(\"\");\n lines.push(`export interface ${typeName}Output ${jsonSchemaToTsType(api.outputSchema)}`);\n lines.push(\"\");\n }\n\n // API 응답 래퍼\n lines.push(\"// ─── Response Wrapper ─────────────────────────────────────\");\n lines.push(\"\");\n lines.push(\"interface ClawfireResponse<T> {\");\n lines.push(\" data: T;\");\n lines.push(\"}\");\n lines.push(\"\");\n lines.push(\"interface ClawfireError {\");\n lines.push(\" error: {\");\n lines.push(\" code: string;\");\n lines.push(\" message: string;\");\n lines.push(\" details?: unknown;\");\n lines.push(\" };\");\n lines.push(\"}\");\n lines.push(\"\");\n\n // HTTP 클라이언트\n lines.push(\"// ─── HTTP Client ─────────────────────────────────────────\");\n lines.push(\"\");\n lines.push(\"type GetTokenFn = () => Promise<string | null>;\");\n lines.push(\"\");\n lines.push(\"let _baseUrl = \" + JSON.stringify(baseUrl) + \";\");\n lines.push(\"let _getToken: GetTokenFn = async () => null;\");\n lines.push(\"\");\n lines.push(\"/**\");\n lines.push(\" * API 클라이언트 설정\");\n lines.push(\" * @param baseUrl - API 기본 URL (예: https://us-central1-myproject.cloudfunctions.net/api)\");\n lines.push(\" * @param getToken - 인증 토큰 반환 함수\");\n lines.push(\" */\");\n lines.push(\"export function configureClient(baseUrl: string, getToken?: GetTokenFn) {\");\n lines.push(\" _baseUrl = baseUrl;\");\n lines.push(\" if (getToken) _getToken = getToken;\");\n lines.push(\"}\");\n lines.push(\"\");\n lines.push(\"async function call<TInput, TOutput>(path: string, input: TInput): Promise<TOutput> {\");\n lines.push(\" const token = await _getToken();\");\n lines.push(\" const headers: Record<string, string> = {\");\n lines.push(' \"Content-Type\": \"application/json\",');\n lines.push(\" };\");\n lines.push(' if (token) headers[\"Authorization\"] = `Bearer ${token}`;');\n lines.push(\"\");\n lines.push(\" const res = await fetch(`${_baseUrl}/api${path}`, {\");\n lines.push(' method: \"POST\",');\n lines.push(\" headers,\");\n lines.push(\" body: JSON.stringify(input),\");\n lines.push(\" });\");\n lines.push(\"\");\n lines.push(\" const json = await res.json();\");\n lines.push(\"\");\n lines.push(\" if (!res.ok) {\");\n lines.push(\" const err = json as ClawfireError;\");\n lines.push(\" throw new Error(err.error?.message || `API error: ${res.status}`);\");\n lines.push(\" }\");\n lines.push(\"\");\n lines.push(\" return (json as ClawfireResponse<TOutput>).data;\");\n lines.push(\"}\");\n lines.push(\"\");\n\n // API 네임스페이스 객체 생성\n lines.push(\"// ─── API Client ────────────────────────────────────────────\");\n lines.push(\"\");\n\n const tree = buildApiTree(manifest.apis);\n lines.push(generateApiObject(tree));\n\n return lines.join(\"\\n\");\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────\n\nfunction pathToTypeName(path: string): string {\n return path\n .split(\"/\")\n .filter(Boolean)\n .filter((p) => !p.startsWith(\":\"))\n .map((p) => p.charAt(0).toUpperCase() + p.slice(1))\n .join(\"\");\n}\n\ninterface ApiTreeNode {\n apis: Array<{ name: string; path: string; typeName: string; meta: ManifestEntry[\"meta\"] }>;\n children: Record<string, ApiTreeNode>;\n}\n\nfunction buildApiTree(apis: ManifestEntry[]): ApiTreeNode {\n const root: ApiTreeNode = { apis: [], children: {} };\n\n for (const api of apis) {\n const parts = api.path.split(\"/\").filter(Boolean).filter((p) => !p.startsWith(\":\"));\n let node = root;\n\n for (let i = 0; i < parts.length - 1; i++) {\n if (!node.children[parts[i]]) {\n node.children[parts[i]] = { apis: [], children: {} };\n }\n node = node.children[parts[i]];\n }\n\n const name = parts[parts.length - 1] || \"index\";\n const typeName = pathToTypeName(api.path);\n node.apis.push({ name, path: api.path, typeName, meta: api.meta });\n }\n\n return root;\n}\n\nfunction generateApiObject(tree: ApiTreeNode, indent = \"\"): string {\n const lines: string[] = [];\n lines.push(`${indent}export const api = {`);\n\n for (const [name, child] of Object.entries(tree.children)) {\n lines.push(`${indent} ${name}: {`);\n\n // 자식 API\n for (const api of child.apis) {\n lines.push(`${indent} /** ${api.meta.description}${api.meta.auth ? ` [${api.meta.auth}]` : \"\"} */`);\n lines.push(\n `${indent} ${api.name}: (input: ${api.typeName}Input): Promise<${api.typeName}Output> => call(\"${api.path}\", input),`,\n );\n }\n\n // 자식 네임스페이스\n for (const [subName, subChild] of Object.entries(child.children)) {\n lines.push(`${indent} ${subName}: {`);\n for (const subApi of subChild.apis) {\n lines.push(`${indent} /** ${subApi.meta.description}${subApi.meta.auth ? ` [${subApi.meta.auth}]` : \"\"} */`);\n lines.push(\n `${indent} ${subApi.name}: (input: ${subApi.typeName}Input): Promise<${subApi.typeName}Output> => call(\"${subApi.path}\", input),`,\n );\n }\n // 추가 깊이 지원\n for (const [deepName, deepChild] of Object.entries(subChild.children)) {\n lines.push(`${indent} ${deepName}: {`);\n for (const deepApi of deepChild.apis) {\n lines.push(`${indent} /** ${deepApi.meta.description} */`);\n lines.push(\n `${indent} ${deepApi.name}: (input: ${deepApi.typeName}Input): Promise<${deepApi.typeName}Output> => call(\"${deepApi.path}\", input),`,\n );\n }\n lines.push(`${indent} },`);\n }\n lines.push(`${indent} },`);\n }\n\n lines.push(`${indent} },`);\n }\n\n // 루트 API\n for (const api of tree.apis) {\n lines.push(`${indent} /** ${api.meta.description} */`);\n lines.push(\n `${indent} ${api.name}: (input: ${api.typeName}Input): Promise<${api.typeName}Output> => call(\"${api.path}\", input),`,\n );\n }\n\n lines.push(`${indent}};`);\n return lines.join(\"\\n\");\n}\n\nfunction jsonSchemaToTsType(schema: Record<string, unknown>): string {\n if (!schema) return \"{}\";\n\n const type = schema.type as string;\n\n switch (type) {\n case \"object\": {\n const props = schema.properties as Record<string, Record<string, unknown>> | undefined;\n if (!props) return \"Record<string, unknown>\";\n const required = (schema.required as string[]) || [];\n\n const fields: string[] = [];\n for (const [key, propSchema] of Object.entries(props)) {\n const isRequired = required.includes(key);\n const tsType = jsonSchemaToInlineType(propSchema);\n fields.push(` ${key}${isRequired ? \"\" : \"?\"}: ${tsType};`);\n }\n return `{\\n${fields.join(\"\\n\")}\\n}`;\n }\n default:\n return \"{}\";\n }\n}\n\nfunction jsonSchemaToInlineType(schema: Record<string, unknown>): string {\n if (!schema) return \"unknown\";\n\n // const 값\n if (\"const\" in schema) return JSON.stringify(schema.const);\n\n // enum\n if (schema.enum) return (schema.enum as string[]).map((v) => JSON.stringify(v)).join(\" | \");\n\n // nullable\n const nullable = schema.nullable ? \" | null\" : \"\";\n const optional = schema.optional ? \"\" : \"\";\n\n const type = schema.type as string;\n\n switch (type) {\n case \"string\": return `string${nullable}`;\n case \"number\": return `number${nullable}`;\n case \"boolean\": return `boolean${nullable}`;\n case \"array\": {\n const items = schema.items as Record<string, unknown> | undefined;\n const itemType = items ? jsonSchemaToInlineType(items) : \"unknown\";\n return `${itemType}[]${nullable}`;\n }\n case \"object\": {\n const props = schema.properties as Record<string, Record<string, unknown>> | undefined;\n if (!props) {\n const additionalProps = schema.additionalProperties as Record<string, unknown> | undefined;\n if (additionalProps) return `Record<string, ${jsonSchemaToInlineType(additionalProps)}>${nullable}`;\n return `Record<string, unknown>${nullable}`;\n }\n const required = (schema.required as string[]) || [];\n const fields = Object.entries(props)\n .map(([k, v]) => `${k}${required.includes(k) ? \"\" : \"?\"}: ${jsonSchemaToInlineType(v)}`)\n .join(\"; \");\n return `{ ${fields} }${nullable}`;\n }\n default:\n if (schema.oneOf) {\n return (schema.oneOf as Record<string, unknown>[])\n .map(jsonSchemaToInlineType)\n .join(\" | \");\n }\n return \"unknown\";\n }\n}\n\n/**\n * 매니페스트에서 manifest.json 파일 내용 생성\n */\nexport function generateManifestJson(manifest: Manifest): string {\n return JSON.stringify(manifest, null, 2);\n}\n","/**\n * Clawfire Playground\n *\n * 웹 기반 API 탐색기: API 목록, 인증 테스트, 요청/응답 뷰어\n * 단일 HTML 파일로 생성되어 Firebase Hosting에 배포됩니다.\n */\n\nexport function generatePlaygroundHtml(options?: {\n title?: string;\n apiBaseUrl?: string;\n}): string {\n const title = options?.title || \"Clawfire Playground\";\n const apiBaseUrl = options?.apiBaseUrl || \"\";\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${title}</title>\n <style>\n :root {\n --bg: #0a0a0a;\n --surface: #141414;\n --surface2: #1e1e1e;\n --border: #2a2a2a;\n --text: #e5e5e5;\n --text2: #a3a3a3;\n --accent: #f97316;\n --accent2: #fb923c;\n --green: #22c55e;\n --red: #ef4444;\n --blue: #3b82f6;\n --yellow: #eab308;\n --radius: 8px;\n --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n --mono: 'JetBrains Mono', 'Fira Code', monospace;\n }\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { font-family: var(--font); background: var(--bg); color: var(--text); min-height: 100vh; }\n\n .layout { display: grid; grid-template-columns: 320px 1fr; min-height: 100vh; }\n .sidebar { background: var(--surface); border-right: 1px solid var(--border); overflow-y: auto; }\n .main { padding: 24px; overflow-y: auto; }\n\n .logo { padding: 20px; border-bottom: 1px solid var(--border); }\n .logo h1 { font-size: 20px; font-weight: 700; color: var(--accent); }\n .logo p { font-size: 12px; color: var(--text2); margin-top: 4px; }\n\n .auth-section { padding: 16px; border-bottom: 1px solid var(--border); }\n .auth-section label { font-size: 12px; color: var(--text2); display: block; margin-bottom: 6px; }\n .auth-input { width: 100%; padding: 8px 12px; background: var(--surface2); border: 1px solid var(--border);\n border-radius: var(--radius); color: var(--text); font-family: var(--mono); font-size: 12px; }\n .auth-status { font-size: 11px; margin-top: 6px; }\n .auth-status.ok { color: var(--green); }\n .auth-status.no { color: var(--text2); }\n\n .search { padding: 12px 16px; border-bottom: 1px solid var(--border); }\n .search input { width: 100%; padding: 8px 12px; background: var(--surface2); border: 1px solid var(--border);\n border-radius: var(--radius); color: var(--text); font-size: 13px; }\n\n .api-list { padding: 8px 0; }\n .api-group { padding: 4px 0; }\n .api-group-title { padding: 8px 16px; font-size: 11px; color: var(--text2); text-transform: uppercase;\n letter-spacing: 0.5px; font-weight: 600; }\n .api-item { padding: 8px 16px; cursor: pointer; transition: background 0.15s; display: flex; align-items: center;\n gap: 8px; font-size: 13px; }\n .api-item:hover { background: var(--surface2); }\n .api-item.active { background: var(--surface2); border-left: 2px solid var(--accent); }\n .api-badge { font-size: 10px; padding: 2px 6px; border-radius: 4px; font-weight: 600; }\n .badge-public { background: #22c55e20; color: var(--green); }\n .badge-auth { background: #3b82f620; color: var(--blue); }\n .badge-role { background: #eab30820; color: var(--yellow); }\n .badge-reauth { background: #ef444420; color: var(--red); }\n\n .panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 16px; }\n .panel-header { padding: 16px; border-bottom: 1px solid var(--border); display: flex; align-items: center;\n justify-content: space-between; }\n .panel-header h2 { font-size: 16px; font-weight: 600; }\n .panel-body { padding: 16px; }\n\n .field { margin-bottom: 12px; }\n .field label { font-size: 12px; color: var(--text2); display: block; margin-bottom: 4px; }\n .field-type { font-size: 11px; color: var(--text2); font-family: var(--mono); }\n .field-required { color: var(--red); font-size: 11px; }\n\n textarea, input[type=\"text\"] { width: 100%; padding: 10px 14px; background: var(--surface2);\n border: 1px solid var(--border); border-radius: var(--radius); color: var(--text);\n font-family: var(--mono); font-size: 13px; resize: vertical; }\n textarea { min-height: 200px; }\n\n .btn { padding: 10px 20px; border: none; border-radius: var(--radius); font-size: 14px;\n font-weight: 600; cursor: pointer; transition: all 0.15s; }\n .btn-primary { background: var(--accent); color: white; }\n .btn-primary:hover { background: var(--accent2); }\n .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }\n\n .response-section { margin-top: 16px; }\n .status-badge { font-size: 12px; padding: 4px 8px; border-radius: 4px; font-weight: 600; }\n .status-ok { background: #22c55e20; color: var(--green); }\n .status-err { background: #ef444420; color: var(--red); }\n pre { background: var(--surface2); padding: 16px; border-radius: var(--radius); overflow-x: auto;\n font-family: var(--mono); font-size: 13px; line-height: 1.5; white-space: pre-wrap; }\n\n .schema-info { font-size: 12px; color: var(--text2); line-height: 1.6; }\n .schema-info code { background: var(--surface2); padding: 2px 6px; border-radius: 4px; font-family: var(--mono);\n font-size: 11px; }\n\n .empty-state { text-align: center; padding: 80px 40px; color: var(--text2); }\n .empty-state h2 { font-size: 24px; margin-bottom: 8px; color: var(--text); }\n\n .timer { font-size: 12px; color: var(--text2); font-family: var(--mono); }\n\n @media (max-width: 768px) {\n .layout { grid-template-columns: 1fr; }\n .sidebar { max-height: 40vh; }\n }\n </style>\n</head>\n<body>\n <div class=\"layout\">\n <div class=\"sidebar\">\n <div class=\"logo\">\n <h1>Clawfire</h1>\n <p>API Playground</p>\n </div>\n <div class=\"auth-section\">\n <label>Bearer Token</label>\n <input type=\"text\" class=\"auth-input\" id=\"token-input\" placeholder=\"Paste your ID token...\">\n <div class=\"auth-status no\" id=\"auth-status\">Not authenticated</div>\n </div>\n <div class=\"search\">\n <input type=\"text\" id=\"search-input\" placeholder=\"Search APIs...\">\n </div>\n <div class=\"api-list\" id=\"api-list\"></div>\n </div>\n <div class=\"main\" id=\"main-content\">\n <div class=\"empty-state\">\n <h2>Select an API</h2>\n <p>Choose an API from the sidebar to test it.</p>\n </div>\n </div>\n </div>\n\n <script>\n const BASE_URL = ${JSON.stringify(apiBaseUrl)} || window.location.origin;\n let manifest = null;\n let selectedApi = null;\n\n async function loadManifest() {\n try {\n const res = await fetch(BASE_URL + '/api/__manifest', { method: 'POST' });\n manifest = await res.json();\n renderApiList(manifest.apis);\n } catch (e) {\n document.getElementById('api-list').innerHTML =\n '<div style=\"padding:16px;color:var(--red);font-size:13px;\">Failed to load API manifest. Make sure your server is running.</div>';\n }\n }\n\n function renderApiList(apis) {\n const groups = {};\n apis.forEach(api => {\n const parts = api.path.split('/').filter(Boolean);\n const group = parts.length > 1 ? parts[0] : 'root';\n if (!groups[group]) groups[group] = [];\n groups[group].push(api);\n });\n\n const el = document.getElementById('api-list');\n el.innerHTML = Object.entries(groups).map(([group, items]) =>\n '<div class=\"api-group\">' +\n '<div class=\"api-group-title\">' + group + '</div>' +\n items.map(api => {\n const auth = api.meta.auth || 'public';\n const badgeClass = 'badge-' + auth;\n return '<div class=\"api-item\" onclick=\"selectApi(\\\\'' + api.path + '\\\\')\">' +\n '<span class=\"api-badge ' + badgeClass + '\">' + auth.toUpperCase() + '</span>' +\n '<span>' + api.path + '</span>' +\n '</div>';\n }).join('') +\n '</div>'\n ).join('');\n }\n\n function selectApi(path) {\n selectedApi = manifest.apis.find(a => a.path === path);\n if (!selectedApi) return;\n\n document.querySelectorAll('.api-item').forEach(el => el.classList.remove('active'));\n event.currentTarget?.classList.add('active');\n\n const main = document.getElementById('main-content');\n const exampleInput = selectedApi.meta.exampleInput\n ? JSON.stringify(selectedApi.meta.exampleInput, null, 2)\n : generateExampleFromSchema(selectedApi.inputSchema);\n\n main.innerHTML =\n '<div class=\"panel\">' +\n '<div class=\"panel-header\">' +\n '<h2>POST ' + selectedApi.path + '</h2>' +\n '<span class=\"api-badge badge-' + (selectedApi.meta.auth || 'public') + '\">' +\n (selectedApi.meta.auth || 'public').toUpperCase() + '</span>' +\n '</div>' +\n '<div class=\"panel-body\">' +\n '<p style=\"color:var(--text2);margin-bottom:16px;\">' + (selectedApi.meta.description || '') + '</p>' +\n (selectedApi.meta.tags ? '<div style=\"margin-bottom:12px;\">' + selectedApi.meta.tags.map(t =>\n '<span style=\"background:var(--surface2);padding:2px 8px;border-radius:4px;font-size:11px;margin-right:4px;\">' + t + '</span>'\n ).join('') + '</div>' : '') +\n '<div class=\"schema-info\" style=\"margin-bottom:16px;\">' +\n '<strong>Input Schema:</strong><br>' + renderSchemaInfo(selectedApi.inputSchema) +\n '</div>' +\n '<div class=\"schema-info\" style=\"margin-bottom:16px;\">' +\n '<strong>Output Schema:</strong><br>' + renderSchemaInfo(selectedApi.outputSchema) +\n '</div>' +\n '</div>' +\n '</div>' +\n '<div class=\"panel\">' +\n '<div class=\"panel-header\"><h2>Request</h2></div>' +\n '<div class=\"panel-body\">' +\n '<textarea id=\"req-body\" placeholder=\"Request JSON body\">' + exampleInput + '</textarea>' +\n '<div style=\"margin-top:12px;display:flex;align-items:center;gap:12px;\">' +\n '<button class=\"btn btn-primary\" onclick=\"sendRequest()\">Send Request</button>' +\n '<span class=\"timer\" id=\"timer\"></span>' +\n '</div>' +\n '</div>' +\n '</div>' +\n '<div class=\"response-section\" id=\"response-section\"></div>';\n }\n\n function renderSchemaInfo(schema) {\n if (!schema || !schema.properties) return '<code>void</code>';\n return Object.entries(schema.properties).map(([key, prop]) => {\n const required = schema.required?.includes(key);\n const type = prop.type || 'unknown';\n const enumVals = prop.enum ? ' (' + prop.enum.join(', ') + ')' : '';\n return '<code>' + key + '</code>: <span class=\"field-type\">' + type + enumVals + '</span>' +\n (required ? ' <span class=\"field-required\">required</span>' : ' <span style=\"color:var(--text2);font-size:11px;\">optional</span>');\n }).join('<br>');\n }\n\n function generateExampleFromSchema(schema) {\n if (!schema || !schema.properties) return '{}';\n const obj = {};\n for (const [key, prop] of Object.entries(schema.properties)) {\n if (prop.enum) { obj[key] = prop.enum[0]; continue; }\n switch (prop.type) {\n case 'string': obj[key] = prop.format === 'email' ? 'user@example.com' : 'string'; break;\n case 'number': obj[key] = 0; break;\n case 'boolean': obj[key] = false; break;\n case 'array': obj[key] = []; break;\n case 'object': obj[key] = {}; break;\n default: obj[key] = null;\n }\n }\n return JSON.stringify(obj, null, 2);\n }\n\n async function sendRequest() {\n if (!selectedApi) return;\n const body = document.getElementById('req-body').value;\n const token = document.getElementById('token-input').value;\n const timer = document.getElementById('timer');\n const section = document.getElementById('response-section');\n\n let parsed;\n try { parsed = JSON.parse(body); } catch {\n section.innerHTML = '<div class=\"panel\"><div class=\"panel-body\"><pre style=\"color:var(--red)\">Invalid JSON</pre></div></div>';\n return;\n }\n\n const start = performance.now();\n timer.textContent = 'Sending...';\n\n try {\n const headers = { 'Content-Type': 'application/json' };\n if (token) headers['Authorization'] = 'Bearer ' + token;\n\n const res = await fetch(BASE_URL + '/api' + selectedApi.path, {\n method: 'POST', headers, body: JSON.stringify(parsed)\n });\n const elapsed = Math.round(performance.now() - start);\n timer.textContent = elapsed + 'ms';\n\n const json = await res.json();\n const isOk = res.ok;\n\n section.innerHTML =\n '<div class=\"panel\">' +\n '<div class=\"panel-header\">' +\n '<h2>Response</h2>' +\n '<span class=\"status-badge ' + (isOk ? 'status-ok' : 'status-err') + '\">' +\n res.status + ' ' + res.statusText + '</span>' +\n '</div>' +\n '<div class=\"panel-body\"><pre>' + syntaxHighlight(JSON.stringify(json, null, 2)) + '</pre></div>' +\n '</div>';\n } catch (e) {\n const elapsed = Math.round(performance.now() - start);\n timer.textContent = elapsed + 'ms';\n section.innerHTML =\n '<div class=\"panel\"><div class=\"panel-body\"><pre style=\"color:var(--red)\">Network error: ' + e.message + '</pre></div></div>';\n }\n }\n\n function syntaxHighlight(json) {\n return json.replace(/(\"(\\\\\\\\u[a-fA-F0-9]{4}|\\\\\\\\[^u]|[^\\\\\\\\\"])*\"(\\\\s*:)?)|\\\\b(true|false|null)\\\\b|-?\\\\d+(\\\\.\\\\d+)?([eE][+-]?\\\\d+)?/g,\n function(match) {\n let cls = 'color:#eab308';\n if (/^\"/.test(match)) {\n if (/:$/.test(match)) cls = 'color:#3b82f6';\n else cls = 'color:#22c55e';\n } else if (/true|false/.test(match)) cls = 'color:#f97316';\n else if (/null/.test(match)) cls = 'color:#ef4444';\n return '<span style=\"' + cls + '\">' + match + '</span>';\n }\n );\n }\n\n // Search\n document.getElementById('search-input')?.addEventListener('input', (e) => {\n if (!manifest) return;\n const q = e.target.value.toLowerCase();\n const filtered = manifest.apis.filter(a => a.path.toLowerCase().includes(q) || a.meta.description?.toLowerCase().includes(q));\n renderApiList(filtered);\n });\n\n // Token status\n document.getElementById('token-input')?.addEventListener('input', (e) => {\n const el = document.getElementById('auth-status');\n if (e.target.value) {\n el.textContent = 'Token set';\n el.className = 'auth-status ok';\n } else {\n el.textContent = 'Not authenticated';\n el.className = 'auth-status no';\n }\n });\n\n loadManifest();\n </script>\n</body>\n</html>`;\n}\n","/**\n * Clawfire Schema & Contract System\n *\n * 모든 API는 input/output schema + meta + handler로 구성된 \"계약(Contract)\"으로 정의됩니다.\n * Zod를 사용하여 타입 안전성과 런타임 검증을 동시에 보장합니다.\n */\nimport { z, type ZodType, type ZodObject, type ZodRawShape } from \"zod\";\n\n// ─── Types ───────────────────────────────────────────────────────────\n\n/** 인증 컨텍스트 */\nexport interface AuthContext {\n uid: string;\n email?: string;\n emailVerified?: boolean;\n role?: string;\n customClaims?: Record<string, unknown>;\n token?: string;\n}\n\n/** API 핸들러에 전달되는 컨텍스트 */\nexport interface HandlerContext {\n auth: AuthContext | null;\n /** 재인증 여부 (민감 작업용) */\n reauthenticated?: boolean;\n /** 원본 요청 헤더 */\n headers?: Record<string, string>;\n /** 요청 IP */\n ip?: string;\n}\n\n/** 권한 수준 */\nexport type AuthLevel = \"public\" | \"authenticated\" | \"role\" | \"reauth\";\n\n/** API 메타데이터 */\nexport interface APIMeta {\n /** API 설명 (AI/Playground용) */\n description: string;\n /** 태그 (그룹화용) */\n tags?: string[];\n /** 인증 요구 수준 */\n auth?: AuthLevel;\n /** 필요 역할 (auth가 'role'일 때) */\n roles?: string[];\n /** 재인증 필요 여부 */\n reauth?: boolean;\n /** Rate limit (초당 요청 수) */\n rateLimit?: number;\n /** 비활성화 여부 */\n deprecated?: boolean;\n /** 예시 입력값 */\n exampleInput?: unknown;\n /** 예시 출력값 */\n exampleOutput?: unknown;\n}\n\n/** API 계약 정의 */\nexport interface APIContract<\n TInput extends ZodType = ZodType,\n TOutput extends ZodType = ZodType,\n> {\n /** 입력 스키마 */\n input: TInput;\n /** 출력 스키마 */\n output: TOutput;\n /** 메타데이터 */\n meta: APIMeta;\n /** 핸들러 함수 */\n handler: (\n input: z.infer<TInput>,\n ctx: HandlerContext,\n ) => Promise<z.infer<TOutput>>;\n}\n\n/** 모델 필드 정의 */\nexport interface ModelField {\n type: \"string\" | \"number\" | \"boolean\" | \"timestamp\" | \"array\" | \"map\" | \"reference\" | \"geopoint\";\n required?: boolean;\n description?: string;\n default?: unknown;\n /** 배열 아이템 타입 */\n items?: ModelField;\n /** 맵 값 타입 */\n values?: ModelField;\n /** 참조 대상 컬렉션 */\n ref?: string;\n /** enum 값 리스트 */\n enum?: string[];\n}\n\n/** 모델 정의 */\nexport interface ModelDefinition {\n /** 컬렉션 이름 */\n collection: string;\n /** 필드 정의 */\n fields: Record<string, ModelField>;\n /** 서브컬렉션 */\n subcollections?: Record<string, ModelDefinition>;\n /** 인덱스 */\n indexes?: ModelIndex[];\n /** 보안 규칙 */\n rules?: ModelRules;\n /** 타임스탬프 자동 생성 */\n timestamps?: boolean;\n /** 소프트 삭제 */\n softDelete?: boolean;\n}\n\n/** Firestore 인덱스 */\nexport interface ModelIndex {\n fields: Array<{ field: string; order?: \"asc\" | \"desc\" }>;\n}\n\n/** 모델 보안 규칙 */\nexport interface ModelRules {\n read?: AuthLevel;\n create?: AuthLevel;\n update?: AuthLevel;\n delete?: AuthLevel;\n readRoles?: string[];\n createRoles?: string[];\n updateRoles?: string[];\n deleteRoles?: string[];\n /** 소유자만 읽기/쓰기 가능 필드 */\n ownerField?: string;\n}\n\n// ─── Builders ────────────────────────────────────────────────────────\n\n/**\n * API 계약 정의\n *\n * @example\n * ```ts\n * export default defineAPI({\n * input: z.object({ name: z.string() }),\n * output: z.object({ id: z.string(), name: z.string() }),\n * meta: { description: \"상품 생성\", auth: \"authenticated\" },\n * handler: async (input, ctx) => {\n * const id = await db.create(\"products\", input);\n * return { id, ...input };\n * }\n * });\n * ```\n */\nexport function defineAPI<\n TInput extends ZodType,\n TOutput extends ZodType,\n>(contract: APIContract<TInput, TOutput>): APIContract<TInput, TOutput> {\n return contract;\n}\n\n/**\n * 모델(Firestore 컬렉션) 정의\n *\n * @example\n * ```ts\n * export const Product = defineModel({\n * collection: \"products\",\n * fields: {\n * name: { type: \"string\", required: true },\n * price: { type: \"number\", required: true },\n * tags: { type: \"array\", items: { type: \"string\" } },\n * },\n * timestamps: true,\n * rules: { read: \"public\", create: \"authenticated\" }\n * });\n * ```\n */\nexport function defineModel(definition: ModelDefinition): ModelDefinition {\n return {\n timestamps: true,\n ...definition,\n };\n}\n\n// ─── Schema Utilities ────────────────────────────────────────────────\n\n/** Zod 스키마에서 JSON Schema 생성 (Playground/문서용) */\nexport function zodToJsonSchema(schema: ZodType): Record<string, unknown> {\n return extractZodShape(schema);\n}\n\nfunction extractZodShape(schema: ZodType): Record<string, unknown> {\n const def = (schema as any)._def;\n\n if (!def) return { type: \"unknown\" };\n\n switch (def.typeName) {\n case \"ZodObject\": {\n const shape = (schema as ZodObject<ZodRawShape>).shape;\n const properties: Record<string, unknown> = {};\n const required: string[] = [];\n\n for (const [key, value] of Object.entries(shape)) {\n properties[key] = extractZodShape(value as ZodType);\n if (!(value as any).isOptional?.()) {\n const innerDef = (value as any)._def;\n if (innerDef?.typeName !== \"ZodOptional\" && innerDef?.typeName !== \"ZodDefault\") {\n required.push(key);\n }\n }\n }\n\n return { type: \"object\", properties, ...(required.length > 0 ? { required } : {}) };\n }\n case \"ZodString\":\n return { type: \"string\", ...(def.checks?.length ? extractStringChecks(def.checks) : {}) };\n case \"ZodNumber\":\n return { type: \"number\" };\n case \"ZodBoolean\":\n return { type: \"boolean\" };\n case \"ZodArray\":\n return { type: \"array\", items: extractZodShape(def.type) };\n case \"ZodEnum\":\n return { type: \"string\", enum: def.values };\n case \"ZodOptional\":\n return { ...extractZodShape(def.innerType), optional: true };\n case \"ZodDefault\":\n return { ...extractZodShape(def.innerType), default: def.defaultValue() };\n case \"ZodNullable\":\n return { ...extractZodShape(def.innerType), nullable: true };\n case \"ZodLiteral\":\n return { type: typeof def.value, const: def.value };\n case \"ZodUnion\":\n return { oneOf: def.options.map((o: ZodType) => extractZodShape(o)) };\n case \"ZodRecord\":\n return { type: \"object\", additionalProperties: extractZodShape(def.valueType) };\n case \"ZodDate\":\n return { type: \"string\", format: \"date-time\" };\n default:\n return { type: \"unknown\" };\n }\n}\n\nfunction extractStringChecks(checks: Array<{ kind: string; value?: unknown }>): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const check of checks) {\n switch (check.kind) {\n case \"min\": result.minLength = check.value; break;\n case \"max\": result.maxLength = check.value; break;\n case \"email\": result.format = \"email\"; break;\n case \"url\": result.format = \"uri\"; break;\n case \"uuid\": result.format = \"uuid\"; break;\n }\n }\n return result;\n}\n\n/** 모델 정의에서 Zod 스키마 자동 생성 */\nexport function modelToZodSchema(model: ModelDefinition): ZodObject<ZodRawShape> {\n const shape: ZodRawShape = {};\n\n for (const [key, field] of Object.entries(model.fields)) {\n let fieldSchema: ZodType = fieldToZod(field);\n if (!field.required) {\n fieldSchema = fieldSchema.optional();\n }\n shape[key] = fieldSchema;\n }\n\n if (model.timestamps) {\n shape.createdAt = z.string().datetime().optional();\n shape.updatedAt = z.string().datetime().optional();\n }\n\n if (model.softDelete) {\n shape.deletedAt = z.string().datetime().nullable().optional();\n }\n\n return z.object(shape);\n}\n\nfunction fieldToZod(field: ModelField): ZodType {\n switch (field.type) {\n case \"string\":\n if (field.enum) return z.enum(field.enum as [string, ...string[]]);\n return z.string();\n case \"number\":\n return z.number();\n case \"boolean\":\n return z.boolean();\n case \"timestamp\":\n return z.string().datetime();\n case \"array\":\n if (field.items) return z.array(fieldToZod(field.items));\n return z.array(z.unknown());\n case \"map\":\n if (field.values) return z.record(z.string(), fieldToZod(field.values));\n return z.record(z.string(), z.unknown());\n case \"reference\":\n return z.string(); // 참조는 문서 경로 문자열\n case \"geopoint\":\n return z.object({ latitude: z.number(), longitude: z.number() });\n default:\n return z.unknown();\n }\n}\n\n// ─── Manifest ────────────────────────────────────────────────────────\n\n/** API 매니페스트 항목 */\nexport interface ManifestEntry {\n path: string;\n method: \"POST\"; // Clawfire는 모두 POST\n meta: APIMeta;\n inputSchema: Record<string, unknown>;\n outputSchema: Record<string, unknown>;\n}\n\n/** 전체 매니페스트 */\nexport interface Manifest {\n version: string;\n generatedAt: string;\n apis: ManifestEntry[];\n models: Record<string, ModelDefinition>;\n}\n\n/** 계약에서 매니페스트 항목 생성 */\nexport function contractToManifest(\n path: string,\n contract: APIContract,\n): ManifestEntry {\n return {\n path,\n method: \"POST\",\n meta: contract.meta,\n inputSchema: zodToJsonSchema(contract.input),\n outputSchema: zodToJsonSchema(contract.output),\n };\n}\n","/**\n * Clawfire Route Discovery\n *\n * 파일 시스템에서 라우트 자동 발견 (빌드 타임 & 런타임)\n */\nimport { resolve, relative, join } from \"path\";\nimport { existsSync, readdirSync, statSync } from \"fs\";\n\nexport interface DiscoveredRoute {\n /** 파일 경로 (상대) */\n filePath: string;\n /** API 경로 (/products/list) */\n apiPath: string;\n /** 동적 파라미터 이름 */\n params: string[];\n}\n\n/**\n * routes 디렉터리에서 라우트 파일 자동 발견\n *\n * @param routesDir - routes 디렉터리 절대 경로\n * @returns 발견된 라우트 목록\n *\n * @example\n * ```\n * functions/routes/products/list.ts → { apiPath: \"/products/list\", params: [] }\n * functions/routes/products/[id]/get.ts → { apiPath: \"/products/:id/get\", params: [\"id\"] }\n * functions/routes/health.ts → { apiPath: \"/health\", params: [] }\n * ```\n */\nexport function discoverRoutes(routesDir: string): DiscoveredRoute[] {\n if (!existsSync(routesDir)) {\n return [];\n }\n\n const routes: DiscoveredRoute[] = [];\n scanDirectory(routesDir, routesDir, routes);\n return routes.sort((a, b) => a.apiPath.localeCompare(b.apiPath));\n}\n\nfunction scanDirectory(baseDir: string, currentDir: string, routes: DiscoveredRoute[]): void {\n const entries = readdirSync(currentDir);\n\n for (const entry of entries) {\n const fullPath = join(currentDir, entry);\n const stat = statSync(fullPath);\n\n if (stat.isDirectory()) {\n // 숨김 디렉터리, node_modules 무시\n if (entry.startsWith(\".\") || entry === \"node_modules\") continue;\n scanDirectory(baseDir, fullPath, routes);\n } else if (stat.isFile()) {\n // .ts, .js 파일만\n if (!entry.endsWith(\".ts\") && !entry.endsWith(\".js\")) continue;\n // index, _로 시작하는 파일 무시\n if (entry.startsWith(\"_\")) continue;\n // .d.ts 무시\n if (entry.endsWith(\".d.ts\")) continue;\n\n const relativePath = relative(baseDir, fullPath);\n const route = filePathToRoute(relativePath);\n routes.push(route);\n }\n }\n}\n\nfunction filePathToRoute(filePath: string): DiscoveredRoute {\n const params: string[] = [];\n\n // 확장자 제거\n let routePath = filePath.replace(/\\.(ts|js)$/, \"\");\n\n // Windows 경로 → POSIX\n routePath = routePath.replace(/\\\\/g, \"/\");\n\n // index 파일은 디렉터리 자체\n if (routePath.endsWith(\"/index\") || routePath === \"index\") {\n routePath = routePath.replace(/\\/?index$/, \"\");\n }\n\n // [param] → :param 변환\n routePath = routePath.replace(/\\[([^\\]]+)\\]/g, (_, param) => {\n params.push(param);\n return `:${param}`;\n });\n\n // 앞에 / 추가\n const apiPath = `/${routePath}`;\n\n return {\n filePath,\n apiPath,\n params,\n };\n}\n\n/**\n * 라우트 파일에서 import하여 라우터에 등록하는 코드 생성 (빌드 타임)\n */\nexport function generateRouteImports(routes: DiscoveredRoute[], routesDir: string): string {\n const lines: string[] = [\n '// AUTO-GENERATED by Clawfire — DO NOT EDIT',\n '// This file is regenerated whenever routes change.',\n '',\n 'import { createRouter } from \"clawfire/functions\";',\n '',\n ];\n\n routes.forEach((route, i) => {\n const importPath = `./${route.filePath.replace(/\\.(ts|js)$/, \".js\")}`;\n lines.push(`import route_${i} from \"${importPath}\";`);\n });\n\n lines.push('');\n lines.push('export function registerAllRoutes(router: ReturnType<typeof createRouter>) {');\n\n routes.forEach((route, i) => {\n lines.push(` router.register(\"${route.apiPath}\", route_${i});`);\n });\n\n lines.push(' return router;');\n lines.push('}');\n\n return lines.join('\\n');\n}\n"],"mappings":"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AAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uBA8HO,KAAK,UAAU,UAAU,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqMjD;;;AChVA,SAAS,SAAyD;;;ACDlE,SAAkB,UAAU,YAAY;AACxC,SAAS,YAAY,aAAa,gBAAgB;AAwB3C,SAAS,eAAe,WAAsC;AACnE,MAAI,CAAC,WAAW,SAAS,GAAG;AAC1B,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,SAA4B,CAAC;AACnC,gBAAc,WAAW,WAAW,MAAM;AAC1C,SAAO,OAAO,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,cAAc,EAAE,OAAO,CAAC;AACjE;AAEA,SAAS,cAAc,SAAiB,YAAoB,QAAiC;AAC3F,QAAM,UAAU,YAAY,UAAU;AAEtC,aAAW,SAAS,SAAS;AAC3B,UAAM,WAAW,KAAK,YAAY,KAAK;AACvC,UAAM,OAAO,SAAS,QAAQ;AAE9B,QAAI,KAAK,YAAY,GAAG;AAEtB,UAAI,MAAM,WAAW,GAAG,KAAK,UAAU,eAAgB;AACvD,oBAAc,SAAS,UAAU,MAAM;AAAA,IACzC,WAAW,KAAK,OAAO,GAAG;AAExB,UAAI,CAAC,MAAM,SAAS,KAAK,KAAK,CAAC,MAAM,SAAS,KAAK,EAAG;AAEtD,UAAI,MAAM,WAAW,GAAG,EAAG;AAE3B,UAAI,MAAM,SAAS,OAAO,EAAG;AAE7B,YAAM,eAAe,SAAS,SAAS,QAAQ;AAC/C,YAAM,QAAQ,gBAAgB,YAAY;AAC1C,aAAO,KAAK,KAAK;AAAA,IACnB;AAAA,EACF;AACF;AAEA,SAAS,gBAAgB,UAAmC;AAC1D,QAAM,SAAmB,CAAC;AAG1B,MAAI,YAAY,SAAS,QAAQ,cAAc,EAAE;AAGjD,cAAY,UAAU,QAAQ,OAAO,GAAG;AAGxC,MAAI,UAAU,SAAS,QAAQ,KAAK,cAAc,SAAS;AACzD,gBAAY,UAAU,QAAQ,aAAa,EAAE;AAAA,EAC/C;AAGA,cAAY,UAAU,QAAQ,iBAAiB,CAAC,GAAG,UAAU;AAC3D,WAAO,KAAK,KAAK;AACjB,WAAO,IAAI,KAAK;AAAA,EAClB,CAAC;AAGD,QAAM,UAAU,IAAI,SAAS;AAE7B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAKO,SAAS,qBAAqB,QAA2B,WAA2B;AACzF,QAAM,QAAkB;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO,QAAQ,CAAC,OAAO,MAAM;AAC3B,UAAM,aAAa,KAAK,MAAM,SAAS,QAAQ,cAAc,KAAK,CAAC;AACnE,UAAM,KAAK,gBAAgB,CAAC,UAAU,UAAU,IAAI;AAAA,EACtD,CAAC;AAED,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,8EAA8E;AAEzF,SAAO,QAAQ,CAAC,OAAO,MAAM;AAC3B,UAAM,KAAK,sBAAsB,MAAM,OAAO,YAAY,CAAC,IAAI;AAAA,EACjE,CAAC;AAED,QAAM,KAAK,kBAAkB;AAC7B,QAAM,KAAK,GAAG;AAEd,SAAO,MAAM,KAAK,IAAI;AACxB;","names":[]}
@@ -1382,7 +1382,29 @@ function generateDashboardHtml(options) {
1382
1382
  <div id="service-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px;"></div>
1383
1383
  </div>
1384
1384
 
1385
- <!-- Section 2: Config Overview -->
1385
+ <!-- Section 2: Deploy -->
1386
+ <div style="margin-bottom:32px;">
1387
+ <h2 style="font-size:18px;font-weight:700;color:#f97316;margin-bottom:16px;">Deploy</h2>
1388
+ <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px;">
1389
+ <div style="padding:16px;border-radius:8px;border:1px solid #2a2a2a;background:#141414;">
1390
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
1391
+ <span style="font-size:16px;">&#127760;</span>
1392
+ <span style="font-weight:600;color:#e5e5e5;">Hosting</span>
1393
+ </div>
1394
+ <div style="font-size:13px;color:#a3a3a3;margin-bottom:12px;">Deploy your app to Firebase Hosting</div>
1395
+ <button id="deploy-hosting-btn" onclick="deployHosting()" style="padding:8px 20px;background:#3b82f6;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;">
1396
+ Deploy Hosting
1397
+ </button>
1398
+ <div id="deploy-hosting-status" style="display:none;margin-top:10px;font-size:13px;padding:10px 14px;border-radius:6px;"></div>
1399
+ <div id="deploy-hosting-url" style="display:none;margin-top:8px;padding:10px 14px;border-radius:6px;background:#0a1a0a;border:1px solid #22c55e;">
1400
+ <div style="font-size:11px;color:#a3a3a3;margin-bottom:4px;">Live URL</div>
1401
+ <a id="deploy-hosting-link" href="#" target="_blank" style="color:#22c55e;font-family:monospace;font-size:14px;text-decoration:none;word-break:break-all;"></a>
1402
+ </div>
1403
+ </div>
1404
+ </div>
1405
+ </div>
1406
+
1407
+ <!-- Section 3: Config Overview -->
1386
1408
  <div style="margin-bottom:32px;">
1387
1409
  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
1388
1410
  <h2 style="font-size:18px;font-weight:700;color:#f97316;">Config Overview</h2>
@@ -1406,7 +1428,7 @@ function generateDashboardHtml(options) {
1406
1428
  </div>
1407
1429
  </div>
1408
1430
 
1409
- <!-- Section 3: Environment Variables -->
1431
+ <!-- Section 4: Environment Variables -->
1410
1432
  <div style="margin-bottom:32px;">
1411
1433
  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
1412
1434
  <h2 style="font-size:18px;font-weight:700;color:#f97316;">Environment Variables</h2>
@@ -2006,16 +2028,22 @@ function generateDashboardHtml(options) {
2006
2028
  var statusColors = { configured: '#22c55e', placeholder: '#eab308', missing: '#666' };
2007
2029
  var statusLabels = { configured: 'Ready', placeholder: 'Needs Setup', missing: 'Not Configured' };
2008
2030
 
2031
+ var enableableServices = { 'Hosting': 'hosting', 'Firestore': 'firestore', 'Storage': 'storage' };
2009
2032
  data.services.forEach(function(svc) {
2010
2033
  var card = document.createElement('div');
2011
2034
  card.style.cssText = 'padding:16px;border-radius:8px;border:1px solid #2a2a2a;background:#141414;';
2035
+ var enableBtn = '';
2036
+ if (svc.status === 'missing' && enableableServices[svc.name]) {
2037
+ enableBtn = '<button onclick="enableService(\\'' + enableableServices[svc.name] + '\\')" style="margin-top:8px;padding:4px 12px;background:#f97316;color:#000;border:none;border-radius:4px;font-size:11px;font-weight:600;cursor:pointer;">Enable</button>';
2038
+ }
2012
2039
  card.innerHTML =
2013
2040
  '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">' +
2014
2041
  '<span style="width:8px;height:8px;border-radius:50%;background:' + statusColors[svc.status] + ';display:inline-block;"></span>' +
2015
2042
  '<span style="font-weight:600;color:#e5e5e5;">' + svc.name + '</span>' +
2016
2043
  '</div>' +
2017
2044
  '<div style="font-size:12px;color:' + statusColors[svc.status] + ';">' + statusLabels[svc.status] + '</div>' +
2018
- (svc.detail ? '<div style="font-size:11px;color:#666;margin-top:4px;">' + svc.detail + '</div>' : '');
2045
+ (svc.detail ? '<div style="font-size:11px;color:#666;margin-top:4px;">' + svc.detail + '</div>' : '') +
2046
+ enableBtn;
2019
2047
  grid.appendChild(card);
2020
2048
  });
2021
2049
 
@@ -2200,6 +2228,67 @@ function generateDashboardHtml(options) {
2200
2228
  });
2201
2229
  }
2202
2230
 
2231
+ // \u2500\u2500\u2500 Deploy Hosting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2232
+ window.deployHosting = function() {
2233
+ var btn = document.getElementById('deploy-hosting-btn');
2234
+ var status = document.getElementById('deploy-hosting-status');
2235
+ var urlBox = document.getElementById('deploy-hosting-url');
2236
+ btn.disabled = true;
2237
+ btn.textContent = 'Deploying...';
2238
+ status.textContent = 'Deploying to Firebase Hosting... This may take up to 2 minutes.';
2239
+ status.style.cssText = 'display:block;margin-top:10px;font-size:13px;padding:10px 14px;border-radius:6px;background:#0a0a1a;border:1px solid #3b82f6;color:#3b82f6;';
2240
+ urlBox.style.display = 'none';
2241
+
2242
+ fetch(API + '/__dev/deploy/hosting', { method: 'POST' })
2243
+ .then(function(r) { return r.json(); })
2244
+ .then(function(data) {
2245
+ if (data.success) {
2246
+ status.textContent = data.message;
2247
+ status.style.cssText = 'display:block;margin-top:10px;font-size:13px;padding:10px 14px;border-radius:6px;background:#0a1a0a;border:1px solid #22c55e;color:#22c55e;';
2248
+ if (data.url) {
2249
+ var link = document.getElementById('deploy-hosting-link');
2250
+ link.href = data.url;
2251
+ link.textContent = data.url;
2252
+ urlBox.style.display = 'block';
2253
+ }
2254
+ } else {
2255
+ status.textContent = data.message;
2256
+ status.style.cssText = 'display:block;margin-top:10px;font-size:13px;padding:10px 14px;border-radius:6px;background:#1c0808;border:1px solid #ef4444;color:#ef4444;';
2257
+ }
2258
+ btn.disabled = false;
2259
+ btn.textContent = 'Deploy Hosting';
2260
+ // Refresh firebase status
2261
+ fetch(API + '/__dev/firebase-status').then(function(r) { return r.json(); }).then(function(d) { renderFirebaseStatus(d); }).catch(function(){});
2262
+ })
2263
+ .catch(function(err) {
2264
+ status.textContent = 'Error: ' + err.message;
2265
+ status.style.cssText = 'display:block;margin-top:10px;font-size:13px;padding:10px 14px;border-radius:6px;background:#1c0808;border:1px solid #ef4444;color:#ef4444;';
2266
+ btn.disabled = false;
2267
+ btn.textContent = 'Deploy Hosting';
2268
+ });
2269
+ };
2270
+
2271
+ // \u2500\u2500\u2500 Enable Service \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2272
+ window.enableService = function(service) {
2273
+ fetch(API + '/__dev/enable-service', {
2274
+ method: 'POST',
2275
+ headers: { 'Content-Type': 'application/json' },
2276
+ body: JSON.stringify({ service: service })
2277
+ })
2278
+ .then(function(r) { return r.json(); })
2279
+ .then(function(data) {
2280
+ if (data.success) {
2281
+ // Refresh firebase status to show updated service cards
2282
+ fetch(API + '/__dev/firebase-status').then(function(r) { return r.json(); }).then(function(d) { renderFirebaseStatus(d); }).catch(function(){});
2283
+ } else {
2284
+ alert('Failed to enable ' + service + ': ' + data.message);
2285
+ }
2286
+ })
2287
+ .catch(function(err) {
2288
+ alert('Error: ' + err.message);
2289
+ });
2290
+ };
2291
+
2203
2292
  // \u2500\u2500\u2500 Environment Variables \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2204
2293
  function renderEnvVars(data) {
2205
2294
  envData = data.variables || [];
@@ -2669,6 +2758,113 @@ var FirebaseSetup = class {
2669
2758
  this.saveState({ webAppId: appId, webAppDisplayName: displayName });
2670
2759
  return { success: true, message: `Web app "${displayName}" selected.` };
2671
2760
  }
2761
+ // ─── Deploy ─────────────────────────────────────────────────────────
2762
+ async deployHosting() {
2763
+ const firebaseJsonPath = resolve4(this.projectDir, "firebase.json");
2764
+ if (!existsSync5(firebaseJsonPath)) {
2765
+ return { success: false, message: "firebase.json not found. Enable hosting first." };
2766
+ }
2767
+ try {
2768
+ const config = JSON.parse(readFileSync4(firebaseJsonPath, "utf-8"));
2769
+ if (!config.hosting) {
2770
+ return { success: false, message: "Hosting not configured in firebase.json. Click Enable first." };
2771
+ }
2772
+ } catch {
2773
+ return { success: false, message: "Invalid firebase.json." };
2774
+ }
2775
+ try {
2776
+ const output = await this.execTimeout(
2777
+ "firebase",
2778
+ ["deploy", "--only", "hosting", "--json"],
2779
+ 12e4
2780
+ // 2 min timeout
2781
+ );
2782
+ let url = "";
2783
+ try {
2784
+ const data = JSON.parse(output);
2785
+ if (data?.result) {
2786
+ for (const key of Object.keys(data.result)) {
2787
+ if (key.startsWith("hosting") && typeof data.result[key] === "object") {
2788
+ url = data.result[key]?.url || data.result[key]?.site?.url || "";
2789
+ if (url) break;
2790
+ }
2791
+ }
2792
+ }
2793
+ } catch {
2794
+ const urlMatch = output.match(/https:\/\/[a-z0-9-]+\.web\.app/i);
2795
+ if (urlMatch) url = urlMatch[0];
2796
+ }
2797
+ if (!url) {
2798
+ const state = this.loadState();
2799
+ if (state.projectId) {
2800
+ url = `https://${state.projectId}.web.app`;
2801
+ }
2802
+ }
2803
+ return { success: true, url: url || void 0, message: "Hosting deployed successfully!" };
2804
+ } catch (err) {
2805
+ const msg = err instanceof Error ? err.message : "Unknown error";
2806
+ return { success: false, message: `Deploy failed: ${msg}` };
2807
+ }
2808
+ }
2809
+ // ─── Service Enable ────────────────────────────────────────────────
2810
+ enableService(service) {
2811
+ const firebaseJsonPath = resolve4(this.projectDir, "firebase.json");
2812
+ let config = {};
2813
+ if (existsSync5(firebaseJsonPath)) {
2814
+ try {
2815
+ config = JSON.parse(readFileSync4(firebaseJsonPath, "utf-8"));
2816
+ } catch {
2817
+ config = {};
2818
+ }
2819
+ }
2820
+ switch (service) {
2821
+ case "hosting": {
2822
+ if (!config.hosting) {
2823
+ config.hosting = {
2824
+ public: "public",
2825
+ ignore: ["firebase.json", "**/.*", "**/node_modules/**"],
2826
+ rewrites: [{ source: "**", destination: "/index.html" }]
2827
+ };
2828
+ }
2829
+ break;
2830
+ }
2831
+ case "firestore": {
2832
+ if (!config.firestore) {
2833
+ config.firestore = {
2834
+ rules: "firestore.rules",
2835
+ indexes: "firestore.indexes.json"
2836
+ };
2837
+ }
2838
+ const rulesPath = resolve4(this.projectDir, "firestore.rules");
2839
+ if (!existsSync5(rulesPath)) {
2840
+ writeFileSync2(
2841
+ rulesPath,
2842
+ "rules_version = '2';\nservice cloud.firestore {\n match /databases/{database}/documents {\n match /{document=**} {\n allow read, write: if request.auth != null;\n }\n }\n}\n"
2843
+ );
2844
+ }
2845
+ const indexesPath = resolve4(this.projectDir, "firestore.indexes.json");
2846
+ if (!existsSync5(indexesPath)) {
2847
+ writeFileSync2(indexesPath, JSON.stringify({ indexes: [], fieldOverrides: [] }, null, 2) + "\n");
2848
+ }
2849
+ break;
2850
+ }
2851
+ case "storage": {
2852
+ if (!config.storage) {
2853
+ config.storage = { rules: "storage.rules" };
2854
+ }
2855
+ const storageRulesPath = resolve4(this.projectDir, "storage.rules");
2856
+ if (!existsSync5(storageRulesPath)) {
2857
+ writeFileSync2(
2858
+ storageRulesPath,
2859
+ "rules_version = '2';\nservice firebase.storage {\n match /b/{bucket}/o {\n match /{allPaths=**} {\n allow read, write: if request.auth != null;\n }\n }\n}\n"
2860
+ );
2861
+ }
2862
+ break;
2863
+ }
2864
+ }
2865
+ writeFileSync2(firebaseJsonPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2866
+ return { success: true, message: `${service} enabled in firebase.json.` };
2867
+ }
2672
2868
  // ─── Helpers ───────────────────────────────────────────────────────
2673
2869
  execTimeout(command, args, timeoutMs) {
2674
2870
  return new Promise((resolve6, reject) => {
@@ -2950,8 +3146,8 @@ var DevServer = class {
2950
3146
  onSetupRoutes: options.onSetupRoutes || (() => {
2951
3147
  })
2952
3148
  };
2953
- this.routesDir = resolve5(this.options.projectDir, "app/routes");
2954
- this.schemasDir = resolve5(this.options.projectDir, "app/schemas");
3149
+ this.routesDir = resolve5(this.options.projectDir, "functions/routes");
3150
+ this.schemasDir = resolve5(this.options.projectDir, "functions/schemas");
2955
3151
  this.publicDir = resolve5(this.options.projectDir, "public");
2956
3152
  this.pagesDir = resolve5(this.options.projectDir, "app/pages");
2957
3153
  this.componentsDir = resolve5(this.options.projectDir, "app/components");
@@ -3191,7 +3387,23 @@ function switchTab(tab) {
3191
3387
  // Lazy-load dashboard data on first click
3192
3388
  if (window._loadDashboard) window._loadDashboard();
3193
3389
  }
3390
+ // Persist active tab across reloads (auto-fill writes config \u2192 watcher reloads page)
3391
+ try { localStorage.setItem('clawfire-active-tab', tab); } catch(e) {}
3194
3392
  }
3393
+ // Restore saved tab on page load
3394
+ (function() {
3395
+ try {
3396
+ var saved = localStorage.getItem('clawfire-active-tab');
3397
+ if (saved === 'dashboard') {
3398
+ var fn = function() { switchTab('dashboard'); };
3399
+ if (document.readyState === 'loading') {
3400
+ document.addEventListener('DOMContentLoaded', fn);
3401
+ } else {
3402
+ fn();
3403
+ }
3404
+ }
3405
+ } catch(e) {}
3406
+ })();
3195
3407
  </script>`;
3196
3408
  const liveReloadScript = `
3197
3409
  <script>
@@ -3497,7 +3709,7 @@ ${liveReloadScript}
3497
3709
  }
3498
3710
  console.log("");
3499
3711
  if (watching) {
3500
- const watchDirs = ["app/routes/", "app/schemas/", "public/"];
3712
+ const watchDirs = ["functions/routes/", "functions/schemas/", "public/"];
3501
3713
  if (pagesActive) watchDirs.push("app/pages/", "app/components/");
3502
3714
  console.log(` \x1B[35mHot Reload\x1B[0m : \x1B[32mON\x1B[0m`);
3503
3715
  console.log(` \x1B[2mWatching: ${watchDirs.join(", ")}\x1B[0m`);
@@ -3670,6 +3882,35 @@ ${liveReloadScript}
3670
3882
  });
3671
3883
  return;
3672
3884
  }
3885
+ if (url.pathname === "/__dev/deploy/hosting" && req.method === "POST") {
3886
+ this.firebaseSetup.deployHosting().then((result) => {
3887
+ clearFirebaseStatusCache();
3888
+ sendJson(result);
3889
+ }).catch((err) => sendJson({ success: false, message: err instanceof Error ? err.message : "Failed" }, 500));
3890
+ return;
3891
+ }
3892
+ if (url.pathname === "/__dev/enable-service" && req.method === "POST") {
3893
+ let body = "";
3894
+ req.on("data", (chunk) => {
3895
+ body += chunk;
3896
+ });
3897
+ req.on("end", () => {
3898
+ try {
3899
+ const data = JSON.parse(body);
3900
+ const service = data.service;
3901
+ if (!service || !["hosting", "firestore", "storage"].includes(service)) {
3902
+ sendJson({ success: false, message: "Invalid service. Use: hosting, firestore, storage" }, 400);
3903
+ return;
3904
+ }
3905
+ const result = this.firebaseSetup.enableService(service);
3906
+ clearFirebaseStatusCache();
3907
+ sendJson(result);
3908
+ } catch {
3909
+ sendJson({ success: false, message: "Invalid JSON body" }, 400);
3910
+ }
3911
+ });
3912
+ return;
3913
+ }
3673
3914
  if (url.pathname === "/__dev/setup/select-web-app" && req.method === "POST") {
3674
3915
  let body = "";
3675
3916
  req.on("data", (chunk) => {