clawfire 0.5.0 → 0.5.1

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/dist/cli.js CHANGED
@@ -209,7 +209,7 @@ async function runDevServer() {
209
209
  const port = portArg ? parseInt(portArg.split("=")[1], 10) : 3e3;
210
210
  const apiPort = apiPortArg ? parseInt(apiPortArg.split("=")[1], 10) : 3456;
211
211
  const noHotReload = args.includes("--no-hot-reload");
212
- const { startDevServer } = await import("./dev-server-5ATZVQJT.js");
212
+ const { startDevServer } = await import("./dev-server-7S6FZNJT.js");
213
213
  await startDevServer({
214
214
  projectDir,
215
215
  port,
@@ -1382,7 +1382,29 @@ function generateDashboardHtml(options) {
1382
1382
  <div id="service-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px;"></div>
1383
1383
  </div>
1384
1384
 
1385
- <!-- Section 2: Config Overview -->
1385
+ <!-- Section 2: Deploy -->
1386
+ <div style="margin-bottom:32px;">
1387
+ <h2 style="font-size:18px;font-weight:700;color:#f97316;margin-bottom:16px;">Deploy</h2>
1388
+ <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px;">
1389
+ <div style="padding:16px;border-radius:8px;border:1px solid #2a2a2a;background:#141414;">
1390
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
1391
+ <span style="font-size:16px;">&#127760;</span>
1392
+ <span style="font-weight:600;color:#e5e5e5;">Hosting</span>
1393
+ </div>
1394
+ <div style="font-size:13px;color:#a3a3a3;margin-bottom:12px;">Deploy your app to Firebase Hosting</div>
1395
+ <button id="deploy-hosting-btn" onclick="deployHosting()" style="padding:8px 20px;background:#3b82f6;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;">
1396
+ Deploy Hosting
1397
+ </button>
1398
+ <div id="deploy-hosting-status" style="display:none;margin-top:10px;font-size:13px;padding:10px 14px;border-radius:6px;"></div>
1399
+ <div id="deploy-hosting-url" style="display:none;margin-top:8px;padding:10px 14px;border-radius:6px;background:#0a1a0a;border:1px solid #22c55e;">
1400
+ <div style="font-size:11px;color:#a3a3a3;margin-bottom:4px;">Live URL</div>
1401
+ <a id="deploy-hosting-link" href="#" target="_blank" style="color:#22c55e;font-family:monospace;font-size:14px;text-decoration:none;word-break:break-all;"></a>
1402
+ </div>
1403
+ </div>
1404
+ </div>
1405
+ </div>
1406
+
1407
+ <!-- Section 3: Config Overview -->
1386
1408
  <div style="margin-bottom:32px;">
1387
1409
  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
1388
1410
  <h2 style="font-size:18px;font-weight:700;color:#f97316;">Config Overview</h2>
@@ -1406,7 +1428,7 @@ function generateDashboardHtml(options) {
1406
1428
  </div>
1407
1429
  </div>
1408
1430
 
1409
- <!-- Section 3: Environment Variables -->
1431
+ <!-- Section 4: Environment Variables -->
1410
1432
  <div style="margin-bottom:32px;">
1411
1433
  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
1412
1434
  <h2 style="font-size:18px;font-weight:700;color:#f97316;">Environment Variables</h2>
@@ -2006,16 +2028,22 @@ function generateDashboardHtml(options) {
2006
2028
  var statusColors = { configured: '#22c55e', placeholder: '#eab308', missing: '#666' };
2007
2029
  var statusLabels = { configured: 'Ready', placeholder: 'Needs Setup', missing: 'Not Configured' };
2008
2030
 
2031
+ var enableableServices = { 'Hosting': 'hosting', 'Firestore': 'firestore', 'Storage': 'storage' };
2009
2032
  data.services.forEach(function(svc) {
2010
2033
  var card = document.createElement('div');
2011
2034
  card.style.cssText = 'padding:16px;border-radius:8px;border:1px solid #2a2a2a;background:#141414;';
2035
+ var enableBtn = '';
2036
+ if (svc.status === 'missing' && enableableServices[svc.name]) {
2037
+ enableBtn = '<button onclick="enableService(\\'' + enableableServices[svc.name] + '\\')" style="margin-top:8px;padding:4px 12px;background:#f97316;color:#000;border:none;border-radius:4px;font-size:11px;font-weight:600;cursor:pointer;">Enable</button>';
2038
+ }
2012
2039
  card.innerHTML =
2013
2040
  '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">' +
2014
2041
  '<span style="width:8px;height:8px;border-radius:50%;background:' + statusColors[svc.status] + ';display:inline-block;"></span>' +
2015
2042
  '<span style="font-weight:600;color:#e5e5e5;">' + svc.name + '</span>' +
2016
2043
  '</div>' +
2017
2044
  '<div style="font-size:12px;color:' + statusColors[svc.status] + ';">' + statusLabels[svc.status] + '</div>' +
2018
- (svc.detail ? '<div style="font-size:11px;color:#666;margin-top:4px;">' + svc.detail + '</div>' : '');
2045
+ (svc.detail ? '<div style="font-size:11px;color:#666;margin-top:4px;">' + svc.detail + '</div>' : '') +
2046
+ enableBtn;
2019
2047
  grid.appendChild(card);
2020
2048
  });
2021
2049
 
@@ -2200,6 +2228,67 @@ function generateDashboardHtml(options) {
2200
2228
  });
2201
2229
  }
2202
2230
 
2231
+ // \u2500\u2500\u2500 Deploy Hosting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2232
+ window.deployHosting = function() {
2233
+ var btn = document.getElementById('deploy-hosting-btn');
2234
+ var status = document.getElementById('deploy-hosting-status');
2235
+ var urlBox = document.getElementById('deploy-hosting-url');
2236
+ btn.disabled = true;
2237
+ btn.textContent = 'Deploying...';
2238
+ status.textContent = 'Deploying to Firebase Hosting... This may take up to 2 minutes.';
2239
+ status.style.cssText = 'display:block;margin-top:10px;font-size:13px;padding:10px 14px;border-radius:6px;background:#0a0a1a;border:1px solid #3b82f6;color:#3b82f6;';
2240
+ urlBox.style.display = 'none';
2241
+
2242
+ fetch(API + '/__dev/deploy/hosting', { method: 'POST' })
2243
+ .then(function(r) { return r.json(); })
2244
+ .then(function(data) {
2245
+ if (data.success) {
2246
+ status.textContent = data.message;
2247
+ status.style.cssText = 'display:block;margin-top:10px;font-size:13px;padding:10px 14px;border-radius:6px;background:#0a1a0a;border:1px solid #22c55e;color:#22c55e;';
2248
+ if (data.url) {
2249
+ var link = document.getElementById('deploy-hosting-link');
2250
+ link.href = data.url;
2251
+ link.textContent = data.url;
2252
+ urlBox.style.display = 'block';
2253
+ }
2254
+ } else {
2255
+ status.textContent = data.message;
2256
+ status.style.cssText = 'display:block;margin-top:10px;font-size:13px;padding:10px 14px;border-radius:6px;background:#1c0808;border:1px solid #ef4444;color:#ef4444;';
2257
+ }
2258
+ btn.disabled = false;
2259
+ btn.textContent = 'Deploy Hosting';
2260
+ // Refresh firebase status
2261
+ fetch(API + '/__dev/firebase-status').then(function(r) { return r.json(); }).then(function(d) { renderFirebaseStatus(d); }).catch(function(){});
2262
+ })
2263
+ .catch(function(err) {
2264
+ status.textContent = 'Error: ' + err.message;
2265
+ status.style.cssText = 'display:block;margin-top:10px;font-size:13px;padding:10px 14px;border-radius:6px;background:#1c0808;border:1px solid #ef4444;color:#ef4444;';
2266
+ btn.disabled = false;
2267
+ btn.textContent = 'Deploy Hosting';
2268
+ });
2269
+ };
2270
+
2271
+ // \u2500\u2500\u2500 Enable Service \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2272
+ window.enableService = function(service) {
2273
+ fetch(API + '/__dev/enable-service', {
2274
+ method: 'POST',
2275
+ headers: { 'Content-Type': 'application/json' },
2276
+ body: JSON.stringify({ service: service })
2277
+ })
2278
+ .then(function(r) { return r.json(); })
2279
+ .then(function(data) {
2280
+ if (data.success) {
2281
+ // Refresh firebase status to show updated service cards
2282
+ fetch(API + '/__dev/firebase-status').then(function(r) { return r.json(); }).then(function(d) { renderFirebaseStatus(d); }).catch(function(){});
2283
+ } else {
2284
+ alert('Failed to enable ' + service + ': ' + data.message);
2285
+ }
2286
+ })
2287
+ .catch(function(err) {
2288
+ alert('Error: ' + err.message);
2289
+ });
2290
+ };
2291
+
2203
2292
  // \u2500\u2500\u2500 Environment Variables \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2204
2293
  function renderEnvVars(data) {
2205
2294
  envData = data.variables || [];
@@ -2669,6 +2758,113 @@ var FirebaseSetup = class {
2669
2758
  this.saveState({ webAppId: appId, webAppDisplayName: displayName });
2670
2759
  return { success: true, message: `Web app "${displayName}" selected.` };
2671
2760
  }
2761
+ // ─── Deploy ─────────────────────────────────────────────────────────
2762
+ async deployHosting() {
2763
+ const firebaseJsonPath = resolve4(this.projectDir, "firebase.json");
2764
+ if (!existsSync5(firebaseJsonPath)) {
2765
+ return { success: false, message: "firebase.json not found. Enable hosting first." };
2766
+ }
2767
+ try {
2768
+ const config = JSON.parse(readFileSync4(firebaseJsonPath, "utf-8"));
2769
+ if (!config.hosting) {
2770
+ return { success: false, message: "Hosting not configured in firebase.json. Click Enable first." };
2771
+ }
2772
+ } catch {
2773
+ return { success: false, message: "Invalid firebase.json." };
2774
+ }
2775
+ try {
2776
+ const output = await this.execTimeout(
2777
+ "firebase",
2778
+ ["deploy", "--only", "hosting", "--json"],
2779
+ 12e4
2780
+ // 2 min timeout
2781
+ );
2782
+ let url = "";
2783
+ try {
2784
+ const data = JSON.parse(output);
2785
+ if (data?.result) {
2786
+ for (const key of Object.keys(data.result)) {
2787
+ if (key.startsWith("hosting") && typeof data.result[key] === "object") {
2788
+ url = data.result[key]?.url || data.result[key]?.site?.url || "";
2789
+ if (url) break;
2790
+ }
2791
+ }
2792
+ }
2793
+ } catch {
2794
+ const urlMatch = output.match(/https:\/\/[a-z0-9-]+\.web\.app/i);
2795
+ if (urlMatch) url = urlMatch[0];
2796
+ }
2797
+ if (!url) {
2798
+ const state = this.loadState();
2799
+ if (state.projectId) {
2800
+ url = `https://${state.projectId}.web.app`;
2801
+ }
2802
+ }
2803
+ return { success: true, url: url || void 0, message: "Hosting deployed successfully!" };
2804
+ } catch (err) {
2805
+ const msg = err instanceof Error ? err.message : "Unknown error";
2806
+ return { success: false, message: `Deploy failed: ${msg}` };
2807
+ }
2808
+ }
2809
+ // ─── Service Enable ────────────────────────────────────────────────
2810
+ enableService(service) {
2811
+ const firebaseJsonPath = resolve4(this.projectDir, "firebase.json");
2812
+ let config = {};
2813
+ if (existsSync5(firebaseJsonPath)) {
2814
+ try {
2815
+ config = JSON.parse(readFileSync4(firebaseJsonPath, "utf-8"));
2816
+ } catch {
2817
+ config = {};
2818
+ }
2819
+ }
2820
+ switch (service) {
2821
+ case "hosting": {
2822
+ if (!config.hosting) {
2823
+ config.hosting = {
2824
+ public: "public",
2825
+ ignore: ["firebase.json", "**/.*", "**/node_modules/**"],
2826
+ rewrites: [{ source: "**", destination: "/index.html" }]
2827
+ };
2828
+ }
2829
+ break;
2830
+ }
2831
+ case "firestore": {
2832
+ if (!config.firestore) {
2833
+ config.firestore = {
2834
+ rules: "firestore.rules",
2835
+ indexes: "firestore.indexes.json"
2836
+ };
2837
+ }
2838
+ const rulesPath = resolve4(this.projectDir, "firestore.rules");
2839
+ if (!existsSync5(rulesPath)) {
2840
+ writeFileSync2(
2841
+ rulesPath,
2842
+ "rules_version = '2';\nservice cloud.firestore {\n match /databases/{database}/documents {\n match /{document=**} {\n allow read, write: if request.auth != null;\n }\n }\n}\n"
2843
+ );
2844
+ }
2845
+ const indexesPath = resolve4(this.projectDir, "firestore.indexes.json");
2846
+ if (!existsSync5(indexesPath)) {
2847
+ writeFileSync2(indexesPath, JSON.stringify({ indexes: [], fieldOverrides: [] }, null, 2) + "\n");
2848
+ }
2849
+ break;
2850
+ }
2851
+ case "storage": {
2852
+ if (!config.storage) {
2853
+ config.storage = { rules: "storage.rules" };
2854
+ }
2855
+ const storageRulesPath = resolve4(this.projectDir, "storage.rules");
2856
+ if (!existsSync5(storageRulesPath)) {
2857
+ writeFileSync2(
2858
+ storageRulesPath,
2859
+ "rules_version = '2';\nservice firebase.storage {\n match /b/{bucket}/o {\n match /{allPaths=**} {\n allow read, write: if request.auth != null;\n }\n }\n}\n"
2860
+ );
2861
+ }
2862
+ break;
2863
+ }
2864
+ }
2865
+ writeFileSync2(firebaseJsonPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2866
+ return { success: true, message: `${service} enabled in firebase.json.` };
2867
+ }
2672
2868
  // ─── Helpers ───────────────────────────────────────────────────────
2673
2869
  execTimeout(command, args, timeoutMs) {
2674
2870
  return new Promise((resolve6, reject) => {
@@ -3191,7 +3387,23 @@ function switchTab(tab) {
3191
3387
  // Lazy-load dashboard data on first click
3192
3388
  if (window._loadDashboard) window._loadDashboard();
3193
3389
  }
3390
+ // Persist active tab across reloads (auto-fill writes config \u2192 watcher reloads page)
3391
+ try { localStorage.setItem('clawfire-active-tab', tab); } catch(e) {}
3194
3392
  }
3393
+ // Restore saved tab on page load
3394
+ (function() {
3395
+ try {
3396
+ var saved = localStorage.getItem('clawfire-active-tab');
3397
+ if (saved === 'dashboard') {
3398
+ var fn = function() { switchTab('dashboard'); };
3399
+ if (document.readyState === 'loading') {
3400
+ document.addEventListener('DOMContentLoaded', fn);
3401
+ } else {
3402
+ fn();
3403
+ }
3404
+ }
3405
+ } catch(e) {}
3406
+ })();
3195
3407
  </script>`;
3196
3408
  const liveReloadScript = `
3197
3409
  <script>
@@ -3670,6 +3882,35 @@ ${liveReloadScript}
3670
3882
  });
3671
3883
  return;
3672
3884
  }
3885
+ if (url.pathname === "/__dev/deploy/hosting" && req.method === "POST") {
3886
+ this.firebaseSetup.deployHosting().then((result) => {
3887
+ clearFirebaseStatusCache();
3888
+ sendJson(result);
3889
+ }).catch((err) => sendJson({ success: false, message: err instanceof Error ? err.message : "Failed" }, 500));
3890
+ return;
3891
+ }
3892
+ if (url.pathname === "/__dev/enable-service" && req.method === "POST") {
3893
+ let body = "";
3894
+ req.on("data", (chunk) => {
3895
+ body += chunk;
3896
+ });
3897
+ req.on("end", () => {
3898
+ try {
3899
+ const data = JSON.parse(body);
3900
+ const service = data.service;
3901
+ if (!service || !["hosting", "firestore", "storage"].includes(service)) {
3902
+ sendJson({ success: false, message: "Invalid service. Use: hosting, firestore, storage" }, 400);
3903
+ return;
3904
+ }
3905
+ const result = this.firebaseSetup.enableService(service);
3906
+ clearFirebaseStatusCache();
3907
+ sendJson(result);
3908
+ } catch {
3909
+ sendJson({ success: false, message: "Invalid JSON body" }, 400);
3910
+ }
3911
+ });
3912
+ return;
3913
+ }
3673
3914
  if (url.pathname === "/__dev/setup/select-web-app" && req.method === "POST") {
3674
3915
  let body = "";
3675
3916
  req.on("data", (chunk) => {
package/dist/dev.cjs CHANGED
@@ -1794,7 +1794,29 @@ function generateDashboardHtml(options) {
1794
1794
  <div id="service-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px;"></div>
1795
1795
  </div>
1796
1796
 
1797
- <!-- Section 2: Config Overview -->
1797
+ <!-- Section 2: Deploy -->
1798
+ <div style="margin-bottom:32px;">
1799
+ <h2 style="font-size:18px;font-weight:700;color:#f97316;margin-bottom:16px;">Deploy</h2>
1800
+ <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px;">
1801
+ <div style="padding:16px;border-radius:8px;border:1px solid #2a2a2a;background:#141414;">
1802
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
1803
+ <span style="font-size:16px;">&#127760;</span>
1804
+ <span style="font-weight:600;color:#e5e5e5;">Hosting</span>
1805
+ </div>
1806
+ <div style="font-size:13px;color:#a3a3a3;margin-bottom:12px;">Deploy your app to Firebase Hosting</div>
1807
+ <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;">
1808
+ Deploy Hosting
1809
+ </button>
1810
+ <div id="deploy-hosting-status" style="display:none;margin-top:10px;font-size:13px;padding:10px 14px;border-radius:6px;"></div>
1811
+ <div id="deploy-hosting-url" style="display:none;margin-top:8px;padding:10px 14px;border-radius:6px;background:#0a1a0a;border:1px solid #22c55e;">
1812
+ <div style="font-size:11px;color:#a3a3a3;margin-bottom:4px;">Live URL</div>
1813
+ <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>
1814
+ </div>
1815
+ </div>
1816
+ </div>
1817
+ </div>
1818
+
1819
+ <!-- Section 3: Config Overview -->
1798
1820
  <div style="margin-bottom:32px;">
1799
1821
  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
1800
1822
  <h2 style="font-size:18px;font-weight:700;color:#f97316;">Config Overview</h2>
@@ -1818,7 +1840,7 @@ function generateDashboardHtml(options) {
1818
1840
  </div>
1819
1841
  </div>
1820
1842
 
1821
- <!-- Section 3: Environment Variables -->
1843
+ <!-- Section 4: Environment Variables -->
1822
1844
  <div style="margin-bottom:32px;">
1823
1845
  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
1824
1846
  <h2 style="font-size:18px;font-weight:700;color:#f97316;">Environment Variables</h2>
@@ -2418,16 +2440,22 @@ function generateDashboardHtml(options) {
2418
2440
  var statusColors = { configured: '#22c55e', placeholder: '#eab308', missing: '#666' };
2419
2441
  var statusLabels = { configured: 'Ready', placeholder: 'Needs Setup', missing: 'Not Configured' };
2420
2442
 
2443
+ var enableableServices = { 'Hosting': 'hosting', 'Firestore': 'firestore', 'Storage': 'storage' };
2421
2444
  data.services.forEach(function(svc) {
2422
2445
  var card = document.createElement('div');
2423
2446
  card.style.cssText = 'padding:16px;border-radius:8px;border:1px solid #2a2a2a;background:#141414;';
2447
+ var enableBtn = '';
2448
+ if (svc.status === 'missing' && enableableServices[svc.name]) {
2449
+ 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>';
2450
+ }
2424
2451
  card.innerHTML =
2425
2452
  '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">' +
2426
2453
  '<span style="width:8px;height:8px;border-radius:50%;background:' + statusColors[svc.status] + ';display:inline-block;"></span>' +
2427
2454
  '<span style="font-weight:600;color:#e5e5e5;">' + svc.name + '</span>' +
2428
2455
  '</div>' +
2429
2456
  '<div style="font-size:12px;color:' + statusColors[svc.status] + ';">' + statusLabels[svc.status] + '</div>' +
2430
- (svc.detail ? '<div style="font-size:11px;color:#666;margin-top:4px;">' + svc.detail + '</div>' : '');
2457
+ (svc.detail ? '<div style="font-size:11px;color:#666;margin-top:4px;">' + svc.detail + '</div>' : '') +
2458
+ enableBtn;
2431
2459
  grid.appendChild(card);
2432
2460
  });
2433
2461
 
@@ -2612,6 +2640,67 @@ function generateDashboardHtml(options) {
2612
2640
  });
2613
2641
  }
2614
2642
 
2643
+ // \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
2644
+ window.deployHosting = function() {
2645
+ var btn = document.getElementById('deploy-hosting-btn');
2646
+ var status = document.getElementById('deploy-hosting-status');
2647
+ var urlBox = document.getElementById('deploy-hosting-url');
2648
+ btn.disabled = true;
2649
+ btn.textContent = 'Deploying...';
2650
+ status.textContent = 'Deploying to Firebase Hosting... This may take up to 2 minutes.';
2651
+ 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;';
2652
+ urlBox.style.display = 'none';
2653
+
2654
+ fetch(API + '/__dev/deploy/hosting', { method: 'POST' })
2655
+ .then(function(r) { return r.json(); })
2656
+ .then(function(data) {
2657
+ if (data.success) {
2658
+ status.textContent = data.message;
2659
+ 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;';
2660
+ if (data.url) {
2661
+ var link = document.getElementById('deploy-hosting-link');
2662
+ link.href = data.url;
2663
+ link.textContent = data.url;
2664
+ urlBox.style.display = 'block';
2665
+ }
2666
+ } else {
2667
+ status.textContent = data.message;
2668
+ 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;';
2669
+ }
2670
+ btn.disabled = false;
2671
+ btn.textContent = 'Deploy Hosting';
2672
+ // Refresh firebase status
2673
+ fetch(API + '/__dev/firebase-status').then(function(r) { return r.json(); }).then(function(d) { renderFirebaseStatus(d); }).catch(function(){});
2674
+ })
2675
+ .catch(function(err) {
2676
+ status.textContent = 'Error: ' + err.message;
2677
+ 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;';
2678
+ btn.disabled = false;
2679
+ btn.textContent = 'Deploy Hosting';
2680
+ });
2681
+ };
2682
+
2683
+ // \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
2684
+ window.enableService = function(service) {
2685
+ fetch(API + '/__dev/enable-service', {
2686
+ method: 'POST',
2687
+ headers: { 'Content-Type': 'application/json' },
2688
+ body: JSON.stringify({ service: service })
2689
+ })
2690
+ .then(function(r) { return r.json(); })
2691
+ .then(function(data) {
2692
+ if (data.success) {
2693
+ // Refresh firebase status to show updated service cards
2694
+ fetch(API + '/__dev/firebase-status').then(function(r) { return r.json(); }).then(function(d) { renderFirebaseStatus(d); }).catch(function(){});
2695
+ } else {
2696
+ alert('Failed to enable ' + service + ': ' + data.message);
2697
+ }
2698
+ })
2699
+ .catch(function(err) {
2700
+ alert('Error: ' + err.message);
2701
+ });
2702
+ };
2703
+
2615
2704
  // \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
2616
2705
  function renderEnvVars(data) {
2617
2706
  envData = data.variables || [];
@@ -3081,6 +3170,113 @@ var FirebaseSetup = class {
3081
3170
  this.saveState({ webAppId: appId, webAppDisplayName: displayName });
3082
3171
  return { success: true, message: `Web app "${displayName}" selected.` };
3083
3172
  }
3173
+ // ─── Deploy ─────────────────────────────────────────────────────────
3174
+ async deployHosting() {
3175
+ const firebaseJsonPath = (0, import_node_path4.resolve)(this.projectDir, "firebase.json");
3176
+ if (!(0, import_node_fs4.existsSync)(firebaseJsonPath)) {
3177
+ return { success: false, message: "firebase.json not found. Enable hosting first." };
3178
+ }
3179
+ try {
3180
+ const config = JSON.parse((0, import_node_fs4.readFileSync)(firebaseJsonPath, "utf-8"));
3181
+ if (!config.hosting) {
3182
+ return { success: false, message: "Hosting not configured in firebase.json. Click Enable first." };
3183
+ }
3184
+ } catch {
3185
+ return { success: false, message: "Invalid firebase.json." };
3186
+ }
3187
+ try {
3188
+ const output = await this.execTimeout(
3189
+ "firebase",
3190
+ ["deploy", "--only", "hosting", "--json"],
3191
+ 12e4
3192
+ // 2 min timeout
3193
+ );
3194
+ let url = "";
3195
+ try {
3196
+ const data = JSON.parse(output);
3197
+ if (data?.result) {
3198
+ for (const key of Object.keys(data.result)) {
3199
+ if (key.startsWith("hosting") && typeof data.result[key] === "object") {
3200
+ url = data.result[key]?.url || data.result[key]?.site?.url || "";
3201
+ if (url) break;
3202
+ }
3203
+ }
3204
+ }
3205
+ } catch {
3206
+ const urlMatch = output.match(/https:\/\/[a-z0-9-]+\.web\.app/i);
3207
+ if (urlMatch) url = urlMatch[0];
3208
+ }
3209
+ if (!url) {
3210
+ const state = this.loadState();
3211
+ if (state.projectId) {
3212
+ url = `https://${state.projectId}.web.app`;
3213
+ }
3214
+ }
3215
+ return { success: true, url: url || void 0, message: "Hosting deployed successfully!" };
3216
+ } catch (err) {
3217
+ const msg = err instanceof Error ? err.message : "Unknown error";
3218
+ return { success: false, message: `Deploy failed: ${msg}` };
3219
+ }
3220
+ }
3221
+ // ─── Service Enable ────────────────────────────────────────────────
3222
+ enableService(service) {
3223
+ const firebaseJsonPath = (0, import_node_path4.resolve)(this.projectDir, "firebase.json");
3224
+ let config = {};
3225
+ if ((0, import_node_fs4.existsSync)(firebaseJsonPath)) {
3226
+ try {
3227
+ config = JSON.parse((0, import_node_fs4.readFileSync)(firebaseJsonPath, "utf-8"));
3228
+ } catch {
3229
+ config = {};
3230
+ }
3231
+ }
3232
+ switch (service) {
3233
+ case "hosting": {
3234
+ if (!config.hosting) {
3235
+ config.hosting = {
3236
+ public: "public",
3237
+ ignore: ["firebase.json", "**/.*", "**/node_modules/**"],
3238
+ rewrites: [{ source: "**", destination: "/index.html" }]
3239
+ };
3240
+ }
3241
+ break;
3242
+ }
3243
+ case "firestore": {
3244
+ if (!config.firestore) {
3245
+ config.firestore = {
3246
+ rules: "firestore.rules",
3247
+ indexes: "firestore.indexes.json"
3248
+ };
3249
+ }
3250
+ const rulesPath = (0, import_node_path4.resolve)(this.projectDir, "firestore.rules");
3251
+ if (!(0, import_node_fs4.existsSync)(rulesPath)) {
3252
+ (0, import_node_fs4.writeFileSync)(
3253
+ rulesPath,
3254
+ "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"
3255
+ );
3256
+ }
3257
+ const indexesPath = (0, import_node_path4.resolve)(this.projectDir, "firestore.indexes.json");
3258
+ if (!(0, import_node_fs4.existsSync)(indexesPath)) {
3259
+ (0, import_node_fs4.writeFileSync)(indexesPath, JSON.stringify({ indexes: [], fieldOverrides: [] }, null, 2) + "\n");
3260
+ }
3261
+ break;
3262
+ }
3263
+ case "storage": {
3264
+ if (!config.storage) {
3265
+ config.storage = { rules: "storage.rules" };
3266
+ }
3267
+ const storageRulesPath = (0, import_node_path4.resolve)(this.projectDir, "storage.rules");
3268
+ if (!(0, import_node_fs4.existsSync)(storageRulesPath)) {
3269
+ (0, import_node_fs4.writeFileSync)(
3270
+ storageRulesPath,
3271
+ "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"
3272
+ );
3273
+ }
3274
+ break;
3275
+ }
3276
+ }
3277
+ (0, import_node_fs4.writeFileSync)(firebaseJsonPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
3278
+ return { success: true, message: `${service} enabled in firebase.json.` };
3279
+ }
3084
3280
  // ─── Helpers ───────────────────────────────────────────────────────
3085
3281
  execTimeout(command, args, timeoutMs) {
3086
3282
  return new Promise((resolve7, reject) => {
@@ -3603,7 +3799,23 @@ function switchTab(tab) {
3603
3799
  // Lazy-load dashboard data on first click
3604
3800
  if (window._loadDashboard) window._loadDashboard();
3605
3801
  }
3802
+ // Persist active tab across reloads (auto-fill writes config \u2192 watcher reloads page)
3803
+ try { localStorage.setItem('clawfire-active-tab', tab); } catch(e) {}
3606
3804
  }
3805
+ // Restore saved tab on page load
3806
+ (function() {
3807
+ try {
3808
+ var saved = localStorage.getItem('clawfire-active-tab');
3809
+ if (saved === 'dashboard') {
3810
+ var fn = function() { switchTab('dashboard'); };
3811
+ if (document.readyState === 'loading') {
3812
+ document.addEventListener('DOMContentLoaded', fn);
3813
+ } else {
3814
+ fn();
3815
+ }
3816
+ }
3817
+ } catch(e) {}
3818
+ })();
3607
3819
  </script>`;
3608
3820
  const liveReloadScript = `
3609
3821
  <script>
@@ -4082,6 +4294,35 @@ ${liveReloadScript}
4082
4294
  });
4083
4295
  return;
4084
4296
  }
4297
+ if (url.pathname === "/__dev/deploy/hosting" && req.method === "POST") {
4298
+ this.firebaseSetup.deployHosting().then((result) => {
4299
+ clearFirebaseStatusCache();
4300
+ sendJson(result);
4301
+ }).catch((err) => sendJson({ success: false, message: err instanceof Error ? err.message : "Failed" }, 500));
4302
+ return;
4303
+ }
4304
+ if (url.pathname === "/__dev/enable-service" && req.method === "POST") {
4305
+ let body = "";
4306
+ req.on("data", (chunk) => {
4307
+ body += chunk;
4308
+ });
4309
+ req.on("end", () => {
4310
+ try {
4311
+ const data = JSON.parse(body);
4312
+ const service = data.service;
4313
+ if (!service || !["hosting", "firestore", "storage"].includes(service)) {
4314
+ sendJson({ success: false, message: "Invalid service. Use: hosting, firestore, storage" }, 400);
4315
+ return;
4316
+ }
4317
+ const result = this.firebaseSetup.enableService(service);
4318
+ clearFirebaseStatusCache();
4319
+ sendJson(result);
4320
+ } catch {
4321
+ sendJson({ success: false, message: "Invalid JSON body" }, 400);
4322
+ }
4323
+ });
4324
+ return;
4325
+ }
4085
4326
  if (url.pathname === "/__dev/setup/select-web-app" && req.method === "POST") {
4086
4327
  let body = "";
4087
4328
  req.on("data", (chunk) => {