@groundctl/cli 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +184 -39
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -274,7 +274,7 @@ function generateProjectState(db, projectName) {
|
|
|
274
274
|
md += "\n";
|
|
275
275
|
}
|
|
276
276
|
if (decisions.length > 0) {
|
|
277
|
-
md += "##
|
|
277
|
+
md += "## Architecture log\n";
|
|
278
278
|
for (const d of decisions) {
|
|
279
279
|
md += `- ${d.session_id}: ${d.description}`;
|
|
280
280
|
if (d.rationale) md += ` \u2014 ${d.rationale}`;
|
|
@@ -1118,10 +1118,10 @@ function parseTranscript(transcriptPath, sessionId, projectPath) {
|
|
|
1118
1118
|
if (filePath && isFilePath(filePath)) {
|
|
1119
1119
|
const content2 = tool.input.content;
|
|
1120
1120
|
const lines2 = content2 ? countContentLines(content2) : 0;
|
|
1121
|
-
const
|
|
1122
|
-
if (!filesMap.has(
|
|
1123
|
-
filesMap.set(
|
|
1124
|
-
path:
|
|
1121
|
+
const rel2 = relativePath(filePath, projectPath);
|
|
1122
|
+
if (!filesMap.has(rel2)) {
|
|
1123
|
+
filesMap.set(rel2, {
|
|
1124
|
+
path: rel2,
|
|
1125
1125
|
operation: "created",
|
|
1126
1126
|
linesChanged: lines2
|
|
1127
1127
|
});
|
|
@@ -1131,8 +1131,8 @@ function parseTranscript(transcriptPath, sessionId, projectPath) {
|
|
|
1131
1131
|
if (tool.name === "Edit" && succeeded) {
|
|
1132
1132
|
const filePath = tool.input.file_path;
|
|
1133
1133
|
if (filePath && isFilePath(filePath)) {
|
|
1134
|
-
const
|
|
1135
|
-
const existing = filesMap.get(
|
|
1134
|
+
const rel2 = relativePath(filePath, projectPath);
|
|
1135
|
+
const existing = filesMap.get(rel2);
|
|
1136
1136
|
const oldStr = tool.input.old_string;
|
|
1137
1137
|
const newStr = tool.input.new_string;
|
|
1138
1138
|
const lines2 = Math.max(
|
|
@@ -1142,8 +1142,8 @@ function parseTranscript(transcriptPath, sessionId, projectPath) {
|
|
|
1142
1142
|
if (existing) {
|
|
1143
1143
|
existing.linesChanged += lines2;
|
|
1144
1144
|
} else {
|
|
1145
|
-
filesMap.set(
|
|
1146
|
-
path:
|
|
1145
|
+
filesMap.set(rel2, {
|
|
1146
|
+
path: rel2,
|
|
1147
1147
|
operation: "modified",
|
|
1148
1148
|
linesChanged: lines2
|
|
1149
1149
|
});
|
|
@@ -1213,8 +1213,8 @@ function extractBashFileOps(command, filesMap, projectPath) {
|
|
|
1213
1213
|
const rmMatches = command.matchAll(/\brm\s+(?:-[rf]+\s+)?([^\s;&|]+\.[a-zA-Z0-9]+)/g);
|
|
1214
1214
|
for (const m of rmMatches) {
|
|
1215
1215
|
if (isFilePath(m[1])) {
|
|
1216
|
-
const
|
|
1217
|
-
filesMap.set(
|
|
1216
|
+
const rel2 = relativePath(m[1], projectPath);
|
|
1217
|
+
filesMap.set(rel2, { path: rel2, operation: "deleted", linesChanged: 0 });
|
|
1218
1218
|
}
|
|
1219
1219
|
}
|
|
1220
1220
|
}
|
|
@@ -1444,7 +1444,7 @@ ${sessionRow.summary}
|
|
|
1444
1444
|
md += "\n";
|
|
1445
1445
|
}
|
|
1446
1446
|
if (decisions.length > 0) {
|
|
1447
|
-
md += "##
|
|
1447
|
+
md += "## Architecture log\n";
|
|
1448
1448
|
for (const d of decisions) {
|
|
1449
1449
|
md += `- ${d.description}`;
|
|
1450
1450
|
if (d.rationale) md += ` \u2014 ${d.rationale}`;
|
|
@@ -1569,7 +1569,7 @@ async function reportCommand(options) {
|
|
|
1569
1569
|
console.log(chalk9.green(`
|
|
1570
1570
|
\u2713 SESSION_REPORT.md written (session ${session.id})
|
|
1571
1571
|
`));
|
|
1572
|
-
console.log(chalk9.gray(` ${files.length} files \xB7 ${decisions.length}
|
|
1572
|
+
console.log(chalk9.gray(` ${files.length} files \xB7 ${decisions.length} arch log entries \xB7 ${completedFeatures.length} features completed`));
|
|
1573
1573
|
console.log("");
|
|
1574
1574
|
}
|
|
1575
1575
|
|
|
@@ -1640,7 +1640,7 @@ async function healthCommand() {
|
|
|
1640
1640
|
const decMark = decisionCount > 0 ? "\u2705" : "\u26A0\uFE0F ";
|
|
1641
1641
|
const decColor = decisionCount > 0 ? chalk10.green : chalk10.yellow;
|
|
1642
1642
|
console.log(
|
|
1643
|
-
` ${decMark}
|
|
1643
|
+
` ${decMark} Arch log ${decColor(decisionCount + " entries")}` + chalk10.gray(` +${decisionScore}pts`)
|
|
1644
1644
|
);
|
|
1645
1645
|
const claimMark = staleClaims === 0 ? "\u2705" : "\u26A0\uFE0F ";
|
|
1646
1646
|
const claimColor = staleClaims === 0 ? chalk10.green : chalk10.red;
|
|
@@ -1656,7 +1656,7 @@ async function healthCommand() {
|
|
|
1656
1656
|
const recommendations = [];
|
|
1657
1657
|
if (testFiles === 0) recommendations.push("Write tests before your next feature (0 test files found).");
|
|
1658
1658
|
if (staleClaims > 0) recommendations.push(`Release ${staleClaims} stale claim(s) with groundctl complete <feature>.`);
|
|
1659
|
-
if (decisionCount === 0) recommendations.push("
|
|
1659
|
+
if (decisionCount === 0) recommendations.push("Log architecture decisions during sessions so agents understand the why.");
|
|
1660
1660
|
if (featurePct < 0.5 && total > 0) recommendations.push(`${counts.pending} features pending \u2014 run groundctl next to pick one.`);
|
|
1661
1661
|
if (recommendations.length > 0) {
|
|
1662
1662
|
console.log(chalk10.bold(" Recommendations:"));
|
|
@@ -1668,35 +1668,180 @@ async function healthCommand() {
|
|
|
1668
1668
|
}
|
|
1669
1669
|
|
|
1670
1670
|
// src/commands/dashboard.ts
|
|
1671
|
-
import {
|
|
1671
|
+
import { createServer } from "http";
|
|
1672
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
1672
1673
|
import { join as join7, dirname as dirname2 } from "path";
|
|
1673
|
-
import {
|
|
1674
|
-
import { spawn } from "child_process";
|
|
1674
|
+
import { exec } from "child_process";
|
|
1675
1675
|
import chalk11 from "chalk";
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
return;
|
|
1676
|
+
import initSqlJs2 from "sql.js";
|
|
1677
|
+
function findDbPath(startDir = process.cwd()) {
|
|
1678
|
+
let dir = startDir;
|
|
1679
|
+
for (let i = 0; i < 10; i++) {
|
|
1680
|
+
const candidate = join7(dir, ".groundctl", "db.sqlite");
|
|
1681
|
+
if (existsSync5(candidate)) return candidate;
|
|
1682
|
+
const parent = dirname2(dir);
|
|
1683
|
+
if (parent === dir) break;
|
|
1684
|
+
dir = parent;
|
|
1686
1685
|
}
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
const
|
|
1692
|
-
|
|
1693
|
-
|
|
1686
|
+
return null;
|
|
1687
|
+
}
|
|
1688
|
+
async function readDb(dbPath) {
|
|
1689
|
+
const SQL = await initSqlJs2();
|
|
1690
|
+
const buffer = readFileSync5(dbPath);
|
|
1691
|
+
const db = new SQL.Database(buffer);
|
|
1692
|
+
function q(sql, params = []) {
|
|
1693
|
+
const stmt = db.prepare(sql);
|
|
1694
|
+
stmt.bind(params);
|
|
1695
|
+
const rows = [];
|
|
1696
|
+
while (stmt.step()) rows.push(stmt.getAsObject());
|
|
1697
|
+
stmt.free();
|
|
1698
|
+
return rows;
|
|
1699
|
+
}
|
|
1700
|
+
const features = q("SELECT * FROM features ORDER BY CASE status WHEN 'in_progress' THEN 0 WHEN 'pending' THEN 1 ELSE 2 END, CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END");
|
|
1701
|
+
const sessions = q("SELECT * FROM sessions ORDER BY started_at DESC LIMIT 20");
|
|
1702
|
+
const claims = q("SELECT c.*, f.name as feature_name FROM claims c JOIN features f ON c.feature_id = f.id WHERE c.released_at IS NULL");
|
|
1703
|
+
const decisions = q("SELECT d.*, s.id as sess FROM decisions d JOIN sessions s ON d.session_id = s.id ORDER BY d.id DESC LIMIT 30");
|
|
1704
|
+
const files = q("SELECT * FROM files_modified ORDER BY id DESC LIMIT 100");
|
|
1705
|
+
const total = features.length;
|
|
1706
|
+
const done = features.filter((f) => f.status === "done").length;
|
|
1707
|
+
const pct = total > 0 ? Math.round(done / total * 100) : 0;
|
|
1708
|
+
const testFiles = files.filter((f) => /\.(test|spec)\./.test(f.path) || f.path.includes("__tests__")).length;
|
|
1709
|
+
const decCount = decisions.length;
|
|
1710
|
+
const stale = claims.filter((c) => Date.now() - new Date(c.claimed_at).getTime() > 864e5).length;
|
|
1711
|
+
const health = Math.min(100, Math.round(
|
|
1712
|
+
done / Math.max(1, total) * 40 + (testFiles > 0 ? Math.min(20, testFiles * 5) : 0) + (decCount > 0 ? Math.min(20, decCount * 2) : 0) + (stale === 0 ? 10 : 0)
|
|
1713
|
+
));
|
|
1714
|
+
db.close();
|
|
1715
|
+
return { features, sessions, claims, decisions, files, meta: { total, done, pct, health, testFiles, decCount, stale } };
|
|
1716
|
+
}
|
|
1717
|
+
function bar(done, total, w = 20) {
|
|
1718
|
+
const n = total > 0 ? Math.round(done / total * w) : 0;
|
|
1719
|
+
return "\u2588".repeat(n) + "\u2591".repeat(w - n);
|
|
1720
|
+
}
|
|
1721
|
+
function esc(s) {
|
|
1722
|
+
return String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1723
|
+
}
|
|
1724
|
+
function rel(ts) {
|
|
1725
|
+
if (!ts) return "\u2014";
|
|
1726
|
+
const m = Math.floor((Date.now() - new Date(ts).getTime()) / 6e4);
|
|
1727
|
+
if (m < 1) return "just now";
|
|
1728
|
+
if (m < 60) return `${m}m ago`;
|
|
1729
|
+
const h = Math.floor(m / 60);
|
|
1730
|
+
if (h < 24) return `${h}h ago`;
|
|
1731
|
+
return `${Math.floor(h / 24)}d ago`;
|
|
1732
|
+
}
|
|
1733
|
+
function renderHtml(data, projectName, dbPath) {
|
|
1734
|
+
const { features, sessions, claims, decisions, files, meta } = data;
|
|
1735
|
+
const sc = meta.pct >= 70 ? "#4ade80" : meta.pct >= 40 ? "#facc15" : "#f87171";
|
|
1736
|
+
const hc = meta.health >= 70 ? "#4ade80" : meta.health >= 40 ? "#facc15" : "#f87171";
|
|
1737
|
+
const claimRows = claims.length ? claims.map((c) => `<div class="claim-row"><span class="cy">\u25CF</span><span class="cn">${esc(c.feature_name)}</span><span class="cd">session ${esc(c.session_id)}</span><span class="cb">${rel(c.claimed_at)}</span></div>`).join("") : `<div class="empty">No active claims</div>`;
|
|
1738
|
+
const featRows = features.map((f) => {
|
|
1739
|
+
const cls = f.status === "done" ? "done" : f.status === "in_progress" ? "active" : "pend";
|
|
1740
|
+
const icon = f.status === "done" ? "\u2713" : f.status === "in_progress" ? "\u25CF" : "\u25CB";
|
|
1741
|
+
return `<div class="fr ${cls}"><span class="fi">${icon}</span><span class="fn">${esc(f.name)}</span><span class="fp p-${esc(f.priority)}">${esc(f.priority)}</span><span class="fs">${esc(f.status)}</span></div>`;
|
|
1742
|
+
}).join("");
|
|
1743
|
+
const sessRows = sessions.length ? sessions.map((s) => {
|
|
1744
|
+
const fc = files.filter((f) => f.session_id === s.id).length;
|
|
1745
|
+
const dc = decisions.filter((d) => d.session_id === s.id).length;
|
|
1746
|
+
return `<div class="sr"><span class="si">${esc(s.id)}</span><span class="sd">${s.ended_at ? "\u25CF" : "\u25CC"}</span><span class="ss">${esc((s.summary ?? "").slice(0, 80))}</span><span class="sm">${fc} files \xB7 ${dc} dec \xB7 ${rel(s.started_at)}</span></div>`;
|
|
1747
|
+
}).join("") : `<div class="empty">No sessions yet</div>`;
|
|
1748
|
+
const decRows = decisions.slice(0, 10).length ? decisions.slice(0, 10).map((d) => `<div class="dr"><span class="di">${esc(d.sess)}</span><span class="dt">${esc(d.description.slice(0, 100))}</span>${d.rationale ? `<span class="dra">${esc(d.rationale.slice(0, 80))}</span>` : ""}</div>`).join("") : `<div class="empty">No decisions documented yet</div>`;
|
|
1749
|
+
return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
1750
|
+
<title>${esc(projectName)} \u2014 groundctl</title><meta http-equiv="refresh" content="10">
|
|
1751
|
+
<style>
|
|
1752
|
+
:root{--bg:#0a0a0a;--b2:#111;--b3:#1a1a1a;--br:#222;--tx:#e0e0e0;--dm:#666;--md:#999;--gn:#4ade80;--yw:#facc15;--bl:#60a5fa;--rd:#f87171;--mo:"Berkeley Mono","IBM Plex Mono","Fira Code",ui-monospace,monospace}
|
|
1753
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
1754
|
+
body{background:var(--bg);color:var(--tx);font-family:var(--mo);font-size:13px;line-height:1.6}
|
|
1755
|
+
.layout{display:grid;grid-template-columns:280px 1fr;min-height:100vh}
|
|
1756
|
+
.sidebar{border-right:1px solid var(--br);padding:24px 20px;display:flex;flex-direction:column;gap:28px}
|
|
1757
|
+
.main{padding:24px;display:flex;flex-direction:column;gap:20px;overflow:auto}
|
|
1758
|
+
.pn{font-size:1.1rem;font-weight:700;color:#fff;margin-bottom:4px}
|
|
1759
|
+
.pc{color:${sc};font-size:.9rem;margin-bottom:8px}
|
|
1760
|
+
.pb{font-size:.85rem;color:var(--gn);letter-spacing:.5px;margin-bottom:4px}
|
|
1761
|
+
.ps{font-size:.75rem;color:var(--dm)}
|
|
1762
|
+
.hl{font-size:.7rem;color:var(--dm);text-transform:uppercase;letter-spacing:.08em}
|
|
1763
|
+
.hs{font-size:1.8rem;font-weight:700;color:${hc}}
|
|
1764
|
+
.hi{margin-top:10px;display:flex;flex-direction:column;gap:4px}
|
|
1765
|
+
.hi>div{font-size:.8rem;color:var(--md);display:flex;gap:8px}
|
|
1766
|
+
.ok{color:var(--gn)}.warn{color:var(--yw)}.bad{color:var(--rd)}
|
|
1767
|
+
.sec{background:var(--b2);border:1px solid var(--br);border-radius:8px;overflow:hidden}
|
|
1768
|
+
.sh{padding:10px 16px;border-bottom:1px solid var(--br);font-size:.7rem;text-transform:uppercase;letter-spacing:.1em;color:var(--dm);display:flex;justify-content:space-between}
|
|
1769
|
+
.sc{background:var(--b3);border-radius:4px;padding:1px 6px;font-size:.7rem;color:var(--md)}
|
|
1770
|
+
.claim-row{display:flex;align-items:center;gap:10px;padding:10px 16px;border-bottom:1px solid var(--br)}
|
|
1771
|
+
.claim-row:last-child{border-bottom:none}
|
|
1772
|
+
.cy{color:var(--yw)}.cn{flex:1;color:#fff}.cd{color:var(--dm);font-size:.8rem}.cb{color:var(--bl);font-size:.8rem}
|
|
1773
|
+
.fr{display:grid;grid-template-columns:20px 1fr 70px 90px;gap:8px;padding:8px 16px;border-bottom:1px solid var(--br);align-items:center}
|
|
1774
|
+
.fr:last-child{border-bottom:none}
|
|
1775
|
+
.done{opacity:.45}.active{background:rgba(250,204,21,.04)}
|
|
1776
|
+
.done .fi{color:var(--gn)}.active .fi{color:var(--yw)}.pend .fi{color:var(--dm)}
|
|
1777
|
+
.fn{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1778
|
+
.fp{font-size:.75rem;text-align:right}.fs{font-size:.75rem;color:var(--dm);text-align:right}
|
|
1779
|
+
.p-critical{color:var(--rd)}.p-high{color:var(--yw)}.p-medium{color:var(--md)}.p-low{color:var(--dm)}
|
|
1780
|
+
.sr{display:grid;grid-template-columns:60px 16px 1fr auto;gap:8px;padding:10px 16px;border-bottom:1px solid var(--br);align-items:start}
|
|
1781
|
+
.sr:last-child{border-bottom:none}
|
|
1782
|
+
.si{color:var(--bl);font-weight:600;font-size:.85rem}.sd{color:var(--gn)}.ss{color:var(--md);font-size:.8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sm{color:var(--dm);font-size:.75rem;white-space:nowrap;text-align:right}
|
|
1783
|
+
.dr{padding:10px 16px;border-bottom:1px solid var(--br);display:grid;grid-template-columns:50px 1fr;gap:8px}
|
|
1784
|
+
.dr:last-child{border-bottom:none}
|
|
1785
|
+
.di{color:var(--bl);font-size:.8rem}.dt{color:var(--tx);font-size:.8rem}.dra{grid-column:2;font-size:.75rem;color:var(--dm);font-style:italic}
|
|
1786
|
+
.empty{padding:16px;color:var(--dm);font-size:.85rem;text-align:center}
|
|
1787
|
+
.rn{font-size:.7rem;color:var(--dm);text-align:center;padding-top:8px}
|
|
1788
|
+
.top{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
|
1789
|
+
@media(max-width:800px){.layout{grid-template-columns:1fr}.sidebar{border-right:none;border-bottom:1px solid var(--br)}.top{grid-template-columns:1fr}}
|
|
1790
|
+
</style></head><body>
|
|
1791
|
+
<div class="layout">
|
|
1792
|
+
<aside class="sidebar">
|
|
1793
|
+
<div><div class="pn">${esc(projectName)}</div><div class="pc">${meta.pct}% implemented</div><div class="pb">${bar(meta.done, meta.total)}</div><div class="ps">${meta.done}/${meta.total} features done</div></div>
|
|
1794
|
+
<div><div class="hl">Health Score</div><div class="hs">${meta.health}<span style="font-size:1rem;color:var(--dm)">/100</span></div>
|
|
1795
|
+
<div class="hi">
|
|
1796
|
+
<div><span class="${meta.done > 0 ? "ok" : "warn"}">${meta.done > 0 ? "\u2713" : "\u26A0"}</span><span>Features ${meta.done}/${meta.total}</span></div>
|
|
1797
|
+
<div><span class="${meta.testFiles > 0 ? "ok" : "bad"}">${meta.testFiles > 0 ? "\u2713" : "\u2717"}</span><span>Tests ${meta.testFiles} files</span></div>
|
|
1798
|
+
<div><span class="${meta.decCount > 0 ? "ok" : "warn"}">${meta.decCount > 0 ? "\u2713" : "\u26A0"}</span><span>Architecture log ${meta.decCount} entries</span></div>
|
|
1799
|
+
<div><span class="${meta.stale === 0 ? "ok" : "bad"}">${meta.stale === 0 ? "\u2713" : "\u2717"}</span><span>Claims ${meta.stale > 0 ? meta.stale + " stale" : "healthy"}</span></div>
|
|
1800
|
+
</div></div>
|
|
1801
|
+
<div class="rn">auto-refresh 10s<br><span style="color:var(--br)">${esc(dbPath.split("/").slice(-3).join("/"))}</span></div>
|
|
1802
|
+
</aside>
|
|
1803
|
+
<main class="main">
|
|
1804
|
+
<div class="sec"><div class="sh"><span>Claims live</span><span class="sc">${claims.length}</span></div>${claimRows}</div>
|
|
1805
|
+
<div class="top">
|
|
1806
|
+
<div class="sec"><div class="sh"><span>Features</span><span class="sc">${features.length}</span></div>${featRows}</div>
|
|
1807
|
+
<div class="sec"><div class="sh"><span>Session timeline</span><span class="sc">${sessions.length}</span></div>${sessRows}</div>
|
|
1808
|
+
</div>
|
|
1809
|
+
<div class="sec"><div class="sh"><span>Recent decisions</span><span class="sc">${decisions.length}</span></div>${decRows}</div>
|
|
1810
|
+
</main>
|
|
1811
|
+
</div></body></html>`;
|
|
1812
|
+
}
|
|
1813
|
+
async function dashboardCommand(options) {
|
|
1814
|
+
const port = parseInt(options.port ?? "4242");
|
|
1815
|
+
const server = createServer(async (req, res) => {
|
|
1816
|
+
if (req.url !== "/" && req.url !== "") {
|
|
1817
|
+
res.writeHead(404);
|
|
1818
|
+
res.end("Not found");
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
const dbPath = findDbPath();
|
|
1822
|
+
if (!dbPath) {
|
|
1823
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1824
|
+
res.end(`<!DOCTYPE html><html><body style="background:#0a0a0a;color:#e0e0e0;font-family:monospace;padding:40px"><h2>groundctl</h2><p style="color:#f87171">No .groundctl/db.sqlite found.</p><p>Run: <code>groundctl init</code></p></body></html>`);
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
try {
|
|
1828
|
+
const data = await readDb(dbPath);
|
|
1829
|
+
const name = process.cwd().split("/").pop() ?? "project";
|
|
1830
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1831
|
+
res.end(renderHtml(data, name, dbPath));
|
|
1832
|
+
} catch (err) {
|
|
1833
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
1834
|
+
res.end(`Error: ${err.message}`);
|
|
1835
|
+
}
|
|
1694
1836
|
});
|
|
1695
|
-
|
|
1696
|
-
console.
|
|
1837
|
+
server.listen(port, "127.0.0.1", () => {
|
|
1838
|
+
console.log(chalk11.bold(`
|
|
1839
|
+
groundctl dashboard \u2192 `) + chalk11.blue(`http://localhost:${port}`) + "\n");
|
|
1840
|
+
console.log(chalk11.gray(" Auto-refreshes every 10s. Press Ctrl+C to stop.\n"));
|
|
1841
|
+
exec(`open http://localhost:${port} 2>/dev/null || xdg-open http://localhost:${port} 2>/dev/null || true`);
|
|
1697
1842
|
});
|
|
1698
|
-
await new Promise((
|
|
1699
|
-
|
|
1843
|
+
await new Promise((_, reject) => {
|
|
1844
|
+
server.on("error", reject);
|
|
1700
1845
|
});
|
|
1701
1846
|
}
|
|
1702
1847
|
|