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.
- package/README.md +10 -10
- package/dist/cli.js +76 -16
- package/dist/codegen.cjs.map +1 -1
- package/dist/codegen.d.cts +1 -1
- package/dist/codegen.d.ts +1 -1
- package/dist/codegen.js.map +1 -1
- package/dist/{dev-server-5ATZVQJT.js → dev-server-GD445Q6F.js} +247 -6
- package/dist/dev.cjs +247 -6
- package/dist/dev.cjs.map +1 -1
- package/dist/dev.js +247 -6
- package/dist/dev.js.map +1 -1
- package/dist/{discover-DYNqz_ym.d.cts → discover-8p9Mujyt.d.cts} +3 -3
- package/dist/{discover-DYNqz_ym.d.ts → discover-8p9Mujyt.d.ts} +3 -3
- package/dist/functions.cjs.map +1 -1
- package/dist/functions.d.cts +1 -1
- package/dist/functions.d.ts +1 -1
- package/dist/functions.js.map +1 -1
- package/package.json +1 -1
- package/templates/CLAUDE.md +22 -19
- package/templates/functions/index.ts +3 -3
- package/templates/starter/.claude/skills/clawfire-api/SKILL.md +8 -8
- package/templates/starter/.claude/skills/clawfire-diagnose/SKILL.md +6 -6
- package/templates/starter/.claude/skills/clawfire-model/SKILL.md +2 -2
- package/templates/starter/CLAUDE.md +33 -31
- package/templates/starter/app/pages/index.html +7 -6
- package/templates/starter/functions/index.ts +52 -0
- package/templates/starter/functions/package.json +22 -0
- package/templates/starter/functions/tsconfig.json +18 -0
- package/templates/starter/package.json +4 -2
- package/templates/starter/tsconfig.json +1 -1
- /package/templates/{app → functions}/routes/auth/login.ts +0 -0
- /package/templates/{app → functions}/routes/health.ts +0 -0
- /package/templates/{app → functions}/schemas/user.ts +0 -0
- /package/templates/starter/{app → functions}/routes/health.ts +0 -0
- /package/templates/starter/{app → functions}/routes/todos/create.ts +0 -0
- /package/templates/starter/{app → functions}/routes/todos/delete.ts +0 -0
- /package/templates/starter/{app → functions}/routes/todos/list.ts +0 -0
- /package/templates/starter/{app → functions}/routes/todos/update.ts +0 -0
- /package/templates/starter/{app → functions}/schemas/todo.ts +0 -0
- /package/templates/starter/{app → functions}/store.ts +0 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Clawfire lets you build Firebase apps by talking to AI. Define your API contract
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
14
14
|
- **Contract-based API** — Define input/output with Zod schemas. Runtime validation, type safety, and docs come for free.
|
|
15
|
-
- **File-based Routing** — `
|
|
15
|
+
- **File-based Routing** — `functions/routes/todos/create.ts` becomes `POST /api/todos/create`. No configuration.
|
|
16
16
|
- **Page Routing** — `app/pages/about.html` becomes `/about`. Layouts, components, and SPA navigation built in.
|
|
17
17
|
- **Layouts & Components** — `_layout.html` wraps pages via `<slot />`. `<c-nav />` inserts `app/components/nav.html`.
|
|
18
18
|
- **SPA Navigation** — Internal links swap page content without full reload. Tailwind CSS via CDN.
|
|
@@ -61,7 +61,7 @@ This starts **two servers** in a single process:
|
|
|
61
61
|
POST /api/todos/update [public] Update a todo
|
|
62
62
|
|
|
63
63
|
Hot Reload : ON
|
|
64
|
-
Watching:
|
|
64
|
+
Watching: functions/routes/, functions/schemas/, app/pages/, app/components/, public/
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
### 3. Open in Your Browser
|
|
@@ -136,7 +136,6 @@ dev: {
|
|
|
136
136
|
```
|
|
137
137
|
my-app/
|
|
138
138
|
app/
|
|
139
|
-
store.ts In-memory data store (works without Firebase)
|
|
140
139
|
pages/ File-based page routing (frontend)
|
|
141
140
|
_layout.html Root layout (wraps all pages via <slot />)
|
|
142
141
|
_404.html 404 error page
|
|
@@ -147,6 +146,8 @@ my-app/
|
|
|
147
146
|
components/ Reusable HTML components
|
|
148
147
|
nav.html <c-nav /> navigation bar
|
|
149
148
|
footer.html <c-footer /> page footer
|
|
149
|
+
functions/
|
|
150
|
+
store.ts In-memory data store (works without Firebase)
|
|
150
151
|
routes/ API route handlers (file-based routing)
|
|
151
152
|
health.ts → POST /api/health
|
|
152
153
|
todos/
|
|
@@ -156,12 +157,11 @@ my-app/
|
|
|
156
157
|
delete.ts → POST /api/todos/delete
|
|
157
158
|
schemas/ Firestore model definitions
|
|
158
159
|
todo.ts
|
|
160
|
+
index.ts Firebase Functions entry point (for deploy)
|
|
159
161
|
public/ Static assets (CSS, images, fonts)
|
|
160
162
|
generated/ Auto-generated files (DO NOT EDIT)
|
|
161
163
|
api-client.ts Typed API client
|
|
162
164
|
manifest.json API manifest
|
|
163
|
-
functions/
|
|
164
|
-
index.ts Firebase Functions entry point (for deploy)
|
|
165
165
|
dev.ts Dev server entry point
|
|
166
166
|
clawfire.config.ts Configuration
|
|
167
167
|
firebase.json Firebase config
|
|
@@ -176,10 +176,10 @@ my-app/
|
|
|
176
176
|
|
|
177
177
|
### Define an API
|
|
178
178
|
|
|
179
|
-
Every API endpoint is a file in `
|
|
179
|
+
Every API endpoint is a file in `functions/routes/` that exports a `defineAPI` contract:
|
|
180
180
|
|
|
181
181
|
```ts
|
|
182
|
-
//
|
|
182
|
+
// functions/routes/products/list.ts
|
|
183
183
|
import { defineAPI, z } from "clawfire";
|
|
184
184
|
|
|
185
185
|
export default defineAPI({
|
|
@@ -211,14 +211,14 @@ export default defineAPI({
|
|
|
211
211
|
- `input` and `output` must be `z.object({})`
|
|
212
212
|
- `meta.description` is required
|
|
213
213
|
- `handler` must be `async`
|
|
214
|
-
- File path = API path: `
|
|
214
|
+
- File path = API path: `functions/routes/products/list.ts` → `POST /api/products/list`
|
|
215
215
|
|
|
216
216
|
### Define a Model
|
|
217
217
|
|
|
218
218
|
Models define Firestore collections and auto-generate security rules:
|
|
219
219
|
|
|
220
220
|
```ts
|
|
221
|
-
//
|
|
221
|
+
// functions/schemas/product.ts
|
|
222
222
|
import { defineModel } from "clawfire";
|
|
223
223
|
|
|
224
224
|
export const Product = defineModel({
|
|
@@ -342,7 +342,7 @@ const router = createRouter({
|
|
|
342
342
|
});
|
|
343
343
|
|
|
344
344
|
// Register routes
|
|
345
|
-
import listProducts from "
|
|
345
|
+
import listProducts from "./routes/products/list";
|
|
346
346
|
router.register("/products/list", listProducts);
|
|
347
347
|
|
|
348
348
|
export const api = functions.https.onRequest((req, res) => {
|
package/dist/cli.js
CHANGED
|
@@ -115,13 +115,12 @@ async function initProject() {
|
|
|
115
115
|
} else {
|
|
116
116
|
console.log(" (Templates not found, creating minimal structure)");
|
|
117
117
|
const dirs = [
|
|
118
|
-
"
|
|
119
|
-
"
|
|
118
|
+
"functions/routes",
|
|
119
|
+
"functions/schemas",
|
|
120
120
|
"app/pages",
|
|
121
121
|
"app/components",
|
|
122
122
|
"generated",
|
|
123
|
-
"public"
|
|
124
|
-
"functions"
|
|
123
|
+
"public"
|
|
125
124
|
];
|
|
126
125
|
for (const dir of dirs) {
|
|
127
126
|
const fullPath = resolve(projectDir, dir);
|
|
@@ -131,7 +130,7 @@ async function initProject() {
|
|
|
131
130
|
}
|
|
132
131
|
}
|
|
133
132
|
}
|
|
134
|
-
for (const dir of ["generated", "functions", ".claude/skills"]) {
|
|
133
|
+
for (const dir of ["generated", "functions/routes", "functions/schemas", ".claude/skills"]) {
|
|
135
134
|
const fullPath = resolve(projectDir, dir);
|
|
136
135
|
if (!existsSync(fullPath)) {
|
|
137
136
|
mkdirSync(fullPath, { recursive: true });
|
|
@@ -164,6 +163,8 @@ async function initProject() {
|
|
|
164
163
|
functionsIndex,
|
|
165
164
|
`/**
|
|
166
165
|
* Clawfire Firebase Functions Entry Point
|
|
166
|
+
*
|
|
167
|
+
* Routes are registered from functions/routes/.
|
|
167
168
|
*/
|
|
168
169
|
import * as admin from "firebase-admin";
|
|
169
170
|
import * as functions from "firebase-functions";
|
|
@@ -171,13 +172,18 @@ import { createRouter, createAdminDB, createSecurityMiddleware } from "clawfire/
|
|
|
171
172
|
|
|
172
173
|
admin.initializeApp();
|
|
173
174
|
|
|
174
|
-
const db = createAdminDB(admin.firestore());
|
|
175
|
+
export const db = createAdminDB(admin.firestore());
|
|
175
176
|
const router = createRouter({
|
|
176
177
|
auth: admin.auth(),
|
|
177
178
|
cors: [],
|
|
178
179
|
middleware: createSecurityMiddleware(),
|
|
179
180
|
});
|
|
180
181
|
|
|
182
|
+
// \u2500\u2500 Register Routes \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
|
|
183
|
+
import healthRoute from "./routes/health.js";
|
|
184
|
+
router.register("/health", healthRoute);
|
|
185
|
+
|
|
186
|
+
// \u2500\u2500 Export Cloud Function \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
|
|
181
187
|
export const api = functions.https.onRequest((req, res) => {
|
|
182
188
|
router.handleRequest(req as any, res as any);
|
|
183
189
|
});
|
|
@@ -185,18 +191,72 @@ export const api = functions.https.onRequest((req, res) => {
|
|
|
185
191
|
);
|
|
186
192
|
console.log(" Created functions/index.ts");
|
|
187
193
|
}
|
|
194
|
+
const functionsPkg = resolve(projectDir, "functions/package.json");
|
|
195
|
+
if (!existsSync(functionsPkg)) {
|
|
196
|
+
writeFileSync(
|
|
197
|
+
functionsPkg,
|
|
198
|
+
JSON.stringify({
|
|
199
|
+
name: "functions",
|
|
200
|
+
private: true,
|
|
201
|
+
type: "module",
|
|
202
|
+
scripts: {
|
|
203
|
+
build: "tsc",
|
|
204
|
+
"build:watch": "tsc --watch"
|
|
205
|
+
},
|
|
206
|
+
main: "dist/index.js",
|
|
207
|
+
engines: { node: "20" },
|
|
208
|
+
dependencies: {
|
|
209
|
+
clawfire: "latest",
|
|
210
|
+
"firebase-admin": "^13.0.0",
|
|
211
|
+
"firebase-functions": "^6.0.0"
|
|
212
|
+
},
|
|
213
|
+
devDependencies: {
|
|
214
|
+
typescript: "^5.5.0",
|
|
215
|
+
"@types/node": "^20.0.0"
|
|
216
|
+
}
|
|
217
|
+
}, null, 2)
|
|
218
|
+
);
|
|
219
|
+
console.log(" Created functions/package.json");
|
|
220
|
+
}
|
|
221
|
+
const functionsTsconfig = resolve(projectDir, "functions/tsconfig.json");
|
|
222
|
+
if (!existsSync(functionsTsconfig)) {
|
|
223
|
+
writeFileSync(
|
|
224
|
+
functionsTsconfig,
|
|
225
|
+
JSON.stringify({
|
|
226
|
+
compilerOptions: {
|
|
227
|
+
target: "ES2022",
|
|
228
|
+
module: "ESNext",
|
|
229
|
+
moduleResolution: "bundler",
|
|
230
|
+
esModuleInterop: true,
|
|
231
|
+
strict: true,
|
|
232
|
+
skipLibCheck: true,
|
|
233
|
+
outDir: "dist",
|
|
234
|
+
rootDir: ".",
|
|
235
|
+
declaration: true,
|
|
236
|
+
resolveJsonModule: true,
|
|
237
|
+
isolatedModules: true,
|
|
238
|
+
sourceMap: true
|
|
239
|
+
},
|
|
240
|
+
include: ["**/*.ts"],
|
|
241
|
+
exclude: ["node_modules", "dist"]
|
|
242
|
+
}, null, 2)
|
|
243
|
+
);
|
|
244
|
+
console.log(" Created functions/tsconfig.json");
|
|
245
|
+
}
|
|
188
246
|
console.log("\n \x1B[32m\u2713\x1B[0m Clawfire project initialized!\n");
|
|
189
247
|
console.log(" Project structure:");
|
|
190
|
-
console.log(" \x1B[
|
|
191
|
-
console.log(" \x1B[
|
|
192
|
-
console.log(" \x1B[
|
|
193
|
-
console.log(" \x1B[36mapp/
|
|
194
|
-
console.log(" \x1B[
|
|
248
|
+
console.log(" \x1B[36mfunctions/routes/\x1B[0m \u2192 API endpoints (POST, Zod validation)");
|
|
249
|
+
console.log(" \x1B[36mfunctions/schemas/\x1B[0m \u2192 Firestore model definitions");
|
|
250
|
+
console.log(" \x1B[36mfunctions/index.ts\x1B[0m \u2192 Firebase Functions entry point");
|
|
251
|
+
console.log(" \x1B[36mapp/pages/\x1B[0m \u2192 File-based page routing (Tailwind + layouts)");
|
|
252
|
+
console.log(" \x1B[36mapp/components/\x1B[0m \u2192 Reusable HTML components (<c-name />)");
|
|
253
|
+
console.log(" \x1B[36mpublic/\x1B[0m \u2192 Static assets (CSS, images, fonts)");
|
|
195
254
|
console.log("");
|
|
196
255
|
console.log(" Next steps:");
|
|
197
256
|
console.log(" \x1B[36m1.\x1B[0m npm install");
|
|
198
|
-
console.log(" \x1B[36m2.\x1B[0m npm
|
|
199
|
-
console.log(" \x1B[36m3.\x1B[0m
|
|
257
|
+
console.log(" \x1B[36m2.\x1B[0m cd functions && npm install && cd ..");
|
|
258
|
+
console.log(" \x1B[36m3.\x1B[0m npm run dev");
|
|
259
|
+
console.log(" \x1B[36m4.\x1B[0m Open \x1B[1mhttp://localhost:3000\x1B[0m");
|
|
200
260
|
console.log("");
|
|
201
261
|
console.log(" Your app is ready with a Todo demo, page routing, and API playground.");
|
|
202
262
|
console.log("");
|
|
@@ -209,7 +269,7 @@ async function runDevServer() {
|
|
|
209
269
|
const port = portArg ? parseInt(portArg.split("=")[1], 10) : 3e3;
|
|
210
270
|
const apiPort = apiPortArg ? parseInt(apiPortArg.split("=")[1], 10) : 3456;
|
|
211
271
|
const noHotReload = args.includes("--no-hot-reload");
|
|
212
|
-
const { startDevServer } = await import("./dev-server-
|
|
272
|
+
const { startDevServer } = await import("./dev-server-GD445Q6F.js");
|
|
213
273
|
await startDevServer({
|
|
214
274
|
projectDir,
|
|
215
275
|
port,
|
|
@@ -219,9 +279,9 @@ async function runDevServer() {
|
|
|
219
279
|
}
|
|
220
280
|
async function runCodegen() {
|
|
221
281
|
const projectDir = process.cwd();
|
|
222
|
-
const routesDir = resolve(projectDir, "
|
|
282
|
+
const routesDir = resolve(projectDir, "functions/routes");
|
|
223
283
|
if (!existsSync(routesDir)) {
|
|
224
|
-
console.error("Error:
|
|
284
|
+
console.error("Error: functions/routes/ directory not found. Run 'clawfire init' first.");
|
|
225
285
|
process.exit(1);
|
|
226
286
|
}
|
|
227
287
|
const { discoverRoutes, generateRouteImports } = await import("./discover-BPMAZFBD.js");
|
package/dist/codegen.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/codegen.ts","../src/codegen/client-gen.ts","../src/playground/html.ts","../src/core/schema.ts","../src/routing/discover.ts"],"sourcesContent":["/**\n * Clawfire Codegen Entry Point\n *\n * 클라이언트 코드 생성, 매니페스트 생성\n */\n\nexport { generateClientCode, generateManifestJson } from \"./codegen/index.js\";\nexport { generatePlaygroundHtml } from \"./playground/index.js\";\nexport { discoverRoutes, generateRouteImports, type DiscoveredRoute } from \"./routing/index.js\";\n","/**\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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AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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,iBAAkE;;;ACDlE,kBAAwC;AACxC,gBAAkD;AAwB3C,SAAS,eAAe,WAAsC;AACnE,MAAI,KAAC,sBAAW,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,cAAU,uBAAY,UAAU;AAEtC,aAAW,SAAS,SAAS;AAC3B,UAAM,eAAW,kBAAK,YAAY,KAAK;AACvC,UAAM,WAAO,oBAAS,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,mBAAe,sBAAS,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.ts","../src/codegen/client-gen.ts","../src/playground/html.ts","../src/core/schema.ts","../src/routing/discover.ts"],"sourcesContent":["/**\n * Clawfire Codegen Entry Point\n *\n * 클라이언트 코드 생성, 매니페스트 생성\n */\n\nexport { generateClientCode, generateManifestJson } from \"./codegen/index.js\";\nexport { generatePlaygroundHtml } from \"./playground/index.js\";\nexport { discoverRoutes, generateRouteImports, type DiscoveredRoute } from \"./routing/index.js\";\n","/**\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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AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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,iBAAkE;;;ACDlE,kBAAwC;AACxC,gBAAkD;AAwB3C,SAAS,eAAe,WAAsC;AACnE,MAAI,KAAC,sBAAW,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,cAAU,uBAAY,UAAU;AAEtC,aAAW,SAAS,SAAS;AAC3B,UAAM,eAAW,kBAAK,YAAY,KAAK;AACvC,UAAM,WAAO,oBAAS,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,mBAAe,sBAAS,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":[]}
|
package/dist/codegen.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { M as Manifest } from './schema-BJsictSV.cjs';
|
|
2
2
|
export { generatePlaygroundHtml } from './playground.cjs';
|
|
3
|
-
export { D as DiscoveredRoute, d as discoverRoutes, g as generateRouteImports } from './discover-
|
|
3
|
+
export { D as DiscoveredRoute, d as discoverRoutes, g as generateRouteImports } from './discover-8p9Mujyt.cjs';
|
|
4
4
|
import 'zod';
|
|
5
5
|
|
|
6
6
|
/**
|
package/dist/codegen.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { M as Manifest } from './schema-BJsictSV.js';
|
|
2
2
|
export { generatePlaygroundHtml } from './playground.js';
|
|
3
|
-
export { D as DiscoveredRoute, d as discoverRoutes, g as generateRouteImports } from './discover-
|
|
3
|
+
export { D as DiscoveredRoute, d as discoverRoutes, g as generateRouteImports } from './discover-8p9Mujyt.js';
|
|
4
4
|
import 'zod';
|
|
5
5
|
|
|
6
6
|
/**
|