@inneranimalmedia/agentsam-sdk 1.5.1 → 1.7.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.
@@ -0,0 +1,472 @@
1
+ /**
2
+ * CMS template generator.
3
+ * Returns a flat file-tree object keyed by relative path → file content string.
4
+ */
5
+
6
+ export function cmsTemplates(config) {
7
+ const { projectName, siteTitle, navStyle, pages, templateStyle, contactEmail, cfAccountId } = config;
8
+
9
+ const files = {};
10
+
11
+ // ── package.json ──────────────────────────────────────────────────────────
12
+ files['package.json'] = JSON.stringify({
13
+ name: projectName,
14
+ version: '0.1.0',
15
+ private: true,
16
+ scripts: {
17
+ deploy: 'wrangler deploy',
18
+ dev: 'wrangler dev',
19
+ 'db:migrate': `wrangler d1 execute ${projectName} --file=migrations/001_init.sql --remote`,
20
+ },
21
+ devDependencies: {
22
+ wrangler: '^3.0.0',
23
+ },
24
+ }, null, 2);
25
+
26
+ // ── wrangler.toml ─────────────────────────────────────────────────────────
27
+ files['wrangler.toml'] = `name = "${projectName}"
28
+ main = "src/index.js"
29
+ compatibility_date = "2024-01-01"
30
+ account_id = "${cfAccountId}"
31
+
32
+ [[d1_databases]]
33
+ binding = "DB"
34
+ database_name = "${projectName}"
35
+ database_id = "REPLACE_WITH_YOUR_D1_ID"
36
+
37
+ [[r2_buckets]]
38
+ binding = "ASSETS"
39
+ bucket_name = "${projectName}"
40
+ `;
41
+
42
+ // ── D1 migration ──────────────────────────────────────────────────────────
43
+ let migrationSql = `-- ${projectName} initial schema\n\n`;
44
+
45
+ migrationSql += `CREATE TABLE IF NOT EXISTS cms_pages (
46
+ id TEXT PRIMARY KEY,
47
+ slug TEXT NOT NULL UNIQUE,
48
+ title TEXT NOT NULL,
49
+ template TEXT NOT NULL DEFAULT 'default',
50
+ content_json TEXT,
51
+ status TEXT NOT NULL DEFAULT 'draft',
52
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
53
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
54
+ );\n\n`;
55
+
56
+ migrationSql += `CREATE TABLE IF NOT EXISTS cms_nav_items (
57
+ id TEXT PRIMARY KEY,
58
+ label TEXT NOT NULL,
59
+ href TEXT NOT NULL,
60
+ position INTEGER NOT NULL DEFAULT 0,
61
+ parent_id TEXT,
62
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
63
+ );\n\n`;
64
+
65
+ if (pages.includes('blog')) {
66
+ migrationSql += `CREATE TABLE IF NOT EXISTS cms_posts (
67
+ id TEXT PRIMARY KEY,
68
+ slug TEXT NOT NULL UNIQUE,
69
+ title TEXT NOT NULL,
70
+ body TEXT,
71
+ published_at INTEGER,
72
+ status TEXT NOT NULL DEFAULT 'draft',
73
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
74
+ );\n\n`;
75
+ }
76
+
77
+ if (contactEmail) {
78
+ migrationSql += `CREATE TABLE IF NOT EXISTS cms_contact_submissions (
79
+ id TEXT PRIMARY KEY,
80
+ name TEXT,
81
+ email TEXT,
82
+ message TEXT,
83
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
84
+ );\n\n`;
85
+ }
86
+
87
+ // Seed nav items from selected pages
88
+ const navOrder = ['home', 'about', 'services', 'blog', 'contact'];
89
+ const navPages = navOrder.filter(p => pages.includes(p));
90
+ navPages.forEach((page, i) => {
91
+ const label = page.charAt(0).toUpperCase() + page.slice(1);
92
+ const href = page === 'home' ? '/' : `/${page}`;
93
+ migrationSql += `INSERT OR IGNORE INTO cms_nav_items (id, label, href, position) VALUES ('nav_${page}', '${label}', '${href}', ${i});\n`;
94
+ });
95
+
96
+ files['migrations/001_init.sql'] = migrationSql;
97
+
98
+ // ── Worker entry point ────────────────────────────────────────────────────
99
+ files['src/index.js'] = workerEntry(config);
100
+
101
+ // ── Route handlers ────────────────────────────────────────────────────────
102
+ files['src/routes/pages.js'] = pagesRoute(config);
103
+ files['src/routes/nav.js'] = navRoute();
104
+
105
+ if (pages.includes('blog')) {
106
+ files['src/routes/blog.js'] = blogRoute();
107
+ }
108
+
109
+ if (contactEmail) {
110
+ files['src/routes/contact.js'] = contactRoute(contactEmail);
111
+ }
112
+
113
+ // ── HTML shell template ───────────────────────────────────────────────────
114
+ files['src/templates/shell.js'] = htmlShell(config);
115
+
116
+ // ── Nav component ─────────────────────────────────────────────────────────
117
+ files['src/templates/nav.js'] = navComponent(navStyle, siteTitle);
118
+
119
+ // ── Page templates ────────────────────────────────────────────────────────
120
+ pages.forEach(page => {
121
+ files[`src/templates/pages/${page}.js`] = pageTemplate(page, siteTitle, templateStyle);
122
+ });
123
+
124
+ // ── README ────────────────────────────────────────────────────────────────
125
+ files['README.md'] = readme(config);
126
+
127
+ return files;
128
+ }
129
+
130
+ // ── Worker entry ─────────────────────────────────────────────────────────────
131
+ function workerEntry({ projectName, pages, contactEmail }) {
132
+ const imports = [`import { handlePages } from './routes/pages.js';`,
133
+ `import { handleNav } from './routes/nav.js';`];
134
+
135
+ if (pages.includes('blog')) imports.push(`import { handleBlog } from './routes/blog.js';`);
136
+ if (contactEmail) imports.push(`import { handleContact } from './routes/contact.js';`);
137
+
138
+ return `${imports.join('\n')}
139
+
140
+ export default {
141
+ async fetch(request, env, ctx) {
142
+ const url = new URL(request.url);
143
+ const { pathname } = url;
144
+
145
+ // CORS preflight
146
+ if (request.method === 'OPTIONS') {
147
+ return new Response(null, {
148
+ headers: {
149
+ 'Access-Control-Allow-Origin': '*',
150
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
151
+ 'Access-Control-Allow-Headers': 'Content-Type',
152
+ },
153
+ });
154
+ }
155
+
156
+ // API routes
157
+ if (pathname.startsWith('/api/nav')) return handleNav(request, env);
158
+ ${pages.includes('blog') ? ` if (pathname.startsWith('/api/blog')) return handleBlog(request, env);\n` : ''}${contactEmail ? ` if (pathname === '/api/contact' && request.method === 'POST') return handleContact(request, env);\n` : ''}
159
+ // Page routes — catch-all
160
+ return handlePages(request, env);
161
+ },
162
+ };
163
+ `;
164
+ }
165
+
166
+ // ── Route: pages ─────────────────────────────────────────────────────────────
167
+ function pagesRoute({ pages, templateStyle, siteTitle }) {
168
+ return `import { renderShell } from '../templates/shell.js';
169
+ import { renderNav } from '../templates/nav.js';
170
+ ${pages.map(p => `import { render${cap(p)}Page } from '../templates/pages/${p}.js';`).join('\n')}
171
+
172
+ const PAGE_MAP = {
173
+ ${pages.map(p => ` '${p === 'home' ? '/' : `/${p}`}': render${cap(p)}Page,`).join('\n')}
174
+ };
175
+
176
+ export async function handlePages(request, env) {
177
+ const url = new URL(request.url);
178
+ const slug = url.pathname.replace(/\\/$/, '') || '/';
179
+
180
+ const renderPage = PAGE_MAP[slug];
181
+ if (!renderPage) {
182
+ return new Response(renderShell('404', renderNav([]), '<h1>Page not found</h1>'), {
183
+ status: 404,
184
+ headers: { 'Content-Type': 'text/html;charset=UTF-8' },
185
+ });
186
+ }
187
+
188
+ // Load nav from D1
189
+ const navRows = await env.DB.prepare(
190
+ 'SELECT label, href FROM cms_nav_items ORDER BY position ASC'
191
+ ).all();
192
+ const nav = renderNav(navRows.results ?? []);
193
+
194
+ ${templateStyle === 'fragment'
195
+ ? ` // Load page fragment from R2
196
+ const fragmentKey = \`pages/\${slug === '/' ? 'home' : slug.slice(1)}/content.html\`;
197
+ const fragment = await env.ASSETS.get(fragmentKey);
198
+ const content = fragment ? await fragment.text() : renderPage();`
199
+ : templateStyle === 'json'
200
+ ? ` // Load page content from D1
201
+ const pageRow = await env.DB.prepare(
202
+ 'SELECT content_json FROM cms_pages WHERE slug = ? AND status = \\'published\\''
203
+ ).bind(slug === '/' ? 'home' : slug.slice(1)).first();
204
+ const content = renderPage(pageRow?.content_json ? JSON.parse(pageRow.content_json) : {});`
205
+ : ` const content = renderPage();`}
206
+
207
+ const html = renderShell('${siteTitle}', nav, content);
208
+ return new Response(html, {
209
+ headers: { 'Content-Type': 'text/html;charset=UTF-8' },
210
+ });
211
+ }
212
+ `;
213
+ }
214
+
215
+ // ── Route: nav ───────────────────────────────────────────────────────────────
216
+ function navRoute() {
217
+ return `export async function handleNav(request, env) {
218
+ const rows = await env.DB.prepare(
219
+ 'SELECT id, label, href, position FROM cms_nav_items ORDER BY position ASC'
220
+ ).all();
221
+ return Response.json(rows.results ?? []);
222
+ }
223
+ `;
224
+ }
225
+
226
+ // ── Route: blog ──────────────────────────────────────────────────────────────
227
+ function blogRoute() {
228
+ return `import { renderShell } from '../templates/shell.js';
229
+ import { renderNav } from '../templates/nav.js';
230
+
231
+ export async function handleBlog(request, env) {
232
+ const url = new URL(request.url);
233
+ const slug = url.pathname.replace('/blog/', '').replace('/blog', '');
234
+
235
+ if (slug && slug !== '/') {
236
+ // Single post
237
+ const post = await env.DB.prepare(
238
+ 'SELECT * FROM cms_posts WHERE slug = ? AND status = \\'published\\''
239
+ ).bind(slug).first();
240
+
241
+ if (!post) return new Response('Post not found', { status: 404 });
242
+
243
+ const navRows = await env.DB.prepare('SELECT label, href FROM cms_nav_items ORDER BY position ASC').all();
244
+ const html = renderShell(post.title, renderNav(navRows.results ?? []), \`
245
+ <article>
246
+ <h1>\${post.title}</h1>
247
+ <div class="post-body">\${post.body ?? ''}</div>
248
+ </article>
249
+ \`);
250
+ return new Response(html, { headers: { 'Content-Type': 'text/html;charset=UTF-8' } });
251
+ }
252
+
253
+ // Post list
254
+ const posts = await env.DB.prepare(
255
+ 'SELECT slug, title, published_at FROM cms_posts WHERE status = \\'published\\' ORDER BY published_at DESC LIMIT 20'
256
+ ).all();
257
+
258
+ const navRows = await env.DB.prepare('SELECT label, href FROM cms_nav_items ORDER BY position ASC').all();
259
+ const listHtml = (posts.results ?? []).map(p =>
260
+ \`<li><a href="/blog/\${p.slug}">\${p.title}</a></li>\`
261
+ ).join('');
262
+
263
+ const html = renderShell('Blog', renderNav(navRows.results ?? []), \`
264
+ <h1>Blog</h1>
265
+ <ul class="post-list">\${listHtml}</ul>
266
+ \`);
267
+ return new Response(html, { headers: { 'Content-Type': 'text/html;charset=UTF-8' } });
268
+ }
269
+ `;
270
+ }
271
+
272
+ // ── Route: contact ────────────────────────────────────────────────────────────
273
+ function contactRoute(email) {
274
+ return `export async function handleContact(request, env) {
275
+ let body;
276
+ try {
277
+ body = await request.json();
278
+ } catch {
279
+ return Response.json({ error: 'Invalid JSON' }, { status: 400 });
280
+ }
281
+
282
+ const { name, email: fromEmail, message } = body;
283
+ if (!name || !fromEmail || !message) {
284
+ return Response.json({ error: 'name, email, and message are required' }, { status: 400 });
285
+ }
286
+
287
+ // Save to D1
288
+ const id = crypto.randomUUID();
289
+ await env.DB.prepare(
290
+ 'INSERT INTO cms_contact_submissions (id, name, email, message) VALUES (?, ?, ?, ?)'
291
+ ).bind(id, name, fromEmail, message).run();
292
+
293
+ // Send via Resend (requires RESEND_API_KEY secret)
294
+ if (env.RESEND_API_KEY) {
295
+ await fetch('https://api.resend.com/emails', {
296
+ method: 'POST',
297
+ headers: {
298
+ Authorization: \`Bearer \${env.RESEND_API_KEY}\`,
299
+ 'Content-Type': 'application/json',
300
+ },
301
+ body: JSON.stringify({
302
+ from: 'noreply@yourdomain.com',
303
+ to: '${email}',
304
+ subject: \`New contact form submission from \${name}\`,
305
+ text: \`Name: \${name}\\nEmail: \${fromEmail}\\n\\n\${message}\`,
306
+ }),
307
+ });
308
+ }
309
+
310
+ return Response.json({ ok: true, id });
311
+ }
312
+ `;
313
+ }
314
+
315
+ // ── Template: HTML shell ──────────────────────────────────────────────────────
316
+ function htmlShell({ siteTitle }) {
317
+ return `export function renderShell(pageTitle, navHtml, bodyHtml) {
318
+ return \`<!DOCTYPE html>
319
+ <html lang="en">
320
+ <head>
321
+ <meta charset="UTF-8" />
322
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
323
+ <title>\${pageTitle} — ${siteTitle}</title>
324
+ <style>
325
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
326
+ body { font-family: system-ui, sans-serif; color: #1a1a1a; background: #fff; }
327
+ a { color: inherit; }
328
+ main { max-width: 1100px; margin: 0 auto; padding: 2rem 1rem; }
329
+ h1 { font-size: 2rem; margin-bottom: 1rem; }
330
+ h2 { font-size: 1.4rem; margin-bottom: 0.75rem; }
331
+ p { line-height: 1.65; margin-bottom: 1rem; }
332
+ </style>
333
+ </head>
334
+ <body>
335
+ \${navHtml}
336
+ <main>\${bodyHtml}</main>
337
+ </body>
338
+ </html>\`;
339
+ }
340
+ `;
341
+ }
342
+
343
+ // ── Template: nav component ───────────────────────────────────────────────────
344
+ function navComponent(navStyle, siteTitle) {
345
+ const styles = {
346
+ topbar: `
347
+ nav { display: flex; align-items: center; gap: 2rem; background: #111; padding: 0 1.5rem; height: 56px; }
348
+ nav .site-title { color: #fff; font-weight: 700; font-size: 1.1rem; text-decoration: none; margin-right: auto; }
349
+ nav a { color: #ccc; text-decoration: none; font-size: 0.9rem; }
350
+ nav a:hover { color: #fff; }`,
351
+ sidebar: `
352
+ .nav-sidebar { position: fixed; top: 0; left: 0; width: 220px; height: 100vh; background: #111; padding: 1.5rem 1rem; display: flex; flex-direction: column; gap: 0.5rem; }
353
+ .nav-sidebar .site-title { color: #fff; font-weight: 700; font-size: 1.1rem; text-decoration: none; margin-bottom: 1rem; }
354
+ .nav-sidebar a { color: #ccc; text-decoration: none; font-size: 0.9rem; padding: 0.4rem 0.6rem; border-radius: 4px; }
355
+ .nav-sidebar a:hover { background: #222; color: #fff; }
356
+ body { padding-left: 220px; }`,
357
+ minimal: `
358
+ nav { display: flex; align-items: center; justify-content: space-between; padding: 1rem 1.5rem; border-bottom: 1px solid #eee; }
359
+ nav .site-title { font-weight: 700; font-size: 1.1rem; text-decoration: none; }
360
+ .nav-toggle { background: none; border: none; cursor: pointer; font-size: 1.2rem; }`,
361
+ }[navStyle];
362
+
363
+ const navClass = navStyle === 'sidebar' ? 'class="nav-sidebar"' : '';
364
+
365
+ return `export function renderNav(items) {
366
+ const links = items.map(i => \`<a href="\${i.href}">\${i.label}</a>\`).join('');
367
+ return \`<style>${styles}</style>
368
+ <nav ${navClass}>
369
+ <a class="site-title" href="/">${siteTitle}</a>
370
+ \${links}
371
+ </nav>\`;
372
+ }
373
+ `;
374
+ }
375
+
376
+ // ── Template: individual page templates ──────────────────────────────────────
377
+ function pageTemplate(page, siteTitle, templateStyle) {
378
+ const pageContent = {
379
+ home: `<section class="hero">
380
+ <h1>Welcome to ${siteTitle}</h1>
381
+ <p>Your tagline goes here.</p>
382
+ </section>`,
383
+ about: `<h1>About Us</h1>
384
+ <p>Tell your story here.</p>`,
385
+ services: `<h1>Services</h1>
386
+ <ul>
387
+ <li>Service one</li>
388
+ <li>Service two</li>
389
+ <li>Service three</li>
390
+ </ul>`,
391
+ contact: `<h1>Contact</h1>
392
+ <form id="contact-form">
393
+ <div><label>Name<br><input type="text" name="name" required /></label></div>
394
+ <div><label>Email<br><input type="email" name="email" required /></label></div>
395
+ <div><label>Message<br><textarea name="message" required></textarea></label></div>
396
+ <button type="submit">Send</button>
397
+ </form>
398
+ <script>
399
+ document.getElementById('contact-form').addEventListener('submit', async (e) => {
400
+ e.preventDefault();
401
+ const data = Object.fromEntries(new FormData(e.target));
402
+ const res = await fetch('/api/contact', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(data) });
403
+ if (res.ok) { e.target.reset(); alert('Message sent!'); }
404
+ });
405
+ </script>`,
406
+ blog: `<h1>Blog</h1><p>Loading posts...</p>`,
407
+ privacy: `<h1>Privacy Policy</h1>
408
+ <p>Last updated: ${new Date().toLocaleDateString()}</p>
409
+ <p>We do not sell your data. Replace this with your actual privacy policy.</p>`,
410
+ };
411
+
412
+ const body = pageContent[page] ?? `<h1>${cap(page)}</h1><p>Content coming soon.</p>`;
413
+
414
+ if (templateStyle === 'json') {
415
+ return `export function render${cap(page)}Page(data = {}) {
416
+ return \`${body}\`;
417
+ }
418
+ `;
419
+ }
420
+
421
+ return `export function render${cap(page)}Page() {
422
+ return \`${body}\`;
423
+ }
424
+ `;
425
+ }
426
+
427
+ // ── README ────────────────────────────────────────────────────────────────────
428
+ function readme({ projectName, siteTitle, pages, navStyle, templateStyle }) {
429
+ return `# ${siteTitle}
430
+
431
+ Scaffolded by [@inneranimalmedia/agentsam-sdk](https://github.com/SamPrimeaux/agentsam-sdk).
432
+
433
+ ## Stack
434
+
435
+ - Cloudflare Worker (entry: \`src/index.js\`)
436
+ - D1 database (binding: \`DB\`)
437
+ - R2 bucket (binding: \`ASSETS\`)
438
+ - Template style: \`${templateStyle}\`
439
+ - Nav style: \`${navStyle}\`
440
+
441
+ ## Pages
442
+
443
+ ${pages.map(p => `- \`${p === 'home' ? '/' : `/${p}`}\` — ${cap(p)}`).join('\n')}
444
+
445
+ ## Deploy
446
+
447
+ \`\`\`bash
448
+ npm install
449
+
450
+ # Create D1 (copy the returned database_id into wrangler.toml)
451
+ npx wrangler d1 create ${projectName}
452
+
453
+ # Run migration
454
+ npx wrangler d1 execute ${projectName} --file=migrations/001_init.sql --remote
455
+
456
+ # Deploy
457
+ npx wrangler deploy
458
+ \`\`\`
459
+
460
+ ## Environment secrets
461
+
462
+ | Secret | Purpose |
463
+ |--------|---------|
464
+ | \`RESEND_API_KEY\` | Contact form email delivery (optional) |
465
+
466
+ Set with: \`npx wrangler secret put RESEND_API_KEY\`
467
+ `;
468
+ }
469
+
470
+ function cap(str) {
471
+ return str.charAt(0).toUpperCase() + str.slice(1);
472
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Worker API template generator.
3
+ */
4
+
5
+ export function workerApiTemplates({ projectName, routes, cfAccountId }) {
6
+ const files = {};
7
+
8
+ files['package.json'] = JSON.stringify({
9
+ name: projectName,
10
+ version: '0.1.0',
11
+ private: true,
12
+ scripts: {
13
+ deploy: 'wrangler deploy',
14
+ dev: 'wrangler dev',
15
+ 'db:migrate': `wrangler d1 execute ${projectName} --file=migrations/001_init.sql --remote`,
16
+ },
17
+ devDependencies: { wrangler: '^3.0.0' },
18
+ }, null, 2);
19
+
20
+ files['wrangler.toml'] = `name = "${projectName}"
21
+ main = "src/index.js"
22
+ compatibility_date = "2024-01-01"
23
+ account_id = "${cfAccountId}"
24
+
25
+ [[d1_databases]]
26
+ binding = "DB"
27
+ database_name = "${projectName}"
28
+ database_id = "REPLACE_WITH_YOUR_D1_ID"
29
+ `;
30
+
31
+ // Entry
32
+ const routeImports = routes.map(r => `import { handle${cap(r)} } from './routes/${r}.js';`).join('\n');
33
+ const routeMatches = routes.map(r => {
34
+ const path = r === 'health' ? `pathname === '/health'`
35
+ : r === 'auth' ? `pathname.startsWith('/auth')`
36
+ : r === 'webhook' ? `pathname.startsWith('/webhooks')`
37
+ : `pathname.startsWith('/${r}')`;
38
+ return ` if (${path}) return handle${cap(r)}(request, env);`;
39
+ }).join('\n');
40
+
41
+ files['src/index.js'] = `${routeImports}
42
+
43
+ export default {
44
+ async fetch(request, env) {
45
+ const url = new URL(request.url);
46
+ const { pathname } = url;
47
+
48
+ if (request.method === 'OPTIONS') {
49
+ return new Response(null, {
50
+ headers: {
51
+ 'Access-Control-Allow-Origin': '*',
52
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
53
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
54
+ },
55
+ });
56
+ }
57
+
58
+ ${routeMatches}
59
+
60
+ return Response.json({ error: 'Not found' }, { status: 404 });
61
+ },
62
+ };
63
+ `;
64
+
65
+ // Route stubs
66
+ if (routes.includes('health')) {
67
+ files['src/routes/health.js'] = `export async function handleHealth(request, env) {
68
+ return Response.json({ ok: true, ts: Date.now() });
69
+ }
70
+ `;
71
+ }
72
+
73
+ if (routes.includes('auth')) {
74
+ files['src/routes/auth.js'] = `export async function handleAuth(request, env) {
75
+ const url = new URL(request.url);
76
+ if (url.pathname === '/auth/token' && request.method === 'POST') {
77
+ // TODO: issue token
78
+ return Response.json({ token: 'replace-me' });
79
+ }
80
+ return Response.json({ error: 'Unknown auth route' }, { status: 404 });
81
+ }
82
+ `;
83
+ }
84
+
85
+ if (routes.includes('users')) {
86
+ files['src/routes/users.js'] = `export async function handleUsers(request, env) {
87
+ const url = new URL(request.url);
88
+ const id = url.pathname.replace('/users/', '').replace('/users', '') || null;
89
+
90
+ if (request.method === 'GET' && !id) {
91
+ const rows = await env.DB.prepare('SELECT * FROM users LIMIT 50').all();
92
+ return Response.json(rows.results ?? []);
93
+ }
94
+ if (request.method === 'GET' && id) {
95
+ const row = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(id).first();
96
+ if (!row) return Response.json({ error: 'Not found' }, { status: 404 });
97
+ return Response.json(row);
98
+ }
99
+ if (request.method === 'POST') {
100
+ const body = await request.json();
101
+ const newId = crypto.randomUUID();
102
+ await env.DB.prepare('INSERT INTO users (id, email) VALUES (?, ?)').bind(newId, body.email).run();
103
+ return Response.json({ id: newId }, { status: 201 });
104
+ }
105
+ if (request.method === 'DELETE' && id) {
106
+ await env.DB.prepare('DELETE FROM users WHERE id = ?').bind(id).run();
107
+ return Response.json({ ok: true });
108
+ }
109
+ return Response.json({ error: 'Method not allowed' }, { status: 405 });
110
+ }
111
+ `;
112
+ }
113
+
114
+ if (routes.includes('content')) {
115
+ files['src/routes/content.js'] = `export async function handleContent(request, env) {
116
+ const url = new URL(request.url);
117
+ const slug = url.pathname.replace('/content/', '').replace('/content', '') || null;
118
+
119
+ if (request.method === 'GET' && !slug) {
120
+ const rows = await env.DB.prepare("SELECT id, slug, title, status FROM cms_pages LIMIT 50").all();
121
+ return Response.json(rows.results ?? []);
122
+ }
123
+ if (request.method === 'GET' && slug) {
124
+ const row = await env.DB.prepare('SELECT * FROM cms_pages WHERE slug = ?').bind(slug).first();
125
+ if (!row) return Response.json({ error: 'Not found' }, { status: 404 });
126
+ return Response.json(row);
127
+ }
128
+ if (request.method === 'POST') {
129
+ const body = await request.json();
130
+ const id = crypto.randomUUID();
131
+ await env.DB.prepare(
132
+ 'INSERT INTO cms_pages (id, slug, title, template, content_json) VALUES (?, ?, ?, ?, ?)'
133
+ ).bind(id, body.slug, body.title, body.template ?? 'default', JSON.stringify(body.content ?? {})).run();
134
+ return Response.json({ id }, { status: 201 });
135
+ }
136
+ return Response.json({ error: 'Method not allowed' }, { status: 405 });
137
+ }
138
+ `;
139
+ }
140
+
141
+ if (routes.includes('webhook')) {
142
+ files['src/routes/webhook.js'] = `export async function handleWebhook(request, env) {
143
+ const url = new URL(request.url);
144
+ const type = url.pathname.replace('/webhooks/', '').replace('/webhooks', '') || 'unknown';
145
+
146
+ let body;
147
+ try { body = await request.json(); } catch { body = {}; }
148
+
149
+ console.log(\`[webhook] type=\${type}\`, JSON.stringify(body));
150
+
151
+ // TODO: dispatch based on type
152
+ return Response.json({ received: true, type });
153
+ }
154
+ `;
155
+ }
156
+
157
+ // Migration
158
+ let sql = `-- ${projectName} initial schema\n\n`;
159
+ if (routes.includes('users')) {
160
+ sql += `CREATE TABLE IF NOT EXISTS users (
161
+ id TEXT PRIMARY KEY,
162
+ email TEXT NOT NULL UNIQUE,
163
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
164
+ );\n\n`;
165
+ }
166
+ if (routes.includes('content')) {
167
+ sql += `CREATE TABLE IF NOT EXISTS cms_pages (
168
+ id TEXT PRIMARY KEY,
169
+ slug TEXT NOT NULL UNIQUE,
170
+ title TEXT NOT NULL,
171
+ template TEXT NOT NULL DEFAULT 'default',
172
+ content_json TEXT,
173
+ status TEXT NOT NULL DEFAULT 'draft',
174
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
175
+ );\n\n`;
176
+ }
177
+
178
+ files['migrations/001_init.sql'] = sql;
179
+
180
+ files['README.md'] = `# ${projectName}
181
+
182
+ Scaffolded by [@inneranimalmedia/agentsam-sdk](https://github.com/SamPrimeaux/agentsam-sdk).
183
+
184
+ ## Routes
185
+
186
+ ${routes.map(r => `- \`/${r}\``).join('\n')}
187
+
188
+ ## Deploy
189
+
190
+ \`\`\`bash
191
+ npm install
192
+ npx wrangler d1 create ${projectName}
193
+ # paste database_id into wrangler.toml
194
+ npx wrangler d1 execute ${projectName} --file=migrations/001_init.sql --remote
195
+ npx wrangler deploy
196
+ \`\`\`
197
+ `;
198
+
199
+ return files;
200
+ }
201
+
202
+ function cap(str) {
203
+ return str.charAt(0).toUpperCase() + str.slice(1);
204
+ }