clawfire 0.1.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 +182 -0
- package/dist/admin.cjs +309 -0
- package/dist/admin.cjs.map +1 -0
- package/dist/admin.d.cts +93 -0
- package/dist/admin.d.ts +93 -0
- package/dist/admin.js +274 -0
- package/dist/admin.js.map +1 -0
- package/dist/auth-DQ3cifhb.d.cts +55 -0
- package/dist/auth-DtnUPbXT.d.ts +55 -0
- package/dist/chunk-37Y2XI7X.js +75 -0
- package/dist/chunk-YGIPORYL.js +339 -0
- package/dist/cli.js +241 -0
- package/dist/client.cjs +97 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +4 -0
- package/dist/client.d.ts +4 -0
- package/dist/client.js +68 -0
- package/dist/client.js.map +1 -0
- package/dist/codegen.cjs +648 -0
- package/dist/codegen.cjs.map +1 -0
- package/dist/codegen.d.cts +25 -0
- package/dist/codegen.d.ts +25 -0
- package/dist/codegen.js +617 -0
- package/dist/codegen.js.map +1 -0
- package/dist/config-QMBJRn9G.d.cts +46 -0
- package/dist/config-QMBJRn9G.d.ts +46 -0
- package/dist/dev-server-QAVWINAT.js +973 -0
- package/dist/dev.cjs +1388 -0
- package/dist/dev.cjs.map +1 -0
- package/dist/dev.d.cts +111 -0
- package/dist/dev.d.ts +111 -0
- package/dist/dev.js +1349 -0
- package/dist/dev.js.map +1 -0
- package/dist/discover-BPMAZFBD.js +9 -0
- package/dist/discover-DYNqz_ym.d.cts +28 -0
- package/dist/discover-DYNqz_ym.d.ts +28 -0
- package/dist/errors-s_mP7rs9.d.cts +33 -0
- package/dist/errors-s_mP7rs9.d.ts +33 -0
- package/dist/functions.cjs +1156 -0
- package/dist/functions.cjs.map +1 -0
- package/dist/functions.d.cts +115 -0
- package/dist/functions.d.ts +115 -0
- package/dist/functions.js +1108 -0
- package/dist/functions.js.map +1 -0
- package/dist/hosting-7WVFHAYJ.js +85 -0
- package/dist/html-PCUCJGBH.js +7 -0
- package/dist/index.cjs +349 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +312 -0
- package/dist/index.js.map +1 -0
- package/dist/playground.cjs +364 -0
- package/dist/playground.cjs.map +1 -0
- package/dist/playground.d.cts +12 -0
- package/dist/playground.d.ts +12 -0
- package/dist/playground.js +337 -0
- package/dist/playground.js.map +1 -0
- package/dist/router-BVB_I-tu.d.ts +65 -0
- package/dist/router-Cikk8Heq.d.cts +65 -0
- package/dist/schema-BJsictSV.d.cts +172 -0
- package/dist/schema-BJsictSV.d.ts +172 -0
- package/package.json +150 -0
- package/templates/CLAUDE.md +71 -0
- package/templates/app/routes/auth/login.ts +35 -0
- package/templates/app/routes/health.ts +20 -0
- package/templates/app/schemas/user.ts +26 -0
- package/templates/clawfire.config.ts +25 -0
- package/templates/functions/index.ts +43 -0
- package/templates/starter/.claude/skills/clawfire-api/SKILL.md +131 -0
- package/templates/starter/.claude/skills/clawfire-auth/SKILL.md +111 -0
- package/templates/starter/.claude/skills/clawfire-deploy/SKILL.md +95 -0
- package/templates/starter/.claude/skills/clawfire-diagnose/SKILL.md +99 -0
- package/templates/starter/.claude/skills/clawfire-model/SKILL.md +128 -0
- package/templates/starter/CLAUDE.md +227 -0
- package/templates/starter/app/routes/health.ts +20 -0
- package/templates/starter/app/routes/todos/create.ts +25 -0
- package/templates/starter/app/routes/todos/delete.ts +20 -0
- package/templates/starter/app/routes/todos/list.ts +26 -0
- package/templates/starter/app/routes/todos/update.ts +32 -0
- package/templates/starter/app/schemas/todo.ts +16 -0
- package/templates/starter/app/store.ts +56 -0
- package/templates/starter/clawfire.config.ts +25 -0
- package/templates/starter/dev.ts +12 -0
- package/templates/starter/package.json +19 -0
- package/templates/starter/public/index.html +365 -0
- package/templates/starter/tsconfig.json +17 -0
package/dist/codegen.js
ADDED
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
// src/codegen/client-gen.ts
|
|
2
|
+
function generateClientCode(manifest, options) {
|
|
3
|
+
const baseUrl = options?.baseUrl || "";
|
|
4
|
+
const lines = [];
|
|
5
|
+
lines.push("// AUTO-GENERATED by Clawfire \u2014 DO NOT EDIT");
|
|
6
|
+
lines.push("// Regenerate: clawfire codegen");
|
|
7
|
+
lines.push("");
|
|
8
|
+
lines.push("/* eslint-disable */");
|
|
9
|
+
lines.push("");
|
|
10
|
+
lines.push("// \u2500\u2500\u2500 Types \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");
|
|
11
|
+
lines.push("");
|
|
12
|
+
for (const api of manifest.apis) {
|
|
13
|
+
const typeName = pathToTypeName(api.path);
|
|
14
|
+
lines.push(`/** ${api.meta.description} */`);
|
|
15
|
+
lines.push(`export interface ${typeName}Input ${jsonSchemaToTsType(api.inputSchema)}`);
|
|
16
|
+
lines.push("");
|
|
17
|
+
lines.push(`export interface ${typeName}Output ${jsonSchemaToTsType(api.outputSchema)}`);
|
|
18
|
+
lines.push("");
|
|
19
|
+
}
|
|
20
|
+
lines.push("// \u2500\u2500\u2500 Response Wrapper \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");
|
|
21
|
+
lines.push("");
|
|
22
|
+
lines.push("interface ClawfireResponse<T> {");
|
|
23
|
+
lines.push(" data: T;");
|
|
24
|
+
lines.push("}");
|
|
25
|
+
lines.push("");
|
|
26
|
+
lines.push("interface ClawfireError {");
|
|
27
|
+
lines.push(" error: {");
|
|
28
|
+
lines.push(" code: string;");
|
|
29
|
+
lines.push(" message: string;");
|
|
30
|
+
lines.push(" details?: unknown;");
|
|
31
|
+
lines.push(" };");
|
|
32
|
+
lines.push("}");
|
|
33
|
+
lines.push("");
|
|
34
|
+
lines.push("// \u2500\u2500\u2500 HTTP Client \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");
|
|
35
|
+
lines.push("");
|
|
36
|
+
lines.push("type GetTokenFn = () => Promise<string | null>;");
|
|
37
|
+
lines.push("");
|
|
38
|
+
lines.push("let _baseUrl = " + JSON.stringify(baseUrl) + ";");
|
|
39
|
+
lines.push("let _getToken: GetTokenFn = async () => null;");
|
|
40
|
+
lines.push("");
|
|
41
|
+
lines.push("/**");
|
|
42
|
+
lines.push(" * API \uD074\uB77C\uC774\uC5B8\uD2B8 \uC124\uC815");
|
|
43
|
+
lines.push(" * @param baseUrl - API \uAE30\uBCF8 URL (\uC608: https://us-central1-myproject.cloudfunctions.net/api)");
|
|
44
|
+
lines.push(" * @param getToken - \uC778\uC99D \uD1A0\uD070 \uBC18\uD658 \uD568\uC218");
|
|
45
|
+
lines.push(" */");
|
|
46
|
+
lines.push("export function configureClient(baseUrl: string, getToken?: GetTokenFn) {");
|
|
47
|
+
lines.push(" _baseUrl = baseUrl;");
|
|
48
|
+
lines.push(" if (getToken) _getToken = getToken;");
|
|
49
|
+
lines.push("}");
|
|
50
|
+
lines.push("");
|
|
51
|
+
lines.push("async function call<TInput, TOutput>(path: string, input: TInput): Promise<TOutput> {");
|
|
52
|
+
lines.push(" const token = await _getToken();");
|
|
53
|
+
lines.push(" const headers: Record<string, string> = {");
|
|
54
|
+
lines.push(' "Content-Type": "application/json",');
|
|
55
|
+
lines.push(" };");
|
|
56
|
+
lines.push(' if (token) headers["Authorization"] = `Bearer ${token}`;');
|
|
57
|
+
lines.push("");
|
|
58
|
+
lines.push(" const res = await fetch(`${_baseUrl}/api${path}`, {");
|
|
59
|
+
lines.push(' method: "POST",');
|
|
60
|
+
lines.push(" headers,");
|
|
61
|
+
lines.push(" body: JSON.stringify(input),");
|
|
62
|
+
lines.push(" });");
|
|
63
|
+
lines.push("");
|
|
64
|
+
lines.push(" const json = await res.json();");
|
|
65
|
+
lines.push("");
|
|
66
|
+
lines.push(" if (!res.ok) {");
|
|
67
|
+
lines.push(" const err = json as ClawfireError;");
|
|
68
|
+
lines.push(" throw new Error(err.error?.message || `API error: ${res.status}`);");
|
|
69
|
+
lines.push(" }");
|
|
70
|
+
lines.push("");
|
|
71
|
+
lines.push(" return (json as ClawfireResponse<TOutput>).data;");
|
|
72
|
+
lines.push("}");
|
|
73
|
+
lines.push("");
|
|
74
|
+
lines.push("// \u2500\u2500\u2500 API Client \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");
|
|
75
|
+
lines.push("");
|
|
76
|
+
const tree = buildApiTree(manifest.apis);
|
|
77
|
+
lines.push(generateApiObject(tree));
|
|
78
|
+
return lines.join("\n");
|
|
79
|
+
}
|
|
80
|
+
function pathToTypeName(path) {
|
|
81
|
+
return path.split("/").filter(Boolean).filter((p) => !p.startsWith(":")).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
82
|
+
}
|
|
83
|
+
function buildApiTree(apis) {
|
|
84
|
+
const root = { apis: [], children: {} };
|
|
85
|
+
for (const api of apis) {
|
|
86
|
+
const parts = api.path.split("/").filter(Boolean).filter((p) => !p.startsWith(":"));
|
|
87
|
+
let node = root;
|
|
88
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
89
|
+
if (!node.children[parts[i]]) {
|
|
90
|
+
node.children[parts[i]] = { apis: [], children: {} };
|
|
91
|
+
}
|
|
92
|
+
node = node.children[parts[i]];
|
|
93
|
+
}
|
|
94
|
+
const name = parts[parts.length - 1] || "index";
|
|
95
|
+
const typeName = pathToTypeName(api.path);
|
|
96
|
+
node.apis.push({ name, path: api.path, typeName, meta: api.meta });
|
|
97
|
+
}
|
|
98
|
+
return root;
|
|
99
|
+
}
|
|
100
|
+
function generateApiObject(tree, indent = "") {
|
|
101
|
+
const lines = [];
|
|
102
|
+
lines.push(`${indent}export const api = {`);
|
|
103
|
+
for (const [name, child] of Object.entries(tree.children)) {
|
|
104
|
+
lines.push(`${indent} ${name}: {`);
|
|
105
|
+
for (const api of child.apis) {
|
|
106
|
+
lines.push(`${indent} /** ${api.meta.description}${api.meta.auth ? ` [${api.meta.auth}]` : ""} */`);
|
|
107
|
+
lines.push(
|
|
108
|
+
`${indent} ${api.name}: (input: ${api.typeName}Input): Promise<${api.typeName}Output> => call("${api.path}", input),`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
for (const [subName, subChild] of Object.entries(child.children)) {
|
|
112
|
+
lines.push(`${indent} ${subName}: {`);
|
|
113
|
+
for (const subApi of subChild.apis) {
|
|
114
|
+
lines.push(`${indent} /** ${subApi.meta.description}${subApi.meta.auth ? ` [${subApi.meta.auth}]` : ""} */`);
|
|
115
|
+
lines.push(
|
|
116
|
+
`${indent} ${subApi.name}: (input: ${subApi.typeName}Input): Promise<${subApi.typeName}Output> => call("${subApi.path}", input),`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
for (const [deepName, deepChild] of Object.entries(subChild.children)) {
|
|
120
|
+
lines.push(`${indent} ${deepName}: {`);
|
|
121
|
+
for (const deepApi of deepChild.apis) {
|
|
122
|
+
lines.push(`${indent} /** ${deepApi.meta.description} */`);
|
|
123
|
+
lines.push(
|
|
124
|
+
`${indent} ${deepApi.name}: (input: ${deepApi.typeName}Input): Promise<${deepApi.typeName}Output> => call("${deepApi.path}", input),`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
lines.push(`${indent} },`);
|
|
128
|
+
}
|
|
129
|
+
lines.push(`${indent} },`);
|
|
130
|
+
}
|
|
131
|
+
lines.push(`${indent} },`);
|
|
132
|
+
}
|
|
133
|
+
for (const api of tree.apis) {
|
|
134
|
+
lines.push(`${indent} /** ${api.meta.description} */`);
|
|
135
|
+
lines.push(
|
|
136
|
+
`${indent} ${api.name}: (input: ${api.typeName}Input): Promise<${api.typeName}Output> => call("${api.path}", input),`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
lines.push(`${indent}};`);
|
|
140
|
+
return lines.join("\n");
|
|
141
|
+
}
|
|
142
|
+
function jsonSchemaToTsType(schema) {
|
|
143
|
+
if (!schema) return "{}";
|
|
144
|
+
const type = schema.type;
|
|
145
|
+
switch (type) {
|
|
146
|
+
case "object": {
|
|
147
|
+
const props = schema.properties;
|
|
148
|
+
if (!props) return "Record<string, unknown>";
|
|
149
|
+
const required = schema.required || [];
|
|
150
|
+
const fields = [];
|
|
151
|
+
for (const [key, propSchema] of Object.entries(props)) {
|
|
152
|
+
const isRequired = required.includes(key);
|
|
153
|
+
const tsType = jsonSchemaToInlineType(propSchema);
|
|
154
|
+
fields.push(` ${key}${isRequired ? "" : "?"}: ${tsType};`);
|
|
155
|
+
}
|
|
156
|
+
return `{
|
|
157
|
+
${fields.join("\n")}
|
|
158
|
+
}`;
|
|
159
|
+
}
|
|
160
|
+
default:
|
|
161
|
+
return "{}";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function jsonSchemaToInlineType(schema) {
|
|
165
|
+
if (!schema) return "unknown";
|
|
166
|
+
if ("const" in schema) return JSON.stringify(schema.const);
|
|
167
|
+
if (schema.enum) return schema.enum.map((v) => JSON.stringify(v)).join(" | ");
|
|
168
|
+
const nullable = schema.nullable ? " | null" : "";
|
|
169
|
+
const optional = schema.optional ? "" : "";
|
|
170
|
+
const type = schema.type;
|
|
171
|
+
switch (type) {
|
|
172
|
+
case "string":
|
|
173
|
+
return `string${nullable}`;
|
|
174
|
+
case "number":
|
|
175
|
+
return `number${nullable}`;
|
|
176
|
+
case "boolean":
|
|
177
|
+
return `boolean${nullable}`;
|
|
178
|
+
case "array": {
|
|
179
|
+
const items = schema.items;
|
|
180
|
+
const itemType = items ? jsonSchemaToInlineType(items) : "unknown";
|
|
181
|
+
return `${itemType}[]${nullable}`;
|
|
182
|
+
}
|
|
183
|
+
case "object": {
|
|
184
|
+
const props = schema.properties;
|
|
185
|
+
if (!props) {
|
|
186
|
+
const additionalProps = schema.additionalProperties;
|
|
187
|
+
if (additionalProps) return `Record<string, ${jsonSchemaToInlineType(additionalProps)}>${nullable}`;
|
|
188
|
+
return `Record<string, unknown>${nullable}`;
|
|
189
|
+
}
|
|
190
|
+
const required = schema.required || [];
|
|
191
|
+
const fields = Object.entries(props).map(([k, v]) => `${k}${required.includes(k) ? "" : "?"}: ${jsonSchemaToInlineType(v)}`).join("; ");
|
|
192
|
+
return `{ ${fields} }${nullable}`;
|
|
193
|
+
}
|
|
194
|
+
default:
|
|
195
|
+
if (schema.oneOf) {
|
|
196
|
+
return schema.oneOf.map(jsonSchemaToInlineType).join(" | ");
|
|
197
|
+
}
|
|
198
|
+
return "unknown";
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function generateManifestJson(manifest) {
|
|
202
|
+
return JSON.stringify(manifest, null, 2);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/playground/html.ts
|
|
206
|
+
function generatePlaygroundHtml(options) {
|
|
207
|
+
const title = options?.title || "Clawfire Playground";
|
|
208
|
+
const apiBaseUrl = options?.apiBaseUrl || "";
|
|
209
|
+
return `<!DOCTYPE html>
|
|
210
|
+
<html lang="en">
|
|
211
|
+
<head>
|
|
212
|
+
<meta charset="UTF-8">
|
|
213
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
214
|
+
<title>${title}</title>
|
|
215
|
+
<style>
|
|
216
|
+
:root {
|
|
217
|
+
--bg: #0a0a0a;
|
|
218
|
+
--surface: #141414;
|
|
219
|
+
--surface2: #1e1e1e;
|
|
220
|
+
--border: #2a2a2a;
|
|
221
|
+
--text: #e5e5e5;
|
|
222
|
+
--text2: #a3a3a3;
|
|
223
|
+
--accent: #f97316;
|
|
224
|
+
--accent2: #fb923c;
|
|
225
|
+
--green: #22c55e;
|
|
226
|
+
--red: #ef4444;
|
|
227
|
+
--blue: #3b82f6;
|
|
228
|
+
--yellow: #eab308;
|
|
229
|
+
--radius: 8px;
|
|
230
|
+
--font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
231
|
+
--mono: 'JetBrains Mono', 'Fira Code', monospace;
|
|
232
|
+
}
|
|
233
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
234
|
+
body { font-family: var(--font); background: var(--bg); color: var(--text); min-height: 100vh; }
|
|
235
|
+
|
|
236
|
+
.layout { display: grid; grid-template-columns: 320px 1fr; min-height: 100vh; }
|
|
237
|
+
.sidebar { background: var(--surface); border-right: 1px solid var(--border); overflow-y: auto; }
|
|
238
|
+
.main { padding: 24px; overflow-y: auto; }
|
|
239
|
+
|
|
240
|
+
.logo { padding: 20px; border-bottom: 1px solid var(--border); }
|
|
241
|
+
.logo h1 { font-size: 20px; font-weight: 700; color: var(--accent); }
|
|
242
|
+
.logo p { font-size: 12px; color: var(--text2); margin-top: 4px; }
|
|
243
|
+
|
|
244
|
+
.auth-section { padding: 16px; border-bottom: 1px solid var(--border); }
|
|
245
|
+
.auth-section label { font-size: 12px; color: var(--text2); display: block; margin-bottom: 6px; }
|
|
246
|
+
.auth-input { width: 100%; padding: 8px 12px; background: var(--surface2); border: 1px solid var(--border);
|
|
247
|
+
border-radius: var(--radius); color: var(--text); font-family: var(--mono); font-size: 12px; }
|
|
248
|
+
.auth-status { font-size: 11px; margin-top: 6px; }
|
|
249
|
+
.auth-status.ok { color: var(--green); }
|
|
250
|
+
.auth-status.no { color: var(--text2); }
|
|
251
|
+
|
|
252
|
+
.search { padding: 12px 16px; border-bottom: 1px solid var(--border); }
|
|
253
|
+
.search input { width: 100%; padding: 8px 12px; background: var(--surface2); border: 1px solid var(--border);
|
|
254
|
+
border-radius: var(--radius); color: var(--text); font-size: 13px; }
|
|
255
|
+
|
|
256
|
+
.api-list { padding: 8px 0; }
|
|
257
|
+
.api-group { padding: 4px 0; }
|
|
258
|
+
.api-group-title { padding: 8px 16px; font-size: 11px; color: var(--text2); text-transform: uppercase;
|
|
259
|
+
letter-spacing: 0.5px; font-weight: 600; }
|
|
260
|
+
.api-item { padding: 8px 16px; cursor: pointer; transition: background 0.15s; display: flex; align-items: center;
|
|
261
|
+
gap: 8px; font-size: 13px; }
|
|
262
|
+
.api-item:hover { background: var(--surface2); }
|
|
263
|
+
.api-item.active { background: var(--surface2); border-left: 2px solid var(--accent); }
|
|
264
|
+
.api-badge { font-size: 10px; padding: 2px 6px; border-radius: 4px; font-weight: 600; }
|
|
265
|
+
.badge-public { background: #22c55e20; color: var(--green); }
|
|
266
|
+
.badge-auth { background: #3b82f620; color: var(--blue); }
|
|
267
|
+
.badge-role { background: #eab30820; color: var(--yellow); }
|
|
268
|
+
.badge-reauth { background: #ef444420; color: var(--red); }
|
|
269
|
+
|
|
270
|
+
.panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 16px; }
|
|
271
|
+
.panel-header { padding: 16px; border-bottom: 1px solid var(--border); display: flex; align-items: center;
|
|
272
|
+
justify-content: space-between; }
|
|
273
|
+
.panel-header h2 { font-size: 16px; font-weight: 600; }
|
|
274
|
+
.panel-body { padding: 16px; }
|
|
275
|
+
|
|
276
|
+
.field { margin-bottom: 12px; }
|
|
277
|
+
.field label { font-size: 12px; color: var(--text2); display: block; margin-bottom: 4px; }
|
|
278
|
+
.field-type { font-size: 11px; color: var(--text2); font-family: var(--mono); }
|
|
279
|
+
.field-required { color: var(--red); font-size: 11px; }
|
|
280
|
+
|
|
281
|
+
textarea, input[type="text"] { width: 100%; padding: 10px 14px; background: var(--surface2);
|
|
282
|
+
border: 1px solid var(--border); border-radius: var(--radius); color: var(--text);
|
|
283
|
+
font-family: var(--mono); font-size: 13px; resize: vertical; }
|
|
284
|
+
textarea { min-height: 200px; }
|
|
285
|
+
|
|
286
|
+
.btn { padding: 10px 20px; border: none; border-radius: var(--radius); font-size: 14px;
|
|
287
|
+
font-weight: 600; cursor: pointer; transition: all 0.15s; }
|
|
288
|
+
.btn-primary { background: var(--accent); color: white; }
|
|
289
|
+
.btn-primary:hover { background: var(--accent2); }
|
|
290
|
+
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
291
|
+
|
|
292
|
+
.response-section { margin-top: 16px; }
|
|
293
|
+
.status-badge { font-size: 12px; padding: 4px 8px; border-radius: 4px; font-weight: 600; }
|
|
294
|
+
.status-ok { background: #22c55e20; color: var(--green); }
|
|
295
|
+
.status-err { background: #ef444420; color: var(--red); }
|
|
296
|
+
pre { background: var(--surface2); padding: 16px; border-radius: var(--radius); overflow-x: auto;
|
|
297
|
+
font-family: var(--mono); font-size: 13px; line-height: 1.5; white-space: pre-wrap; }
|
|
298
|
+
|
|
299
|
+
.schema-info { font-size: 12px; color: var(--text2); line-height: 1.6; }
|
|
300
|
+
.schema-info code { background: var(--surface2); padding: 2px 6px; border-radius: 4px; font-family: var(--mono);
|
|
301
|
+
font-size: 11px; }
|
|
302
|
+
|
|
303
|
+
.empty-state { text-align: center; padding: 80px 40px; color: var(--text2); }
|
|
304
|
+
.empty-state h2 { font-size: 24px; margin-bottom: 8px; color: var(--text); }
|
|
305
|
+
|
|
306
|
+
.timer { font-size: 12px; color: var(--text2); font-family: var(--mono); }
|
|
307
|
+
|
|
308
|
+
@media (max-width: 768px) {
|
|
309
|
+
.layout { grid-template-columns: 1fr; }
|
|
310
|
+
.sidebar { max-height: 40vh; }
|
|
311
|
+
}
|
|
312
|
+
</style>
|
|
313
|
+
</head>
|
|
314
|
+
<body>
|
|
315
|
+
<div class="layout">
|
|
316
|
+
<div class="sidebar">
|
|
317
|
+
<div class="logo">
|
|
318
|
+
<h1>Clawfire</h1>
|
|
319
|
+
<p>API Playground</p>
|
|
320
|
+
</div>
|
|
321
|
+
<div class="auth-section">
|
|
322
|
+
<label>Bearer Token</label>
|
|
323
|
+
<input type="text" class="auth-input" id="token-input" placeholder="Paste your ID token...">
|
|
324
|
+
<div class="auth-status no" id="auth-status">Not authenticated</div>
|
|
325
|
+
</div>
|
|
326
|
+
<div class="search">
|
|
327
|
+
<input type="text" id="search-input" placeholder="Search APIs...">
|
|
328
|
+
</div>
|
|
329
|
+
<div class="api-list" id="api-list"></div>
|
|
330
|
+
</div>
|
|
331
|
+
<div class="main" id="main-content">
|
|
332
|
+
<div class="empty-state">
|
|
333
|
+
<h2>Select an API</h2>
|
|
334
|
+
<p>Choose an API from the sidebar to test it.</p>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<script>
|
|
340
|
+
const BASE_URL = ${JSON.stringify(apiBaseUrl)} || window.location.origin;
|
|
341
|
+
let manifest = null;
|
|
342
|
+
let selectedApi = null;
|
|
343
|
+
|
|
344
|
+
async function loadManifest() {
|
|
345
|
+
try {
|
|
346
|
+
const res = await fetch(BASE_URL + '/api/__manifest', { method: 'POST' });
|
|
347
|
+
manifest = await res.json();
|
|
348
|
+
renderApiList(manifest.apis);
|
|
349
|
+
} catch (e) {
|
|
350
|
+
document.getElementById('api-list').innerHTML =
|
|
351
|
+
'<div style="padding:16px;color:var(--red);font-size:13px;">Failed to load API manifest. Make sure your server is running.</div>';
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function renderApiList(apis) {
|
|
356
|
+
const groups = {};
|
|
357
|
+
apis.forEach(api => {
|
|
358
|
+
const parts = api.path.split('/').filter(Boolean);
|
|
359
|
+
const group = parts.length > 1 ? parts[0] : 'root';
|
|
360
|
+
if (!groups[group]) groups[group] = [];
|
|
361
|
+
groups[group].push(api);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const el = document.getElementById('api-list');
|
|
365
|
+
el.innerHTML = Object.entries(groups).map(([group, items]) =>
|
|
366
|
+
'<div class="api-group">' +
|
|
367
|
+
'<div class="api-group-title">' + group + '</div>' +
|
|
368
|
+
items.map(api => {
|
|
369
|
+
const auth = api.meta.auth || 'public';
|
|
370
|
+
const badgeClass = 'badge-' + auth;
|
|
371
|
+
return '<div class="api-item" onclick="selectApi(\\'' + api.path + '\\')">' +
|
|
372
|
+
'<span class="api-badge ' + badgeClass + '">' + auth.toUpperCase() + '</span>' +
|
|
373
|
+
'<span>' + api.path + '</span>' +
|
|
374
|
+
'</div>';
|
|
375
|
+
}).join('') +
|
|
376
|
+
'</div>'
|
|
377
|
+
).join('');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function selectApi(path) {
|
|
381
|
+
selectedApi = manifest.apis.find(a => a.path === path);
|
|
382
|
+
if (!selectedApi) return;
|
|
383
|
+
|
|
384
|
+
document.querySelectorAll('.api-item').forEach(el => el.classList.remove('active'));
|
|
385
|
+
event.currentTarget?.classList.add('active');
|
|
386
|
+
|
|
387
|
+
const main = document.getElementById('main-content');
|
|
388
|
+
const exampleInput = selectedApi.meta.exampleInput
|
|
389
|
+
? JSON.stringify(selectedApi.meta.exampleInput, null, 2)
|
|
390
|
+
: generateExampleFromSchema(selectedApi.inputSchema);
|
|
391
|
+
|
|
392
|
+
main.innerHTML =
|
|
393
|
+
'<div class="panel">' +
|
|
394
|
+
'<div class="panel-header">' +
|
|
395
|
+
'<h2>POST ' + selectedApi.path + '</h2>' +
|
|
396
|
+
'<span class="api-badge badge-' + (selectedApi.meta.auth || 'public') + '">' +
|
|
397
|
+
(selectedApi.meta.auth || 'public').toUpperCase() + '</span>' +
|
|
398
|
+
'</div>' +
|
|
399
|
+
'<div class="panel-body">' +
|
|
400
|
+
'<p style="color:var(--text2);margin-bottom:16px;">' + (selectedApi.meta.description || '') + '</p>' +
|
|
401
|
+
(selectedApi.meta.tags ? '<div style="margin-bottom:12px;">' + selectedApi.meta.tags.map(t =>
|
|
402
|
+
'<span style="background:var(--surface2);padding:2px 8px;border-radius:4px;font-size:11px;margin-right:4px;">' + t + '</span>'
|
|
403
|
+
).join('') + '</div>' : '') +
|
|
404
|
+
'<div class="schema-info" style="margin-bottom:16px;">' +
|
|
405
|
+
'<strong>Input Schema:</strong><br>' + renderSchemaInfo(selectedApi.inputSchema) +
|
|
406
|
+
'</div>' +
|
|
407
|
+
'<div class="schema-info" style="margin-bottom:16px;">' +
|
|
408
|
+
'<strong>Output Schema:</strong><br>' + renderSchemaInfo(selectedApi.outputSchema) +
|
|
409
|
+
'</div>' +
|
|
410
|
+
'</div>' +
|
|
411
|
+
'</div>' +
|
|
412
|
+
'<div class="panel">' +
|
|
413
|
+
'<div class="panel-header"><h2>Request</h2></div>' +
|
|
414
|
+
'<div class="panel-body">' +
|
|
415
|
+
'<textarea id="req-body" placeholder="Request JSON body">' + exampleInput + '</textarea>' +
|
|
416
|
+
'<div style="margin-top:12px;display:flex;align-items:center;gap:12px;">' +
|
|
417
|
+
'<button class="btn btn-primary" onclick="sendRequest()">Send Request</button>' +
|
|
418
|
+
'<span class="timer" id="timer"></span>' +
|
|
419
|
+
'</div>' +
|
|
420
|
+
'</div>' +
|
|
421
|
+
'</div>' +
|
|
422
|
+
'<div class="response-section" id="response-section"></div>';
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function renderSchemaInfo(schema) {
|
|
426
|
+
if (!schema || !schema.properties) return '<code>void</code>';
|
|
427
|
+
return Object.entries(schema.properties).map(([key, prop]) => {
|
|
428
|
+
const required = schema.required?.includes(key);
|
|
429
|
+
const type = prop.type || 'unknown';
|
|
430
|
+
const enumVals = prop.enum ? ' (' + prop.enum.join(', ') + ')' : '';
|
|
431
|
+
return '<code>' + key + '</code>: <span class="field-type">' + type + enumVals + '</span>' +
|
|
432
|
+
(required ? ' <span class="field-required">required</span>' : ' <span style="color:var(--text2);font-size:11px;">optional</span>');
|
|
433
|
+
}).join('<br>');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function generateExampleFromSchema(schema) {
|
|
437
|
+
if (!schema || !schema.properties) return '{}';
|
|
438
|
+
const obj = {};
|
|
439
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
440
|
+
if (prop.enum) { obj[key] = prop.enum[0]; continue; }
|
|
441
|
+
switch (prop.type) {
|
|
442
|
+
case 'string': obj[key] = prop.format === 'email' ? 'user@example.com' : 'string'; break;
|
|
443
|
+
case 'number': obj[key] = 0; break;
|
|
444
|
+
case 'boolean': obj[key] = false; break;
|
|
445
|
+
case 'array': obj[key] = []; break;
|
|
446
|
+
case 'object': obj[key] = {}; break;
|
|
447
|
+
default: obj[key] = null;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return JSON.stringify(obj, null, 2);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function sendRequest() {
|
|
454
|
+
if (!selectedApi) return;
|
|
455
|
+
const body = document.getElementById('req-body').value;
|
|
456
|
+
const token = document.getElementById('token-input').value;
|
|
457
|
+
const timer = document.getElementById('timer');
|
|
458
|
+
const section = document.getElementById('response-section');
|
|
459
|
+
|
|
460
|
+
let parsed;
|
|
461
|
+
try { parsed = JSON.parse(body); } catch {
|
|
462
|
+
section.innerHTML = '<div class="panel"><div class="panel-body"><pre style="color:var(--red)">Invalid JSON</pre></div></div>';
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const start = performance.now();
|
|
467
|
+
timer.textContent = 'Sending...';
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
471
|
+
if (token) headers['Authorization'] = 'Bearer ' + token;
|
|
472
|
+
|
|
473
|
+
const res = await fetch(BASE_URL + '/api' + selectedApi.path, {
|
|
474
|
+
method: 'POST', headers, body: JSON.stringify(parsed)
|
|
475
|
+
});
|
|
476
|
+
const elapsed = Math.round(performance.now() - start);
|
|
477
|
+
timer.textContent = elapsed + 'ms';
|
|
478
|
+
|
|
479
|
+
const json = await res.json();
|
|
480
|
+
const isOk = res.ok;
|
|
481
|
+
|
|
482
|
+
section.innerHTML =
|
|
483
|
+
'<div class="panel">' +
|
|
484
|
+
'<div class="panel-header">' +
|
|
485
|
+
'<h2>Response</h2>' +
|
|
486
|
+
'<span class="status-badge ' + (isOk ? 'status-ok' : 'status-err') + '">' +
|
|
487
|
+
res.status + ' ' + res.statusText + '</span>' +
|
|
488
|
+
'</div>' +
|
|
489
|
+
'<div class="panel-body"><pre>' + syntaxHighlight(JSON.stringify(json, null, 2)) + '</pre></div>' +
|
|
490
|
+
'</div>';
|
|
491
|
+
} catch (e) {
|
|
492
|
+
const elapsed = Math.round(performance.now() - start);
|
|
493
|
+
timer.textContent = elapsed + 'ms';
|
|
494
|
+
section.innerHTML =
|
|
495
|
+
'<div class="panel"><div class="panel-body"><pre style="color:var(--red)">Network error: ' + e.message + '</pre></div></div>';
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function syntaxHighlight(json) {
|
|
500
|
+
return json.replace(/("(\\\\u[a-fA-F0-9]{4}|\\\\[^u]|[^\\\\"])*"(\\s*:)?)|\\b(true|false|null)\\b|-?\\d+(\\.\\d+)?([eE][+-]?\\d+)?/g,
|
|
501
|
+
function(match) {
|
|
502
|
+
let cls = 'color:#eab308';
|
|
503
|
+
if (/^"/.test(match)) {
|
|
504
|
+
if (/:$/.test(match)) cls = 'color:#3b82f6';
|
|
505
|
+
else cls = 'color:#22c55e';
|
|
506
|
+
} else if (/true|false/.test(match)) cls = 'color:#f97316';
|
|
507
|
+
else if (/null/.test(match)) cls = 'color:#ef4444';
|
|
508
|
+
return '<span style="' + cls + '">' + match + '</span>';
|
|
509
|
+
}
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Search
|
|
514
|
+
document.getElementById('search-input')?.addEventListener('input', (e) => {
|
|
515
|
+
if (!manifest) return;
|
|
516
|
+
const q = e.target.value.toLowerCase();
|
|
517
|
+
const filtered = manifest.apis.filter(a => a.path.toLowerCase().includes(q) || a.meta.description?.toLowerCase().includes(q));
|
|
518
|
+
renderApiList(filtered);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// Token status
|
|
522
|
+
document.getElementById('token-input')?.addEventListener('input', (e) => {
|
|
523
|
+
const el = document.getElementById('auth-status');
|
|
524
|
+
if (e.target.value) {
|
|
525
|
+
el.textContent = 'Token set';
|
|
526
|
+
el.className = 'auth-status ok';
|
|
527
|
+
} else {
|
|
528
|
+
el.textContent = 'Not authenticated';
|
|
529
|
+
el.className = 'auth-status no';
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
loadManifest();
|
|
534
|
+
</script>
|
|
535
|
+
</body>
|
|
536
|
+
</html>`;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/core/schema.ts
|
|
540
|
+
import { z } from "zod";
|
|
541
|
+
|
|
542
|
+
// src/routing/discover.ts
|
|
543
|
+
import { relative, join } from "path";
|
|
544
|
+
import { existsSync, readdirSync, statSync } from "fs";
|
|
545
|
+
function discoverRoutes(routesDir) {
|
|
546
|
+
if (!existsSync(routesDir)) {
|
|
547
|
+
return [];
|
|
548
|
+
}
|
|
549
|
+
const routes = [];
|
|
550
|
+
scanDirectory(routesDir, routesDir, routes);
|
|
551
|
+
return routes.sort((a, b) => a.apiPath.localeCompare(b.apiPath));
|
|
552
|
+
}
|
|
553
|
+
function scanDirectory(baseDir, currentDir, routes) {
|
|
554
|
+
const entries = readdirSync(currentDir);
|
|
555
|
+
for (const entry of entries) {
|
|
556
|
+
const fullPath = join(currentDir, entry);
|
|
557
|
+
const stat = statSync(fullPath);
|
|
558
|
+
if (stat.isDirectory()) {
|
|
559
|
+
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
560
|
+
scanDirectory(baseDir, fullPath, routes);
|
|
561
|
+
} else if (stat.isFile()) {
|
|
562
|
+
if (!entry.endsWith(".ts") && !entry.endsWith(".js")) continue;
|
|
563
|
+
if (entry.startsWith("_")) continue;
|
|
564
|
+
if (entry.endsWith(".d.ts")) continue;
|
|
565
|
+
const relativePath = relative(baseDir, fullPath);
|
|
566
|
+
const route = filePathToRoute(relativePath);
|
|
567
|
+
routes.push(route);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
function filePathToRoute(filePath) {
|
|
572
|
+
const params = [];
|
|
573
|
+
let routePath = filePath.replace(/\.(ts|js)$/, "");
|
|
574
|
+
routePath = routePath.replace(/\\/g, "/");
|
|
575
|
+
if (routePath.endsWith("/index") || routePath === "index") {
|
|
576
|
+
routePath = routePath.replace(/\/?index$/, "");
|
|
577
|
+
}
|
|
578
|
+
routePath = routePath.replace(/\[([^\]]+)\]/g, (_, param) => {
|
|
579
|
+
params.push(param);
|
|
580
|
+
return `:${param}`;
|
|
581
|
+
});
|
|
582
|
+
const apiPath = `/${routePath}`;
|
|
583
|
+
return {
|
|
584
|
+
filePath,
|
|
585
|
+
apiPath,
|
|
586
|
+
params
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
function generateRouteImports(routes, routesDir) {
|
|
590
|
+
const lines = [
|
|
591
|
+
"// AUTO-GENERATED by Clawfire \u2014 DO NOT EDIT",
|
|
592
|
+
"// This file is regenerated whenever routes change.",
|
|
593
|
+
"",
|
|
594
|
+
'import { createRouter } from "clawfire/functions";',
|
|
595
|
+
""
|
|
596
|
+
];
|
|
597
|
+
routes.forEach((route, i) => {
|
|
598
|
+
const importPath = `./${route.filePath.replace(/\.(ts|js)$/, ".js")}`;
|
|
599
|
+
lines.push(`import route_${i} from "${importPath}";`);
|
|
600
|
+
});
|
|
601
|
+
lines.push("");
|
|
602
|
+
lines.push("export function registerAllRoutes(router: ReturnType<typeof createRouter>) {");
|
|
603
|
+
routes.forEach((route, i) => {
|
|
604
|
+
lines.push(` router.register("${route.apiPath}", route_${i});`);
|
|
605
|
+
});
|
|
606
|
+
lines.push(" return router;");
|
|
607
|
+
lines.push("}");
|
|
608
|
+
return lines.join("\n");
|
|
609
|
+
}
|
|
610
|
+
export {
|
|
611
|
+
discoverRoutes,
|
|
612
|
+
generateClientCode,
|
|
613
|
+
generateManifestJson,
|
|
614
|
+
generatePlaygroundHtml,
|
|
615
|
+
generateRouteImports
|
|
616
|
+
};
|
|
617
|
+
//# sourceMappingURL=codegen.js.map
|