clawfire 0.2.1 → 0.3.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
@@ -1,7 +1,7 @@
1
1
  // src/dev/dev-server.ts
2
2
  import http from "http";
3
- import { resolve as resolve3, relative as relative3, extname as extname3 } from "path";
4
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
3
+ import { resolve as resolve5, relative as relative3, extname as extname3 } from "path";
4
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
5
5
  import { pathToFileURL } from "url";
6
6
 
7
7
  // src/core/schema.ts
@@ -1192,6 +1192,627 @@ ${html}
1192
1192
  }
1193
1193
  };
1194
1194
 
1195
+ // src/dev/env-manager.ts
1196
+ import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync4 } from "fs";
1197
+ import { resolve as resolve3 } from "path";
1198
+ var KEY_PATTERN = /^[A-Z_][A-Z0-9_]*$/i;
1199
+ var PLACEHOLDER_PATTERNS = [
1200
+ /^YOUR_/i,
1201
+ /^CHANGE_ME$/i,
1202
+ /^TODO$/i,
1203
+ /^REPLACE_/i,
1204
+ /^XXX/i,
1205
+ /^$/
1206
+ ];
1207
+ var EnvManager = class {
1208
+ envPath;
1209
+ descriptionsPath;
1210
+ constructor(projectDir) {
1211
+ this.envPath = resolve3(projectDir, ".env");
1212
+ this.descriptionsPath = resolve3(projectDir, ".env.descriptions.json");
1213
+ }
1214
+ /** Read all env variables with descriptions and placeholder detection */
1215
+ read() {
1216
+ const descriptions = this.readDescriptions();
1217
+ const variables = [];
1218
+ if (!existsSync4(this.envPath)) {
1219
+ return { variables, descriptions };
1220
+ }
1221
+ const content = readFileSync2(this.envPath, "utf-8");
1222
+ const lines = content.split("\n");
1223
+ for (let i = 0; i < lines.length; i++) {
1224
+ const line = lines[i].trim();
1225
+ if (!line || line.startsWith("#")) continue;
1226
+ const eqIdx = line.indexOf("=");
1227
+ if (eqIdx === -1) continue;
1228
+ const key = line.slice(0, eqIdx).trim();
1229
+ const value = line.slice(eqIdx + 1).trim();
1230
+ if (!KEY_PATTERN.test(key)) continue;
1231
+ variables.push({
1232
+ key,
1233
+ value,
1234
+ description: descriptions[key] || "",
1235
+ isPlaceholder: isPlaceholder(value),
1236
+ line: i + 1
1237
+ });
1238
+ }
1239
+ return { variables, descriptions };
1240
+ }
1241
+ /** Set or update an env variable */
1242
+ set(key, value, description) {
1243
+ if (!KEY_PATTERN.test(key)) {
1244
+ throw new Error(`Invalid key: "${key}" \u2014 must match ${KEY_PATTERN}`);
1245
+ }
1246
+ let lines = [];
1247
+ if (existsSync4(this.envPath)) {
1248
+ lines = readFileSync2(this.envPath, "utf-8").split("\n");
1249
+ }
1250
+ let found = false;
1251
+ for (let i = 0; i < lines.length; i++) {
1252
+ const trimmed = lines[i].trim();
1253
+ if (trimmed.startsWith("#") || !trimmed) continue;
1254
+ const eqIdx = trimmed.indexOf("=");
1255
+ if (eqIdx === -1) continue;
1256
+ const existingKey = trimmed.slice(0, eqIdx).trim();
1257
+ if (existingKey === key) {
1258
+ lines[i] = `${key}=${value}`;
1259
+ found = true;
1260
+ break;
1261
+ }
1262
+ }
1263
+ if (!found) {
1264
+ if (lines.length > 0 && lines[lines.length - 1].trim() !== "") {
1265
+ lines.push(`${key}=${value}`);
1266
+ } else {
1267
+ lines.push(`${key}=${value}`);
1268
+ }
1269
+ }
1270
+ writeFileSync(this.envPath, lines.join("\n"), "utf-8");
1271
+ if (description !== void 0) {
1272
+ const descriptions = this.readDescriptions();
1273
+ descriptions[key] = description;
1274
+ this.writeDescriptions(descriptions);
1275
+ }
1276
+ }
1277
+ /** Delete an env variable */
1278
+ delete(key) {
1279
+ if (!existsSync4(this.envPath)) return;
1280
+ const lines = readFileSync2(this.envPath, "utf-8").split("\n");
1281
+ const filtered = lines.filter((line) => {
1282
+ const trimmed = line.trim();
1283
+ if (trimmed.startsWith("#") || !trimmed) return true;
1284
+ const eqIdx = trimmed.indexOf("=");
1285
+ if (eqIdx === -1) return true;
1286
+ return trimmed.slice(0, eqIdx).trim() !== key;
1287
+ });
1288
+ writeFileSync(this.envPath, filtered.join("\n"), "utf-8");
1289
+ const descriptions = this.readDescriptions();
1290
+ if (key in descriptions) {
1291
+ delete descriptions[key];
1292
+ this.writeDescriptions(descriptions);
1293
+ }
1294
+ }
1295
+ // ─── Private ─────────────────────────────────────────────────────
1296
+ readDescriptions() {
1297
+ if (!existsSync4(this.descriptionsPath)) return {};
1298
+ try {
1299
+ return JSON.parse(readFileSync2(this.descriptionsPath, "utf-8"));
1300
+ } catch {
1301
+ return {};
1302
+ }
1303
+ }
1304
+ writeDescriptions(descriptions) {
1305
+ writeFileSync(
1306
+ this.descriptionsPath,
1307
+ JSON.stringify(descriptions, null, 2) + "\n",
1308
+ "utf-8"
1309
+ );
1310
+ }
1311
+ };
1312
+ function isPlaceholder(value) {
1313
+ return PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(value));
1314
+ }
1315
+
1316
+ // src/dev/firebase-status.ts
1317
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
1318
+ import { resolve as resolve4 } from "path";
1319
+ import { execFile } from "child_process";
1320
+ var cachedStatus = null;
1321
+ var cacheTime = 0;
1322
+ var CACHE_TTL = 3e4;
1323
+ async function checkFirebaseStatus(projectDir) {
1324
+ const now = Date.now();
1325
+ if (cachedStatus && now - cacheTime < CACHE_TTL) {
1326
+ return cachedStatus;
1327
+ }
1328
+ const [fileStatus, cliStatus] = await Promise.all([
1329
+ checkFiles(projectDir),
1330
+ checkCli(projectDir)
1331
+ ]);
1332
+ const status = {
1333
+ cli: cliStatus,
1334
+ project: fileStatus.project,
1335
+ services: fileStatus.services,
1336
+ configWarnings: fileStatus.configWarnings,
1337
+ timestamp: now
1338
+ };
1339
+ cachedStatus = status;
1340
+ cacheTime = now;
1341
+ return status;
1342
+ }
1343
+ function clearFirebaseStatusCache() {
1344
+ cachedStatus = null;
1345
+ cacheTime = 0;
1346
+ }
1347
+ function checkFiles(projectDir) {
1348
+ const services = [];
1349
+ const configWarnings = [];
1350
+ const firebaseJsonPath = resolve4(projectDir, "firebase.json");
1351
+ let hasFirebaseJson = false;
1352
+ let firebaseConfig = {};
1353
+ if (existsSync5(firebaseJsonPath)) {
1354
+ hasFirebaseJson = true;
1355
+ try {
1356
+ firebaseConfig = JSON.parse(readFileSync3(firebaseJsonPath, "utf-8"));
1357
+ } catch {
1358
+ }
1359
+ }
1360
+ if (firebaseConfig.hosting) {
1361
+ services.push({ name: "Hosting", status: "configured", detail: "firebase.json" });
1362
+ } else {
1363
+ services.push({ name: "Hosting", status: "missing" });
1364
+ }
1365
+ const firestoreRulesPath = resolve4(projectDir, "firestore.rules");
1366
+ if (firebaseConfig.firestore) {
1367
+ if (existsSync5(firestoreRulesPath)) {
1368
+ services.push({ name: "Firestore", status: "configured", detail: "Rules file found" });
1369
+ } else {
1370
+ services.push({ name: "Firestore", status: "placeholder", detail: "Configured but no rules file" });
1371
+ }
1372
+ } else {
1373
+ services.push({ name: "Firestore", status: "missing" });
1374
+ }
1375
+ const functionsIndexPath = resolve4(projectDir, "functions/index.ts");
1376
+ if (firebaseConfig.functions) {
1377
+ if (existsSync5(functionsIndexPath)) {
1378
+ services.push({ name: "Functions", status: "configured", detail: "functions/index.ts found" });
1379
+ } else {
1380
+ services.push({ name: "Functions", status: "placeholder", detail: "Configured but no entry file" });
1381
+ }
1382
+ } else {
1383
+ services.push({ name: "Functions", status: "missing" });
1384
+ }
1385
+ const storageRulesPath = resolve4(projectDir, "storage.rules");
1386
+ if (firebaseConfig.storage) {
1387
+ if (existsSync5(storageRulesPath)) {
1388
+ services.push({ name: "Storage", status: "configured", detail: "Rules file found" });
1389
+ } else {
1390
+ services.push({ name: "Storage", status: "placeholder", detail: "Configured but no rules file" });
1391
+ }
1392
+ } else {
1393
+ services.push({ name: "Storage", status: "missing" });
1394
+ }
1395
+ const indexesPath = resolve4(projectDir, "firestore.indexes.json");
1396
+ if (existsSync5(indexesPath)) {
1397
+ services.push({ name: "Indexes", status: "configured", detail: "firestore.indexes.json" });
1398
+ } else if (firebaseConfig.firestore) {
1399
+ services.push({ name: "Indexes", status: "placeholder", detail: "No indexes file" });
1400
+ }
1401
+ const configPath = resolve4(projectDir, "clawfire.config.ts");
1402
+ if (existsSync5(configPath)) {
1403
+ try {
1404
+ const configContent = readFileSync3(configPath, "utf-8");
1405
+ const placeholderMatches = configContent.match(/YOUR_[A-Z_]+/g);
1406
+ if (placeholderMatches) {
1407
+ for (const match of new Set(placeholderMatches)) {
1408
+ configWarnings.push(`Placeholder found: ${match}`);
1409
+ }
1410
+ }
1411
+ } catch {
1412
+ }
1413
+ }
1414
+ let projectId = "";
1415
+ const firebasercPath = resolve4(projectDir, ".firebaserc");
1416
+ if (existsSync5(firebasercPath)) {
1417
+ try {
1418
+ const rc = JSON.parse(readFileSync3(firebasercPath, "utf-8"));
1419
+ projectId = rc?.projects?.default || "";
1420
+ } catch {
1421
+ }
1422
+ }
1423
+ return {
1424
+ project: { id: projectId, hasFirebaseJson },
1425
+ services,
1426
+ configWarnings
1427
+ };
1428
+ }
1429
+ async function checkCli(projectDir) {
1430
+ const result = {
1431
+ installed: false,
1432
+ version: "",
1433
+ authenticated: false,
1434
+ user: ""
1435
+ };
1436
+ try {
1437
+ const version = await execWithTimeout("firebase", ["--version"], projectDir, 5e3);
1438
+ result.installed = true;
1439
+ result.version = version.trim();
1440
+ } catch {
1441
+ return result;
1442
+ }
1443
+ try {
1444
+ const loginOutput = await execWithTimeout(
1445
+ "firebase",
1446
+ ["login:list", "--json"],
1447
+ projectDir,
1448
+ 5e3
1449
+ );
1450
+ const loginData = JSON.parse(loginOutput);
1451
+ if (loginData?.result && Array.isArray(loginData.result) && loginData.result.length > 0) {
1452
+ result.authenticated = true;
1453
+ result.user = loginData.result[0]?.user?.email || loginData.result[0]?.email || "";
1454
+ }
1455
+ } catch {
1456
+ }
1457
+ return result;
1458
+ }
1459
+ function execWithTimeout(command, args, cwd, timeoutMs) {
1460
+ return new Promise((resolve6, reject) => {
1461
+ const proc = execFile(command, args, { cwd, timeout: timeoutMs }, (err, stdout) => {
1462
+ if (err) {
1463
+ reject(err);
1464
+ } else {
1465
+ resolve6(stdout);
1466
+ }
1467
+ });
1468
+ const timer = setTimeout(() => {
1469
+ proc.kill("SIGTERM");
1470
+ reject(new Error("Timeout"));
1471
+ }, timeoutMs + 500);
1472
+ proc.on("exit", () => clearTimeout(timer));
1473
+ });
1474
+ }
1475
+
1476
+ // src/dev/dashboard-html.ts
1477
+ function generateDashboardHtml(options) {
1478
+ const { apiPort } = options;
1479
+ return `
1480
+ <div id="dashboard-content" style="padding:24px;max-width:1200px;margin:0 auto;font-family:var(--font, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);">
1481
+
1482
+ <!-- Loading State -->
1483
+ <div id="dash-loading" style="text-align:center;padding:60px 0;color:#a3a3a3;">
1484
+ <div style="font-size:24px;margin-bottom:8px;">Loading dashboard...</div>
1485
+ </div>
1486
+
1487
+ <!-- Dashboard Sections (hidden until loaded) -->
1488
+ <div id="dash-loaded" style="display:none;">
1489
+
1490
+ <!-- Section 1: Firebase Status -->
1491
+ <div style="margin-bottom:32px;">
1492
+ <h2 style="font-size:18px;font-weight:700;color:#f97316;margin-bottom:16px;">Firebase Status</h2>
1493
+
1494
+ <!-- CLI Banner -->
1495
+ <div id="cli-banner" style="padding:12px 16px;border-radius:8px;border:1px solid #2a2a2a;background:#141414;margin-bottom:16px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
1496
+ <span id="cli-dot" style="width:10px;height:10px;border-radius:50%;background:#666;display:inline-block;flex-shrink:0;"></span>
1497
+ <span id="cli-text" style="color:#e5e5e5;font-size:14px;">Checking CLI...</span>
1498
+ <span id="cli-project" style="color:#a3a3a3;font-size:12px;margin-left:auto;font-family:monospace;"></span>
1499
+ </div>
1500
+
1501
+ <!-- Service Cards Grid -->
1502
+ <div id="service-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px;"></div>
1503
+ </div>
1504
+
1505
+ <!-- Section 2: Config Overview -->
1506
+ <div style="margin-bottom:32px;">
1507
+ <h2 style="font-size:18px;font-weight:700;color:#f97316;margin-bottom:16px;">Config Overview</h2>
1508
+ <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>
1510
+ </div>
1511
+ </div>
1512
+
1513
+ <!-- Section 3: Environment Variables -->
1514
+ <div style="margin-bottom:32px;">
1515
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
1516
+ <h2 style="font-size:18px;font-weight:700;color:#f97316;">Environment Variables</h2>
1517
+ <button id="env-add-btn" onclick="showEnvModal()" style="padding:6px 14px;background:#f97316;color:#000;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;">+ Add Variable</button>
1518
+ </div>
1519
+ <div id="env-table-wrap" style="border-radius:8px;border:1px solid #2a2a2a;background:#141414;overflow:hidden;">
1520
+ <table id="env-table" style="width:100%;border-collapse:collapse;font-size:13px;">
1521
+ <thead>
1522
+ <tr style="border-bottom:1px solid #2a2a2a;">
1523
+ <th style="padding:10px 16px;text-align:left;color:#a3a3a3;font-weight:500;">Key</th>
1524
+ <th style="padding:10px 16px;text-align:left;color:#a3a3a3;font-weight:500;">Value</th>
1525
+ <th style="padding:10px 16px;text-align:left;color:#a3a3a3;font-weight:500;">Description</th>
1526
+ <th style="padding:10px 16px;text-align:right;color:#a3a3a3;font-weight:500;width:120px;">Actions</th>
1527
+ </tr>
1528
+ </thead>
1529
+ <tbody id="env-tbody"></tbody>
1530
+ </table>
1531
+ <div id="env-empty" style="display:none;padding:32px;text-align:center;color:#666;">
1532
+ No environment variables found. Click "+ Add Variable" to create one.
1533
+ </div>
1534
+ </div>
1535
+ </div>
1536
+ </div>
1537
+
1538
+ <!-- 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;">
1540
+ <div style="background:#1e1e1e;border:1px solid #2a2a2a;border-radius:12px;padding:24px;width:440px;max-width:90vw;">
1541
+ <h3 id="modal-title" style="font-size:16px;font-weight:700;color:#e5e5e5;margin-bottom:16px;">Add Variable</h3>
1542
+ <div style="margin-bottom:12px;">
1543
+ <label style="display:block;font-size:12px;color:#a3a3a3;margin-bottom:4px;">Key</label>
1544
+ <input id="modal-key" type="text" placeholder="API_KEY" style="width:100%;padding:8px 12px;background:#0a0a0a;border:1px solid #2a2a2a;border-radius:6px;color:#e5e5e5;font-family:monospace;font-size:14px;outline:none;" />
1545
+ </div>
1546
+ <div style="margin-bottom:12px;">
1547
+ <label style="display:block;font-size:12px;color:#a3a3a3;margin-bottom:4px;">Value</label>
1548
+ <input id="modal-value" type="text" placeholder="your-value-here" style="width:100%;padding:8px 12px;background:#0a0a0a;border:1px solid #2a2a2a;border-radius:6px;color:#e5e5e5;font-family:monospace;font-size:14px;outline:none;" />
1549
+ </div>
1550
+ <div style="margin-bottom:20px;">
1551
+ <label style="display:block;font-size:12px;color:#a3a3a3;margin-bottom:4px;">Description (optional)</label>
1552
+ <input id="modal-desc" type="text" placeholder="What this variable is for" style="width:100%;padding:8px 12px;background:#0a0a0a;border:1px solid #2a2a2a;border-radius:6px;color:#e5e5e5;font-size:14px;outline:none;" />
1553
+ </div>
1554
+ <div id="modal-error" style="display:none;padding:8px 12px;background:#1c0808;border:1px solid #ef4444;border-radius:6px;color:#ef4444;font-size:12px;margin-bottom:12px;"></div>
1555
+ <div style="display:flex;gap:8px;justify-content:flex-end;">
1556
+ <button onclick="hideEnvModal()" style="padding:8px 16px;background:transparent;border:1px solid #2a2a2a;border-radius:6px;color:#a3a3a3;cursor:pointer;font-size:13px;">Cancel</button>
1557
+ <button id="modal-save-btn" onclick="saveEnvVar()" style="padding:8px 16px;background:#f97316;border:none;border-radius:6px;color:#000;font-weight:600;cursor:pointer;font-size:13px;">Save</button>
1558
+ </div>
1559
+ </div>
1560
+ </div>
1561
+ </div>
1562
+
1563
+ <script>
1564
+ (function() {
1565
+ var API = 'http://localhost:${apiPort}';
1566
+ var envData = [];
1567
+ var editingKey = null;
1568
+
1569
+ // \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
1570
+ window._loadDashboard = function() {
1571
+ if (window._dashboardLoaded) return;
1572
+ window._dashboardLoaded = true;
1573
+ Promise.all([
1574
+ fetch(API + '/__dev/firebase-status').then(function(r) { return r.json(); }),
1575
+ fetch(API + '/__dev/config').then(function(r) { return r.json(); }),
1576
+ fetch(API + '/__dev/env').then(function(r) { return r.json(); }),
1577
+ ]).then(function(results) {
1578
+ renderFirebaseStatus(results[0]);
1579
+ renderConfig(results[1]);
1580
+ renderEnvVars(results[2]);
1581
+ document.getElementById('dash-loading').style.display = 'none';
1582
+ document.getElementById('dash-loaded').style.display = 'block';
1583
+ }).catch(function(err) {
1584
+ document.getElementById('dash-loading').innerHTML =
1585
+ '<div style="color:#ef4444;">Failed to load dashboard data</div>' +
1586
+ '<div style="color:#666;font-size:13px;margin-top:8px;">' + err.message + '</div>';
1587
+ });
1588
+ };
1589
+
1590
+ // \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
+ function renderFirebaseStatus(data) {
1592
+ // CLI Banner
1593
+ var dot = document.getElementById('cli-dot');
1594
+ var text = document.getElementById('cli-text');
1595
+ var proj = document.getElementById('cli-project');
1596
+
1597
+ if (!data.cli.installed) {
1598
+ dot.style.background = '#ef4444';
1599
+ text.textContent = 'Firebase CLI not installed';
1600
+ } else if (!data.cli.authenticated) {
1601
+ dot.style.background = '#eab308';
1602
+ text.textContent = 'Firebase CLI v' + data.cli.version + ' \u2014 Not logged in';
1603
+ } else {
1604
+ dot.style.background = '#22c55e';
1605
+ text.textContent = 'Firebase CLI v' + data.cli.version + ' \u2014 ' + data.cli.user;
1606
+ }
1607
+
1608
+ if (data.project.id) {
1609
+ proj.textContent = 'Project: ' + data.project.id;
1610
+ } else {
1611
+ proj.textContent = 'No active project';
1612
+ proj.style.color = '#eab308';
1613
+ }
1614
+
1615
+ // Service Cards
1616
+ var grid = document.getElementById('service-grid');
1617
+ grid.innerHTML = '';
1618
+ var statusColors = { configured: '#22c55e', placeholder: '#eab308', missing: '#666' };
1619
+ var statusLabels = { configured: 'Ready', placeholder: 'Needs Setup', missing: 'Not Configured' };
1620
+
1621
+ data.services.forEach(function(svc) {
1622
+ var card = document.createElement('div');
1623
+ card.style.cssText = 'padding:16px;border-radius:8px;border:1px solid #2a2a2a;background:#141414;';
1624
+ card.innerHTML =
1625
+ '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">' +
1626
+ '<span style="width:8px;height:8px;border-radius:50%;background:' + statusColors[svc.status] + ';display:inline-block;"></span>' +
1627
+ '<span style="font-weight:600;color:#e5e5e5;">' + svc.name + '</span>' +
1628
+ '</div>' +
1629
+ '<div style="font-size:12px;color:' + statusColors[svc.status] + ';">' + statusLabels[svc.status] + '</div>' +
1630
+ (svc.detail ? '<div style="font-size:11px;color:#666;margin-top:4px;">' + svc.detail + '</div>' : '');
1631
+ grid.appendChild(card);
1632
+ });
1633
+
1634
+ // Config Warnings
1635
+ if (data.configWarnings && data.configWarnings.length > 0) {
1636
+ var warningCard = document.createElement('div');
1637
+ warningCard.style.cssText = 'padding:16px;border-radius:8px;border:1px solid #eab308;background:#1a1a0a;grid-column:1/-1;';
1638
+ warningCard.innerHTML =
1639
+ '<div style="font-weight:600;color:#eab308;margin-bottom:6px;">Config Warnings</div>' +
1640
+ data.configWarnings.map(function(w) {
1641
+ return '<div style="font-size:12px;color:#eab308;font-family:monospace;">' + escHtml(w) + '</div>';
1642
+ }).join('');
1643
+ grid.appendChild(warningCard);
1644
+ }
1645
+ }
1646
+
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
1648
+ 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>';
1652
+ return;
1653
+ }
1654
+ var html = '';
1655
+ data.fields.forEach(function(field) {
1656
+ var color = field.isPlaceholder ? '#eab308' : '#a3a3a3';
1657
+ 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>';
1663
+ });
1664
+ el.innerHTML = html;
1665
+ }
1666
+
1667
+ // \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
+ function renderEnvVars(data) {
1669
+ envData = data.variables || [];
1670
+ var tbody = document.getElementById('env-tbody');
1671
+ var empty = document.getElementById('env-empty');
1672
+ var table = document.getElementById('env-table');
1673
+
1674
+ if (envData.length === 0) {
1675
+ table.style.display = 'none';
1676
+ empty.style.display = 'block';
1677
+ return;
1678
+ }
1679
+
1680
+ table.style.display = 'table';
1681
+ empty.style.display = 'none';
1682
+ tbody.innerHTML = '';
1683
+
1684
+ envData.forEach(function(v) {
1685
+ var tr = document.createElement('tr');
1686
+ tr.style.borderBottom = '1px solid #2a2a2a';
1687
+ var maskedVal = v.value ? v.value.slice(0, 3) + '...' + v.value.slice(-2) : '';
1688
+ if (v.value.length <= 5) maskedVal = '***';
1689
+ var placeholderBadge = v.isPlaceholder ? ' <span style="background:#eab30822;color:#eab308;padding:1px 6px;border-radius:4px;font-size:10px;">PLACEHOLDER</span>' : '';
1690
+ tr.innerHTML =
1691
+ '<td style="padding:10px 16px;color:#e5e5e5;font-family:monospace;white-space:nowrap;">' + escHtml(v.key) + placeholderBadge + '</td>' +
1692
+ '<td style="padding:10px 16px;color:#a3a3a3;font-family:monospace;">' +
1693
+ '<span class="env-val" data-key="' + escAttr(v.key) + '" data-masked="' + escAttr(maskedVal) + '" data-full="' + escAttr(v.value) + '">' + escHtml(maskedVal) + '</span>' +
1694
+ ' <button onclick="toggleReveal(this)" style="background:none;border:none;color:#666;cursor:pointer;font-size:11px;padding:2px 4px;">reveal</button>' +
1695
+ '</td>' +
1696
+ '<td style="padding:10px 16px;color:#666;font-size:12px;">' + escHtml(v.description || '') + '</td>' +
1697
+ '<td style="padding:10px 16px;text-align:right;white-space:nowrap;">' +
1698
+ '<button onclick="editEnvVar(\\'' + escAttr(v.key) + '\\')" style="background:none;border:none;color:#3b82f6;cursor:pointer;font-size:12px;padding:4px 8px;">Edit</button>' +
1699
+ '<button onclick="deleteEnvVar(\\'' + escAttr(v.key) + '\\')" style="background:none;border:none;color:#ef4444;cursor:pointer;font-size:12px;padding:4px 8px;">Delete</button>' +
1700
+ '</td>';
1701
+ tbody.appendChild(tr);
1702
+ });
1703
+ }
1704
+
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
1706
+ window.showEnvModal = function(key) {
1707
+ editingKey = key || null;
1708
+ var modal = document.getElementById('env-modal');
1709
+ var title = document.getElementById('modal-title');
1710
+ var keyInput = document.getElementById('modal-key');
1711
+ var valInput = document.getElementById('modal-value');
1712
+ var descInput = document.getElementById('modal-desc');
1713
+ var errorEl = document.getElementById('modal-error');
1714
+ errorEl.style.display = 'none';
1715
+
1716
+ if (editingKey) {
1717
+ title.textContent = 'Edit Variable';
1718
+ keyInput.value = editingKey;
1719
+ keyInput.readOnly = true;
1720
+ keyInput.style.opacity = '0.5';
1721
+ var existing = envData.find(function(v) { return v.key === editingKey; });
1722
+ valInput.value = existing ? existing.value : '';
1723
+ descInput.value = existing ? existing.description : '';
1724
+ } else {
1725
+ title.textContent = 'Add Variable';
1726
+ keyInput.value = '';
1727
+ keyInput.readOnly = false;
1728
+ keyInput.style.opacity = '1';
1729
+ valInput.value = '';
1730
+ descInput.value = '';
1731
+ }
1732
+
1733
+ modal.style.display = 'flex';
1734
+ (editingKey ? valInput : keyInput).focus();
1735
+ };
1736
+
1737
+ window.hideEnvModal = function() {
1738
+ document.getElementById('env-modal').style.display = 'none';
1739
+ editingKey = null;
1740
+ };
1741
+
1742
+ window.editEnvVar = function(key) {
1743
+ showEnvModal(key);
1744
+ };
1745
+
1746
+ window.saveEnvVar = function() {
1747
+ var key = document.getElementById('modal-key').value.trim();
1748
+ var value = document.getElementById('modal-value').value;
1749
+ var desc = document.getElementById('modal-desc').value.trim();
1750
+ var errorEl = document.getElementById('modal-error');
1751
+
1752
+ if (!key) {
1753
+ errorEl.textContent = 'Key is required';
1754
+ errorEl.style.display = 'block';
1755
+ return;
1756
+ }
1757
+ if (!/^[A-Z_][A-Z0-9_]*$/i.test(key)) {
1758
+ errorEl.textContent = 'Invalid key format. Use UPPER_SNAKE_CASE.';
1759
+ errorEl.style.display = 'block';
1760
+ return;
1761
+ }
1762
+
1763
+ fetch(API + '/__dev/env', {
1764
+ method: 'POST',
1765
+ headers: { 'Content-Type': 'application/json' },
1766
+ body: JSON.stringify({ action: 'set', key: key, value: value, description: desc || undefined })
1767
+ }).then(function(r) { return r.json(); })
1768
+ .then(function(data) {
1769
+ if (data.error) {
1770
+ errorEl.textContent = data.error;
1771
+ errorEl.style.display = 'block';
1772
+ return;
1773
+ }
1774
+ hideEnvModal();
1775
+ return fetch(API + '/__dev/env').then(function(r) { return r.json(); });
1776
+ })
1777
+ .then(function(data) { if (data) renderEnvVars(data); })
1778
+ .catch(function(err) {
1779
+ errorEl.textContent = err.message;
1780
+ errorEl.style.display = 'block';
1781
+ });
1782
+ };
1783
+
1784
+ window.deleteEnvVar = function(key) {
1785
+ if (!confirm('Delete ' + key + '?')) return;
1786
+ fetch(API + '/__dev/env', {
1787
+ method: 'POST',
1788
+ headers: { 'Content-Type': 'application/json' },
1789
+ body: JSON.stringify({ action: 'delete', key: key })
1790
+ }).then(function() {
1791
+ return fetch(API + '/__dev/env').then(function(r) { return r.json(); });
1792
+ }).then(function(data) { renderEnvVars(data); })
1793
+ .catch(function() {});
1794
+ };
1795
+
1796
+ window.toggleReveal = function(btn) {
1797
+ var span = btn.previousElementSibling;
1798
+ var masked = span.getAttribute('data-masked');
1799
+ var full = span.getAttribute('data-full');
1800
+ if (span.textContent === masked) {
1801
+ span.textContent = full;
1802
+ btn.textContent = 'hide';
1803
+ } else {
1804
+ span.textContent = masked;
1805
+ btn.textContent = 'reveal';
1806
+ }
1807
+ };
1808
+
1809
+ // \u2500\u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1810
+ function escHtml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
1811
+ function escAttr(s) { return String(s).replace(/&/g,'&amp;').replace(/'/g,"\\\\'").replace(/"/g,'&quot;').replace(/</g,'&lt;'); }
1812
+ })();
1813
+ </script>`;
1814
+ }
1815
+
1195
1816
  // src/dev/dev-server.ts
