@groundctl/cli 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +221 -21
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -736,14 +736,40 @@ function timeSince(isoDate) {
|
|
|
736
736
|
// src/commands/claim.ts
|
|
737
737
|
import chalk3 from "chalk";
|
|
738
738
|
import { randomUUID } from "crypto";
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
const feature = queryOne(
|
|
739
|
+
function findFeature(db, term) {
|
|
740
|
+
return queryOne(
|
|
742
741
|
db,
|
|
743
742
|
`SELECT id, name, status FROM features
|
|
744
|
-
WHERE id = ? OR name = ?
|
|
745
|
-
|
|
743
|
+
WHERE id = ? OR name = ?
|
|
744
|
+
UNION ALL
|
|
745
|
+
SELECT id, name, status FROM features
|
|
746
|
+
WHERE (id LIKE ? OR name LIKE ?)
|
|
747
|
+
AND id != ? AND name != ?
|
|
748
|
+
UNION ALL
|
|
749
|
+
SELECT id, name, status FROM features
|
|
750
|
+
WHERE (id LIKE ? OR name LIKE ?)
|
|
751
|
+
AND id NOT LIKE ? AND name NOT LIKE ?
|
|
752
|
+
AND id != ? AND name != ?
|
|
753
|
+
LIMIT 1`,
|
|
754
|
+
[
|
|
755
|
+
term,
|
|
756
|
+
term,
|
|
757
|
+
`${term}%`,
|
|
758
|
+
`${term}%`,
|
|
759
|
+
term,
|
|
760
|
+
term,
|
|
761
|
+
`%${term}%`,
|
|
762
|
+
`%${term}%`,
|
|
763
|
+
`${term}%`,
|
|
764
|
+
`${term}%`,
|
|
765
|
+
term,
|
|
766
|
+
term
|
|
767
|
+
]
|
|
746
768
|
);
|
|
769
|
+
}
|
|
770
|
+
async function claimCommand(featureIdOrName, options) {
|
|
771
|
+
const db = await openDb();
|
|
772
|
+
const feature = findFeature(db, featureIdOrName);
|
|
747
773
|
if (!feature) {
|
|
748
774
|
console.log(chalk3.red(`
|
|
749
775
|
Feature "${featureIdOrName}" not found.
|
|
@@ -821,12 +847,7 @@ async function claimCommand(featureIdOrName, options) {
|
|
|
821
847
|
}
|
|
822
848
|
async function completeCommand(featureIdOrName) {
|
|
823
849
|
const db = await openDb();
|
|
824
|
-
const feature =
|
|
825
|
-
db,
|
|
826
|
-
`SELECT id, name, status FROM features
|
|
827
|
-
WHERE id = ? OR name = ? OR name LIKE ?`,
|
|
828
|
-
[featureIdOrName, featureIdOrName, `%${featureIdOrName}%`]
|
|
829
|
-
);
|
|
850
|
+
const feature = findFeature(db, featureIdOrName);
|
|
830
851
|
if (!feature) {
|
|
831
852
|
console.log(chalk3.red(`
|
|
832
853
|
Feature "${featureIdOrName}" not found.
|
|
@@ -1097,10 +1118,10 @@ function parseTranscript(transcriptPath, sessionId, projectPath) {
|
|
|
1097
1118
|
if (filePath && isFilePath(filePath)) {
|
|
1098
1119
|
const content2 = tool.input.content;
|
|
1099
1120
|
const lines2 = content2 ? countContentLines(content2) : 0;
|
|
1100
|
-
const
|
|
1101
|
-
if (!filesMap.has(
|
|
1102
|
-
filesMap.set(
|
|
1103
|
-
path:
|
|
1121
|
+
const rel2 = relativePath(filePath, projectPath);
|
|
1122
|
+
if (!filesMap.has(rel2)) {
|
|
1123
|
+
filesMap.set(rel2, {
|
|
1124
|
+
path: rel2,
|
|
1104
1125
|
operation: "created",
|
|
1105
1126
|
linesChanged: lines2
|
|
1106
1127
|
});
|
|
@@ -1110,8 +1131,8 @@ function parseTranscript(transcriptPath, sessionId, projectPath) {
|
|
|
1110
1131
|
if (tool.name === "Edit" && succeeded) {
|
|
1111
1132
|
const filePath = tool.input.file_path;
|
|
1112
1133
|
if (filePath && isFilePath(filePath)) {
|
|
1113
|
-
const
|
|
1114
|
-
const existing = filesMap.get(
|
|
1134
|
+
const rel2 = relativePath(filePath, projectPath);
|
|
1135
|
+
const existing = filesMap.get(rel2);
|
|
1115
1136
|
const oldStr = tool.input.old_string;
|
|
1116
1137
|
const newStr = tool.input.new_string;
|
|
1117
1138
|
const lines2 = Math.max(
|
|
@@ -1121,8 +1142,8 @@ function parseTranscript(transcriptPath, sessionId, projectPath) {
|
|
|
1121
1142
|
if (existing) {
|
|
1122
1143
|
existing.linesChanged += lines2;
|
|
1123
1144
|
} else {
|
|
1124
|
-
filesMap.set(
|
|
1125
|
-
path:
|
|
1145
|
+
filesMap.set(rel2, {
|
|
1146
|
+
path: rel2,
|
|
1126
1147
|
operation: "modified",
|
|
1127
1148
|
linesChanged: lines2
|
|
1128
1149
|
});
|
|
@@ -1192,8 +1213,8 @@ function extractBashFileOps(command, filesMap, projectPath) {
|
|
|
1192
1213
|
const rmMatches = command.matchAll(/\brm\s+(?:-[rf]+\s+)?([^\s;&|]+\.[a-zA-Z0-9]+)/g);
|
|
1193
1214
|
for (const m of rmMatches) {
|
|
1194
1215
|
if (isFilePath(m[1])) {
|
|
1195
|
-
const
|
|
1196
|
-
filesMap.set(
|
|
1216
|
+
const rel2 = relativePath(m[1], projectPath);
|
|
1217
|
+
filesMap.set(rel2, { path: rel2, operation: "deleted", linesChanged: 0 });
|
|
1197
1218
|
}
|
|
1198
1219
|
}
|
|
1199
1220
|
}
|
|
@@ -1646,6 +1667,184 @@ async function healthCommand() {
|
|
|
1646
1667
|
}
|
|
1647
1668
|
}
|
|
1648
1669
|
|
|
1670
|
+
// src/commands/dashboard.ts
|
|
1671
|
+
import { createServer } from "http";
|
|
1672
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
1673
|
+
import { join as join7, dirname as dirname2 } from "path";
|
|
1674
|
+
import { exec } from "child_process";
|
|
1675
|
+
import chalk11 from "chalk";
|
|
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;
|
|
1685
|
+
}
|
|
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>Decisions ${meta.decCount}</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
|
+
}
|
|
1836
|
+
});
|
|
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`);
|
|
1842
|
+
});
|
|
1843
|
+
await new Promise((_, reject) => {
|
|
1844
|
+
server.on("error", reject);
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1649
1848
|
// src/index.ts
|
|
1650
1849
|
var require2 = createRequire(import.meta.url);
|
|
1651
1850
|
var pkg = require2("../package.json");
|
|
@@ -1670,4 +1869,5 @@ program.command("ingest").description("Parse a transcript and write session data
|
|
|
1670
1869
|
);
|
|
1671
1870
|
program.command("report").description("Generate SESSION_REPORT.md from SQLite").option("-s, --session <id>", "Report for a specific session").option("--all", "Generate report for all sessions").action(reportCommand);
|
|
1672
1871
|
program.command("health").description("Show product health score").action(healthCommand);
|
|
1872
|
+
program.command("dashboard").description("Start web dashboard on port 4242").option("-p, --port <port>", "Port number", "4242").action(dashboardCommand);
|
|
1673
1873
|
program.parse();
|