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/cli.js +1 -1
- package/dist/{dev-server-ZGXJARNY.js → dev-server-TGEKP4YE.js} +792 -31
- package/dist/dev.cjs +796 -35
- package/dist/dev.cjs.map +1 -1
- package/dist/dev.d.cts +3 -0
- package/dist/dev.d.ts +3 -0
- package/dist/dev.js +792 -31
- package/dist/dev.js.map +1 -1
- package/package.json +1 -1
- package/templates/starter/.vscode/settings.json +11 -0
- package/templates/starter/package.json +2 -1
- package/templates/starter/tailwind.config.js +27 -0
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
|
|
4
|
-
import { existsSync as
|
|
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
1811
|
+
function escAttr(s) { return String(s).replace(/&/g,'&').replace(/'/g,"\\\\'").replace(/"/g,'"').replace(/</g,'<'); }
|
|
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 =
|
|
1451
|
-
this.schemasDir =
|
|
1452
|
-
this.publicDir =
|
|
1453
|
-
this.pagesDir =
|
|
1454
|
-
this.componentsDir =
|
|
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((
|
|
1472
|
-
this.apiServer.listen(this.options.apiPort, () =>
|
|
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((
|
|
1476
|
-
this.frontendServer.listen(this.options.port, () =>
|
|
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((
|
|
1491
|
-
this.apiServer.close(() =>
|
|
2113
|
+
await new Promise((resolve6) => {
|
|
2114
|
+
this.apiServer.close(() => resolve6());
|
|
1492
2115
|
});
|
|
1493
2116
|
}
|
|
1494
2117
|
if (this.frontendServer) {
|
|
1495
|
-
await new Promise((
|
|
1496
|
-
this.frontendServer.close(() =>
|
|
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 (!
|
|
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 =
|
|
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 (
|
|
2215
|
+
if (existsSync6(this.routesDir)) {
|
|
1592
2216
|
this.watcher.watchDir(this.routesDir, "route-change");
|
|
1593
2217
|
}
|
|
1594
|
-
if (
|
|
2218
|
+
if (existsSync6(this.schemasDir)) {
|
|
1595
2219
|
this.watcher.watchDir(this.schemasDir, "schema-change");
|
|
1596
2220
|
}
|
|
1597
|
-
const configFile =
|
|
1598
|
-
if (
|
|
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 (
|
|
2225
|
+
if (existsSync6(this.publicDir)) {
|
|
1602
2226
|
this.watcher.watchDirFrontend(this.publicDir);
|
|
1603
2227
|
}
|
|
1604
|
-
if (
|
|
2228
|
+
if (existsSync6(this.pagesDir)) {
|
|
1605
2229
|
this.watcher.watchDir(this.pagesDir, "page-change");
|
|
1606
2230
|
}
|
|
1607
|
-
if (
|
|
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
|
-
|
|
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 (!
|
|
2392
|
+
if (!existsSync6(filePath)) return false;
|
|
1713
2393
|
try {
|
|
1714
|
-
const content =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1833
|
-
if (
|
|
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);
|