clawfire 0.3.2 → 0.4.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 +1 -1
- package/dist/{dev-server-MIUKPOMC.js → dev-server-DRZ52WIA.js} +1013 -65
- package/dist/dev.cjs +1017 -69
- package/dist/dev.cjs.map +1 -1
- package/dist/dev.d.cts +1 -0
- package/dist/dev.d.ts +1 -0
- package/dist/dev.js +1013 -65
- package/dist/dev.js.map +1 -1
- package/package.json +1 -1
package/dist/dev.cjs
CHANGED
|
@@ -38,8 +38,8 @@ module.exports = __toCommonJS(dev_exports);
|
|
|
38
38
|
|
|
39
39
|
// src/dev/dev-server.ts
|
|
40
40
|
var import_node_http = __toESM(require("http"), 1);
|
|
41
|
-
var
|
|
42
|
-
var
|
|
41
|
+
var import_node_path5 = require("path");
|
|
42
|
+
var import_node_fs5 = require("fs");
|
|
43
43
|
var import_node_url = require("url");
|
|
44
44
|
|
|
45
45
|
// src/core/schema.ts
|
|
@@ -1523,12 +1523,12 @@ async function fetchFirebaseSdkConfig(projectDir) {
|
|
|
1523
1523
|
throw new Error("Could not parse Firebase SDK config from CLI output");
|
|
1524
1524
|
}
|
|
1525
1525
|
function execWithTimeout(command, args, cwd, timeoutMs) {
|
|
1526
|
-
return new Promise((
|
|
1526
|
+
return new Promise((resolve7, reject) => {
|
|
1527
1527
|
const proc = (0, import_node_child_process.execFile)(command, args, { cwd, timeout: timeoutMs }, (err, stdout) => {
|
|
1528
1528
|
if (err) {
|
|
1529
1529
|
reject(err);
|
|
1530
1530
|
} else {
|
|
1531
|
-
|
|
1531
|
+
resolve7(stdout);
|
|
1532
1532
|
}
|
|
1533
1533
|
});
|
|
1534
1534
|
const timer = setTimeout(() => {
|
|
@@ -1553,6 +1553,143 @@ function generateDashboardHtml(options) {
|
|
|
1553
1553
|
<!-- Dashboard Sections (hidden until loaded) -->
|
|
1554
1554
|
<div id="dash-loaded" style="display:none;">
|
|
1555
1555
|
|
|
1556
|
+
<!-- Section 0: Firebase Setup Wizard -->
|
|
1557
|
+
<div id="setup-wizard" style="margin-bottom:32px;">
|
|
1558
|
+
<h2 style="font-size:18px;font-weight:700;color:#f97316;margin-bottom:16px;">Firebase Setup</h2>
|
|
1559
|
+
<div id="setup-card" style="padding:20px;border-radius:12px;border:1px solid #2a2a2a;background:#141414;">
|
|
1560
|
+
|
|
1561
|
+
<!-- Step indicators -->
|
|
1562
|
+
<div id="setup-steps" style="display:flex;gap:0;margin-bottom:20px;overflow:hidden;border-radius:8px;border:1px solid #2a2a2a;">
|
|
1563
|
+
<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;">
|
|
1564
|
+
1. CLI Install
|
|
1565
|
+
</div>
|
|
1566
|
+
<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;">
|
|
1567
|
+
2. Login
|
|
1568
|
+
</div>
|
|
1569
|
+
<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;">
|
|
1570
|
+
3. Select Project
|
|
1571
|
+
</div>
|
|
1572
|
+
<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;">
|
|
1573
|
+
4. Auto-fill
|
|
1574
|
+
</div>
|
|
1575
|
+
</div>
|
|
1576
|
+
|
|
1577
|
+
<!-- Step 1: CLI Install -->
|
|
1578
|
+
<div id="step-1" style="display:none;">
|
|
1579
|
+
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
|
|
1580
|
+
<span id="step1-icon" style="font-size:20px;">⚪</span>
|
|
1581
|
+
<div>
|
|
1582
|
+
<div style="font-weight:600;color:#e5e5e5;font-size:15px;">Firebase CLI</div>
|
|
1583
|
+
<div id="step1-detail" style="font-size:13px;color:#a3a3a3;">Checking...</div>
|
|
1584
|
+
</div>
|
|
1585
|
+
</div>
|
|
1586
|
+
<div id="step1-action" style="display:none;">
|
|
1587
|
+
<button id="install-cli-btn" onclick="installFirebaseCli()" style="padding:8px 20px;background:#f97316;color:#000;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;">
|
|
1588
|
+
Install Firebase CLI
|
|
1589
|
+
</button>
|
|
1590
|
+
<div id="install-status" style="display:none;margin-top:8px;font-size:13px;padding:8px 12px;border-radius:6px;"></div>
|
|
1591
|
+
</div>
|
|
1592
|
+
</div>
|
|
1593
|
+
|
|
1594
|
+
<!-- Step 2: Login -->
|
|
1595
|
+
<div id="step-2" style="display:none;">
|
|
1596
|
+
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
|
|
1597
|
+
<span id="step2-icon" style="font-size:20px;">⚪</span>
|
|
1598
|
+
<div>
|
|
1599
|
+
<div style="font-weight:600;color:#e5e5e5;font-size:15px;">Firebase Authentication</div>
|
|
1600
|
+
<div id="step2-detail" style="font-size:13px;color:#a3a3a3;">Checking...</div>
|
|
1601
|
+
</div>
|
|
1602
|
+
</div>
|
|
1603
|
+
<div id="step2-action" style="display:none;">
|
|
1604
|
+
<button id="login-btn" onclick="startFirebaseLogin(false)" style="padding:8px 20px;background:#3b82f6;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;">
|
|
1605
|
+
Login to Firebase
|
|
1606
|
+
</button>
|
|
1607
|
+
<button id="reauth-btn" onclick="startFirebaseLogin(true)" style="display:none;padding:8px 20px;background:#eab308;color:#000;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;">
|
|
1608
|
+
Re-authenticate
|
|
1609
|
+
</button>
|
|
1610
|
+
<button id="cancel-login-btn" onclick="cancelFirebaseLogin()" style="display:none;padding:8px 20px;background:transparent;border:1px solid #2a2a2a;border-radius:6px;color:#a3a3a3;font-size:13px;cursor:pointer;margin-left:8px;">
|
|
1611
|
+
Cancel
|
|
1612
|
+
</button>
|
|
1613
|
+
</div>
|
|
1614
|
+
<!-- Login progress / auth URL -->
|
|
1615
|
+
<div id="login-progress" style="display:none;margin-top:12px;padding:16px;border-radius:8px;background:#0a0a1a;border:1px solid #3b82f6;">
|
|
1616
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
|
|
1617
|
+
<span id="login-spinner" style="display:inline-block;width:14px;height:14px;border:2px solid #3b82f6;border-top-color:transparent;border-radius:50%;animation:spin 1s linear infinite;"></span>
|
|
1618
|
+
<span id="login-spinner-text" style="color:#e5e5e5;font-size:13px;font-weight:600;">Starting login...</span>
|
|
1619
|
+
</div>
|
|
1620
|
+
<div id="login-url-box" style="display:none;margin-top:12px;">
|
|
1621
|
+
<div style="font-size:13px;color:#e5e5e5;margin-bottom:6px;font-weight:600;">Step 1: Open this link to authenticate</div>
|
|
1622
|
+
<a id="login-url-link" href="#" target="_blank" rel="noopener" style="display:inline-block;padding:8px 16px;background:#3b82f6;color:#fff;border-radius:6px;font-size:13px;text-decoration:none;margin-bottom:8px;">Open Google Login</a>
|
|
1623
|
+
<div style="font-size:11px;color:#666;margin-top:4px;word-break:break-all;font-family:monospace;" id="login-url-raw"></div>
|
|
1624
|
+
</div>
|
|
1625
|
+
<div id="auth-code-box" style="display:none;margin-top:12px;">
|
|
1626
|
+
<div style="font-size:13px;color:#e5e5e5;margin-bottom:6px;font-weight:600;">Step 2: Paste the authorization code</div>
|
|
1627
|
+
<div style="font-size:12px;color:#a3a3a3;margin-bottom:8px;">After signing in, Google will show an authorization code. Copy it and paste below.</div>
|
|
1628
|
+
<div style="display:flex;gap:8px;">
|
|
1629
|
+
<input id="auth-code-input" type="text" placeholder="Paste authorization code here"
|
|
1630
|
+
style="flex:1;padding:8px 12px;background:#0a0a0a;border:1px solid #2a2a2a;border-radius:6px;color:#e5e5e5;font-family:monospace;font-size:14px;outline:none;" />
|
|
1631
|
+
<button id="auth-code-submit" onclick="submitAuthCode()" style="padding:8px 16px;background:#22c55e;color:#000;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap;">
|
|
1632
|
+
Submit Code
|
|
1633
|
+
</button>
|
|
1634
|
+
</div>
|
|
1635
|
+
<div id="auth-code-error" style="display:none;margin-top:6px;font-size:12px;color:#ef4444;"></div>
|
|
1636
|
+
</div>
|
|
1637
|
+
<div id="login-output" style="display:none;margin-top:8px;max-height:80px;overflow-y:auto;padding:8px;background:#000;border-radius:4px;font-family:monospace;font-size:11px;color:#666;white-space:pre-wrap;"></div>
|
|
1638
|
+
</div>
|
|
1639
|
+
<div id="login-result" style="display:none;margin-top:8px;font-size:13px;padding:8px 12px;border-radius:6px;"></div>
|
|
1640
|
+
</div>
|
|
1641
|
+
|
|
1642
|
+
<!-- Step 3: Select Project -->
|
|
1643
|
+
<div id="step-3" style="display:none;">
|
|
1644
|
+
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
|
|
1645
|
+
<span id="step3-icon" style="font-size:20px;">⚪</span>
|
|
1646
|
+
<div>
|
|
1647
|
+
<div style="font-weight:600;color:#e5e5e5;font-size:15px;">Firebase Project</div>
|
|
1648
|
+
<div id="step3-detail" style="font-size:13px;color:#a3a3a3;">Checking...</div>
|
|
1649
|
+
</div>
|
|
1650
|
+
</div>
|
|
1651
|
+
<div id="step3-action" style="display:none;">
|
|
1652
|
+
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
|
|
1653
|
+
<select id="project-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;">
|
|
1654
|
+
<option value="">Loading projects...</option>
|
|
1655
|
+
</select>
|
|
1656
|
+
<button id="select-project-btn" onclick="selectFirebaseProject()" style="padding:8px 20px;background:#22c55e;color:#000;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;">
|
|
1657
|
+
Use This Project
|
|
1658
|
+
</button>
|
|
1659
|
+
<button onclick="refreshProjectList()" style="padding:8px 12px;background:transparent;border:1px solid #2a2a2a;border-radius:6px;color:#a3a3a3;font-size:13px;cursor:pointer;" title="Refresh">
|
|
1660
|
+
Refresh
|
|
1661
|
+
</button>
|
|
1662
|
+
</div>
|
|
1663
|
+
<div id="project-status" style="display:none;margin-top:8px;font-size:13px;padding:8px 12px;border-radius:6px;"></div>
|
|
1664
|
+
</div>
|
|
1665
|
+
</div>
|
|
1666
|
+
|
|
1667
|
+
<!-- Step 4: Done / Auto-fill -->
|
|
1668
|
+
<div id="step-4" style="display:none;">
|
|
1669
|
+
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
|
|
1670
|
+
<span id="step4-icon" style="font-size:20px;">⚪</span>
|
|
1671
|
+
<div>
|
|
1672
|
+
<div style="font-weight:600;color:#e5e5e5;font-size:15px;">Auto-fill Config</div>
|
|
1673
|
+
<div id="step4-detail" style="font-size:13px;color:#a3a3a3;">Ready to auto-fill your clawfire.config.ts</div>
|
|
1674
|
+
</div>
|
|
1675
|
+
</div>
|
|
1676
|
+
<div id="step4-action">
|
|
1677
|
+
<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;">
|
|
1678
|
+
Auto-fill Config Now
|
|
1679
|
+
</button>
|
|
1680
|
+
<div id="autofill-wizard-status" style="display:none;margin-top:8px;font-size:13px;padding:8px 12px;border-radius:6px;"></div>
|
|
1681
|
+
</div>
|
|
1682
|
+
</div>
|
|
1683
|
+
|
|
1684
|
+
<!-- All done banner -->
|
|
1685
|
+
<div id="setup-done" style="display:none;padding:16px;border-radius:8px;background:#0a1a0a;border:1px solid #22c55e;text-align:center;">
|
|
1686
|
+
<div style="font-size:16px;color:#22c55e;font-weight:700;margin-bottom:4px;">Setup Complete</div>
|
|
1687
|
+
<div id="setup-done-detail" style="font-size:13px;color:#a3a3a3;"></div>
|
|
1688
|
+
<button onclick="startFirebaseLogin(true)" style="margin-top:8px;padding:6px 14px;background:transparent;border:1px solid #2a2a2a;border-radius:6px;color:#a3a3a3;font-size:12px;cursor:pointer;">Re-authenticate</button>
|
|
1689
|
+
</div>
|
|
1690
|
+
</div>
|
|
1691
|
+
</div>
|
|
1692
|
+
|
|
1556
1693
|
<!-- Section 1: Firebase Status -->
|
|
1557
1694
|
<div style="margin-bottom:32px;">
|
|
1558
1695
|
<h2 style="font-size:18px;font-weight:700;color:#f97316;margin-bottom:16px;">Firebase Status</h2>
|
|
@@ -1642,12 +1779,18 @@ function generateDashboardHtml(options) {
|
|
|
1642
1779
|
</div>
|
|
1643
1780
|
</div>
|
|
1644
1781
|
|
|
1782
|
+
<style>
|
|
1783
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
1784
|
+
#login-spinner { animation: spin 1s linear infinite; }
|
|
1785
|
+
</style>
|
|
1786
|
+
|
|
1645
1787
|
<script>
|
|
1646
1788
|
(function() {
|
|
1647
1789
|
var API = 'http://localhost:${apiPort}';
|
|
1648
1790
|
var envData = [];
|
|
1649
1791
|
var configData = [];
|
|
1650
1792
|
var editingKey = null;
|
|
1793
|
+
var loginPollTimer = null;
|
|
1651
1794
|
|
|
1652
1795
|
// \u2500\u2500\u2500 Load Dashboard Data \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1653
1796
|
window._loadDashboard = function() {
|
|
@@ -1657,10 +1800,12 @@ function generateDashboardHtml(options) {
|
|
|
1657
1800
|
fetch(API + '/__dev/firebase-status').then(function(r) { return r.json(); }),
|
|
1658
1801
|
fetch(API + '/__dev/config').then(function(r) { return r.json(); }),
|
|
1659
1802
|
fetch(API + '/__dev/env').then(function(r) { return r.json(); }),
|
|
1803
|
+
fetch(API + '/__dev/setup/status').then(function(r) { return r.json(); }),
|
|
1660
1804
|
]).then(function(results) {
|
|
1661
1805
|
renderFirebaseStatus(results[0]);
|
|
1662
1806
|
renderConfig(results[1]);
|
|
1663
1807
|
renderEnvVars(results[2]);
|
|
1808
|
+
renderSetupWizard(results[3]);
|
|
1664
1809
|
document.getElementById('dash-loading').style.display = 'none';
|
|
1665
1810
|
document.getElementById('dash-loaded').style.display = 'block';
|
|
1666
1811
|
}).catch(function(err) {
|
|
@@ -1670,6 +1815,388 @@ function generateDashboardHtml(options) {
|
|
|
1670
1815
|
});
|
|
1671
1816
|
};
|
|
1672
1817
|
|
|
1818
|
+
// \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
|
|
1819
|
+
|
|
1820
|
+
function setStepIndicator(activeStep) {
|
|
1821
|
+
for (var i = 1; i <= 4; i++) {
|
|
1822
|
+
var el = document.getElementById('step-ind-' + i);
|
|
1823
|
+
if (i < activeStep) {
|
|
1824
|
+
el.style.background = '#0a1a0a';
|
|
1825
|
+
el.style.color = '#22c55e';
|
|
1826
|
+
} else if (i === activeStep) {
|
|
1827
|
+
el.style.background = '#1a1a2e';
|
|
1828
|
+
el.style.color = '#f97316';
|
|
1829
|
+
} else {
|
|
1830
|
+
el.style.background = '#0a0a0a';
|
|
1831
|
+
el.style.color = '#666';
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
function renderSetupWizard(status) {
|
|
1837
|
+
var GREEN = '\\u2705';
|
|
1838
|
+
var YELLOW = '\\u26A0\\uFE0F';
|
|
1839
|
+
var RED = '\\u274C';
|
|
1840
|
+
var CIRCLE = '\\u26AA';
|
|
1841
|
+
|
|
1842
|
+
// Hide all steps first
|
|
1843
|
+
for (var i = 1; i <= 4; i++) {
|
|
1844
|
+
document.getElementById('step-' + i).style.display = 'none';
|
|
1845
|
+
}
|
|
1846
|
+
document.getElementById('setup-done').style.display = 'none';
|
|
1847
|
+
|
|
1848
|
+
if (status.nextStep === 'done') {
|
|
1849
|
+
// All done!
|
|
1850
|
+
setStepIndicator(5);
|
|
1851
|
+
document.getElementById('setup-done').style.display = 'block';
|
|
1852
|
+
document.getElementById('setup-done-detail').textContent =
|
|
1853
|
+
'Logged in as ' + status.auth.user + ' | Project: ' + status.project.id;
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// Step 1: CLI
|
|
1858
|
+
var step1 = document.getElementById('step-1');
|
|
1859
|
+
step1.style.display = 'block';
|
|
1860
|
+
if (status.cli.installed) {
|
|
1861
|
+
document.getElementById('step1-icon').textContent = GREEN;
|
|
1862
|
+
document.getElementById('step1-detail').textContent = 'Firebase CLI v' + status.cli.version + ' installed';
|
|
1863
|
+
document.getElementById('step1-action').style.display = 'none';
|
|
1864
|
+
} else {
|
|
1865
|
+
document.getElementById('step1-icon').textContent = RED;
|
|
1866
|
+
document.getElementById('step1-detail').textContent = 'Firebase CLI is not installed';
|
|
1867
|
+
document.getElementById('step1-action').style.display = 'block';
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
if (status.nextStep === 'install-cli') {
|
|
1871
|
+
setStepIndicator(1);
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
// Step 2: Auth
|
|
1876
|
+
var step2 = document.getElementById('step-2');
|
|
1877
|
+
step2.style.display = 'block';
|
|
1878
|
+
if (status.auth.authenticated) {
|
|
1879
|
+
document.getElementById('step2-icon').textContent = GREEN;
|
|
1880
|
+
document.getElementById('step2-detail').textContent = 'Logged in as ' + status.auth.user;
|
|
1881
|
+
document.getElementById('step2-action').style.display = 'block';
|
|
1882
|
+
document.getElementById('login-btn').style.display = 'none';
|
|
1883
|
+
document.getElementById('reauth-btn').style.display = 'inline-block';
|
|
1884
|
+
document.getElementById('cancel-login-btn').style.display = 'none';
|
|
1885
|
+
} else {
|
|
1886
|
+
document.getElementById('step2-icon').textContent = RED;
|
|
1887
|
+
document.getElementById('step2-detail').textContent = 'Not logged in to Firebase';
|
|
1888
|
+
document.getElementById('step2-action').style.display = 'block';
|
|
1889
|
+
document.getElementById('login-btn').style.display = 'inline-block';
|
|
1890
|
+
document.getElementById('reauth-btn').style.display = 'none';
|
|
1891
|
+
document.getElementById('cancel-login-btn').style.display = 'none';
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
if (status.nextStep === 'login') {
|
|
1895
|
+
setStepIndicator(2);
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
// Step 3: Project Selection
|
|
1900
|
+
var step3 = document.getElementById('step-3');
|
|
1901
|
+
step3.style.display = 'block';
|
|
1902
|
+
if (status.project.id) {
|
|
1903
|
+
document.getElementById('step3-icon').textContent = GREEN;
|
|
1904
|
+
document.getElementById('step3-detail').textContent = 'Active project: ' + status.project.id;
|
|
1905
|
+
document.getElementById('step3-action').style.display = 'block';
|
|
1906
|
+
} else {
|
|
1907
|
+
document.getElementById('step3-icon').textContent = YELLOW;
|
|
1908
|
+
document.getElementById('step3-detail').textContent = 'No project selected';
|
|
1909
|
+
document.getElementById('step3-action').style.display = 'block';
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
// Load project list
|
|
1913
|
+
loadProjectList(status.project.id);
|
|
1914
|
+
|
|
1915
|
+
if (status.nextStep === 'select-project') {
|
|
1916
|
+
setStepIndicator(3);
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
// Step 4: Auto-fill
|
|
1921
|
+
var step4 = document.getElementById('step-4');
|
|
1922
|
+
step4.style.display = 'block';
|
|
1923
|
+
document.getElementById('step4-icon').textContent = CIRCLE;
|
|
1924
|
+
setStepIndicator(4);
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
// \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
|
|
1928
|
+
window.installFirebaseCli = function() {
|
|
1929
|
+
var btn = document.getElementById('install-cli-btn');
|
|
1930
|
+
var status = document.getElementById('install-status');
|
|
1931
|
+
btn.disabled = true;
|
|
1932
|
+
btn.textContent = 'Installing...';
|
|
1933
|
+
status.style.display = 'block';
|
|
1934
|
+
status.textContent = 'Running npm install -g firebase-tools... This may take a minute.';
|
|
1935
|
+
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;';
|
|
1936
|
+
|
|
1937
|
+
fetch(API + '/__dev/setup/install-cli', { method: 'POST' })
|
|
1938
|
+
.then(function(r) { return r.json(); })
|
|
1939
|
+
.then(function(data) {
|
|
1940
|
+
if (data.success) {
|
|
1941
|
+
status.textContent = data.message;
|
|
1942
|
+
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;';
|
|
1943
|
+
// Refresh wizard
|
|
1944
|
+
setTimeout(refreshSetupStatus, 1000);
|
|
1945
|
+
} else {
|
|
1946
|
+
status.textContent = data.message;
|
|
1947
|
+
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;';
|
|
1948
|
+
btn.disabled = false;
|
|
1949
|
+
btn.textContent = 'Install Firebase CLI';
|
|
1950
|
+
}
|
|
1951
|
+
})
|
|
1952
|
+
.catch(function(err) {
|
|
1953
|
+
status.textContent = 'Error: ' + err.message;
|
|
1954
|
+
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;';
|
|
1955
|
+
btn.disabled = false;
|
|
1956
|
+
btn.textContent = 'Install Firebase CLI';
|
|
1957
|
+
});
|
|
1958
|
+
};
|
|
1959
|
+
|
|
1960
|
+
// \u2500\u2500\u2500 Step 2: Login \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1961
|
+
window.startFirebaseLogin = function(reauth) {
|
|
1962
|
+
var loginBtn = document.getElementById('login-btn');
|
|
1963
|
+
var reauthBtn = document.getElementById('reauth-btn');
|
|
1964
|
+
var cancelBtn = document.getElementById('cancel-login-btn');
|
|
1965
|
+
var progress = document.getElementById('login-progress');
|
|
1966
|
+
var result = document.getElementById('login-result');
|
|
1967
|
+
|
|
1968
|
+
loginBtn.style.display = 'none';
|
|
1969
|
+
reauthBtn.style.display = 'none';
|
|
1970
|
+
cancelBtn.style.display = 'inline-block';
|
|
1971
|
+
progress.style.display = 'block';
|
|
1972
|
+
result.style.display = 'none';
|
|
1973
|
+
document.getElementById('login-url-box').style.display = 'none';
|
|
1974
|
+
document.getElementById('auth-code-box').style.display = 'none';
|
|
1975
|
+
document.getElementById('login-spinner-text').textContent = 'Starting login...';
|
|
1976
|
+
|
|
1977
|
+
fetch(API + '/__dev/setup/login', {
|
|
1978
|
+
method: 'POST',
|
|
1979
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1980
|
+
body: JSON.stringify({ reauth: !!reauth })
|
|
1981
|
+
})
|
|
1982
|
+
.then(function(r) { return r.json(); })
|
|
1983
|
+
.then(function() {
|
|
1984
|
+
// Start polling login state
|
|
1985
|
+
startLoginPoll();
|
|
1986
|
+
})
|
|
1987
|
+
.catch(function(err) {
|
|
1988
|
+
progress.style.display = 'none';
|
|
1989
|
+
result.textContent = 'Failed to start login: ' + err.message;
|
|
1990
|
+
result.style.cssText = 'display:block;margin-top:8px;font-size:13px;padding:8px 12px;border-radius:6px;background:#1c0808;border:1px solid #ef4444;color:#ef4444;';
|
|
1991
|
+
loginBtn.style.display = 'inline-block';
|
|
1992
|
+
cancelBtn.style.display = 'none';
|
|
1993
|
+
});
|
|
1994
|
+
};
|
|
1995
|
+
|
|
1996
|
+
function startLoginPoll() {
|
|
1997
|
+
if (loginPollTimer) clearInterval(loginPollTimer);
|
|
1998
|
+
loginPollTimer = setInterval(function() {
|
|
1999
|
+
fetch(API + '/__dev/setup/login-state')
|
|
2000
|
+
.then(function(r) { return r.json(); })
|
|
2001
|
+
.then(function(state) {
|
|
2002
|
+
// Show auth URL if available
|
|
2003
|
+
if (state.authUrl) {
|
|
2004
|
+
var urlBox = document.getElementById('login-url-box');
|
|
2005
|
+
var urlLink = document.getElementById('login-url-link');
|
|
2006
|
+
var urlRaw = document.getElementById('login-url-raw');
|
|
2007
|
+
urlBox.style.display = 'block';
|
|
2008
|
+
urlLink.href = state.authUrl;
|
|
2009
|
+
urlLink.textContent = 'Open Google Login';
|
|
2010
|
+
if (urlRaw) urlRaw.textContent = state.authUrl;
|
|
2011
|
+
document.getElementById('login-spinner-text').textContent = 'Waiting for authentication...';
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// Show auth code input when waiting for code
|
|
2015
|
+
if (state.waitingForCode) {
|
|
2016
|
+
document.getElementById('auth-code-box').style.display = 'block';
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// Show output log
|
|
2020
|
+
if (state.output) {
|
|
2021
|
+
var outputEl = document.getElementById('login-output');
|
|
2022
|
+
outputEl.style.display = 'block';
|
|
2023
|
+
outputEl.textContent = state.output.slice(-500);
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
// Check if done
|
|
2027
|
+
if (state.completed) {
|
|
2028
|
+
clearInterval(loginPollTimer);
|
|
2029
|
+
loginPollTimer = null;
|
|
2030
|
+
document.getElementById('login-progress').style.display = 'none';
|
|
2031
|
+
document.getElementById('cancel-login-btn').style.display = 'none';
|
|
2032
|
+
var result = document.getElementById('login-result');
|
|
2033
|
+
result.textContent = 'Login successful!';
|
|
2034
|
+
result.style.cssText = 'display:block;margin-top:8px;font-size:13px;padding:8px 12px;border-radius:6px;background:#0a1a0a;border:1px solid #22c55e;color:#22c55e;';
|
|
2035
|
+
// Refresh wizard after short delay
|
|
2036
|
+
setTimeout(refreshSetupStatus, 1500);
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
if (state.error) {
|
|
2040
|
+
clearInterval(loginPollTimer);
|
|
2041
|
+
loginPollTimer = null;
|
|
2042
|
+
document.getElementById('login-progress').style.display = 'none';
|
|
2043
|
+
document.getElementById('cancel-login-btn').style.display = 'none';
|
|
2044
|
+
var result2 = document.getElementById('login-result');
|
|
2045
|
+
result2.textContent = state.error;
|
|
2046
|
+
result2.style.cssText = 'display:block;margin-top:8px;font-size:13px;padding:8px 12px;border-radius:6px;background:#1c0808;border:1px solid #ef4444;color:#ef4444;';
|
|
2047
|
+
document.getElementById('login-btn').style.display = 'inline-block';
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
if (!state.active && !state.completed && !state.error) {
|
|
2051
|
+
clearInterval(loginPollTimer);
|
|
2052
|
+
loginPollTimer = null;
|
|
2053
|
+
}
|
|
2054
|
+
})
|
|
2055
|
+
.catch(function() {});
|
|
2056
|
+
}, 1500);
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
window.submitAuthCode = function() {
|
|
2060
|
+
var input = document.getElementById('auth-code-input');
|
|
2061
|
+
var errEl = document.getElementById('auth-code-error');
|
|
2062
|
+
var btn = document.getElementById('auth-code-submit');
|
|
2063
|
+
var code = input ? input.value.trim() : '';
|
|
2064
|
+
if (!code) {
|
|
2065
|
+
errEl.textContent = 'Please paste the authorization code.';
|
|
2066
|
+
errEl.style.display = 'block';
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
errEl.style.display = 'none';
|
|
2070
|
+
btn.disabled = true;
|
|
2071
|
+
btn.textContent = 'Submitting...';
|
|
2072
|
+
|
|
2073
|
+
fetch(API + '/__dev/setup/submit-auth-code', {
|
|
2074
|
+
method: 'POST',
|
|
2075
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2076
|
+
body: JSON.stringify({ code: code })
|
|
2077
|
+
})
|
|
2078
|
+
.then(function(r) { return r.json(); })
|
|
2079
|
+
.then(function(data) {
|
|
2080
|
+
if (data.success) {
|
|
2081
|
+
document.getElementById('login-spinner-text').textContent = 'Verifying authorization...';
|
|
2082
|
+
document.getElementById('auth-code-box').style.display = 'none';
|
|
2083
|
+
} else {
|
|
2084
|
+
errEl.textContent = data.message || 'Failed to submit code.';
|
|
2085
|
+
errEl.style.display = 'block';
|
|
2086
|
+
}
|
|
2087
|
+
btn.disabled = false;
|
|
2088
|
+
btn.textContent = 'Submit Code';
|
|
2089
|
+
})
|
|
2090
|
+
.catch(function(err) {
|
|
2091
|
+
errEl.textContent = 'Error: ' + err.message;
|
|
2092
|
+
errEl.style.display = 'block';
|
|
2093
|
+
btn.disabled = false;
|
|
2094
|
+
btn.textContent = 'Submit Code';
|
|
2095
|
+
});
|
|
2096
|
+
};
|
|
2097
|
+
|
|
2098
|
+
window.cancelFirebaseLogin = function() {
|
|
2099
|
+
if (loginPollTimer) { clearInterval(loginPollTimer); loginPollTimer = null; }
|
|
2100
|
+
fetch(API + '/__dev/setup/cancel-login', { method: 'POST' }).catch(function() {});
|
|
2101
|
+
document.getElementById('login-progress').style.display = 'none';
|
|
2102
|
+
document.getElementById('cancel-login-btn').style.display = 'none';
|
|
2103
|
+
document.getElementById('login-btn').style.display = 'inline-block';
|
|
2104
|
+
};
|
|
2105
|
+
|
|
2106
|
+
// \u2500\u2500\u2500 Step 3: Project Selection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2107
|
+
function loadProjectList(currentProjectId) {
|
|
2108
|
+
var select = document.getElementById('project-select');
|
|
2109
|
+
select.innerHTML = '<option value="">Loading projects...</option>';
|
|
2110
|
+
select.disabled = true;
|
|
2111
|
+
|
|
2112
|
+
fetch(API + '/__dev/setup/projects')
|
|
2113
|
+
.then(function(r) { return r.json(); })
|
|
2114
|
+
.then(function(data) {
|
|
2115
|
+
select.innerHTML = '';
|
|
2116
|
+
if (data.error) {
|
|
2117
|
+
select.innerHTML = '<option value="">Error: ' + escHtml(data.error) + '</option>';
|
|
2118
|
+
return;
|
|
2119
|
+
}
|
|
2120
|
+
if (!data.projects || data.projects.length === 0) {
|
|
2121
|
+
select.innerHTML = '<option value="">No projects found</option>';
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
select.innerHTML = '<option value="">-- Select a project --</option>';
|
|
2126
|
+
data.projects.forEach(function(p) {
|
|
2127
|
+
var opt = document.createElement('option');
|
|
2128
|
+
opt.value = p.projectId;
|
|
2129
|
+
opt.textContent = p.displayName + ' (' + p.projectId + ')';
|
|
2130
|
+
if (p.projectId === currentProjectId) opt.selected = true;
|
|
2131
|
+
select.appendChild(opt);
|
|
2132
|
+
});
|
|
2133
|
+
select.disabled = false;
|
|
2134
|
+
})
|
|
2135
|
+
.catch(function(err) {
|
|
2136
|
+
select.innerHTML = '<option value="">Failed to load</option>';
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
window.refreshProjectList = function() {
|
|
2141
|
+
loadProjectList('');
|
|
2142
|
+
};
|
|
2143
|
+
|
|
2144
|
+
window.selectFirebaseProject = function() {
|
|
2145
|
+
var select = document.getElementById('project-select');
|
|
2146
|
+
var btn = document.getElementById('select-project-btn');
|
|
2147
|
+
var status = document.getElementById('project-status');
|
|
2148
|
+
var projectId = select.value;
|
|
2149
|
+
|
|
2150
|
+
if (!projectId) {
|
|
2151
|
+
status.textContent = 'Please select a project first.';
|
|
2152
|
+
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;';
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
btn.disabled = true;
|
|
2157
|
+
btn.textContent = 'Setting...';
|
|
2158
|
+
status.style.display = 'none';
|
|
2159
|
+
|
|
2160
|
+
fetch(API + '/__dev/setup/select-project', {
|
|
2161
|
+
method: 'POST',
|
|
2162
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2163
|
+
body: JSON.stringify({ projectId: projectId })
|
|
2164
|
+
})
|
|
2165
|
+
.then(function(r) { return r.json(); })
|
|
2166
|
+
.then(function(data) {
|
|
2167
|
+
if (data.success) {
|
|
2168
|
+
status.textContent = data.message;
|
|
2169
|
+
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;';
|
|
2170
|
+
setTimeout(refreshSetupStatus, 1000);
|
|
2171
|
+
} else {
|
|
2172
|
+
status.textContent = data.message;
|
|
2173
|
+
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;';
|
|
2174
|
+
btn.disabled = false;
|
|
2175
|
+
btn.textContent = 'Use This Project';
|
|
2176
|
+
}
|
|
2177
|
+
})
|
|
2178
|
+
.catch(function(err) {
|
|
2179
|
+
status.textContent = 'Error: ' + err.message;
|
|
2180
|
+
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;';
|
|
2181
|
+
btn.disabled = false;
|
|
2182
|
+
btn.textContent = 'Use This Project';
|
|
2183
|
+
});
|
|
2184
|
+
};
|
|
2185
|
+
|
|
2186
|
+
// \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
|
|
2187
|
+
function refreshSetupStatus() {
|
|
2188
|
+
fetch(API + '/__dev/setup/status')
|
|
2189
|
+
.then(function(r) { return r.json(); })
|
|
2190
|
+
.then(function(status) { renderSetupWizard(status); })
|
|
2191
|
+
.catch(function() {});
|
|
2192
|
+
|
|
2193
|
+
// Also refresh firebase status section
|
|
2194
|
+
fetch(API + '/__dev/firebase-status')
|
|
2195
|
+
.then(function(r) { return r.json(); })
|
|
2196
|
+
.then(function(data) { renderFirebaseStatus(data); })
|
|
2197
|
+
.catch(function() {});
|
|
2198
|
+
}
|
|
2199
|
+
|
|
1673
2200
|
// \u2500\u2500\u2500 Firebase 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\u2500\u2500\u2500\u2500
|
|
1674
2201
|
function renderFirebaseStatus(data) {
|
|
1675
2202
|
var dot = document.getElementById('cli-dot');
|
|
@@ -1681,10 +2208,10 @@ function generateDashboardHtml(options) {
|
|
|
1681
2208
|
text.textContent = 'Firebase CLI not installed';
|
|
1682
2209
|
} else if (!data.cli.authenticated) {
|
|
1683
2210
|
dot.style.background = '#eab308';
|
|
1684
|
-
text.textContent = 'Firebase CLI v' + data.cli.version + '
|
|
2211
|
+
text.textContent = 'Firebase CLI v' + data.cli.version + ' \\u2014 Not logged in';
|
|
1685
2212
|
} else {
|
|
1686
2213
|
dot.style.background = '#22c55e';
|
|
1687
|
-
text.textContent = 'Firebase CLI v' + data.cli.version + '
|
|
2214
|
+
text.textContent = 'Firebase CLI v' + data.cli.version + ' \\u2014 ' + data.cli.user;
|
|
1688
2215
|
}
|
|
1689
2216
|
|
|
1690
2217
|
if (data.project.id) {
|
|
@@ -1772,7 +2299,6 @@ function generateDashboardHtml(options) {
|
|
|
1772
2299
|
var saveBtn = document.getElementById('cfg-save-' + key);
|
|
1773
2300
|
var cancelBtn = document.getElementById('cfg-cancel-' + key);
|
|
1774
2301
|
if (!valSpan || !input) return;
|
|
1775
|
-
// Hide all badges in this cell
|
|
1776
2302
|
var badges = valSpan.parentNode.querySelectorAll('span[style*="PLACEHOLDER"]');
|
|
1777
2303
|
for (var i = 0; i < badges.length; i++) badges[i].style.display = 'none';
|
|
1778
2304
|
valSpan.style.display = 'none';
|
|
@@ -1814,7 +2340,6 @@ function generateDashboardHtml(options) {
|
|
|
1814
2340
|
}).then(function(r) { return r.json(); })
|
|
1815
2341
|
.then(function(data) {
|
|
1816
2342
|
if (data.error) { alert('Error: ' + data.error); saveBtn.textContent = 'Save'; return; }
|
|
1817
|
-
// Reload config
|
|
1818
2343
|
return fetch(API + '/__dev/config').then(function(r) { return r.json(); });
|
|
1819
2344
|
})
|
|
1820
2345
|
.then(function(data) { if (data) renderConfig(data); })
|
|
@@ -1824,23 +2349,22 @@ function generateDashboardHtml(options) {
|
|
|
1824
2349
|
// \u2500\u2500\u2500 Auto-fill from Firebase \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1825
2350
|
window.autoFillConfig = function() {
|
|
1826
2351
|
var btn = document.getElementById('autofill-btn');
|
|
2352
|
+
var wizBtn = document.getElementById('autofill-wizard-btn');
|
|
1827
2353
|
var status = document.getElementById('autofill-status');
|
|
1828
|
-
|
|
1829
|
-
btn.textContent = 'Fetching...';
|
|
1830
|
-
|
|
2354
|
+
var wizStatus = document.getElementById('autofill-wizard-status');
|
|
2355
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Fetching...'; }
|
|
2356
|
+
if (wizBtn) { wizBtn.disabled = true; wizBtn.textContent = 'Fetching...'; }
|
|
2357
|
+
if (status) status.style.display = 'none';
|
|
2358
|
+
if (wizStatus) wizStatus.style.display = 'none';
|
|
1831
2359
|
|
|
1832
2360
|
fetch(API + '/__dev/firebase-sdk-config')
|
|
1833
2361
|
.then(function(r) { return r.json(); })
|
|
1834
2362
|
.then(function(data) {
|
|
1835
2363
|
if (data.error) {
|
|
1836
|
-
|
|
1837
|
-
status.style.cssText = 'display:block;padding:8px 12px;border-radius:6px;margin-bottom:12px;font-size:13px;background:#1c0808;border:1px solid #ef4444;color:#ef4444;';
|
|
1838
|
-
btn.disabled = false;
|
|
1839
|
-
btn.textContent = 'Auto-fill from Firebase';
|
|
2364
|
+
showAutoFillResult(false, data.error);
|
|
1840
2365
|
return;
|
|
1841
2366
|
}
|
|
1842
2367
|
|
|
1843
|
-
// Map SDK config keys to clawfire.config.ts keys
|
|
1844
2368
|
var fields = {};
|
|
1845
2369
|
if (data.apiKey) fields.apiKey = data.apiKey;
|
|
1846
2370
|
if (data.authDomain) fields.authDomain = data.authDomain;
|
|
@@ -1850,14 +2374,10 @@ function generateDashboardHtml(options) {
|
|
|
1850
2374
|
|
|
1851
2375
|
var count = Object.keys(fields).length;
|
|
1852
2376
|
if (count === 0) {
|
|
1853
|
-
|
|
1854
|
-
status.style.cssText = 'display:block;padding:8px 12px;border-radius:6px;margin-bottom:12px;font-size:13px;background:#1a1a0a;border:1px solid #eab308;color:#eab308;';
|
|
1855
|
-
btn.disabled = false;
|
|
1856
|
-
btn.textContent = 'Auto-fill from Firebase';
|
|
2377
|
+
showAutoFillResult(false, 'No web app config found. Create a Web app in Firebase Console first.');
|
|
1857
2378
|
return;
|
|
1858
2379
|
}
|
|
1859
2380
|
|
|
1860
|
-
// Save all fields at once
|
|
1861
2381
|
return fetch(API + '/__dev/config', {
|
|
1862
2382
|
method: 'POST',
|
|
1863
2383
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -1865,27 +2385,37 @@ function generateDashboardHtml(options) {
|
|
|
1865
2385
|
}).then(function(r) { return r.json(); })
|
|
1866
2386
|
.then(function(result) {
|
|
1867
2387
|
if (result.error) {
|
|
1868
|
-
|
|
1869
|
-
status.style.cssText = 'display:block;padding:8px 12px;border-radius:6px;margin-bottom:12px;font-size:13px;background:#1c0808;border:1px solid #ef4444;color:#ef4444;';
|
|
2388
|
+
showAutoFillResult(false, 'Error saving: ' + result.error);
|
|
1870
2389
|
} else {
|
|
1871
|
-
|
|
1872
|
-
status.style.cssText = 'display:block;padding:8px 12px;border-radius:6px;margin-bottom:12px;font-size:13px;background:#0a1a0a;border:1px solid #22c55e;color:#22c55e;';
|
|
1873
|
-
// Reload config view
|
|
2390
|
+
showAutoFillResult(true, count + ' fields updated from Firebase project!');
|
|
1874
2391
|
return fetch(API + '/__dev/config').then(function(r) { return r.json(); })
|
|
1875
2392
|
.then(function(cfg) { renderConfig(cfg); });
|
|
1876
2393
|
}
|
|
1877
2394
|
});
|
|
1878
2395
|
})
|
|
1879
2396
|
.catch(function(err) {
|
|
1880
|
-
|
|
1881
|
-
status.style.cssText = 'display:block;padding:8px 12px;border-radius:6px;margin-bottom:12px;font-size:13px;background:#1c0808;border:1px solid #ef4444;color:#ef4444;';
|
|
2397
|
+
showAutoFillResult(false, 'Failed: ' + err.message);
|
|
1882
2398
|
})
|
|
1883
2399
|
.finally(function() {
|
|
1884
|
-
btn.disabled = false;
|
|
1885
|
-
|
|
2400
|
+
if (btn) { btn.disabled = false; btn.textContent = 'Auto-fill from Firebase'; }
|
|
2401
|
+
if (wizBtn) { wizBtn.disabled = false; wizBtn.textContent = 'Auto-fill Config Now'; }
|
|
1886
2402
|
});
|
|
1887
2403
|
};
|
|
1888
2404
|
|
|
2405
|
+
function showAutoFillResult(success, message) {
|
|
2406
|
+
var targets = ['autofill-status', 'autofill-wizard-status'];
|
|
2407
|
+
targets.forEach(function(id) {
|
|
2408
|
+
var el = document.getElementById(id);
|
|
2409
|
+
if (!el) return;
|
|
2410
|
+
el.textContent = message;
|
|
2411
|
+
if (success) {
|
|
2412
|
+
el.style.cssText = 'display:block;margin-top:8px;font-size:13px;padding:8px 12px;border-radius:6px;background:#0a1a0a;border:1px solid #22c55e;color:#22c55e;';
|
|
2413
|
+
} else {
|
|
2414
|
+
el.style.cssText = 'display:block;margin-top:8px;font-size:13px;padding:8px 12px;border-radius:6px;background:#1c0808;border:1px solid #ef4444;color:#ef4444;';
|
|
2415
|
+
}
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
|
|
1889
2419
|
// \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
|
|
1890
2420
|
function renderEnvVars(data) {
|
|
1891
2421
|
envData = data.variables || [];
|
|
@@ -2035,6 +2565,342 @@ function generateDashboardHtml(options) {
|
|
|
2035
2565
|
</script>`;
|
|
2036
2566
|
}
|
|
2037
2567
|
|
|
2568
|
+
// src/dev/firebase-setup.ts
|
|
2569
|
+
var import_node_child_process2 = require("child_process");
|
|
2570
|
+
var import_node_fs4 = require("fs");
|
|
2571
|
+
var import_node_path4 = require("path");
|
|
2572
|
+
var import_node_os = require("os");
|
|
2573
|
+
var FirebaseSetup = class {
|
|
2574
|
+
projectDir;
|
|
2575
|
+
loginProcess = null;
|
|
2576
|
+
loginSession = {
|
|
2577
|
+
active: false,
|
|
2578
|
+
authUrl: null,
|
|
2579
|
+
waitingForCode: false,
|
|
2580
|
+
completed: false,
|
|
2581
|
+
error: null,
|
|
2582
|
+
output: ""
|
|
2583
|
+
};
|
|
2584
|
+
loginTimeout = null;
|
|
2585
|
+
constructor(projectDir) {
|
|
2586
|
+
this.projectDir = projectDir;
|
|
2587
|
+
}
|
|
2588
|
+
// ─── Status Check ──────────────────────────────────────────────────
|
|
2589
|
+
async getStatus() {
|
|
2590
|
+
const status = {
|
|
2591
|
+
cli: { installed: false, version: "" },
|
|
2592
|
+
auth: { authenticated: false, user: "" },
|
|
2593
|
+
project: { id: "", hasFirebaserc: false },
|
|
2594
|
+
ready: false,
|
|
2595
|
+
nextStep: "install-cli"
|
|
2596
|
+
};
|
|
2597
|
+
try {
|
|
2598
|
+
const version = await this.execTimeout("firebase", ["--version"], 5e3);
|
|
2599
|
+
status.cli.installed = true;
|
|
2600
|
+
status.cli.version = version.trim();
|
|
2601
|
+
} catch {
|
|
2602
|
+
status.nextStep = "install-cli";
|
|
2603
|
+
return status;
|
|
2604
|
+
}
|
|
2605
|
+
try {
|
|
2606
|
+
const loginOutput = await this.execTimeout(
|
|
2607
|
+
"firebase",
|
|
2608
|
+
["login:list", "--json"],
|
|
2609
|
+
5e3
|
|
2610
|
+
);
|
|
2611
|
+
const loginData = JSON.parse(loginOutput);
|
|
2612
|
+
if (loginData?.result && Array.isArray(loginData.result) && loginData.result.length > 0) {
|
|
2613
|
+
status.auth.authenticated = true;
|
|
2614
|
+
status.auth.user = loginData.result[0]?.user?.email || loginData.result[0]?.email || "";
|
|
2615
|
+
}
|
|
2616
|
+
} catch {
|
|
2617
|
+
}
|
|
2618
|
+
if (!status.auth.authenticated) {
|
|
2619
|
+
status.nextStep = "login";
|
|
2620
|
+
return status;
|
|
2621
|
+
}
|
|
2622
|
+
const firebasercPath = (0, import_node_path4.resolve)(this.projectDir, ".firebaserc");
|
|
2623
|
+
if ((0, import_node_fs4.existsSync)(firebasercPath)) {
|
|
2624
|
+
status.project.hasFirebaserc = true;
|
|
2625
|
+
try {
|
|
2626
|
+
const rc = JSON.parse((0, import_node_fs4.readFileSync)(firebasercPath, "utf-8"));
|
|
2627
|
+
status.project.id = rc?.projects?.default || "";
|
|
2628
|
+
} catch {
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
if (!status.project.id) {
|
|
2632
|
+
status.nextStep = "select-project";
|
|
2633
|
+
return status;
|
|
2634
|
+
}
|
|
2635
|
+
status.ready = true;
|
|
2636
|
+
status.nextStep = "done";
|
|
2637
|
+
return status;
|
|
2638
|
+
}
|
|
2639
|
+
// ─── CLI Install ───────────────────────────────────────────────────
|
|
2640
|
+
async installCli() {
|
|
2641
|
+
try {
|
|
2642
|
+
try {
|
|
2643
|
+
await this.execTimeout("firebase", ["--version"], 5e3);
|
|
2644
|
+
return { success: true, message: "Firebase CLI is already installed." };
|
|
2645
|
+
} catch {
|
|
2646
|
+
}
|
|
2647
|
+
await this.execTimeout(
|
|
2648
|
+
"npm",
|
|
2649
|
+
["install", "-g", "firebase-tools"],
|
|
2650
|
+
12e4
|
|
2651
|
+
// 2 min timeout for install
|
|
2652
|
+
);
|
|
2653
|
+
try {
|
|
2654
|
+
const version = await this.execTimeout("firebase", ["--version"], 5e3);
|
|
2655
|
+
return { success: true, message: `Firebase CLI v${version.trim()} installed successfully.` };
|
|
2656
|
+
} catch {
|
|
2657
|
+
return { success: false, message: "Installation completed but CLI not found in PATH. Try restarting your terminal." };
|
|
2658
|
+
}
|
|
2659
|
+
} catch (err) {
|
|
2660
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
2661
|
+
return { success: false, message: `Failed to install Firebase CLI: ${msg}` };
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
// ─── Analytics Pre-config ──────────────────────────────────────────
|
|
2665
|
+
/**
|
|
2666
|
+
* Pre-configure Firebase analytics preference to avoid the interactive
|
|
2667
|
+
* "Allow Firebase to collect..." prompt that breaks non-TTY spawns.
|
|
2668
|
+
*/
|
|
2669
|
+
ensureAnalyticsConfigured() {
|
|
2670
|
+
try {
|
|
2671
|
+
const home = (0, import_node_os.homedir)();
|
|
2672
|
+
const configPath = (0, import_node_path4.join)(home, ".config", "configstore", "firebase-tools.json");
|
|
2673
|
+
if ((0, import_node_fs4.existsSync)(configPath)) {
|
|
2674
|
+
const config = JSON.parse((0, import_node_fs4.readFileSync)(configPath, "utf-8"));
|
|
2675
|
+
if (config.usage === void 0) {
|
|
2676
|
+
config.usage = false;
|
|
2677
|
+
(0, import_node_fs4.writeFileSync)(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
2678
|
+
}
|
|
2679
|
+
} else {
|
|
2680
|
+
const dir = (0, import_node_path4.dirname)(configPath);
|
|
2681
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true });
|
|
2682
|
+
(0, import_node_fs4.writeFileSync)(configPath, JSON.stringify({ usage: false }, null, 2), "utf-8");
|
|
2683
|
+
}
|
|
2684
|
+
} catch {
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
// ─── Login Flow ────────────────────────────────────────────────────
|
|
2688
|
+
/**
|
|
2689
|
+
* Start login using `firebase login --no-localhost`.
|
|
2690
|
+
*
|
|
2691
|
+
* This mode is designed for non-TTY environments:
|
|
2692
|
+
* 1. Prints a Google OAuth URL
|
|
2693
|
+
* 2. User opens URL in browser and authenticates
|
|
2694
|
+
* 3. Google shows an authorization code on screen
|
|
2695
|
+
* 4. User pastes code back → call submitAuthCode()
|
|
2696
|
+
*/
|
|
2697
|
+
startLogin(reauth = false) {
|
|
2698
|
+
this.cleanupLogin();
|
|
2699
|
+
this.ensureAnalyticsConfigured();
|
|
2700
|
+
this.loginSession = {
|
|
2701
|
+
active: true,
|
|
2702
|
+
authUrl: null,
|
|
2703
|
+
waitingForCode: false,
|
|
2704
|
+
completed: false,
|
|
2705
|
+
error: null,
|
|
2706
|
+
output: ""
|
|
2707
|
+
};
|
|
2708
|
+
try {
|
|
2709
|
+
const args = reauth ? ["login", "--reauth", "--no-localhost"] : ["login", "--no-localhost"];
|
|
2710
|
+
const proc = (0, import_node_child_process2.spawn)("firebase", args, {
|
|
2711
|
+
cwd: this.projectDir,
|
|
2712
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2713
|
+
env: { ...process.env, TERM: "dumb" }
|
|
2714
|
+
});
|
|
2715
|
+
this.loginProcess = proc;
|
|
2716
|
+
const appendOutput = (data) => {
|
|
2717
|
+
const text = data.toString();
|
|
2718
|
+
this.loginSession.output += text;
|
|
2719
|
+
const urlMatch = text.match(/(https:\/\/accounts\.google\.com\S+)/);
|
|
2720
|
+
if (urlMatch) {
|
|
2721
|
+
this.loginSession.authUrl = urlMatch[1];
|
|
2722
|
+
this.loginSession.waitingForCode = true;
|
|
2723
|
+
}
|
|
2724
|
+
if (text.includes("Success") || text.includes("Logged in as")) {
|
|
2725
|
+
this.loginSession.completed = true;
|
|
2726
|
+
this.loginSession.active = false;
|
|
2727
|
+
this.loginSession.waitingForCode = false;
|
|
2728
|
+
}
|
|
2729
|
+
if (text.includes("Allow Firebase to collect") || text.includes("(Y/n)")) {
|
|
2730
|
+
try {
|
|
2731
|
+
proc.stdin?.write("n\n");
|
|
2732
|
+
} catch {
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
};
|
|
2736
|
+
proc.stdout?.on("data", appendOutput);
|
|
2737
|
+
proc.stderr?.on("data", appendOutput);
|
|
2738
|
+
proc.on("close", (code) => {
|
|
2739
|
+
this.loginSession.active = false;
|
|
2740
|
+
this.loginSession.waitingForCode = false;
|
|
2741
|
+
if (code === 0) {
|
|
2742
|
+
this.loginSession.completed = true;
|
|
2743
|
+
} else if (!this.loginSession.completed) {
|
|
2744
|
+
const lines = this.loginSession.output.trim().split("\n").filter((l) => l.trim());
|
|
2745
|
+
const lastLines = lines.slice(-3).join(" | ");
|
|
2746
|
+
this.loginSession.error = `Login failed (exit code ${code}).` + (lastLines ? ` Details: ${lastLines}` : "") + ` Try running "firebase login" in your terminal.`;
|
|
2747
|
+
}
|
|
2748
|
+
this.loginProcess = null;
|
|
2749
|
+
});
|
|
2750
|
+
proc.on("error", (err) => {
|
|
2751
|
+
this.loginSession.active = false;
|
|
2752
|
+
this.loginSession.error = err.message;
|
|
2753
|
+
this.loginProcess = null;
|
|
2754
|
+
});
|
|
2755
|
+
this.loginTimeout = setTimeout(() => {
|
|
2756
|
+
if (this.loginSession.active) {
|
|
2757
|
+
this.loginSession.error = "Login timed out (5 minutes). Please try again.";
|
|
2758
|
+
this.cleanupLogin();
|
|
2759
|
+
}
|
|
2760
|
+
}, 5 * 60 * 1e3);
|
|
2761
|
+
} catch (err) {
|
|
2762
|
+
this.loginSession.active = false;
|
|
2763
|
+
this.loginSession.error = err instanceof Error ? err.message : "Failed to start login";
|
|
2764
|
+
}
|
|
2765
|
+
return this.loginSession;
|
|
2766
|
+
}
|
|
2767
|
+
getLoginState() {
|
|
2768
|
+
return { ...this.loginSession };
|
|
2769
|
+
}
|
|
2770
|
+
/**
|
|
2771
|
+
* Submit the authorization code that the user copied from the browser.
|
|
2772
|
+
* This writes the code to the Firebase CLI process stdin.
|
|
2773
|
+
*/
|
|
2774
|
+
submitAuthCode(code) {
|
|
2775
|
+
if (!this.loginProcess || !this.loginProcess.stdin) {
|
|
2776
|
+
return { success: false, message: "No active login process. Please start login again." };
|
|
2777
|
+
}
|
|
2778
|
+
if (!code || !code.trim()) {
|
|
2779
|
+
return { success: false, message: "Authorization code is required." };
|
|
2780
|
+
}
|
|
2781
|
+
try {
|
|
2782
|
+
this.loginProcess.stdin.write(code.trim() + "\n");
|
|
2783
|
+
this.loginSession.waitingForCode = false;
|
|
2784
|
+
return { success: true, message: "Authorization code submitted. Waiting for verification..." };
|
|
2785
|
+
} catch (err) {
|
|
2786
|
+
return { success: false, message: "Failed to submit code: " + (err instanceof Error ? err.message : "unknown error") };
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
cancelLogin() {
|
|
2790
|
+
this.cleanupLogin();
|
|
2791
|
+
this.loginSession.error = "Login cancelled by user.";
|
|
2792
|
+
}
|
|
2793
|
+
cleanupLogin() {
|
|
2794
|
+
if (this.loginProcess) {
|
|
2795
|
+
try {
|
|
2796
|
+
this.loginProcess.kill("SIGTERM");
|
|
2797
|
+
} catch {
|
|
2798
|
+
}
|
|
2799
|
+
this.loginProcess = null;
|
|
2800
|
+
}
|
|
2801
|
+
if (this.loginTimeout) {
|
|
2802
|
+
clearTimeout(this.loginTimeout);
|
|
2803
|
+
this.loginTimeout = null;
|
|
2804
|
+
}
|
|
2805
|
+
this.loginSession.active = false;
|
|
2806
|
+
this.loginSession.waitingForCode = false;
|
|
2807
|
+
}
|
|
2808
|
+
// ─── Project Listing ───────────────────────────────────────────────
|
|
2809
|
+
async listProjects() {
|
|
2810
|
+
try {
|
|
2811
|
+
const output = await this.execTimeout(
|
|
2812
|
+
"firebase",
|
|
2813
|
+
["projects:list", "--json"],
|
|
2814
|
+
3e4
|
|
2815
|
+
// 30s — can be slow on first call
|
|
2816
|
+
);
|
|
2817
|
+
const data = JSON.parse(output);
|
|
2818
|
+
if (data?.result && Array.isArray(data.result)) {
|
|
2819
|
+
const projects = data.result.map((p) => ({
|
|
2820
|
+
projectId: p.projectId || "",
|
|
2821
|
+
displayName: p.displayName || p.projectId || "",
|
|
2822
|
+
projectNumber: p.projectNumber || "",
|
|
2823
|
+
state: p.lifecycleState || p.state || "ACTIVE"
|
|
2824
|
+
}));
|
|
2825
|
+
return { projects };
|
|
2826
|
+
}
|
|
2827
|
+
return { projects: [], error: "Unexpected response format" };
|
|
2828
|
+
} catch (err) {
|
|
2829
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
2830
|
+
if (msg.includes("authenticate") || msg.includes("login") || msg.includes("credential")) {
|
|
2831
|
+
return { projects: [], error: "Session expired. Please re-authenticate first." };
|
|
2832
|
+
}
|
|
2833
|
+
if (msg.includes("Timeout")) {
|
|
2834
|
+
return { projects: [], error: "Request timed out. Please try again." };
|
|
2835
|
+
}
|
|
2836
|
+
return { projects: [], error: msg };
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
// ─── Project Selection ─────────────────────────────────────────────
|
|
2840
|
+
async selectProject(projectId) {
|
|
2841
|
+
if (!projectId || !/^[a-z0-9-]+$/.test(projectId)) {
|
|
2842
|
+
return { success: false, message: "Invalid project ID format." };
|
|
2843
|
+
}
|
|
2844
|
+
try {
|
|
2845
|
+
await this.execTimeout(
|
|
2846
|
+
"firebase",
|
|
2847
|
+
["use", projectId],
|
|
2848
|
+
1e4
|
|
2849
|
+
);
|
|
2850
|
+
const firebasercPath = (0, import_node_path4.resolve)(this.projectDir, ".firebaserc");
|
|
2851
|
+
let rc = {};
|
|
2852
|
+
if ((0, import_node_fs4.existsSync)(firebasercPath)) {
|
|
2853
|
+
try {
|
|
2854
|
+
rc = JSON.parse((0, import_node_fs4.readFileSync)(firebasercPath, "utf-8"));
|
|
2855
|
+
} catch {
|
|
2856
|
+
rc = {};
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
if (!rc.projects || typeof rc.projects !== "object") {
|
|
2860
|
+
rc.projects = {};
|
|
2861
|
+
}
|
|
2862
|
+
rc.projects.default = projectId;
|
|
2863
|
+
(0, import_node_fs4.writeFileSync)(firebasercPath, JSON.stringify(rc, null, 2) + "\n", "utf-8");
|
|
2864
|
+
return { success: true, message: `Active project set to "${projectId}".` };
|
|
2865
|
+
} catch {
|
|
2866
|
+
try {
|
|
2867
|
+
const firebasercPath = (0, import_node_path4.resolve)(this.projectDir, ".firebaserc");
|
|
2868
|
+
const rc = {
|
|
2869
|
+
projects: { default: projectId }
|
|
2870
|
+
};
|
|
2871
|
+
(0, import_node_fs4.writeFileSync)(firebasercPath, JSON.stringify(rc, null, 2) + "\n", "utf-8");
|
|
2872
|
+
return { success: true, message: `Active project set to "${projectId}" (via .firebaserc).` };
|
|
2873
|
+
} catch (writeErr) {
|
|
2874
|
+
const msg = writeErr instanceof Error ? writeErr.message : "Unknown error";
|
|
2875
|
+
return { success: false, message: `Failed to set project: ${msg}` };
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
// ─── Helpers ───────────────────────────────────────────────────────
|
|
2880
|
+
execTimeout(command, args, timeoutMs) {
|
|
2881
|
+
return new Promise((resolve7, reject) => {
|
|
2882
|
+
const proc = (0, import_node_child_process2.execFile)(command, args, { cwd: this.projectDir, timeout: timeoutMs }, (err, stdout, stderr) => {
|
|
2883
|
+
if (err) {
|
|
2884
|
+
const detail = stderr?.trim() || stdout?.trim() || "";
|
|
2885
|
+
const enriched = new Error(`${err.message}${detail ? "\n" + detail : ""}`);
|
|
2886
|
+
reject(enriched);
|
|
2887
|
+
} else {
|
|
2888
|
+
resolve7(stdout);
|
|
2889
|
+
}
|
|
2890
|
+
});
|
|
2891
|
+
const timer = setTimeout(() => {
|
|
2892
|
+
proc.kill("SIGTERM");
|
|
2893
|
+
reject(new Error("Timeout"));
|
|
2894
|
+
}, timeoutMs + 500);
|
|
2895
|
+
proc.on("exit", () => clearTimeout(timer));
|
|
2896
|
+
});
|
|
2897
|
+
}
|
|
2898
|
+
/** Cleanup resources */
|
|
2899
|
+
destroy() {
|
|
2900
|
+
this.cleanupLogin();
|
|
2901
|
+
}
|
|
2902
|
+
};
|
|
2903
|
+
|
|
2038
2904
|
// src/dev/dev-server.ts
|
|
2039
2905
|
var MIME_TYPES = {
|
|
2040
2906
|
html: "text/html; charset=utf-8",
|
|
@@ -2280,6 +3146,7 @@ var DevServer = class {
|
|
|
2280
3146
|
isReloading = false;
|
|
2281
3147
|
pageCompiler;
|
|
2282
3148
|
envManager;
|
|
3149
|
+
firebaseSetup;
|
|
2283
3150
|
constructor(options = {}) {
|
|
2284
3151
|
this.options = {
|
|
2285
3152
|
projectDir: options.projectDir || process.cwd(),
|
|
@@ -2291,13 +3158,14 @@ var DevServer = class {
|
|
|
2291
3158
|
onSetupRoutes: options.onSetupRoutes || (() => {
|
|
2292
3159
|
})
|
|
2293
3160
|
};
|
|
2294
|
-
this.routesDir = (0,
|
|
2295
|
-
this.schemasDir = (0,
|
|
2296
|
-
this.publicDir = (0,
|
|
2297
|
-
this.pagesDir = (0,
|
|
2298
|
-
this.componentsDir = (0,
|
|
3161
|
+
this.routesDir = (0, import_node_path5.resolve)(this.options.projectDir, "app/routes");
|
|
3162
|
+
this.schemasDir = (0, import_node_path5.resolve)(this.options.projectDir, "app/schemas");
|
|
3163
|
+
this.publicDir = (0, import_node_path5.resolve)(this.options.projectDir, "public");
|
|
3164
|
+
this.pagesDir = (0, import_node_path5.resolve)(this.options.projectDir, "app/pages");
|
|
3165
|
+
this.componentsDir = (0, import_node_path5.resolve)(this.options.projectDir, "app/components");
|
|
2299
3166
|
this.pageCompiler = new PageCompiler(this.options.projectDir);
|
|
2300
3167
|
this.envManager = new EnvManager(this.options.projectDir);
|
|
3168
|
+
this.firebaseSetup = new FirebaseSetup(this.options.projectDir);
|
|
2301
3169
|
this.router = createRouter({
|
|
2302
3170
|
cors: ["*"],
|
|
2303
3171
|
rateLimit: 0,
|
|
@@ -2313,18 +3181,19 @@ var DevServer = class {
|
|
|
2313
3181
|
if (this.options.hotReload) {
|
|
2314
3182
|
this.startWatcher();
|
|
2315
3183
|
}
|
|
2316
|
-
await new Promise((
|
|
2317
|
-
this.apiServer.listen(this.options.apiPort, () =>
|
|
3184
|
+
await new Promise((resolve7, reject) => {
|
|
3185
|
+
this.apiServer.listen(this.options.apiPort, () => resolve7());
|
|
2318
3186
|
this.apiServer.on("error", reject);
|
|
2319
3187
|
});
|
|
2320
|
-
await new Promise((
|
|
2321
|
-
this.frontendServer.listen(this.options.port, () =>
|
|
3188
|
+
await new Promise((resolve7, reject) => {
|
|
3189
|
+
this.frontendServer.listen(this.options.port, () => resolve7());
|
|
2322
3190
|
this.frontendServer.on("error", reject);
|
|
2323
3191
|
});
|
|
2324
3192
|
this.printStartupBanner();
|
|
2325
3193
|
}
|
|
2326
3194
|
async stop() {
|
|
2327
3195
|
this.watcher?.close();
|
|
3196
|
+
this.firebaseSetup.destroy();
|
|
2328
3197
|
for (const client of [...this.frontendSseClients, ...this.apiSseClients]) {
|
|
2329
3198
|
client.res.end();
|
|
2330
3199
|
}
|
|
@@ -2332,13 +3201,13 @@ var DevServer = class {
|
|
|
2332
3201
|
this.apiSseClients = [];
|
|
2333
3202
|
this.router.destroy();
|
|
2334
3203
|
if (this.apiServer) {
|
|
2335
|
-
await new Promise((
|
|
2336
|
-
this.apiServer.close(() =>
|
|
3204
|
+
await new Promise((resolve7) => {
|
|
3205
|
+
this.apiServer.close(() => resolve7());
|
|
2337
3206
|
});
|
|
2338
3207
|
}
|
|
2339
3208
|
if (this.frontendServer) {
|
|
2340
|
-
await new Promise((
|
|
2341
|
-
this.frontendServer.close(() =>
|
|
3209
|
+
await new Promise((resolve7) => {
|
|
3210
|
+
this.frontendServer.close(() => resolve7());
|
|
2342
3211
|
});
|
|
2343
3212
|
}
|
|
2344
3213
|
}
|
|
@@ -2354,11 +3223,11 @@ var DevServer = class {
|
|
|
2354
3223
|
await this.options.onSetupRoutes(this.router);
|
|
2355
3224
|
if (this.router.getRoutes().length > 0) return;
|
|
2356
3225
|
}
|
|
2357
|
-
if (!(0,
|
|
3226
|
+
if (!(0, import_node_fs5.existsSync)(this.routesDir)) return;
|
|
2358
3227
|
const discovered = discoverRoutes(this.routesDir);
|
|
2359
3228
|
for (const route of discovered) {
|
|
2360
3229
|
try {
|
|
2361
|
-
const fullPath = (0,
|
|
3230
|
+
const fullPath = (0, import_node_path5.resolve)(this.routesDir, route.filePath);
|
|
2362
3231
|
const fileUrl = (0, import_node_url.pathToFileURL)(fullPath).href;
|
|
2363
3232
|
const mod = await import(`${fileUrl}?v=${++this.importCounter}`);
|
|
2364
3233
|
const contract = mod.default;
|
|
@@ -2373,7 +3242,7 @@ var DevServer = class {
|
|
|
2373
3242
|
async reloadRoutes(event) {
|
|
2374
3243
|
if (this.isReloading) return;
|
|
2375
3244
|
this.isReloading = true;
|
|
2376
|
-
const relPath = (0,
|
|
3245
|
+
const relPath = (0, import_node_path5.relative)(this.options.projectDir, event.filePath);
|
|
2377
3246
|
const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
2378
3247
|
console.log(`
|
|
2379
3248
|
\x1B[33m[${timestamp}]\x1B[0m \x1B[36m${relPath}\x1B[0m changed`);
|
|
@@ -2411,7 +3280,7 @@ var DevServer = class {
|
|
|
2411
3280
|
}
|
|
2412
3281
|
// ─── Frontend Change Handler ───────────────────────────────────────
|
|
2413
3282
|
handleFrontendChange(event) {
|
|
2414
|
-
const relPath = (0,
|
|
3283
|
+
const relPath = (0, import_node_path5.relative)(this.options.projectDir, event.filePath);
|
|
2415
3284
|
const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
2416
3285
|
if (event.type === "css-change") {
|
|
2417
3286
|
console.log(`
|
|
@@ -2434,23 +3303,23 @@ var DevServer = class {
|
|
|
2434
3303
|
// ─── File Watcher ──────────────────────────────────────────────────
|
|
2435
3304
|
startWatcher() {
|
|
2436
3305
|
this.watcher = new FileWatcher(this.options.debounceMs);
|
|
2437
|
-
if ((0,
|
|
3306
|
+
if ((0, import_node_fs5.existsSync)(this.routesDir)) {
|
|
2438
3307
|
this.watcher.watchDir(this.routesDir, "route-change");
|
|
2439
3308
|
}
|
|
2440
|
-
if ((0,
|
|
3309
|
+
if ((0, import_node_fs5.existsSync)(this.schemasDir)) {
|
|
2441
3310
|
this.watcher.watchDir(this.schemasDir, "schema-change");
|
|
2442
3311
|
}
|
|
2443
|
-
const configFile = (0,
|
|
2444
|
-
if ((0,
|
|
3312
|
+
const configFile = (0, import_node_path5.resolve)(this.options.projectDir, "clawfire.config.ts");
|
|
3313
|
+
if ((0, import_node_fs5.existsSync)(configFile)) {
|
|
2445
3314
|
this.watcher.watchFile(configFile, "config-change");
|
|
2446
3315
|
}
|
|
2447
|
-
if ((0,
|
|
3316
|
+
if ((0, import_node_fs5.existsSync)(this.publicDir)) {
|
|
2448
3317
|
this.watcher.watchDirFrontend(this.publicDir);
|
|
2449
3318
|
}
|
|
2450
|
-
if ((0,
|
|
3319
|
+
if ((0, import_node_fs5.existsSync)(this.pagesDir)) {
|
|
2451
3320
|
this.watcher.watchDir(this.pagesDir, "page-change");
|
|
2452
3321
|
}
|
|
2453
|
-
if ((0,
|
|
3322
|
+
if ((0, import_node_fs5.existsSync)(this.componentsDir)) {
|
|
2454
3323
|
this.watcher.watchDir(this.componentsDir, "component-change");
|
|
2455
3324
|
}
|
|
2456
3325
|
this.watcher.on("route-change", (event) => this.reloadRoutes(event));
|
|
@@ -2611,10 +3480,10 @@ ${liveReloadScript}
|
|
|
2611
3480
|
}
|
|
2612
3481
|
// ─── Static File Serving ──────────────────────────────────────────
|
|
2613
3482
|
serveStaticFile(filePath, res) {
|
|
2614
|
-
if (!(0,
|
|
3483
|
+
if (!(0, import_node_fs5.existsSync)(filePath)) return false;
|
|
2615
3484
|
try {
|
|
2616
|
-
const content = (0,
|
|
2617
|
-
const ext = (0,
|
|
3485
|
+
const content = (0, import_node_fs5.readFileSync)(filePath);
|
|
3486
|
+
const ext = (0, import_node_path5.extname)(filePath).slice(1).toLowerCase();
|
|
2618
3487
|
const mime = MIME_TYPES[ext] || "application/octet-stream";
|
|
2619
3488
|
if (ext === "html") {
|
|
2620
3489
|
const html = content.toString("utf-8");
|
|
@@ -2669,9 +3538,9 @@ ${liveReloadScript}
|
|
|
2669
3538
|
this.proxyToApiServer(req, res);
|
|
2670
3539
|
return;
|
|
2671
3540
|
}
|
|
2672
|
-
const ext = (0,
|
|
3541
|
+
const ext = (0, import_node_path5.extname)(url.pathname);
|
|
2673
3542
|
if (ext && STATIC_EXTENSIONS.has(ext)) {
|
|
2674
|
-
const filePath2 = (0,
|
|
3543
|
+
const filePath2 = (0, import_node_path5.resolve)(this.publicDir, url.pathname.slice(1));
|
|
2675
3544
|
if (filePath2.startsWith(this.publicDir) && this.serveStaticFile(filePath2, res)) {
|
|
2676
3545
|
return;
|
|
2677
3546
|
}
|
|
@@ -2722,7 +3591,7 @@ ${liveReloadScript}
|
|
|
2722
3591
|
}
|
|
2723
3592
|
}
|
|
2724
3593
|
const requestedPath = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
|
|
2725
|
-
const filePath = (0,
|
|
3594
|
+
const filePath = (0, import_node_path5.resolve)(this.publicDir, requestedPath);
|
|
2726
3595
|
if (!filePath.startsWith(this.publicDir)) {
|
|
2727
3596
|
res.writeHead(403);
|
|
2728
3597
|
res.end("Forbidden");
|
|
@@ -2731,8 +3600,8 @@ ${liveReloadScript}
|
|
|
2731
3600
|
if (this.serveStaticFile(filePath, res)) {
|
|
2732
3601
|
return;
|
|
2733
3602
|
}
|
|
2734
|
-
const indexPath = (0,
|
|
2735
|
-
if ((0,
|
|
3603
|
+
const indexPath = (0, import_node_path5.resolve)(this.publicDir, "index.html");
|
|
3604
|
+
if ((0, import_node_fs5.existsSync)(indexPath)) {
|
|
2736
3605
|
this.serveStaticFile(indexPath, res);
|
|
2737
3606
|
return;
|
|
2738
3607
|
}
|
|
@@ -2931,18 +3800,97 @@ ${liveReloadScript}
|
|
|
2931
3800
|
});
|
|
2932
3801
|
return;
|
|
2933
3802
|
}
|
|
3803
|
+
if (url.pathname === "/__dev/setup/status" && req.method === "GET") {
|
|
3804
|
+
this.firebaseSetup.getStatus().then((status) => sendJson(status)).catch((err) => sendJson({ error: err instanceof Error ? err.message : "Failed" }, 500));
|
|
3805
|
+
return;
|
|
3806
|
+
}
|
|
3807
|
+
if (url.pathname === "/__dev/setup/install-cli" && req.method === "POST") {
|
|
3808
|
+
this.firebaseSetup.installCli().then((result) => sendJson(result)).catch((err) => sendJson({ success: false, message: err instanceof Error ? err.message : "Failed" }, 500));
|
|
3809
|
+
return;
|
|
3810
|
+
}
|
|
3811
|
+
if (url.pathname === "/__dev/setup/login" && req.method === "POST") {
|
|
3812
|
+
let body = "";
|
|
3813
|
+
req.on("data", (chunk) => {
|
|
3814
|
+
body += chunk;
|
|
3815
|
+
});
|
|
3816
|
+
req.on("end", () => {
|
|
3817
|
+
let reauth = false;
|
|
3818
|
+
try {
|
|
3819
|
+
const data = JSON.parse(body);
|
|
3820
|
+
reauth = !!data.reauth;
|
|
3821
|
+
} catch {
|
|
3822
|
+
}
|
|
3823
|
+
const session = this.firebaseSetup.startLogin(reauth);
|
|
3824
|
+
sendJson(session);
|
|
3825
|
+
});
|
|
3826
|
+
return;
|
|
3827
|
+
}
|
|
3828
|
+
if (url.pathname === "/__dev/setup/login-state" && req.method === "GET") {
|
|
3829
|
+
sendJson(this.firebaseSetup.getLoginState());
|
|
3830
|
+
return;
|
|
3831
|
+
}
|
|
3832
|
+
if (url.pathname === "/__dev/setup/cancel-login" && req.method === "POST") {
|
|
3833
|
+
this.firebaseSetup.cancelLogin();
|
|
3834
|
+
sendJson({ ok: true });
|
|
3835
|
+
return;
|
|
3836
|
+
}
|
|
3837
|
+
if (url.pathname === "/__dev/setup/submit-auth-code" && req.method === "POST") {
|
|
3838
|
+
let body = "";
|
|
3839
|
+
req.on("data", (chunk) => {
|
|
3840
|
+
body += chunk;
|
|
3841
|
+
});
|
|
3842
|
+
req.on("end", () => {
|
|
3843
|
+
try {
|
|
3844
|
+
const data = JSON.parse(body);
|
|
3845
|
+
if (!data.code) {
|
|
3846
|
+
sendJson({ success: false, message: "code is required" }, 400);
|
|
3847
|
+
return;
|
|
3848
|
+
}
|
|
3849
|
+
sendJson(this.firebaseSetup.submitAuthCode(data.code));
|
|
3850
|
+
} catch {
|
|
3851
|
+
sendJson({ success: false, message: "Invalid JSON body" }, 400);
|
|
3852
|
+
}
|
|
3853
|
+
});
|
|
3854
|
+
return;
|
|
3855
|
+
}
|
|
3856
|
+
if (url.pathname === "/__dev/setup/projects" && req.method === "GET") {
|
|
3857
|
+
this.firebaseSetup.listProjects().then((result) => sendJson(result)).catch((err) => sendJson({ projects: [], error: err instanceof Error ? err.message : "Failed" }, 500));
|
|
3858
|
+
return;
|
|
3859
|
+
}
|
|
3860
|
+
if (url.pathname === "/__dev/setup/select-project" && req.method === "POST") {
|
|
3861
|
+
let body = "";
|
|
3862
|
+
req.on("data", (chunk) => {
|
|
3863
|
+
body += chunk;
|
|
3864
|
+
});
|
|
3865
|
+
req.on("end", () => {
|
|
3866
|
+
try {
|
|
3867
|
+
const data = JSON.parse(body);
|
|
3868
|
+
if (!data.projectId) {
|
|
3869
|
+
sendJson({ success: false, message: "projectId is required" }, 400);
|
|
3870
|
+
return;
|
|
3871
|
+
}
|
|
3872
|
+
this.firebaseSetup.selectProject(data.projectId).then((result) => {
|
|
3873
|
+
clearFirebaseStatusCache();
|
|
3874
|
+
sendJson(result);
|
|
3875
|
+
}).catch((err) => sendJson({ success: false, message: err instanceof Error ? err.message : "Failed" }, 500));
|
|
3876
|
+
} catch {
|
|
3877
|
+
sendJson({ success: false, message: "Invalid JSON body" }, 400);
|
|
3878
|
+
}
|
|
3879
|
+
});
|
|
3880
|
+
return;
|
|
3881
|
+
}
|
|
2934
3882
|
res.writeHead(404);
|
|
2935
3883
|
res.end("Not found");
|
|
2936
3884
|
}
|
|
2937
3885
|
// ─── Config Reader ────────────────────────────────────────────────
|
|
2938
3886
|
readProjectConfig() {
|
|
2939
|
-
const configPath = (0,
|
|
3887
|
+
const configPath = (0, import_node_path5.resolve)(this.options.projectDir, "clawfire.config.ts");
|
|
2940
3888
|
const fields = [];
|
|
2941
|
-
if (!(0,
|
|
3889
|
+
if (!(0, import_node_fs5.existsSync)(configPath)) {
|
|
2942
3890
|
return { fields };
|
|
2943
3891
|
}
|
|
2944
3892
|
try {
|
|
2945
|
-
const content = (0,
|
|
3893
|
+
const content = (0, import_node_fs5.readFileSync)(configPath, "utf-8");
|
|
2946
3894
|
const kvPattern = /(\w+)\s*:\s*["'`]([^"'`]*)["'`]/g;
|
|
2947
3895
|
let match;
|
|
2948
3896
|
while ((match = kvPattern.exec(content)) !== null) {
|
|
@@ -2957,11 +3905,11 @@ ${liveReloadScript}
|
|
|
2957
3905
|
}
|
|
2958
3906
|
/** Update a single key's value in clawfire.config.ts */
|
|
2959
3907
|
updateProjectConfig(key, value) {
|
|
2960
|
-
const configPath = (0,
|
|
2961
|
-
if (!(0,
|
|
3908
|
+
const configPath = (0, import_node_path5.resolve)(this.options.projectDir, "clawfire.config.ts");
|
|
3909
|
+
if (!(0, import_node_fs5.existsSync)(configPath)) {
|
|
2962
3910
|
throw new Error("clawfire.config.ts not found");
|
|
2963
3911
|
}
|
|
2964
|
-
let content = (0,
|
|
3912
|
+
let content = (0, import_node_fs5.readFileSync)(configPath, "utf-8");
|
|
2965
3913
|
const pattern = new RegExp(
|
|
2966
3914
|
`(${this.escapeRegex(key)}\\s*:\\s*)["'\`][^"'\`]*["'\`]`
|
|
2967
3915
|
);
|
|
@@ -2969,7 +3917,7 @@ ${liveReloadScript}
|
|
|
2969
3917
|
throw new Error(`Key "${key}" not found in config`);
|
|
2970
3918
|
}
|
|
2971
3919
|
content = content.replace(pattern, `$1"${value.replace(/"/g, '\\"')}"`);
|
|
2972
|
-
(0,
|
|
3920
|
+
(0, import_node_fs5.writeFileSync)(configPath, content, "utf-8");
|
|
2973
3921
|
}
|
|
2974
3922
|
escapeRegex(s) {
|
|
2975
3923
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|