clawfire 0.4.3 → 0.5.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/dist/dev.js CHANGED
@@ -1456,39 +1456,106 @@ async function checkCli(projectDir) {
1456
1456
  }
1457
1457
  return result;
1458
1458
  }
1459
- async function fetchFirebaseSdkConfig(projectDir) {
1460
- const output = await execWithTimeout(
1461
- "firebase",
1462
- ["apps:sdkconfig", "web", "--json"],
1463
- projectDir,
1464
- 15e3
1465
- );
1466
- const data = JSON.parse(output);
1467
- if (data?.result?.sdkConfig) {
1468
- return data.result.sdkConfig;
1469
- }
1470
- if (data?.result?.fileContents) {
1471
- const contents = data.result.fileContents;
1472
- const config = {};
1473
- const extract = (key) => {
1474
- const match = contents.match(new RegExp(`"${key}"\\s*:\\s*"([^"]+)"`));
1475
- return match ? match[1] : void 0;
1476
- };
1477
- config.apiKey = extract("apiKey");
1478
- config.authDomain = extract("authDomain");
1479
- config.projectId = extract("projectId");
1480
- config.storageBucket = extract("storageBucket");
1481
- config.messagingSenderId = extract("messagingSenderId");
1482
- config.appId = extract("appId");
1483
- return config;
1459
+ async function fetchFirebaseSdkConfig(projectDir, appId) {
1460
+ if (appId) {
1461
+ try {
1462
+ const output = await execWithTimeout(
1463
+ "firebase",
1464
+ ["apps:sdkconfig", "web", appId, "--json"],
1465
+ projectDir,
1466
+ 15e3
1467
+ );
1468
+ const config = parseSdkConfigOutput(output);
1469
+ if (config) return config;
1470
+ } catch (err) {
1471
+ const msg = err instanceof Error ? err.message : "Unknown error";
1472
+ throw new Error(`Failed to fetch config for web app ${appId}: ${msg}`);
1473
+ }
1474
+ throw new Error("Could not parse Firebase SDK config from CLI output");
1475
+ }
1476
+ try {
1477
+ const output = await execWithTimeout(
1478
+ "firebase",
1479
+ ["apps:sdkconfig", "web", "--json"],
1480
+ projectDir,
1481
+ 15e3
1482
+ );
1483
+ const config = parseSdkConfigOutput(output);
1484
+ if (config) return config;
1485
+ } catch {
1486
+ }
1487
+ let webApps = [];
1488
+ try {
1489
+ const appsOutput = await execWithTimeout(
1490
+ "firebase",
1491
+ ["apps:list", "--json"],
1492
+ projectDir,
1493
+ 15e3
1494
+ );
1495
+ const appsData = JSON.parse(appsOutput);
1496
+ const allApps = appsData?.result || [];
1497
+ webApps = allApps.filter((a) => a.platform === "WEB").map((a) => ({
1498
+ appId: String(a.appId || ""),
1499
+ displayName: String(a.displayName || "")
1500
+ })).filter((a) => a.appId);
1501
+ } catch {
1502
+ throw new Error(
1503
+ "Failed to fetch Firebase SDK config. Make sure you are logged in and have an active project selected."
1504
+ );
1505
+ }
1506
+ if (webApps.length === 0) {
1507
+ throw new Error(
1508
+ "No web app registered in this Firebase project. Go to Firebase Console > Project Settings > General > Your apps > Add app (Web) to create one, then click Auto-fill again."
1509
+ );
1510
+ }
1511
+ const firstAppId = webApps[0].appId;
1512
+ try {
1513
+ const output = await execWithTimeout(
1514
+ "firebase",
1515
+ ["apps:sdkconfig", "web", firstAppId, "--json"],
1516
+ projectDir,
1517
+ 15e3
1518
+ );
1519
+ const config = parseSdkConfigOutput(output);
1520
+ if (config) return config;
1521
+ } catch (err) {
1522
+ const msg = err instanceof Error ? err.message : "Unknown error";
1523
+ throw new Error(`Failed to fetch config for web app ${firstAppId}: ${msg}`);
1484
1524
  }
1485
1525
  throw new Error("Could not parse Firebase SDK config from CLI output");
1486
1526
  }
1527
+ function parseSdkConfigOutput(output) {
1528
+ try {
1529
+ const data = JSON.parse(output);
1530
+ if (data?.result?.sdkConfig) {
1531
+ return data.result.sdkConfig;
1532
+ }
1533
+ if (data?.result?.fileContents) {
1534
+ const contents = data.result.fileContents;
1535
+ const config = {};
1536
+ const extract = (key) => {
1537
+ const match = contents.match(new RegExp(`"${key}"\\s*:\\s*"([^"]+)"`));
1538
+ return match ? match[1] : void 0;
1539
+ };
1540
+ config.apiKey = extract("apiKey");
1541
+ config.authDomain = extract("authDomain");
1542
+ config.projectId = extract("projectId");
1543
+ config.storageBucket = extract("storageBucket");
1544
+ config.messagingSenderId = extract("messagingSenderId");
1545
+ config.appId = extract("appId");
1546
+ if (config.apiKey || config.projectId) return config;
1547
+ }
1548
+ } catch {
1549
+ }
1550
+ return null;
1551
+ }
1487
1552
  function execWithTimeout(command, args, cwd, timeoutMs) {
1488
1553
  return new Promise((resolve7, reject) => {
1489
- const proc = execFile(command, args, { cwd, timeout: timeoutMs }, (err, stdout) => {
1554
+ const proc = execFile(command, args, { cwd, timeout: timeoutMs }, (err, stdout, stderr) => {
1490
1555
  if (err) {
1491
- reject(err);
1556
+ const detail = stderr?.trim() || stdout?.trim() || "";
1557
+ const enriched = new Error(`${err.message}${detail ? "\n" + detail : ""}`);
1558
+ reject(enriched);
1492
1559
  } else {
1493
1560
  resolve7(stdout);
1494
1561
  }
@@ -1522,17 +1589,20 @@ function generateDashboardHtml(options) {
1522
1589
 
1523
1590
  <!-- Step indicators -->
1524
1591
  <div id="setup-steps" style="display:flex;gap:0;margin-bottom:20px;overflow:hidden;border-radius:8px;border:1px solid #2a2a2a;">
1525
- <div id="step-ind-1" class="step-ind" style="flex:1;padding:8px 12px;text-align:center;font-size:12px;font-weight:600;background:#1a1a2e;color:#f97316;border-right:1px solid #2a2a2a;">
1526
- 1. CLI Install
1592
+ <div id="step-ind-1" class="step-ind" style="flex:1;padding:8px 12px;text-align:center;font-size:11px;font-weight:600;background:#1a1a2e;color:#f97316;border-right:1px solid #2a2a2a;">
1593
+ 1. CLI
1527
1594
  </div>
1528
- <div id="step-ind-2" class="step-ind" style="flex:1;padding:8px 12px;text-align:center;font-size:12px;font-weight:600;background:#0a0a0a;color:#666;border-right:1px solid #2a2a2a;">
1595
+ <div id="step-ind-2" class="step-ind" style="flex:1;padding:8px 12px;text-align:center;font-size:11px;font-weight:600;background:#0a0a0a;color:#666;border-right:1px solid #2a2a2a;">
1529
1596
  2. Login
1530
1597
  </div>
1531
- <div id="step-ind-3" class="step-ind" style="flex:1;padding:8px 12px;text-align:center;font-size:12px;font-weight:600;background:#0a0a0a;color:#666;border-right:1px solid #2a2a2a;">
1532
- 3. Select Project
1598
+ <div id="step-ind-3" class="step-ind" style="flex:1;padding:8px 12px;text-align:center;font-size:11px;font-weight:600;background:#0a0a0a;color:#666;border-right:1px solid #2a2a2a;">
1599
+ 3. Project
1533
1600
  </div>
1534
- <div id="step-ind-4" class="step-ind" style="flex:1;padding:8px 12px;text-align:center;font-size:12px;font-weight:600;background:#0a0a0a;color:#666;">
1535
- 4. Auto-fill
1601
+ <div id="step-ind-4" class="step-ind" style="flex:1;padding:8px 12px;text-align:center;font-size:11px;font-weight:600;background:#0a0a0a;color:#666;border-right:1px solid #2a2a2a;">
1602
+ 4. Web App
1603
+ </div>
1604
+ <div id="step-ind-5" class="step-ind" style="flex:1;padding:8px 12px;text-align:center;font-size:11px;font-weight:600;background:#0a0a0a;color:#666;">
1605
+ 5. Auto-fill
1536
1606
  </div>
1537
1607
  </div>
1538
1608
 
@@ -1607,16 +1677,54 @@ function generateDashboardHtml(options) {
1607
1677
  </div>
1608
1678
  </div>
1609
1679
 
1610
- <!-- Step 4: Done / Auto-fill -->
1680
+ <!-- Step 4: Web App -->
1611
1681
  <div id="step-4" style="display:none;">
1612
1682
  <div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
1613
1683
  <span id="step4-icon" style="font-size:20px;">&#9898;</span>
1684
+ <div>
1685
+ <div style="font-weight:600;color:#e5e5e5;font-size:15px;">Web App</div>
1686
+ <div id="step4-detail" style="font-size:13px;color:#a3a3a3;">Checking web apps...</div>
1687
+ </div>
1688
+ </div>
1689
+ <div id="step4-action" style="display:none;">
1690
+ <!-- Existing web apps list -->
1691
+ <div id="webapp-list" style="display:none;">
1692
+ <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
1693
+ <select id="webapp-select" style="padding:8px 12px;background:#0a0a0a;border:1px solid #2a2a2a;border-radius:6px;color:#e5e5e5;font-size:13px;font-family:monospace;min-width:280px;outline:none;">
1694
+ <option value="">Loading web apps...</option>
1695
+ </select>
1696
+ <button id="select-webapp-btn" onclick="selectWebApp()" style="padding:8px 20px;background:#22c55e;color:#000;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;">
1697
+ Use This App
1698
+ </button>
1699
+ <button onclick="showCreateWebApp()" style="padding:8px 12px;background:transparent;border:1px solid #2a2a2a;border-radius:6px;color:#a3a3a3;font-size:13px;cursor:pointer;">
1700
+ + Create New
1701
+ </button>
1702
+ </div>
1703
+ </div>
1704
+ <!-- Create new web app form -->
1705
+ <div id="webapp-create" style="display:none;">
1706
+ <div style="font-size:13px;color:#a3a3a3;margin-bottom:8px;">No web app found. Create one to get Firebase SDK config:</div>
1707
+ <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
1708
+ <input id="webapp-name" type="text" placeholder="My Web App" style="padding:8px 12px;background:#0a0a0a;border:1px solid #2a2a2a;border-radius:6px;color:#e5e5e5;font-size:13px;min-width:220px;outline:none;" />
1709
+ <button id="create-webapp-btn" onclick="createWebApp()" style="padding:8px 20px;background:#f97316;color:#000;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;">
1710
+ Create Web App
1711
+ </button>
1712
+ </div>
1713
+ </div>
1714
+ <div id="webapp-status" style="display:none;margin-top:8px;font-size:13px;padding:8px 12px;border-radius:6px;"></div>
1715
+ </div>
1716
+ </div>
1717
+
1718
+ <!-- Step 5: Done / Auto-fill -->
1719
+ <div id="step-5" style="display:none;">
1720
+ <div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
1721
+ <span id="step5-icon" style="font-size:20px;">&#9898;</span>
1614
1722
  <div>
1615
1723
  <div style="font-weight:600;color:#e5e5e5;font-size:15px;">Auto-fill Config</div>
1616
- <div id="step4-detail" style="font-size:13px;color:#a3a3a3;">Ready to auto-fill your clawfire.config.ts</div>
1724
+ <div id="step5-detail" style="font-size:13px;color:#a3a3a3;">Ready to auto-fill your clawfire.config.ts</div>
1617
1725
  </div>
1618
1726
  </div>
1619
- <div id="step4-action">
1727
+ <div id="step5-action">
1620
1728
  <button id="autofill-wizard-btn" onclick="autoFillConfig()" style="padding:8px 20px;background:#f97316;color:#000;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;">
1621
1729
  Auto-fill Config Now
1622
1730
  </button>
@@ -1761,7 +1869,7 @@ function generateDashboardHtml(options) {
1761
1869
  // \u2500\u2500\u2500 Setup Wizard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1762
1870
 
1763
1871
  function setStepIndicator(activeStep) {
1764
- for (var i = 1; i <= 4; i++) {
1872
+ for (var i = 1; i <= 5; i++) {
1765
1873
  var el = document.getElementById('step-ind-' + i);
1766
1874
  if (i < activeStep) {
1767
1875
  el.style.background = '#0a1a0a';
@@ -1783,7 +1891,7 @@ function generateDashboardHtml(options) {
1783
1891
  var CIRCLE = '\\u26AA';
1784
1892
 
1785
1893
  // Hide all steps first
1786
- for (var i = 1; i <= 4; i++) {
1894
+ for (var i = 1; i <= 5; i++) {
1787
1895
  document.getElementById('step-' + i).style.display = 'none';
1788
1896
  }
1789
1897
  document.getElementById('setup-done').style.display = 'none';
@@ -1793,10 +1901,13 @@ function generateDashboardHtml(options) {
1793
1901
 
1794
1902
  if (status.nextStep === 'done') {
1795
1903
  // All done!
1796
- setStepIndicator(5);
1904
+ setStepIndicator(6);
1797
1905
  document.getElementById('setup-done').style.display = 'block';
1798
- document.getElementById('setup-done-detail').textContent =
1799
- 'Logged in as ' + status.auth.user + ' | Project: ' + status.project.id;
1906
+ var detail = 'Logged in as ' + status.auth.user + ' | Project: ' + status.project.id;
1907
+ if (status.webApp && status.webApp.displayName) {
1908
+ detail += ' | App: ' + status.webApp.displayName;
1909
+ }
1910
+ document.getElementById('setup-done-detail').textContent = detail;
1800
1911
  return;
1801
1912
  }
1802
1913
 
@@ -1863,11 +1974,32 @@ function generateDashboardHtml(options) {
1863
1974
  return;
1864
1975
  }
1865
1976
 
1866
- // Step 4: Auto-fill
1977
+ // Step 4: Web App
1867
1978
  var step4 = document.getElementById('step-4');
1868
1979
  step4.style.display = 'block';
1869
- document.getElementById('step4-icon').textContent = CIRCLE;
1870
- setStepIndicator(4);
1980
+ if (status.webApp && status.webApp.appId) {
1981
+ document.getElementById('step4-icon').textContent = GREEN;
1982
+ document.getElementById('step4-detail').textContent = 'Web app: ' + (status.webApp.displayName || status.webApp.appId);
1983
+ document.getElementById('step4-action').style.display = 'block';
1984
+ // Show list with current selection
1985
+ loadWebAppList(status.webApp.appId);
1986
+ } else {
1987
+ document.getElementById('step4-icon').textContent = YELLOW;
1988
+ document.getElementById('step4-detail').textContent = 'No web app selected';
1989
+ document.getElementById('step4-action').style.display = 'block';
1990
+ loadWebAppList('');
1991
+ }
1992
+
1993
+ if (status.nextStep === 'create-web-app') {
1994
+ setStepIndicator(4);
1995
+ return;
1996
+ }
1997
+
1998
+ // Step 5: Auto-fill
1999
+ var step5 = document.getElementById('step-5');
2000
+ step5.style.display = 'block';
2001
+ document.getElementById('step5-icon').textContent = CIRCLE;
2002
+ setStepIndicator(5);
1871
2003
  }
1872
2004
 
1873
2005
  // \u2500\u2500\u2500 Step 1: Install CLI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
@@ -2062,6 +2194,148 @@ function generateDashboardHtml(options) {
2062
2194
  });
2063
2195
  };
2064
2196
 
2197
+ // \u2500\u2500\u2500 Step 4: Web App \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2198
+ function loadWebAppList(currentAppId, retryCount) {
2199
+ retryCount = retryCount || 0;
2200
+ var listDiv = document.getElementById('webapp-list');
2201
+ var createDiv = document.getElementById('webapp-create');
2202
+ var select = document.getElementById('webapp-select');
2203
+
2204
+ fetch(API + '/__dev/setup/web-apps')
2205
+ .then(function(r) { return r.json(); })
2206
+ .then(function(data) {
2207
+ if (data.error) {
2208
+ if (retryCount < 2) {
2209
+ setTimeout(function() { loadWebAppList(currentAppId, retryCount + 1); }, 3000);
2210
+ return;
2211
+ }
2212
+ createDiv.style.display = 'block';
2213
+ listDiv.style.display = 'none';
2214
+ document.getElementById('webapp-create').querySelector('div').textContent = 'Error loading apps: ' + data.error;
2215
+ return;
2216
+ }
2217
+ if (!data.apps || data.apps.length === 0) {
2218
+ // No web apps \u2014 show create form
2219
+ createDiv.style.display = 'block';
2220
+ listDiv.style.display = 'none';
2221
+ return;
2222
+ }
2223
+
2224
+ // Show list
2225
+ listDiv.style.display = 'block';
2226
+ createDiv.style.display = 'none';
2227
+ select.innerHTML = '<option value="">-- Select a web app --</option>';
2228
+ data.apps.forEach(function(app) {
2229
+ var opt = document.createElement('option');
2230
+ opt.value = app.appId;
2231
+ opt.setAttribute('data-name', app.displayName || '');
2232
+ opt.textContent = (app.displayName || 'Unnamed') + ' (' + app.appId.slice(-12) + ')';
2233
+ if (app.appId === currentAppId) opt.selected = true;
2234
+ select.appendChild(opt);
2235
+ });
2236
+ select.disabled = false;
2237
+ })
2238
+ .catch(function() {
2239
+ if (retryCount < 2) {
2240
+ setTimeout(function() { loadWebAppList(currentAppId, retryCount + 1); }, 3000);
2241
+ return;
2242
+ }
2243
+ createDiv.style.display = 'block';
2244
+ listDiv.style.display = 'none';
2245
+ });
2246
+ }
2247
+
2248
+ window.showCreateWebApp = function() {
2249
+ document.getElementById('webapp-list').style.display = 'none';
2250
+ document.getElementById('webapp-create').style.display = 'block';
2251
+ document.getElementById('webapp-name').focus();
2252
+ };
2253
+
2254
+ window.selectWebApp = function() {
2255
+ var select = document.getElementById('webapp-select');
2256
+ var status = document.getElementById('webapp-status');
2257
+ var appId = select.value;
2258
+ if (!appId) {
2259
+ status.textContent = 'Please select a web app first.';
2260
+ status.style.cssText = 'display:block;margin-top:8px;font-size:13px;padding:8px 12px;border-radius:6px;background:#1a1a0a;border:1px solid #eab308;color:#eab308;';
2261
+ return;
2262
+ }
2263
+ var selectedOption = select.options[select.selectedIndex];
2264
+ var displayName = selectedOption.getAttribute('data-name') || '';
2265
+
2266
+ var btn = document.getElementById('select-webapp-btn');
2267
+ btn.disabled = true;
2268
+ btn.textContent = 'Saving...';
2269
+
2270
+ fetch(API + '/__dev/setup/select-web-app', {
2271
+ method: 'POST',
2272
+ headers: { 'Content-Type': 'application/json' },
2273
+ body: JSON.stringify({ appId: appId, displayName: displayName })
2274
+ })
2275
+ .then(function(r) { return r.json(); })
2276
+ .then(function(data) {
2277
+ if (data.success) {
2278
+ status.textContent = data.message;
2279
+ status.style.cssText = 'display:block;margin-top:8px;font-size:13px;padding:8px 12px;border-radius:6px;background:#0a1a0a;border:1px solid #22c55e;color:#22c55e;';
2280
+ setTimeout(refreshSetupStatus, 1000);
2281
+ } else {
2282
+ status.textContent = data.message;
2283
+ status.style.cssText = 'display:block;margin-top:8px;font-size:13px;padding:8px 12px;border-radius:6px;background:#1c0808;border:1px solid #ef4444;color:#ef4444;';
2284
+ btn.disabled = false;
2285
+ btn.textContent = 'Use This App';
2286
+ }
2287
+ })
2288
+ .catch(function(err) {
2289
+ status.textContent = 'Error: ' + err.message;
2290
+ status.style.cssText = 'display:block;margin-top:8px;font-size:13px;padding:8px 12px;border-radius:6px;background:#1c0808;border:1px solid #ef4444;color:#ef4444;';
2291
+ btn.disabled = false;
2292
+ btn.textContent = 'Use This App';
2293
+ });
2294
+ };
2295
+
2296
+ window.createWebApp = function() {
2297
+ var nameInput = document.getElementById('webapp-name');
2298
+ var btn = document.getElementById('create-webapp-btn');
2299
+ var status = document.getElementById('webapp-status');
2300
+ var displayName = nameInput.value.trim();
2301
+
2302
+ if (!displayName) {
2303
+ status.textContent = 'Please enter a name for the web app.';
2304
+ status.style.cssText = 'display:block;margin-top:8px;font-size:13px;padding:8px 12px;border-radius:6px;background:#1a1a0a;border:1px solid #eab308;color:#eab308;';
2305
+ return;
2306
+ }
2307
+
2308
+ btn.disabled = true;
2309
+ btn.textContent = 'Creating...';
2310
+ status.textContent = 'Creating web app... This may take a moment.';
2311
+ status.style.cssText = 'display:block;margin-top:8px;font-size:13px;padding:8px 12px;border-radius:6px;background:#0a0a1a;border:1px solid #3b82f6;color:#3b82f6;';
2312
+
2313
+ fetch(API + '/__dev/setup/create-web-app', {
2314
+ method: 'POST',
2315
+ headers: { 'Content-Type': 'application/json' },
2316
+ body: JSON.stringify({ displayName: displayName })
2317
+ })
2318
+ .then(function(r) { return r.json(); })
2319
+ .then(function(data) {
2320
+ if (data.success) {
2321
+ status.textContent = data.message;
2322
+ status.style.cssText = 'display:block;margin-top:8px;font-size:13px;padding:8px 12px;border-radius:6px;background:#0a1a0a;border:1px solid #22c55e;color:#22c55e;';
2323
+ setTimeout(refreshSetupStatus, 1000);
2324
+ } else {
2325
+ status.textContent = data.message;
2326
+ status.style.cssText = 'display:block;margin-top:8px;font-size:13px;padding:8px 12px;border-radius:6px;background:#1c0808;border:1px solid #ef4444;color:#ef4444;';
2327
+ btn.disabled = false;
2328
+ btn.textContent = 'Create Web App';
2329
+ }
2330
+ })
2331
+ .catch(function(err) {
2332
+ status.textContent = 'Error: ' + err.message;
2333
+ status.style.cssText = 'display:block;margin-top:8px;font-size:13px;padding:8px 12px;border-radius:6px;background:#1c0808;border:1px solid #ef4444;color:#ef4444;';
2334
+ btn.disabled = false;
2335
+ btn.textContent = 'Create Web App';
2336
+ });
2337
+ };
2338
+
2065
2339
  // \u2500\u2500\u2500 Refresh Setup Status \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2066
2340
  function refreshSetupStatus() {
2067
2341
  fetch(API + '/__dev/setup/status')
@@ -2237,7 +2511,12 @@ function generateDashboardHtml(options) {
2237
2511
  if (wizStatus) wizStatus.style.display = 'none';
2238
2512
 
2239
2513
  fetch(API + '/__dev/firebase-sdk-config')
2240
- .then(function(r) { return r.json(); })
2514
+ .then(function(r) {
2515
+ if (!r.ok) {
2516
+ return r.json().catch(function() { return { error: 'Server error (' + r.status + ')' }; });
2517
+ }
2518
+ return r.json();
2519
+ })
2241
2520
  .then(function(data) {
2242
2521
  if (data.error) {
2243
2522
  showAutoFillResult(false, data.error);
@@ -2451,15 +2730,34 @@ import { resolve as resolve5, join as join4 } from "path";
2451
2730
  import { tmpdir, platform } from "os";
2452
2731
  var FirebaseSetup = class {
2453
2732
  projectDir;
2733
+ stateFilePath;
2454
2734
  constructor(projectDir) {
2455
2735
  this.projectDir = projectDir;
2736
+ this.stateFilePath = resolve5(projectDir, ".clawfire-setup.json");
2737
+ }
2738
+ // ─── State Persistence ────────────────────────────────────────────
2739
+ loadState() {
2740
+ try {
2741
+ if (existsSync6(this.stateFilePath)) {
2742
+ return JSON.parse(readFileSync4(this.stateFilePath, "utf-8"));
2743
+ }
2744
+ } catch {
2745
+ }
2746
+ return {};
2747
+ }
2748
+ saveState(partial) {
2749
+ const current = this.loadState();
2750
+ const merged = { ...current, ...partial, lastUpdated: (/* @__PURE__ */ new Date()).toISOString() };
2751
+ writeFileSync2(this.stateFilePath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
2456
2752
  }
2457
2753
  // ─── Status Check ──────────────────────────────────────────────────
2458
2754
  async getStatus() {
2755
+ const savedState = this.loadState();
2459
2756
  const status = {
2460
2757
  cli: { installed: false, version: "" },
2461
2758
  auth: { authenticated: false, user: "" },
2462
2759
  project: { id: "", hasFirebaserc: false },
2760
+ webApp: { appId: savedState.webAppId || "", displayName: savedState.webAppDisplayName || "" },
2463
2761
  ready: false,
2464
2762
  nextStep: "install-cli"
2465
2763
  };
@@ -2501,6 +2799,10 @@ var FirebaseSetup = class {
2501
2799
  status.nextStep = "select-project";
2502
2800
  return status;
2503
2801
  }
2802
+ if (!status.webApp.appId) {
2803
+ status.nextStep = "create-web-app";
2804
+ return status;
2805
+ }
2504
2806
  status.ready = true;
2505
2807
  status.nextStep = "done";
2506
2808
  return status;
@@ -2654,6 +2956,12 @@ var FirebaseSetup = class {
2654
2956
  if (!projectId || !/^[a-z0-9-]+$/.test(projectId)) {
2655
2957
  return { success: false, message: "Invalid project ID format." };
2656
2958
  }
2959
+ const savedState = this.loadState();
2960
+ if (savedState.projectId && savedState.projectId !== projectId) {
2961
+ this.saveState({ projectId, webAppId: void 0, webAppDisplayName: void 0 });
2962
+ } else {
2963
+ this.saveState({ projectId });
2964
+ }
2657
2965
  try {
2658
2966
  await this.execTimeout(
2659
2967
  "firebase",
@@ -2689,6 +2997,52 @@ var FirebaseSetup = class {
2689
2997
  }
2690
2998
  }
2691
2999
  }
3000
+ // ─── Web App Management ───────────────────────────────────────────
3001
+ async listWebApps() {
3002
+ try {
3003
+ const output = await this.execTimeout(
3004
+ "firebase",
3005
+ ["apps:list", "--json"],
3006
+ 15e3
3007
+ );
3008
+ const data = JSON.parse(output);
3009
+ const allApps = data?.result || [];
3010
+ const webApps = allApps.filter((a) => a.platform === "WEB").map((a) => ({
3011
+ appId: String(a.appId || ""),
3012
+ displayName: String(a.displayName || "")
3013
+ })).filter((a) => a.appId);
3014
+ return { apps: webApps };
3015
+ } catch (err) {
3016
+ const msg = err instanceof Error ? err.message : "Unknown error";
3017
+ return { apps: [], error: msg };
3018
+ }
3019
+ }
3020
+ async createWebApp(displayName) {
3021
+ if (!displayName || displayName.length < 1) {
3022
+ return { success: false, message: "Display name is required." };
3023
+ }
3024
+ try {
3025
+ const output = await this.execTimeout(
3026
+ "firebase",
3027
+ ["apps:create", "web", displayName, "--json"],
3028
+ 3e4
3029
+ );
3030
+ const data = JSON.parse(output);
3031
+ const appId = data?.result?.appId || "";
3032
+ if (appId) {
3033
+ this.saveState({ webAppId: appId, webAppDisplayName: displayName });
3034
+ return { success: true, appId, message: `Web app "${displayName}" created successfully.` };
3035
+ }
3036
+ return { success: false, message: "App created but could not retrieve app ID." };
3037
+ } catch (err) {
3038
+ const msg = err instanceof Error ? err.message : "Unknown error";
3039
+ return { success: false, message: `Failed to create web app: ${msg}` };
3040
+ }
3041
+ }
3042
+ selectWebApp(appId, displayName) {
3043
+ this.saveState({ webAppId: appId, webAppDisplayName: displayName });
3044
+ return { success: true, message: `Web app "${displayName}" selected.` };
3045
+ }
2692
3046
  // ─── Helpers ───────────────────────────────────────────────────────
2693
3047
  execTimeout(command, args, timeoutMs) {
2694
3048
  return new Promise((resolve7, reject) => {
@@ -3578,7 +3932,8 @@ ${liveReloadScript}
3578
3932
  return;
3579
3933
  }
3580
3934
  if (url.pathname === "/__dev/firebase-sdk-config" && req.method === "GET") {
3581
- fetchFirebaseSdkConfig(this.options.projectDir).then((config) => sendJson(config)).catch((err) => sendJson({ error: err instanceof Error ? err.message : "Failed to fetch SDK config" }, 500));
3935
+ const savedState = this.firebaseSetup.loadState();
3936
+ fetchFirebaseSdkConfig(this.options.projectDir, savedState.webAppId).then((config) => sendJson(config)).catch((err) => sendJson({ error: err instanceof Error ? err.message : "Failed to fetch SDK config" }, 500));
3582
3937
  return;
3583
3938
  }
3584
3939
  if (url.pathname === "/__dev/env" && req.method === "GET") {
@@ -3663,6 +4018,53 @@ ${liveReloadScript}
3663
4018
  });
3664
4019
  return;
3665
4020
  }
4021
+ if (url.pathname === "/__dev/setup/web-apps" && req.method === "GET") {
4022
+ this.firebaseSetup.listWebApps().then((result) => sendJson(result)).catch((err) => sendJson({ apps: [], error: err instanceof Error ? err.message : "Failed" }, 500));
4023
+ return;
4024
+ }
4025
+ if (url.pathname === "/__dev/setup/create-web-app" && req.method === "POST") {
4026
+ let body = "";
4027
+ req.on("data", (chunk) => {
4028
+ body += chunk;
4029
+ });
4030
+ req.on("end", () => {
4031
+ try {
4032
+ const data = JSON.parse(body);
4033
+ if (!data.displayName) {
4034
+ sendJson({ success: false, message: "displayName is required" }, 400);
4035
+ return;
4036
+ }
4037
+ this.firebaseSetup.createWebApp(data.displayName).then((result) => {
4038
+ clearFirebaseStatusCache();
4039
+ sendJson(result);
4040
+ }).catch((err) => sendJson({ success: false, message: err instanceof Error ? err.message : "Failed" }, 500));
4041
+ } catch {
4042
+ sendJson({ success: false, message: "Invalid JSON body" }, 400);
4043
+ }
4044
+ });
4045
+ return;
4046
+ }
4047
+ if (url.pathname === "/__dev/setup/select-web-app" && req.method === "POST") {
4048
+ let body = "";
4049
+ req.on("data", (chunk) => {
4050
+ body += chunk;
4051
+ });
4052
+ req.on("end", () => {
4053
+ try {
4054
+ const data = JSON.parse(body);
4055
+ if (!data.appId) {
4056
+ sendJson({ success: false, message: "appId is required" }, 400);
4057
+ return;
4058
+ }
4059
+ const result = this.firebaseSetup.selectWebApp(data.appId, data.displayName || "");
4060
+ clearFirebaseStatusCache();
4061
+ sendJson(result);
4062
+ } catch {
4063
+ sendJson({ success: false, message: "Invalid JSON body" }, 400);
4064
+ }
4065
+ });
4066
+ return;
4067
+ }
3666
4068
  res.writeHead(404);
3667
4069
  res.end("Not found");
3668
4070
  }