clawfire 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +10 -10
  2. package/dist/cli.js +76 -16
  3. package/dist/codegen.cjs.map +1 -1
  4. package/dist/codegen.d.cts +1 -1
  5. package/dist/codegen.d.ts +1 -1
  6. package/dist/codegen.js.map +1 -1
  7. package/dist/{dev-server-5ATZVQJT.js → dev-server-GD445Q6F.js} +247 -6
  8. package/dist/dev.cjs +247 -6
  9. package/dist/dev.cjs.map +1 -1
  10. package/dist/dev.js +247 -6
  11. package/dist/dev.js.map +1 -1
  12. package/dist/{discover-DYNqz_ym.d.cts → discover-8p9Mujyt.d.cts} +3 -3
  13. package/dist/{discover-DYNqz_ym.d.ts → discover-8p9Mujyt.d.ts} +3 -3
  14. package/dist/functions.cjs.map +1 -1
  15. package/dist/functions.d.cts +1 -1
  16. package/dist/functions.d.ts +1 -1
  17. package/dist/functions.js.map +1 -1
  18. package/package.json +1 -1
  19. package/templates/CLAUDE.md +22 -19
  20. package/templates/functions/index.ts +3 -3
  21. package/templates/starter/.claude/skills/clawfire-api/SKILL.md +8 -8
  22. package/templates/starter/.claude/skills/clawfire-diagnose/SKILL.md +6 -6
  23. package/templates/starter/.claude/skills/clawfire-model/SKILL.md +2 -2
  24. package/templates/starter/CLAUDE.md +33 -31
  25. package/templates/starter/app/pages/index.html +7 -6
  26. package/templates/starter/functions/index.ts +52 -0
  27. package/templates/starter/functions/package.json +22 -0
  28. package/templates/starter/functions/tsconfig.json +18 -0
  29. package/templates/starter/package.json +4 -2
  30. package/templates/starter/tsconfig.json +1 -1
  31. /package/templates/{app → functions}/routes/auth/login.ts +0 -0
  32. /package/templates/{app → functions}/routes/health.ts +0 -0
  33. /package/templates/{app → functions}/schemas/user.ts +0 -0
  34. /package/templates/starter/{app → functions}/routes/health.ts +0 -0
  35. /package/templates/starter/{app → functions}/routes/todos/create.ts +0 -0
  36. /package/templates/starter/{app → functions}/routes/todos/delete.ts +0 -0
  37. /package/templates/starter/{app → functions}/routes/todos/list.ts +0 -0
  38. /package/templates/starter/{app → functions}/routes/todos/update.ts +0 -0
  39. /package/templates/starter/{app → functions}/schemas/todo.ts +0 -0
  40. /package/templates/starter/{app → functions}/store.ts +0 -0
