@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.
Files changed (2) hide show
  1. package/dist/index.js +184 -39
  2. 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 += "## Decisions made\n";
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 rel = relativePath(filePath, projectPath);
1122
- if (!filesMap.has(rel)) {
1123
- filesMap.set(rel, {
1124
- path: rel,
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 rel = relativePath(filePath, projectPath);
1135
- const existing = filesMap.get(rel);
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(rel, {
1146
- path: rel,
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 rel = relativePath(m[1], projectPath);
1217
- filesMap.set(rel, { path: rel, operation: "deleted", linesChanged: 0 });
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 += "## Decisions\n";
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} decisions \xB7 ${completedFeatures.length} features completed`));
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} Decisions ${decColor(decisionCount + " documented")}` + chalk10.gray(` +${decisionScore}pts`)
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("Document decisions during sessions so agents have context.");
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 { existsSync as existsSync5 } from "fs";
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 { fileURLToPath } from "url";
1674
- import { spawn } from "child_process";
1674
+ import { exec } from "child_process";
1675
1675
  import chalk11 from "chalk";
1676
- var __dirname = dirname2(fileURLToPath(import.meta.url));
1677
- async function dashboardCommand(options) {
1678
- const port = options.port ?? "4242";
1679
- const serverPath = join7(__dirname, "..", "..", "..", "dashboard", "src", "server.js");
1680
- if (!existsSync5(serverPath)) {
1681
- console.log(chalk11.red(`
1682
- Dashboard server not found at: ${serverPath}
1683
- `));
1684
- console.log(chalk11.gray(" If running from source: npm run build"));
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
- console.log(chalk11.bold(`
1688
- groundctl dashboard \u2192 http://localhost:${port}
1689
- `));
1690
- console.log(chalk11.gray(" Auto-refreshes every 10s. Press Ctrl+C to stop.\n"));
1691
- const child = spawn(process.execPath, [serverPath], {
1692
- stdio: "inherit",
1693
- env: { ...process.env, GROUNDCTL_PORT: port }
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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
- child.on("error", (err) => {
1696
- console.error(chalk11.red(` Error: ${err.message}`));
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((resolve2) => {
1699
- child.on("close", () => resolve2());
1843
+ await new Promise((_, reject) => {
1844
+ server.on("error", reject);
1700
1845
  });
1701
1846
  }
1702
1847
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groundctl/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Product memory for AI agent builders",
5
5
  "license": "MIT",
6
6
  "bin": {