@groundctl/cli 0.3.0 → 0.3.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 +245 -78
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -98,6 +98,15 @@ function applySchema(db) {
98
98
  db.run("CREATE INDEX IF NOT EXISTS idx_claims_active ON claims(feature_id) WHERE released_at IS NULL");
99
99
  db.run("CREATE INDEX IF NOT EXISTS idx_files_session ON files_modified(session_id)");
100
100
  db.run("CREATE INDEX IF NOT EXISTS idx_decisions_session ON decisions(session_id)");
101
+ const tryAlter = (sql) => {
102
+ try {
103
+ db.run(sql);
104
+ } catch {
105
+ }
106
+ };
107
+ tryAlter("ALTER TABLE features ADD COLUMN progress_done INTEGER");
108
+ tryAlter("ALTER TABLE features ADD COLUMN progress_total INTEGER");
109
+ tryAlter("ALTER TABLE features ADD COLUMN items TEXT");
101
110
  db.run(
102
111
  "INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', ?)",
103
112
  [String(SCHEMA_VERSION)]
@@ -628,49 +637,87 @@ groundctl init \u2014 ${projectName}
628
637
 
629
638
  // src/commands/status.ts
630
639
  import chalk2 from "chalk";
631
- function progressBar(done, total, width = 20) {
632
- if (total === 0) return chalk2.gray("\u2591".repeat(width));
633
- const filled = Math.round(done / total * width);
634
- const empty = width - filled;
635
- return chalk2.green("\u2588".repeat(filled)) + chalk2.gray("\u2591".repeat(empty));
640
+ var BAR_W = 14;
641
+ var NAME_W = 22;
642
+ var PROG_W = 6;
643
+ function progressBar(done, total, width) {
644
+ if (total <= 0) return chalk2.gray("\u2591".repeat(width));
645
+ const filled = Math.min(width, Math.round(done / total * width));
646
+ return chalk2.green("\u2588".repeat(filled)) + chalk2.gray("\u2591".repeat(width - filled));
647
+ }
648
+ function featureBar(status, progressDone, progressTotal) {
649
+ if (progressTotal != null && progressTotal > 0) {
650
+ return progressBar(progressDone ?? 0, progressTotal, BAR_W);
651
+ }
652
+ switch (status) {
653
+ case "done":
654
+ return progressBar(1, 1, BAR_W);
655
+ case "in_progress":
656
+ return progressBar(1, 2, BAR_W);
657
+ case "blocked":
658
+ return chalk2.red("\u2591".repeat(BAR_W));
659
+ default:
660
+ return chalk2.gray("\u2591".repeat(BAR_W));
661
+ }
662
+ }
663
+ function featureProgress(progressDone, progressTotal) {
664
+ if (progressDone != null && progressTotal != null) {
665
+ return `${progressDone}/${progressTotal}`;
666
+ }
667
+ return "";
668
+ }
669
+ function wrapItems(itemsCsv, maxWidth) {
670
+ const items = itemsCsv.split(",").map((s) => s.trim()).filter(Boolean);
671
+ const lines = [];
672
+ let current = "";
673
+ for (const item of items) {
674
+ const next = current ? `${current} \xB7 ${item}` : item;
675
+ if (next.length > maxWidth && current.length > 0) {
676
+ lines.push(current);
677
+ current = item;
678
+ } else {
679
+ current = next;
680
+ }
681
+ }
682
+ if (current) lines.push(current);
683
+ return lines;
684
+ }
685
+ function timeSince(isoDate) {
686
+ const then = (/* @__PURE__ */ new Date(isoDate + "Z")).getTime();
687
+ const ms = Date.now() - then;
688
+ const mins = Math.floor(ms / 6e4);
689
+ if (mins < 60) return `${mins}m`;
690
+ const h = Math.floor(mins / 60);
691
+ const m = mins % 60;
692
+ return `${h}h${m > 0 ? String(m).padStart(2, "0") : ""}`;
636
693
  }
637
694
  async function statusCommand() {
638
695
  const db = await openDb();
639
696
  const projectName = process.cwd().split("/").pop() ?? "unknown";
640
- const statusCounts = query(
641
- db,
642
- "SELECT status, COUNT(*) as count FROM features GROUP BY status"
643
- );
644
- const counts = {
645
- pending: 0,
646
- in_progress: 0,
647
- done: 0,
648
- blocked: 0
649
- };
650
- for (const row of statusCounts) {
651
- counts[row.status] = row.count;
652
- }
653
- const total = counts.pending + counts.in_progress + counts.done + counts.blocked;
654
- const activeClaims = query(
655
- db,
656
- `SELECT c.feature_id, f.name as feature_name, c.session_id, c.claimed_at
657
- FROM claims c
658
- JOIN features f ON c.feature_id = f.id
659
- WHERE c.released_at IS NULL`
660
- );
661
- const available = query(
697
+ const features = query(
662
698
  db,
663
- `SELECT f.id, f.name, f.priority
699
+ `SELECT
700
+ f.id, f.name, f.status, f.priority,
701
+ f.description, f.progress_done, f.progress_total, f.items,
702
+ c.session_id AS claimed_session,
703
+ c.claimed_at AS claimed_at
664
704
  FROM features f
665
- WHERE f.status = 'pending'
666
- AND f.id NOT IN (SELECT feature_id FROM claims WHERE released_at IS NULL)
705
+ LEFT JOIN claims c
706
+ ON c.feature_id = f.id AND c.released_at IS NULL
667
707
  ORDER BY
708
+ CASE f.status
709
+ WHEN 'in_progress' THEN 0
710
+ WHEN 'blocked' THEN 1
711
+ WHEN 'pending' THEN 2
712
+ WHEN 'done' THEN 3
713
+ END,
668
714
  CASE f.priority
669
715
  WHEN 'critical' THEN 0
670
- WHEN 'high' THEN 1
671
- WHEN 'medium' THEN 2
672
- WHEN 'low' THEN 3
673
- END`
716
+ WHEN 'high' THEN 1
717
+ WHEN 'medium' THEN 2
718
+ WHEN 'low' THEN 3
719
+ END,
720
+ f.created_at`
674
721
  );
675
722
  const sessionCount = queryOne(
676
723
  db,
@@ -678,59 +725,63 @@ async function statusCommand() {
678
725
  )?.count ?? 0;
679
726
  closeDb();
680
727
  console.log("");
681
- if (total === 0) {
728
+ if (features.length === 0) {
682
729
  console.log(chalk2.bold(` ${projectName} \u2014 no features tracked yet
683
730
  `));
684
731
  console.log(chalk2.gray(" Add features with: groundctl add feature -n 'my-feature'"));
685
732
  console.log(chalk2.gray(" Then run: groundctl status\n"));
686
733
  return;
687
734
  }
688
- const pct = total > 0 ? Math.round(counts.done / total * 100) : 0;
735
+ const total = features.length;
736
+ const done = features.filter((f) => f.status === "done").length;
737
+ const inProg = features.filter((f) => f.status === "in_progress").length;
738
+ const blocked = features.filter((f) => f.status === "blocked").length;
739
+ const pct = Math.round(done / total * 100);
689
740
  console.log(
690
- chalk2.bold(` ${projectName} \u2014 ${pct}% implemented`) + chalk2.gray(` (${sessionCount} sessions)`)
741
+ chalk2.bold(` ${projectName} \u2014 ${pct}% implemented`) + chalk2.gray(` (${sessionCount} session${sessionCount !== 1 ? "s" : ""})`)
691
742
  );
692
743
  console.log("");
693
- console.log(
694
- ` Features ${progressBar(counts.done, total)} ${counts.done}/${total} done`
695
- );
696
- if (counts.in_progress > 0) {
697
- console.log(chalk2.yellow(` ${counts.in_progress} in progress`));
698
- }
699
- if (counts.blocked > 0) {
700
- console.log(chalk2.red(` ${counts.blocked} blocked`));
701
- }
744
+ const aggBar = progressBar(done, total, 20);
745
+ let aggSuffix = chalk2.white(` ${done}/${total} done`);
746
+ if (inProg > 0) aggSuffix += chalk2.yellow(` ${inProg} in progress`);
747
+ if (blocked > 0) aggSuffix += chalk2.red(` ${blocked} blocked`);
748
+ console.log(` Features ${aggBar}${aggSuffix}`);
702
749
  console.log("");
703
- if (activeClaims.length > 0) {
704
- console.log(chalk2.bold(" Claimed:"));
705
- for (const claim of activeClaims) {
706
- const elapsed = timeSince(claim.claimed_at);
707
- console.log(
708
- chalk2.yellow(` \u25CF ${claim.feature_name} \u2192 session ${claim.session_id} (${elapsed})`)
709
- );
750
+ const maxNameLen = Math.min(NAME_W, Math.max(...features.map((f) => f.name.length)));
751
+ const nameW = Math.max(maxNameLen, 12);
752
+ const contIndent = " ".repeat(4 + nameW + 1);
753
+ const itemsMaxW = Math.max(40, 76 - contIndent.length);
754
+ for (const f of features) {
755
+ const isDone = f.status === "done";
756
+ const isActive = f.status === "in_progress";
757
+ const isBlocked = f.status === "blocked";
758
+ const icon = isDone ? "\u2713" : isActive ? "\u25CF" : isBlocked ? "\u2717" : "\u25CB";
759
+ const iconChalk = isDone ? chalk2.green : isActive ? chalk2.yellow : isBlocked ? chalk2.red : chalk2.gray;
760
+ const nameRaw = f.name.slice(0, nameW).padEnd(nameW);
761
+ const nameChalk = isDone ? chalk2.dim : isActive ? chalk2.white : isBlocked ? chalk2.red : chalk2.gray;
762
+ const pd = f.progress_done ?? null;
763
+ const pt = f.progress_total ?? null;
764
+ const bar2 = featureBar(f.status, pd, pt);
765
+ const prog = featureProgress(pd, pt).padEnd(PROG_W);
766
+ const descRaw = f.description ?? "";
767
+ const descTrunc = descRaw.length > 38 ? descRaw.slice(0, 36) + "\u2026" : descRaw;
768
+ const descStr = descTrunc ? chalk2.gray(` ${descTrunc}`) : "";
769
+ let claimedStr = "";
770
+ if (isActive && f.claimed_session) {
771
+ const elapsed = f.claimed_at ? timeSince(f.claimed_at) : "";
772
+ claimedStr = chalk2.yellow(` \u2192 ${f.claimed_session}${elapsed ? ` (${elapsed})` : ""}`);
710
773
  }
711
- console.log("");
712
- }
713
- if (available.length > 0) {
714
- console.log(chalk2.bold(" Available:"));
715
- for (const feat of available.slice(0, 5)) {
716
- const pColor = feat.priority === "critical" || feat.priority === "high" ? chalk2.red : chalk2.gray;
717
- console.log(` \u25CB ${feat.name} ${pColor(`(${feat.priority})`)}`);
718
- }
719
- if (available.length > 5) {
720
- console.log(chalk2.gray(` ... and ${available.length - 5} more`));
774
+ console.log(
775
+ ` ${iconChalk(icon)} ${nameChalk(nameRaw)} ${bar2} ${prog}${descStr}${claimedStr}`
776
+ );
777
+ if (f.items) {
778
+ const lines = wrapItems(f.items, itemsMaxW);
779
+ for (const line of lines) {
780
+ console.log(chalk2.dim(`${contIndent}${line}`));
781
+ }
721
782
  }
722
- console.log("");
723
783
  }
724
- }
725
- function timeSince(isoDate) {
726
- const then = (/* @__PURE__ */ new Date(isoDate + "Z")).getTime();
727
- const now = Date.now();
728
- const diffMs = now - then;
729
- const mins = Math.floor(diffMs / 6e4);
730
- if (mins < 60) return `${mins}m`;
731
- const hours = Math.floor(mins / 60);
732
- const remainMins = mins % 60;
733
- return `${hours}h${remainMins > 0 ? String(remainMins).padStart(2, "0") : ""}`;
784
+ console.log("");
734
785
  }
735
786
 
736
787
  // src/commands/claim.ts
@@ -1008,6 +1059,11 @@ async function logCommand(options) {
1008
1059
  // src/commands/add.ts
1009
1060
  import chalk7 from "chalk";
1010
1061
  import { randomUUID as randomUUID2 } from "crypto";
1062
+ function parseProgress(s) {
1063
+ const m = s.match(/^(\d+)\/(\d+)$/);
1064
+ if (!m) return null;
1065
+ return { done: parseInt(m[1], 10), total: parseInt(m[2], 10) };
1066
+ }
1011
1067
  async function addCommand(type, options) {
1012
1068
  const db = await openDb();
1013
1069
  if (type === "feature") {
@@ -1018,14 +1074,40 @@ async function addCommand(type, options) {
1018
1074
  }
1019
1075
  const id = options.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1020
1076
  const priority = options.priority ?? "medium";
1077
+ let progressDone = null;
1078
+ let progressTotal = null;
1079
+ if (options.progress) {
1080
+ const p = parseProgress(options.progress);
1081
+ if (p) {
1082
+ progressDone = p.done;
1083
+ progressTotal = p.total;
1084
+ } else {
1085
+ console.log(chalk7.yellow(` \u26A0 --progress "${options.progress}" ignored (expected N/N format)`));
1086
+ }
1087
+ }
1088
+ const items = options.items ? options.items.split(",").map((s) => s.trim()).filter(Boolean).join(",") : null;
1021
1089
  db.run(
1022
- "INSERT INTO features (id, name, priority, description) VALUES (?, ?, ?, ?)",
1023
- [id, options.name, priority, options.description ?? null]
1090
+ `INSERT INTO features
1091
+ (id, name, priority, description, progress_done, progress_total, items)
1092
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
1093
+ [
1094
+ id,
1095
+ options.name,
1096
+ priority,
1097
+ options.description ?? null,
1098
+ progressDone,
1099
+ progressTotal,
1100
+ items
1101
+ ]
1024
1102
  );
1025
1103
  saveDb();
1026
1104
  closeDb();
1105
+ const extras = [];
1106
+ if (progressDone !== null) extras.push(`${progressDone}/${progressTotal}`);
1107
+ if (items) extras.push(`${items.split(",").length} items`);
1108
+ const suffix = extras.length ? chalk7.gray(` \u2014 ${extras.join(", ")}`) : "";
1027
1109
  console.log(chalk7.green(`
1028
- \u2713 Feature added: ${options.name} (${priority})
1110
+ \u2713 Feature added: ${options.name} (${priority})${suffix}
1029
1111
  `));
1030
1112
  } else if (type === "session") {
1031
1113
  const id = options.name ?? randomUUID2().slice(0, 8);
@@ -2092,6 +2174,82 @@ async function watchCommand(options) {
2092
2174
  });
2093
2175
  }
2094
2176
 
2177
+ // src/commands/update.ts
2178
+ import chalk13 from "chalk";
2179
+ function parseProgress2(s) {
2180
+ const m = s.match(/^(\d+)\/(\d+)$/);
2181
+ if (!m) return null;
2182
+ return { done: parseInt(m[1], 10), total: parseInt(m[2], 10) };
2183
+ }
2184
+ async function updateCommand(type, nameOrId, options) {
2185
+ if (type !== "feature") {
2186
+ console.log(chalk13.red(`
2187
+ Unknown type "${type}". Use "feature".
2188
+ `));
2189
+ process.exit(1);
2190
+ }
2191
+ const db = await openDb();
2192
+ const feature = queryOne(
2193
+ db,
2194
+ `SELECT id, name FROM features
2195
+ WHERE id = ?1 OR name = ?1
2196
+ OR id LIKE ?2 OR name LIKE ?2
2197
+ ORDER BY CASE WHEN id = ?1 OR name = ?1 THEN 0 ELSE 1 END
2198
+ LIMIT 1`,
2199
+ [nameOrId, `%${nameOrId}%`]
2200
+ );
2201
+ if (!feature) {
2202
+ console.log(chalk13.red(`
2203
+ Feature "${nameOrId}" not found.
2204
+ `));
2205
+ closeDb();
2206
+ process.exit(1);
2207
+ }
2208
+ const sets = [];
2209
+ const params = [];
2210
+ if (options.description !== void 0) {
2211
+ sets.push("description = ?");
2212
+ params.push(options.description);
2213
+ }
2214
+ if (options.items !== void 0) {
2215
+ const items = options.items.split(",").map((s) => s.trim()).filter(Boolean).join(",");
2216
+ sets.push("items = ?");
2217
+ params.push(items);
2218
+ }
2219
+ if (options.progress !== void 0) {
2220
+ const p = parseProgress2(options.progress);
2221
+ if (!p) {
2222
+ console.log(chalk13.yellow(` \u26A0 --progress "${options.progress}" ignored (expected N/N format)
2223
+ `));
2224
+ } else {
2225
+ sets.push("progress_done = ?", "progress_total = ?");
2226
+ params.push(p.done, p.total);
2227
+ }
2228
+ }
2229
+ if (options.priority !== void 0) {
2230
+ sets.push("priority = ?");
2231
+ params.push(options.priority);
2232
+ }
2233
+ if (options.status !== void 0) {
2234
+ sets.push("status = ?");
2235
+ params.push(options.status);
2236
+ }
2237
+ if (sets.length === 0) {
2238
+ console.log(chalk13.yellow("\n Nothing to update \u2014 pass at least one option.\n"));
2239
+ closeDb();
2240
+ return;
2241
+ }
2242
+ sets.push("updated_at = datetime('now')");
2243
+ params.push(feature.id);
2244
+ db.run(
2245
+ `UPDATE features SET ${sets.join(", ")} WHERE id = ?`,
2246
+ params
2247
+ );
2248
+ saveDb();
2249
+ closeDb();
2250
+ console.log(chalk13.green(` \u2713 Updated: ${feature.name}`));
2251
+ }
2252
+
2095
2253
  // src/index.ts
2096
2254
  var require2 = createRequire(import.meta.url);
2097
2255
  var pkg = require2("../package.json");
@@ -2104,7 +2262,7 @@ program.command("complete <feature>").description("Mark a feature as done and re
2104
2262
  program.command("sync").description("Regenerate PROJECT_STATE.md and AGENTS.md from SQLite").action(syncCommand);
2105
2263
  program.command("next").description("Show next available (unclaimed) feature").action(nextCommand);
2106
2264
  program.command("log").description("Show session timeline").option("-s, --session <id>", "Show details for a specific session").action(logCommand);
2107
- program.command("add <type>").description("Add a feature or session (type: feature, session)").option("-n, --name <name>", "Name").option("-p, --priority <priority>", "Priority (critical, high, medium, low)").option("-d, --description <desc>", "Description").option("--agent <agent>", "Agent type for sessions").action(addCommand);
2265
+ program.command("add <type>").description("Add a feature or session (type: feature, session)").option("-n, --name <name>", "Name").option("-p, --priority <priority>", "Priority (critical, high, medium, low)").option("-d, --description <desc>", "Description").option("--agent <agent>", "Agent type for sessions").option("--items <items>", "Comma-separated list of sub-items (features only)").option("--progress <N/N>", "Progress fraction e.g. 11/11 (features only)").action(addCommand);
2108
2266
  program.command("ingest").description("Parse a transcript and write session data to SQLite").option("--source <source>", "Source agent (claude-code, codex)", "claude-code").option("--session-id <id>", "Session ID").option("--transcript <path>", "Path to transcript JSONL file (auto-detected if omitted)").option("--project-path <path>", "Project path (defaults to cwd)").option("--no-sync", "Skip regenerating markdown after ingest").action(
2109
2267
  (opts) => ingestCommand({
2110
2268
  source: opts.source,
@@ -2123,4 +2281,13 @@ program.command("watch").description("Watch for session end and auto-ingest tran
2123
2281
  projectPath: opts.projectPath
2124
2282
  })
2125
2283
  );
2284
+ program.command("update <type> <name>").description("Update a feature's description, items, progress, or priority").option("-d, --description <desc>", "New description").option("--items <items>", "Comma-separated sub-items").option("--progress <N/N>", "Progress fraction e.g. 3/5").option("-p, --priority <priority>", "New priority").option("--status <status>", "New status (pending|in_progress|done|blocked)").action(
2285
+ (type, name, opts) => updateCommand(type, name, {
2286
+ description: opts.description,
2287
+ items: opts.items,
2288
+ progress: opts.progress,
2289
+ priority: opts.priority,
2290
+ status: opts.status
2291
+ })
2292
+ );
2126
2293
  program.parse();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@groundctl/cli",
3
- "version": "0.3.0",
4
- "description": "Product memory for AI agent builders",
3
+ "version": "0.3.2",
4
+ "description": "Always know what to build next.",
5
5
  "license": "MIT",
6
6
  "bin": {
7
7
  "groundctl": "dist/index.js"