1196
1817
  var MIME_TYPES = {
1197
1818
  html: "text/html; charset=utf-8",
@@ -1436,6 +2057,7 @@ var DevServer = class {
1436
2057
  importCounter = 0;
1437
2058
  isReloading = false;
1438
2059
  pageCompiler;
2060
+ envManager;
1439
2061
  constructor(options = {}) {
1440
2062
  this.options = {
1441
2063
  projectDir: options.projectDir || process.cwd(),
@@ -1447,12 +2069,13 @@ var DevServer = class {
1447
2069
  onSetupRoutes: options.onSetupRoutes || (() => {
1448
2070
  })
1449
2071
  };
1450
- this.routesDir = resolve3(this.options.projectDir, "app/routes");
1451
- this.schemasDir = resolve3(this.options.projectDir, "app/schemas");
1452
- this.publicDir = resolve3(this.options.projectDir, "public");
1453
- this.pagesDir = resolve3(this.options.projectDir, "app/pages");
1454
- this.componentsDir = resolve3(this.options.projectDir, "app/components");
2072
+ this.routesDir = resolve5(this.options.projectDir, "app/routes");
2073
+ this.schemasDir = resolve5(this.options.projectDir, "app/schemas");
2074
+ this.publicDir = resolve5(this.options.projectDir, "public");
2075
+ this.pagesDir = resolve5(this.options.projectDir, "app/pages");
2076
+ this.componentsDir = resolve5(this.options.projectDir, "app/components");
1455
2077
  this.pageCompiler = new PageCompiler(this.options.projectDir);
2078
+ this.envManager = new EnvManager(this.options.projectDir);
1456
2079
  this.router = createRouter({
1457
2080
  cors: ["*"],
1458
2081
  rateLimit: 0,
@@ -1468,12 +2091,12 @@ var DevServer = class {
1468
2091
  if (this.options.hotReload) {
1469
2092
  this.startWatcher();
1470
2093
  }
1471
- await new Promise((resolve4, reject) => {
1472
- this.apiServer.listen(this.options.apiPort, () => resolve4());
2094
+ await new Promise((resolve6, reject) => {
2095
+ this.apiServer.listen(this.options.apiPort, () => resolve6());
1473
2096
  this.apiServer.on("error", reject);
1474
2097
  });
1475
- await new Promise((resolve4, reject) => {
1476
- this.frontendServer.listen(this.options.port, () => resolve4());
2098
+ await new Promise((resolve6, reject) => {
2099
+ this.frontendServer.listen(this.options.port, () => resolve6());
1477
2100
  this.frontendServer.on("error", reject);
1478
2101
  });
1479
2102
  this.printStartupBanner();
@@ -1487,13 +2110,13 @@ var DevServer = class {
1487
2110
  this.apiSseClients = [];
1488
2111
  this.router.destroy();
1489
2112
  if (this.apiServer) {
1490
- await new Promise((resolve4) => {
1491
- this.apiServer.close(() => resolve4());
2113
+ await new Promise((resolve6) => {
2114
+ this.apiServer.close(() => resolve6());
1492
2115
  });
1493
2116
  }
1494
2117
  if (this.frontendServer) {
1495
- await new Promise((resolve4) => {
1496
- this.frontendServer.close(() => resolve4());
2118
+ await new Promise((resolve6) => {
2119
+ this.frontendServer.close(() => resolve6());
1497
2120
  });
1498
2121
  }
1499
2122
  }
@@ -1509,11 +2132,11 @@ var DevServer = class {
1509
2132
  await this.options.onSetupRoutes(this.router);
1510
2133
  if (this.router.getRoutes().length > 0) return;
1511
2134
  }
1512
- if (!existsSync4(this.routesDir)) return;
2135
+ if (!existsSync6(this.routesDir)) return;
1513
2136
  const discovered = discoverRoutes(this.routesDir);
1514
2137
  for (const route of discovered) {
1515
2138
  try {
1516
- const fullPath = resolve3(this.routesDir, route.filePath);
2139
+ const fullPath = resolve5(this.routesDir, route.filePath);
1517
2140
  const fileUrl = pathToFileURL(fullPath).href;
1518
2141
  const mod = await import(`${fileUrl}?v=${++this.importCounter}`);
1519
2142
  const contract = mod.default;
@@ -1536,6 +2159,7 @@ var DevServer = class {
1536
2159
  try {
1537
2160
  await this.loadRoutes();
1538
2161
  this.regeneratePlayground();
2162
+ clearFirebaseStatusCache();
1539
2163
  const routeCount = this.router.getRoutes().length;
1540
2164
  console.log(` \x1B[32m\u2713\x1B[0m ${routeCount} routes loaded`);
1541
2165
  this.broadcastSSE(this.apiSseClients, {
@@ -1588,23 +2212,23 @@ var DevServer = class {
1588
2212
  // ─── File Watcher ──────────────────────────────────────────────────
1589
2213
  startWatcher() {
1590
2214
  this.watcher = new FileWatcher(this.options.debounceMs);
1591
- if (existsSync4(this.routesDir)) {
2215
+ if (existsSync6(this.routesDir)) {
1592
2216
  this.watcher.watchDir(this.routesDir, "route-change");
1593
2217
  }
1594
- if (existsSync4(this.schemasDir)) {
2218
+ if (existsSync6(this.schemasDir)) {
1595
2219
  this.watcher.watchDir(this.schemasDir, "schema-change");
1596
2220
  }
1597
- const configFile = resolve3(this.options.projectDir, "clawfire.config.ts");
1598
- if (existsSync4(configFile)) {
2221
+ const configFile = resolve5(this.options.projectDir, "clawfire.config.ts");
2222
+ if (existsSync6(configFile)) {
1599
2223
  this.watcher.watchFile(configFile, "config-change");
1600
2224
  }
1601
- if (existsSync4(this.publicDir)) {
2225
+ if (existsSync6(this.publicDir)) {
1602
2226
  this.watcher.watchDirFrontend(this.publicDir);
1603
2227
  }
1604
- if (existsSync4(this.pagesDir)) {
2228
+ if (existsSync6(this.pagesDir)) {
1605
2229
  this.watcher.watchDir(this.pagesDir, "page-change");
1606
2230
  }
1607
- if (existsSync4(this.componentsDir)) {
2231
+ if (existsSync6(this.componentsDir)) {
1608
2232
  this.watcher.watchDir(this.componentsDir, "component-change");
1609
2233
  }
1610
2234
  this.watcher.on("route-change", (event) => this.reloadRoutes(event));
@@ -1651,6 +2275,41 @@ var DevServer = class {
1651
2275
  title: "Clawfire Dev Playground",
1652
2276
  apiBaseUrl: `http://localhost:${this.options.apiPort}`
1653
2277
  });
2278
+ const dashboardHtml = generateDashboardHtml({
2279
+ apiPort: this.options.apiPort
2280
+ });
2281
+ const tabBar = `
2282
+ <div id="clawfire-tab-bar" style="position:sticky;top:0;z-index:9999;background:#0a0a0a;border-bottom:1px solid #2a2a2a;padding:0 16px;display:flex;gap:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;">
2283
+ <button onclick="switchTab('apis')" id="tab-btn-apis" style="padding:10px 20px;background:transparent;border:none;border-bottom:2px solid #f97316;color:#f97316;font-size:14px;font-weight:600;cursor:pointer;">APIs</button>
2284
+ <button onclick="switchTab('dashboard')" id="tab-btn-dashboard" style="padding:10px 20px;background:transparent;border:none;border-bottom:2px solid transparent;color:#a3a3a3;font-size:14px;font-weight:600;cursor:pointer;">Dashboard</button>
2285
+ </div>`;
2286
+ const tabScript = `
2287
+ <script>
2288
+ function switchTab(tab) {
2289
+ var apis = document.getElementById('tab-apis');
2290
+ var dashboard = document.getElementById('tab-dashboard');
2291
+ var btnApis = document.getElementById('tab-btn-apis');
2292
+ var btnDash = document.getElementById('tab-btn-dashboard');
2293
+
2294
+ if (tab === 'apis') {
2295
+ apis.style.display = 'block';
2296
+ dashboard.style.display = 'none';
2297
+ btnApis.style.borderBottomColor = '#f97316';
2298
+ btnApis.style.color = '#f97316';
2299
+ btnDash.style.borderBottomColor = 'transparent';
2300
+ btnDash.style.color = '#a3a3a3';
2301
+ } else {
2302
+ apis.style.display = 'none';
2303
+ dashboard.style.display = 'block';
2304
+ btnApis.style.borderBottomColor = 'transparent';
2305
+ btnApis.style.color = '#a3a3a3';
2306
+ btnDash.style.borderBottomColor = '#f97316';
2307
+ btnDash.style.color = '#f97316';
2308
+ // Lazy-load dashboard data on first click
2309
+ if (window._loadDashboard) window._loadDashboard();
2310
+ }
2311
+ }
2312
+ </script>`;
1654
2313
  const liveReloadScript = `
1655
2314
  <script>
1656
2315
  (function() {
@@ -1692,7 +2351,28 @@ var DevServer = class {
1692
2351
  connect();
1693
2352
  })();
1694
2353
  </script>`;
1695
- this.playgroundHtml = baseHtml.replace("</body>", liveReloadScript + "\n</body>");
2354
+ let html = baseHtml;
2355
+ html = html.replace(/<body[^>]*>/, (match) => `${match}
2356
+ ${tabBar}`);
2357
+ const bodyOpenMatch = html.match(/<body[^>]*>/);
2358
+ if (bodyOpenMatch) {
2359
+ const bodyOpenEnd = html.indexOf(bodyOpenMatch[0]) + bodyOpenMatch[0].length;
2360
+ const tabBarEnd = html.indexOf("</div>", bodyOpenEnd) + "</div>".length;
2361
+ const bodyClose = html.lastIndexOf("</body>");
2362
+ const beforeTabBar = html.slice(0, tabBarEnd);
2363
+ const playgroundContent = html.slice(tabBarEnd, bodyClose);
2364
+ const afterBody = html.slice(bodyClose);
2365
+ html = beforeTabBar + `
2366
+ <div id="tab-apis">${playgroundContent}</div>
2367
+ <div id="tab-dashboard" style="display:none;">${dashboardHtml}</div>
2368
+ ${tabScript}
2369
+ ${liveReloadScript}
2370
+ ` + afterBody;
2371
+ } else {
2372
+ html = html.replace("</body>", `${liveReloadScript}
2373
+ </body>`);
2374
+ }
2375
+ this.playgroundHtml = html;
1696
2376
  }
1697
2377
  // ─── Script Injection ──────────────────────────────────────────────
1698
2378
  /**
@@ -1709,9 +2389,9 @@ var DevServer = class {
1709
2389
  }
1710
2390
  // ─── Static File Serving ──────────────────────────────────────────
1711
2391
  serveStaticFile(filePath, res) {
1712
- if (!existsSync4(filePath)) return false;
2392
+ if (!existsSync6(filePath)) return false;
1713
2393
  try {
1714
- const content = readFileSync2(filePath);
2394
+ const content = readFileSync4(filePath);
1715
2395
  const ext = extname3(filePath).slice(1).toLowerCase();
1716
2396
  const mime = MIME_TYPES[ext] || "application/octet-stream";
1717
2397
  if (ext === "html") {
@@ -1769,7 +2449,7 @@ var DevServer = class {
1769
2449
  }
1770
2450
  const ext = extname3(url.pathname);
1771
2451
  if (ext && STATIC_EXTENSIONS.has(ext)) {
1772
- const filePath2 = resolve3(this.publicDir, url.pathname.slice(1));
2452
+ const filePath2 = resolve5(this.publicDir, url.pathname.slice(1));
1773
2453
  if (filePath2.startsWith(this.publicDir) && this.serveStaticFile(filePath2, res)) {
1774
2454
  return;
1775
2455
  }
@@ -1820,7 +2500,7 @@ var DevServer = class {
1820
2500
  }
1821
2501
  }
1822
2502
  const requestedPath = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
1823
- const filePath = resolve3(this.publicDir, requestedPath);
2503
+ const filePath = resolve5(this.publicDir, requestedPath);
1824
2504
  if (!filePath.startsWith(this.publicDir)) {
1825
2505
  res.writeHead(403);
1826
2506
  res.end("Forbidden");
@@ -1829,8 +2509,8 @@ var DevServer = class {
1829
2509
  if (this.serveStaticFile(filePath, res)) {
1830
2510
  return;
1831
2511
  }
1832
- const indexPath = resolve3(this.publicDir, "index.html");
1833
- if (existsSync4(indexPath)) {
2512
+ const indexPath = resolve5(this.publicDir, "index.html");
2513
+ if (existsSync6(indexPath)) {
1834
2514
  this.serveStaticFile(indexPath, res);
1835
2515
  return;
1836
2516
  }
@@ -1906,6 +2586,10 @@ var DevServer = class {
1906
2586
  });
1907
2587
  return;
1908
2588
  }
2589
+ if (url.pathname.startsWith("/__dev/")) {
2590
+ this.handleDevEndpoint(req, res, url);
2591
+ return;
2592
+ }
1909
2593
  res.writeHead(404);
1910
2594
  res.end("Not found");
1911
2595
  }
@@ -1941,6 +2625,83 @@ var DevServer = class {
1941
2625
  \x1B[2mPress Ctrl+C to stop\x1B[0m
1942
2626
  `);
1943
2627
  }
2628
+ // ─── Dev Dashboard Endpoints ──────────────────────────────────────
2629
+ handleDevEndpoint(req, res, url) {
2630
+ res.setHeader("Access-Control-Allow-Origin", "*");
2631
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
2632
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
2633
+ if (req.method === "OPTIONS") {
2634
+ res.writeHead(204);
2635
+ res.end();
2636
+ return;
2637
+ }
2638
+ const sendJson = (data, status = 200) => {
2639
+ res.writeHead(status, { "Content-Type": "application/json" });
2640
+ res.end(JSON.stringify(data));
2641
+ };
2642
+ if (url.pathname === "/__dev/firebase-status" && req.method === "GET") {
2643
+ checkFirebaseStatus(this.options.projectDir).then((status) => sendJson(status)).catch((err) => sendJson({ error: err.message }, 500));
2644
+ return;
2645
+ }
2646
+ if (url.pathname === "/__dev/config" && req.method === "GET") {
2647
+ sendJson(this.readProjectConfig());
2648
+ return;
2649
+ }
2650
+ if (url.pathname === "/__dev/env" && req.method === "GET") {
2651
+ try {
2652
+ sendJson(this.envManager.read());
2653
+ } catch (err) {
2654
+ sendJson({ error: err instanceof Error ? err.message : "Read failed" }, 500);
2655
+ }
2656
+ return;
2657
+ }
2658
+ if (url.pathname === "/__dev/env" && req.method === "POST") {
2659
+ let body = "";
2660
+ req.on("data", (chunk) => {
2661
+ body += chunk;
2662
+ });
2663
+ req.on("end", () => {
2664
+ try {
2665
+ const data = JSON.parse(body);
2666
+ if (data.action === "set") {
2667
+ this.envManager.set(data.key, data.value, data.description);
2668
+ sendJson({ ok: true });
2669
+ } else if (data.action === "delete") {
2670
+ this.envManager.delete(data.key);
2671
+ sendJson({ ok: true });
2672
+ } else {
2673
+ sendJson({ error: "Invalid action" }, 400);
2674
+ }
2675
+ } catch (err) {
2676
+ sendJson({ error: err instanceof Error ? err.message : "Failed" }, 400);
2677
+ }
2678
+ });
2679
+ return;
2680
+ }
2681
+ res.writeHead(404);
2682
+ res.end("Not found");
2683
+ }
2684
+ // ─── Config Reader ────────────────────────────────────────────────
2685
+ readProjectConfig() {
2686
+ const configPath = resolve5(this.options.projectDir, "clawfire.config.ts");
2687
+ const fields = [];
2688
+ if (!existsSync6(configPath)) {
2689
+ return { fields };
2690
+ }
2691
+ try {
2692
+ const content = readFileSync4(configPath, "utf-8");
2693
+ const kvPattern = /(\w+)\s*:\s*["'`]([^"'`]*)["'`]/g;
2694
+ let match;
2695
+ while ((match = kvPattern.exec(content)) !== null) {
2696
+ const key = match[1];
2697
+ const value = match[2];
2698
+ const isPlaceholder2 = /^YOUR_/i.test(value) || /^CHANGE_ME$/i.test(value) || /^TODO$/i.test(value);
2699
+ fields.push({ key, value, isPlaceholder: isPlaceholder2 });
2700
+ }
2701
+ } catch {
2702
+ }
2703
+ return { fields };
2704
+ }
1944
2705
  };
1945
2706
  async function startDevServer(options) {
1946
2707
  const server = new DevServer(options);