package/dist/dev.js CHANGED
@@ -1756,7 +1756,29 @@ function generateDashboardHtml(options) {
1756
1756
  <div id="service-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px;"></div>
1757
1757
  </div>
1758
1758
 
1759
- <!-- Section 2: Config Overview -->
1759
+ <!-- Section 2: Deploy -->
1760
+ <div style="margin-bottom:32px;">
1761
+ <h2 style="font-size:18px;font-weight:700;color:#f97316;margin-bottom:16px;">Deploy</h2>
1762
+ <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px;">
1763
+ <div style="padding:16px;border-radius:8px;border:1px solid #2a2a2a;background:#141414;">
1764
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
1765
+ <span style="font-size:16px;">&#127760;</span>
1766
+ <span style="font-weight:600;color:#e5e5e5;">Hosting</span>
1767
+ </div>
1768
+ <div style="font-size:13px;color:#a3a3a3;margin-bottom:12px;">Deploy your app to Firebase Hosting</div>
1769
+ <button id="deploy-hosting-btn" onclick="deployHosting()" style="padding:8px 20px;background:#3b82f6;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;">
1770
+ Deploy Hosting
1771
+ </button>
1772
+ <div id="deploy-hosting-status" style="display:none;margin-top:10px;font-size:13px;padding:10px 14px;border-radius:6px;"></div>
1773
+ <div id="deploy-hosting-url" style="display:none;margin-top:8px;padding:10px 14px;border-radius:6px;background:#0a1a0a;border:1px solid #22c55e;">
1774
+ <div style="font-size:11px;color:#a3a3a3;margin-bottom:4px;">Live URL</div>
1775
+ <a id="deploy-hosting-link" href="#" target="_blank" style="color:#22c55e;font-family:monospace;font-size:14px;text-decoration:none;word-break:break-all;"></a>
1776
+ </div>
1777
+ </div>
1778
+ </div>
1779
+ </div>
1780
+
1781
+ <!-- Section 3: Config Overview -->
1760
1782
  <div style="margin-bottom:32px;">
1761
1783
  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
1762
1784
  <h2 style="font-size:18px;font-weight:700;color:#f97316;">Config Overview</h2>
@@ -1780,7 +1802,7 @@ function generateDashboardHtml(options) {
1780
1802
  </div>
1781
1803
  </div>
1782
1804
 
1783
- <!-- Section 3: Environment Variables -->
1805
+ <!-- Section 4: Environment Variables -->
1784
1806
  <div style="margin-bottom:32px;">
1785
1807
  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
1786
1808
  <h2 style="font-size:18px;font-weight:700;color:#f97316;">Environment Variables</h2>
@@ -2380,16 +2402,22 @@ function generateDashboardHtml(options) {
2380
2402
  var statusColors = { configured: '#22c55e', placeholder: '#eab308', missing: '#666' };
2381
2403
  var statusLabels = { configured: 'Ready', placeholder: 'Needs Setup', missing: 'Not Configured' };
2382
2404
 
2405
+ var enableableServices = { 'Hosting': 'hosting', 'Firestore': 'firestore', 'Storage': 'storage' };
2383
2406
  data.services.forEach(function(svc) {
2384
2407
  var card = document.createElement('div');
2385
2408
  card.style.cssText = 'padding:16px;border-radius:8px;border:1px solid #2a2a2a;background:#141414;';
2409
+ var enableBtn = '';
2410
+ if (svc.status === 'missing' && enableableServices[svc.name]) {
2411
+ enableBtn = '<button onclick="enableService(\\'' + enableableServices[svc.name] + '\\')" style="margin-top:8px;padding:4px 12px;background:#f97316;color:#000;border:none;border-radius:4px;font-size:11px;font-weight:600;cursor:pointer;">Enable</button>';
2412
+ }
2386
2413
  card.innerHTML =
2387
2414
  '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">' +
2388
2415
  '<span style="width:8px;height:8px;border-radius:50%;background:' + statusColors[svc.status] + ';display:inline-block;"></span>' +
2389
2416
  '<span style="font-weight:600;color:#e5e5e5;">' + svc.name + '</span>' +
2390
2417
  '</div>' +
2391
2418
  '<div style="font-size:12px;color:' + statusColors[svc.status] + ';">' + statusLabels[svc.status] + '</div>' +
2392
- (svc.detail ? '<div style="font-size:11px;color:#666;margin-top:4px;">' + svc.detail + '</div>' : '');
2419
+ (svc.detail ? '<div style="font-size:11px;color:#666;margin-top:4px;">' + svc.detail + '</div>' : '') +
2420
+ enableBtn;
2393
2421
  grid.appendChild(card);
2394
2422
  });
2395
2423
 
@@ -2574,6 +2602,67 @@ function generateDashboardHtml(options) {
2574
2602
  });
2575
2603
  }
2576
2604
 
2605
+ // \u2500\u2500\u2500 Deploy Hosting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2606
+ window.deployHosting = function() {
2607
+ var btn = document.getElementById('deploy-hosting-btn');
2608
+ var status = document.getElementById('deploy-hosting-status');
2609
+ var urlBox = document.getElementById('deploy-hosting-url');
2610
+ btn.disabled = true;
2611
+ btn.textContent = 'Deploying...';
2612
+ status.textContent = 'Deploying to Firebase Hosting... This may take up to 2 minutes.';
2613
+ status.style.cssText = 'display:block;margin-top:10px;font-size:13px;padding:10px 14px;border-radius:6px;background:#0a0a1a;border:1px solid #3b82f6;color:#3b82f6;';
2614
+ urlBox.style.display = 'none';
2615
+
2616
+ fetch(API + '/__dev/deploy/hosting', { method: 'POST' })
2617
+ .then(function(r) { return r.json(); })
2618
+ .then(function(data) {
2619
+ if (data.success) {
2620
+ status.textContent = data.message;
2621
+ status.style.cssText = 'display:block;margin-top:10px;font-size:13px;padding:10px 14px;border-radius:6px;background:#0a1a0a;border:1px solid #22c55e;color:#22c55e;';
2622
+ if (data.url) {
2623
+ var link = document.getElementById('deploy-hosting-link');
2624
+ link.href = data.url;
2625
+ link.textContent = data.url;
2626
+ urlBox.style.display = 'block';
2627
+ }
2628
+ } else {
2629
+ status.textContent = data.message;
2630
+ status.style.cssText = 'display:block;margin-top:10px;font-size:13px;padding:10px 14px;border-radius:6px;background:#1c0808;border:1px solid #ef4444;color:#ef4444;';
2631
+ }
2632
+ btn.disabled = false;
2633
+ btn.textContent = 'Deploy Hosting';
2634
+ // Refresh firebase status
2635
+ fetch(API + '/__dev/firebase-status').then(function(r) { return r.json(); }).then(function(d) { renderFirebaseStatus(d); }).catch(function(){});
2636
+ })
2637
+ .catch(function(err) {
2638
+ status.textContent = 'Error: ' + err.message;
2639
+ status.style.cssText = 'display:block;margin-top:10px;font-size:13px;padding:10px 14px;border-radius:6px;background:#1c0808;border:1px solid #ef4444;color:#ef4444;';
2640
+ btn.disabled = false;
2641
+ btn.textContent = 'Deploy Hosting';
2642
+ });
2643
+ };
2644
+
2645
+ // \u2500\u2500\u2500 Enable Service \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2646
+ window.enableService = function(service) {
2647
+ fetch(API + '/__dev/enable-service', {
2648
+ method: 'POST',
2649
+ headers: { 'Content-Type': 'application/json' },
2650
+ body: JSON.stringify({ service: service })
2651
+ })
2652
+ .then(function(r) { return r.json(); })
2653
+ .then(function(data) {
2654
+ if (data.success) {
2655
+ // Refresh firebase status to show updated service cards
2656
+ fetch(API + '/__dev/firebase-status').then(function(r) { return r.json(); }).then(function(d) { renderFirebaseStatus(d); }).catch(function(){});
2657
+ } else {
2658
+ alert('Failed to enable ' + service + ': ' + data.message);
2659
+ }
2660
+ })
2661
+ .catch(function(err) {
2662
+ alert('Error: ' + err.message);
2663
+ });
2664
+ };
2665
+
2577
2666
  // \u2500\u2500\u2500 Environment Variables \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2578
2667
  function renderEnvVars(data) {
2579
2668
  envData = data.variables || [];
@@ -3043,6 +3132,113 @@ var FirebaseSetup = class {
3043
3132
  this.saveState({ webAppId: appId, webAppDisplayName: displayName });
3044
3133
  return { success: true, message: `Web app "${displayName}" selected.` };
3045
3134
  }
3135
+ // ─── Deploy ─────────────────────────────────────────────────────────
3136
+ async deployHosting() {
3137
+ const firebaseJsonPath = resolve5(this.projectDir, "firebase.json");
3138
+ if (!existsSync6(firebaseJsonPath)) {
3139
+ return { success: false, message: "firebase.json not found. Enable hosting first." };
3140
+ }
3141
+ try {
3142
+ const config = JSON.parse(readFileSync4(firebaseJsonPath, "utf-8"));
3143
+ if (!config.hosting) {
3144
+ return { success: false, message: "Hosting not configured in firebase.json. Click Enable first." };
3145
+ }
3146
+ } catch {
3147
+ return { success: false, message: "Invalid firebase.json." };
3148
+ }
3149
+ try {
3150
+ const output = await this.execTimeout(
3151
+ "firebase",
3152
+ ["deploy", "--only", "hosting", "--json"],
3153
+ 12e4
3154
+ // 2 min timeout
3155
+ );
3156
+ let url = "";
3157
+ try {
3158
+ const data = JSON.parse(output);
3159
+ if (data?.result) {
3160
+ for (const key of Object.keys(data.result)) {
3161
+ if (key.startsWith("hosting") && typeof data.result[key] === "object") {
3162
+ url = data.result[key]?.url || data.result[key]?.site?.url || "";
3163
+ if (url) break;
3164
+ }
3165
+ }
3166
+ }
3167
+ } catch {
3168
+ const urlMatch = output.match(/https:\/\/[a-z0-9-]+\.web\.app/i);
3169
+ if (urlMatch) url = urlMatch[0];
3170
+ }
3171
+ if (!url) {
3172
+ const state = this.loadState();
3173
+ if (state.projectId) {
3174
+ url = `https://${state.projectId}.web.app`;
3175
+ }
3176
+ }
3177
+ return { success: true, url: url || void 0, message: "Hosting deployed successfully!" };
3178
+ } catch (err) {
3179
+ const msg = err instanceof Error ? err.message : "Unknown error";
3180
+ return { success: false, message: `Deploy failed: ${msg}` };
3181
+ }
3182
+ }
3183
+ // ─── Service Enable ────────────────────────────────────────────────
3184
+ enableService(service) {
3185
+ const firebaseJsonPath = resolve5(this.projectDir, "firebase.json");
3186
+ let config = {};
3187
+ if (existsSync6(firebaseJsonPath)) {
3188
+ try {
3189
+ config = JSON.parse(readFileSync4(firebaseJsonPath, "utf-8"));
3190
+ } catch {
3191
+ config = {};
3192
+ }
3193
+ }
3194
+ switch (service) {
3195
+ case "hosting": {
3196
+ if (!config.hosting) {
3197
+ config.hosting = {
3198
+ public: "public",
3199
+ ignore: ["firebase.json", "**/.*", "**/node_modules/**"],
3200
+ rewrites: [{ source: "**", destination: "/index.html" }]
3201
+ };
3202
+ }
3203
+ break;
3204
+ }
3205
+ case "firestore": {
3206
+ if (!config.firestore) {
3207
+ config.firestore = {
3208
+ rules: "firestore.rules",
3209
+ indexes: "firestore.indexes.json"
3210
+ };
3211
+ }
3212
+ const rulesPath = resolve5(this.projectDir, "firestore.rules");
3213
+ if (!existsSync6(rulesPath)) {
3214
+ writeFileSync2(
3215
+ rulesPath,
3216
+ "rules_version = '2';\nservice cloud.firestore {\n match /databases/{database}/documents {\n match /{document=**} {\n allow read, write: if request.auth != null;\n }\n }\n}\n"
3217
+ );
3218
+ }
3219
+ const indexesPath = resolve5(this.projectDir, "firestore.indexes.json");
3220
+ if (!existsSync6(indexesPath)) {
3221
+ writeFileSync2(indexesPath, JSON.stringify({ indexes: [], fieldOverrides: [] }, null, 2) + "\n");
3222
+ }
3223
+ break;
3224
+ }
3225
+ case "storage": {
3226
+ if (!config.storage) {
3227
+ config.storage = { rules: "storage.rules" };
3228
+ }
3229
+ const storageRulesPath = resolve5(this.projectDir, "storage.rules");
3230
+ if (!existsSync6(storageRulesPath)) {
3231
+ writeFileSync2(
3232
+ storageRulesPath,
3233
+ "rules_version = '2';\nservice firebase.storage {\n match /b/{bucket}/o {\n match /{allPaths=**} {\n allow read, write: if request.auth != null;\n }\n }\n}\n"
3234
+ );
3235
+ }
3236
+ break;
3237
+ }
3238
+ }
3239
+ writeFileSync2(firebaseJsonPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
3240
+ return { success: true, message: `${service} enabled in firebase.json.` };
3241
+ }
3046
3242
  // ─── Helpers ───────────────────────────────────────────────────────
3047
3243
  execTimeout(command, args, timeoutMs) {
3048
3244
  return new Promise((resolve7, reject) => {
@@ -3324,8 +3520,8 @@ var DevServer = class {
3324
3520
  onSetupRoutes: options.onSetupRoutes || (() => {
3325
3521
  })
3326
3522
  };
3327
- this.routesDir = resolve6(this.options.projectDir, "app/routes");
3328
- this.schemasDir = resolve6(this.options.projectDir, "app/schemas");
3523
+ this.routesDir = resolve6(this.options.projectDir, "functions/routes");
3524
+ this.schemasDir = resolve6(this.options.projectDir, "functions/schemas");
3329
3525
  this.publicDir = resolve6(this.options.projectDir, "public");
3330
3526
  this.pagesDir = resolve6(this.options.projectDir, "app/pages");
3331
3527
  this.componentsDir = resolve6(this.options.projectDir, "app/components");
@@ -3565,7 +3761,23 @@ function switchTab(tab) {
3565
3761
  // Lazy-load dashboard data on first click
3566
3762
  if (window._loadDashboard) window._loadDashboard();
3567
3763
  }
3764
+ // Persist active tab across reloads (auto-fill writes config \u2192 watcher reloads page)
3765
+ try { localStorage.setItem('clawfire-active-tab', tab); } catch(e) {}
3568
3766
  }
3767
+ // Restore saved tab on page load
3768
+ (function() {
3769
+ try {
3770
+ var saved = localStorage.getItem('clawfire-active-tab');
3771
+ if (saved === 'dashboard') {
3772
+ var fn = function() { switchTab('dashboard'); };
3773
+ if (document.readyState === 'loading') {
3774
+ document.addEventListener('DOMContentLoaded', fn);
3775
+ } else {
3776
+ fn();
3777
+ }
3778
+ }
3779
+ } catch(e) {}
3780
+ })();
3569
3781
  </script>`;
3570
3782
  const liveReloadScript = `
3571
3783
  <script>
@@ -3871,7 +4083,7 @@ ${liveReloadScript}
3871
4083
  }
3872
4084
  console.log("");
3873
4085
  if (watching) {
3874
- const watchDirs = ["app/routes/", "app/schemas/", "public/"];
4086
+ const watchDirs = ["functions/routes/", "functions/schemas/", "public/"];
3875
4087
  if (pagesActive) watchDirs.push("app/pages/", "app/components/");
3876
4088
  console.log(` \x1B[35mHot Reload\x1B[0m : \x1B[32mON\x1B[0m`);
3877
4089
  console.log(` \x1B[2mWatching: ${watchDirs.join(", ")}\x1B[0m`);
@@ -4044,6 +4256,35 @@ ${liveReloadScript}
4044
4256
  });
4045
4257
  return;
4046
4258
  }
4259
+ if (url.pathname === "/__dev/deploy/hosting" && req.method === "POST") {
4260
+ this.firebaseSetup.deployHosting().then((result) => {
4261
+ clearFirebaseStatusCache();
4262
+ sendJson(result);
4263
+ }).catch((err) => sendJson({ success: false, message: err instanceof Error ? err.message : "Failed" }, 500));
4264
+ return;
4265
+ }
4266
+ if (url.pathname === "/__dev/enable-service" && req.method === "POST") {
4267
+ let body = "";
4268
+ req.on("data", (chunk) => {
4269
+ body += chunk;
4270
+ });
4271
+ req.on("end", () => {
4272
+ try {
4273
+ const data = JSON.parse(body);
4274
+ const service = data.service;
4275
+ if (!service || !["hosting", "firestore", "storage"].includes(service)) {
4276
+ sendJson({ success: false, message: "Invalid service. Use: hosting, firestore, storage" }, 400);
4277
+ return;
4278
+ }
4279
+ const result = this.firebaseSetup.enableService(service);
4280
+ clearFirebaseStatusCache();
4281
+ sendJson(result);
4282
+ } catch {
4283
+ sendJson({ success: false, message: "Invalid JSON body" }, 400);
4284
+ }
4285
+ });
4286
+ return;
4287
+ }
4047
4288
  if (url.pathname === "/__dev/setup/select-web-app" && req.method === "POST") {
4048
4289
  let body = "";
4049
4290
  req.on("data", (chunk) => {