clawfire 0.3.1 → 0.3.2

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.d.cts CHANGED
@@ -59,6 +59,9 @@ declare class DevServer {
59
59
  private printStartupBanner;
60
60
  private handleDevEndpoint;
61
61
  private readProjectConfig;
62
+ /** Update a single key's value in clawfire.config.ts */
63
+ private updateProjectConfig;
64
+ private escapeRegex;
62
65
  }
63
66
  /**
64
67
  * Create and start dev server (one-line helper)
package/dist/dev.d.ts CHANGED
@@ -59,6 +59,9 @@ declare class DevServer {
59
59
  private printStartupBanner;
60
60
  private handleDevEndpoint;
61
61
  private readProjectConfig;
62
+ /** Update a single key's value in clawfire.config.ts */
63
+ private updateProjectConfig;
64
+ private escapeRegex;
62
65
  }
63
66
  /**
64
67
  * Create and start dev server (one-line helper)
package/dist/dev.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/dev/dev-server.ts
2
2
  import http from "http";
3
3
  import { resolve as resolve5, relative as relative3, extname as extname3 } from "path";
4
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
4
+ import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
5
5
  import { pathToFileURL } from "url";
6
6
 
7
7
  // src/core/schema.ts
@@ -1456,6 +1456,34 @@ 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;
1484
+ }
1485
+ throw new Error("Could not parse Firebase SDK config from CLI output");
1486
+ }
1459
1487
  function execWithTimeout(command, args, cwd, timeoutMs) {
1460
1488
  return new Promise((resolve6, reject) => {
1461
1489
  const proc = execFile(command, args, { cwd, timeout: timeoutMs }, (err, stdout) => {
@@ -1504,9 +1532,25 @@ function generateDashboardHtml(options) {
1504
1532
 
1505
1533
  <!-- Section 2: Config Overview -->
1506
1534
  <div style="margin-bottom:32px;">
1507
- <h2 style="font-size:18px;font-weight:700;color:#f97316;margin-bottom:16px;">Config Overview</h2>
1535
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
1536
+ <h2 style="font-size:18px;font-weight:700;color:#f97316;">Config Overview</h2>
1537
+ <button id="autofill-btn" onclick="autoFillConfig()" style="padding:6px 14px;background:#3b82f6;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;">Auto-fill from Firebase</button>
1538
+ </div>
1539
+ <div id="autofill-status" style="display:none;padding:8px 12px;border-radius:6px;margin-bottom:12px;font-size:13px;"></div>
1508
1540
  <div id="config-section" style="border-radius:8px;border:1px solid #2a2a2a;background:#141414;overflow:hidden;">
1509
- <div id="config-content" style="padding:16px;font-family:monospace;font-size:13px;line-height:1.8;"></div>
1541
+ <table id="config-table" style="width:100%;border-collapse:collapse;font-size:13px;">
1542
+ <thead>
1543
+ <tr style="border-bottom:1px solid #2a2a2a;">
1544
+ <th style="padding:10px 16px;text-align:left;color:#a3a3a3;font-weight:500;width:180px;">Key</th>
1545
+ <th style="padding:10px 16px;text-align:left;color:#a3a3a3;font-weight:500;">Value</th>
1546
+ <th style="padding:10px 16px;text-align:right;color:#a3a3a3;font-weight:500;width:80px;">Action</th>
1547
+ </tr>
1548
+ </thead>
1549
+ <tbody id="config-tbody"></tbody>
1550
+ </table>
1551
+ <div id="config-empty" style="display:none;padding:32px;text-align:center;color:#666;">
1552
+ No clawfire.config.ts found.
1553
+ </div>
1510
1554
  </div>
1511
1555
  </div>
1512
1556
 
@@ -1536,7 +1580,7 @@ function generateDashboardHtml(options) {
1536
1580
  </div>
1537
1581
 
1538
1582
  <!-- Env Modal -->
1539
- <div id="env-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:10000;display:none;align-items:center;justify-content:center;">
1583
+ <div id="env-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:10000;align-items:center;justify-content:center;">
1540
1584
  <div style="background:#1e1e1e;border:1px solid #2a2a2a;border-radius:12px;padding:24px;width:440px;max-width:90vw;">
1541
1585
  <h3 id="modal-title" style="font-size:16px;font-weight:700;color:#e5e5e5;margin-bottom:16px;">Add Variable</h3>
1542
1586
  <div style="margin-bottom:12px;">
@@ -1564,6 +1608,7 @@ function generateDashboardHtml(options) {
1564
1608
  (function() {
1565
1609
  var API = 'http://localhost:${apiPort}';
1566
1610
  var envData = [];
1611
+ var configData = [];
1567
1612
  var editingKey = null;
1568
1613
 
1569
1614
  // \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
@@ -1589,7 +1634,6 @@ function generateDashboardHtml(options) {
1589
1634
 
1590
1635
  // \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
1591
1636
  function renderFirebaseStatus(data) {
1592
- // CLI Banner
1593
1637
  var dot = document.getElementById('cli-dot');
1594
1638
  var text = document.getElementById('cli-text');
1595
1639
  var proj = document.getElementById('cli-project');
@@ -1631,7 +1675,6 @@ function generateDashboardHtml(options) {
1631
1675
  grid.appendChild(card);
1632
1676
  });
1633
1677
 
1634
- // Config Warnings
1635
1678
  if (data.configWarnings && data.configWarnings.length > 0) {
1636
1679
  var warningCard = document.createElement('div');
1637
1680
  warningCard.style.cssText = 'padding:16px;border-radius:8px;border:1px solid #eab308;background:#1a1a0a;grid-column:1/-1;';
@@ -1644,26 +1687,167 @@ function generateDashboardHtml(options) {
1644
1687
  }
1645
1688
  }
1646
1689
 
1647
- // \u2500\u2500\u2500 Config Overview \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1690
+ // \u2500\u2500\u2500 Config Overview (editable) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1648
1691
  function renderConfig(data) {
1649
- var el = document.getElementById('config-content');
1650
- if (!data || !data.fields || data.fields.length === 0) {
1651
- el.innerHTML = '<span style="color:#666;">No clawfire.config.ts found or config is empty.</span>';
1692
+ var tbody = document.getElementById('config-tbody');
1693
+ var empty = document.getElementById('config-empty');
1694
+ var table = document.getElementById('config-table');
1695
+
1696
+ configData = (data && data.fields) ? data.fields : [];
1697
+
1698
+ if (configData.length === 0) {
1699
+ table.style.display = 'none';
1700
+ empty.style.display = 'block';
1652
1701
  return;
1653
1702
  }
1654
- var html = '';
1655
- data.fields.forEach(function(field) {
1703
+
1704
+ table.style.display = 'table';
1705
+ empty.style.display = 'none';
1706
+ tbody.innerHTML = '';
1707
+
1708
+ configData.forEach(function(field) {
1709
+ var tr = document.createElement('tr');
1710
+ tr.style.borderBottom = '1px solid #2a2a2a';
1711
+ tr.id = 'cfg-row-' + field.key;
1656
1712
  var color = field.isPlaceholder ? '#eab308' : '#a3a3a3';
1657
1713
  var badge = field.isPlaceholder ? ' <span style="background:#eab30822;color:#eab308;padding:1px 6px;border-radius:4px;font-size:10px;">PLACEHOLDER</span>' : '';
1658
- html += '<div style="padding:4px 0;display:flex;gap:8px;align-items:center;">';
1659
- html += '<span style="color:#e5e5e5;min-width:180px;">' + escHtml(field.key) + '</span>';
1660
- html += '<span style="color:' + color + ';">' + escHtml(field.value) + '</span>';
1661
- html += badge;
1662
- html += '</div>';
1714
+ tr.innerHTML =
1715
+ '<td style="padding:10px 16px;color:#e5e5e5;font-family:monospace;white-space:nowrap;">' + escHtml(field.key) + '</td>' +
1716
+ '<td style="padding:10px 16px;font-family:monospace;">' +
1717
+ '<span id="cfg-val-' + field.key + '" style="color:' + color + ';">' + escHtml(field.value) + '</span>' +
1718
+ badge +
1719
+ '<input id="cfg-input-' + field.key + '" type="text" value="' + escHtml(field.value) + '" style="display:none;width:100%;padding:4px 8px;background:#0a0a0a;border:1px solid #2a2a2a;border-radius:4px;color:#e5e5e5;font-family:monospace;font-size:13px;outline:none;" />' +
1720
+ '</td>' +
1721
+ '<td style="padding:10px 16px;text-align:right;white-space:nowrap;">' +
1722
+ '<button id="cfg-edit-' + field.key + '" onclick="editConfigField(\\'' + escHtml(field.key) + '\\')" style="background:none;border:none;color:#3b82f6;cursor:pointer;font-size:12px;padding:4px 8px;">Edit</button>' +
1723
+ '<button id="cfg-save-' + field.key + '" onclick="saveConfigField(\\'' + escHtml(field.key) + '\\')" style="display:none;background:none;border:none;color:#22c55e;cursor:pointer;font-size:12px;padding:4px 8px;">Save</button>' +
1724
+ '<button id="cfg-cancel-' + field.key + '" onclick="cancelConfigEdit(\\'' + escHtml(field.key) + '\\')" style="display:none;background:none;border:none;color:#a3a3a3;cursor:pointer;font-size:12px;padding:4px 8px;">Cancel</button>' +
1725
+ '</td>';
1726
+ tbody.appendChild(tr);
1663
1727
  });
1664
- el.innerHTML = html;
1665
1728
  }
1666
1729
 
1730
+ window.editConfigField = function(key) {
1731
+ var valSpan = document.getElementById('cfg-val-' + key);
1732
+ var input = document.getElementById('cfg-input-' + key);
1733
+ var editBtn = document.getElementById('cfg-edit-' + key);
1734
+ var saveBtn = document.getElementById('cfg-save-' + key);
1735
+ var cancelBtn = document.getElementById('cfg-cancel-' + key);
1736
+ if (!valSpan || !input) return;
1737
+ // Hide all badges in this cell
1738
+ var badges = valSpan.parentNode.querySelectorAll('span[style*="PLACEHOLDER"]');
1739
+ for (var i = 0; i < badges.length; i++) badges[i].style.display = 'none';
1740
+ valSpan.style.display = 'none';
1741
+ input.style.display = 'block';
1742
+ input.focus();
1743
+ input.select();
1744
+ editBtn.style.display = 'none';
1745
+ saveBtn.style.display = 'inline';
1746
+ cancelBtn.style.display = 'inline';
1747
+ };
1748
+
1749
+ window.cancelConfigEdit = function(key) {
1750
+ var valSpan = document.getElementById('cfg-val-' + key);
1751
+ var input = document.getElementById('cfg-input-' + key);
1752
+ var editBtn = document.getElementById('cfg-edit-' + key);
1753
+ var saveBtn = document.getElementById('cfg-save-' + key);
1754
+ var cancelBtn = document.getElementById('cfg-cancel-' + key);
1755
+ if (!valSpan || !input) return;
1756
+ var badges = valSpan.parentNode.querySelectorAll('span[style*="border-radius"]');
1757
+ for (var i = 0; i < badges.length; i++) badges[i].style.display = '';
1758
+ valSpan.style.display = '';
1759
+ input.style.display = 'none';
1760
+ input.value = valSpan.textContent;
1761
+ editBtn.style.display = 'inline';
1762
+ saveBtn.style.display = 'none';
1763
+ cancelBtn.style.display = 'none';
1764
+ };
1765
+
1766
+ window.saveConfigField = function(key) {
1767
+ var input = document.getElementById('cfg-input-' + key);
1768
+ var saveBtn = document.getElementById('cfg-save-' + key);
1769
+ if (!input) return;
1770
+ var value = input.value;
1771
+ saveBtn.textContent = '...';
1772
+ fetch(API + '/__dev/config', {
1773
+ method: 'POST',
1774
+ headers: { 'Content-Type': 'application/json' },
1775
+ body: JSON.stringify({ action: 'set', key: key, value: value })
1776
+ }).then(function(r) { return r.json(); })
1777
+ .then(function(data) {
1778
+ if (data.error) { alert('Error: ' + data.error); saveBtn.textContent = 'Save'; return; }
1779
+ // Reload config
1780
+ return fetch(API + '/__dev/config').then(function(r) { return r.json(); });
1781
+ })
1782
+ .then(function(data) { if (data) renderConfig(data); })
1783
+ .catch(function(err) { alert('Failed: ' + err.message); saveBtn.textContent = 'Save'; });
1784
+ };
1785
+
1786
+ // \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
1787
+ window.autoFillConfig = function() {
1788
+ var btn = document.getElementById('autofill-btn');
1789
+ var status = document.getElementById('autofill-status');
1790
+ btn.disabled = true;
1791
+ btn.textContent = 'Fetching...';
1792
+ status.style.display = 'none';
1793
+
1794
+ fetch(API + '/__dev/firebase-sdk-config')
1795
+ .then(function(r) { return r.json(); })
1796
+ .then(function(data) {
1797
+ if (data.error) {
1798
+ status.textContent = data.error;
1799
+ 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;';
1800
+ btn.disabled = false;
1801
+ btn.textContent = 'Auto-fill from Firebase';
1802
+ return;
1803
+ }
1804
+
1805
+ // Map SDK config keys to clawfire.config.ts keys
1806
+ var fields = {};
1807
+ if (data.apiKey) fields.apiKey = data.apiKey;
1808
+ if (data.authDomain) fields.authDomain = data.authDomain;
1809
+ if (data.projectId) fields.projectId = data.projectId;
1810
+ if (data.storageBucket) fields.storageBucket = data.storageBucket;
1811
+ if (data.appId) fields.appId = data.appId;
1812
+
1813
+ var count = Object.keys(fields).length;
1814
+ if (count === 0) {
1815
+ status.textContent = 'No web app config found. Create a Web app in Firebase Console first.';
1816
+ 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;';
1817
+ btn.disabled = false;
1818
+ btn.textContent = 'Auto-fill from Firebase';
1819
+ return;
1820
+ }
1821
+
1822
+ // Save all fields at once
1823
+ return fetch(API + '/__dev/config', {
1824
+ method: 'POST',
1825
+ headers: { 'Content-Type': 'application/json' },
1826
+ body: JSON.stringify({ action: 'set-multiple', fields: fields })
1827
+ }).then(function(r) { return r.json(); })
1828
+ .then(function(result) {
1829
+ if (result.error) {
1830
+ status.textContent = 'Error saving: ' + result.error;
1831
+ 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;';
1832
+ } else {
1833
+ status.textContent = count + ' fields updated from Firebase project!';
1834
+ 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;';
1835
+ // Reload config view
1836
+ return fetch(API + '/__dev/config').then(function(r) { return r.json(); })
1837
+ .then(function(cfg) { renderConfig(cfg); });
1838
+ }
1839
+ });
1840
+ })
1841
+ .catch(function(err) {
1842
+ status.textContent = 'Failed: ' + err.message;
1843
+ 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;';
1844
+ })
1845
+ .finally(function() {
1846
+ btn.disabled = false;
1847
+ btn.textContent = 'Auto-fill from Firebase';
1848
+ });
1849
+ };
1850
+
1667
1851
  // \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
1668
1852
  function renderEnvVars(data) {
1669
1853
  envData = data.variables || [];
@@ -1702,7 +1886,7 @@ function generateDashboardHtml(options) {
1702
1886
  });
1703
1887
  }
1704
1888
 
1705
- // \u2500\u2500\u2500 Modal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1889
+ // \u2500\u2500\u2500 Env Modal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1706
1890
  window.showEnvModal = function(key) {
1707
1891
  editingKey = key || null;
1708
1892
  var modal = document.getElementById('env-modal');
@@ -2647,6 +2831,37 @@ ${liveReloadScript}
2647
2831
  sendJson(this.readProjectConfig());
2648
2832
  return;
2649
2833
  }
2834
+ if (url.pathname === "/__dev/config" && req.method === "POST") {
2835
+ let body = "";
2836
+ req.on("data", (chunk) => {
2837
+ body += chunk;
2838
+ });
2839
+ req.on("end", () => {
2840
+ try {
2841
+ const data = JSON.parse(body);
2842
+ if (data.action === "set" && data.key && data.value !== void 0) {
2843
+ this.updateProjectConfig(data.key, String(data.value));
2844
+ clearFirebaseStatusCache();
2845
+ sendJson({ ok: true });
2846
+ } else if (data.action === "set-multiple" && data.fields) {
2847
+ for (const [key, value] of Object.entries(data.fields)) {
2848
+ this.updateProjectConfig(key, String(value));
2849
+ }
2850
+ clearFirebaseStatusCache();
2851
+ sendJson({ ok: true });
2852
+ } else {
2853
+ sendJson({ error: "Invalid action" }, 400);
2854
+ }
2855
+ } catch (err) {
2856
+ sendJson({ error: err instanceof Error ? err.message : "Failed" }, 400);
2857
+ }
2858
+ });
2859
+ return;
2860
+ }
2861
+ if (url.pathname === "/__dev/firebase-sdk-config" && req.method === "GET") {
2862
+ fetchFirebaseSdkConfig(this.options.projectDir).then((config) => sendJson(config)).catch((err) => sendJson({ error: err instanceof Error ? err.message : "Failed to fetch SDK config" }, 500));
2863
+ return;
2864
+ }
2650
2865
  if (url.pathname === "/__dev/env" && req.method === "GET") {
2651
2866
  try {
2652
2867
  sendJson(this.envManager.read());
@@ -2702,6 +2917,25 @@ ${liveReloadScript}
2702
2917
  }
2703
2918
  return { fields };
2704
2919
  }
2920
+ /** Update a single key's value in clawfire.config.ts */
2921
+ updateProjectConfig(key, value) {
2922
+ const configPath = resolve5(this.options.projectDir, "clawfire.config.ts");
2923
+ if (!existsSync6(configPath)) {
2924
+ throw new Error("clawfire.config.ts not found");
2925
+ }
2926
+ let content = readFileSync4(configPath, "utf-8");
2927
+ const pattern = new RegExp(
2928
+ `(${this.escapeRegex(key)}\\s*:\\s*)["'\`][^"'\`]*["'\`]`
2929
+ );
2930
+ if (!pattern.test(content)) {
2931
+ throw new Error(`Key "${key}" not found in config`);
2932
+ }
2933
+ content = content.replace(pattern, `$1"${value.replace(/"/g, '\\"')}"`);
2934
+ writeFileSync2(configPath, content, "utf-8");
2935
+ }
2936
+ escapeRegex(s) {
2937
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2938
+ }
2705
2939
  };
2706
2940
  async function startDevServer(options) {
2707
2941
  const server = new DevServer(options);