@groundctl/cli 0.3.1 → 0.4.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/index.js +657 -271
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,9 +5,9 @@ import { Command } from "commander";
|
|
|
5
5
|
import { createRequire } from "module";
|
|
6
6
|
|
|
7
7
|
// src/commands/init.ts
|
|
8
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as
|
|
8
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, chmodSync, readFileSync as readFileSync3, appendFileSync } from "fs";
|
|
9
9
|
import { join as join3 } from "path";
|
|
10
|
-
import
|
|
10
|
+
import chalk2 from "chalk";
|
|
11
11
|
|
|
12
12
|
// src/storage/db.ts
|
|
13
13
|
import initSqlJs from "sql.js";
|
|
@@ -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)]
|
|
@@ -343,8 +352,6 @@ function generateAgentsMd(db, projectName) {
|
|
|
343
352
|
|
|
344
353
|
// src/ingest/git-import.ts
|
|
345
354
|
import { execSync } from "child_process";
|
|
346
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
347
|
-
import { join as join2 } from "path";
|
|
348
355
|
function run(cmd, cwd) {
|
|
349
356
|
try {
|
|
350
357
|
return execSync(cmd, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -400,67 +407,10 @@ function parseGitLog(cwd) {
|
|
|
400
407
|
}
|
|
401
408
|
return commits.reverse();
|
|
402
409
|
}
|
|
403
|
-
function parseProjectStateMd(content) {
|
|
404
|
-
const features = [];
|
|
405
|
-
const lines = content.split("\n");
|
|
406
|
-
let section = "";
|
|
407
|
-
for (const line of lines) {
|
|
408
|
-
const trimmed = line.trim();
|
|
409
|
-
if (trimmed.startsWith("## ")) {
|
|
410
|
-
section = trimmed.toLowerCase();
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
413
|
-
if (section.includes("decision") || section.includes("session") || section.includes("debt") || section.includes("note")) continue;
|
|
414
|
-
if (!trimmed.startsWith("- ") && !trimmed.startsWith("* ")) continue;
|
|
415
|
-
const item = trimmed.slice(2).trim();
|
|
416
|
-
if (!item || item.length < 3) continue;
|
|
417
|
-
const name = item.split("(")[0].split("\u2192")[0].split("\u2014")[0].trim();
|
|
418
|
-
if (!name || name.length < 3 || name.length > 80) continue;
|
|
419
|
-
if (/^\d{4}-\d{2}-\d{2}/.test(name)) continue;
|
|
420
|
-
if (name.split(" ").length > 8) continue;
|
|
421
|
-
let status = "pending";
|
|
422
|
-
let priority = "medium";
|
|
423
|
-
if (section.includes("built") || section.includes("done") || section.includes("complete")) {
|
|
424
|
-
status = "done";
|
|
425
|
-
} else if (section.includes("claimed") || section.includes("in progress") || section.includes("current")) {
|
|
426
|
-
status = "in_progress";
|
|
427
|
-
} else if (section.includes("available") || section.includes("next")) {
|
|
428
|
-
status = "pending";
|
|
429
|
-
} else if (section.includes("blocked")) {
|
|
430
|
-
status = "blocked";
|
|
431
|
-
}
|
|
432
|
-
if (/priority:\s*critical|critical\)/i.test(item)) priority = "critical";
|
|
433
|
-
else if (/priority:\s*high|high\)/i.test(item)) priority = "high";
|
|
434
|
-
else if (/priority:\s*low|low\)/i.test(item)) priority = "low";
|
|
435
|
-
features.push({ name, status, priority });
|
|
436
|
-
}
|
|
437
|
-
return features;
|
|
438
|
-
}
|
|
439
|
-
function featureIdFromName(name) {
|
|
440
|
-
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
|
|
441
|
-
}
|
|
442
410
|
function importFromGit(db, projectPath) {
|
|
443
411
|
let sessionsCreated = 0;
|
|
444
|
-
let featuresImported = 0;
|
|
445
|
-
const psMdPath = join2(projectPath, "PROJECT_STATE.md");
|
|
446
|
-
if (existsSync2(psMdPath)) {
|
|
447
|
-
const content = readFileSync2(psMdPath, "utf-8");
|
|
448
|
-
const features = parseProjectStateMd(content);
|
|
449
|
-
for (const feat of features) {
|
|
450
|
-
const id = featureIdFromName(feat.name);
|
|
451
|
-
if (!id) continue;
|
|
452
|
-
const exists = queryOne(db, "SELECT id FROM features WHERE id = ?", [id]);
|
|
453
|
-
if (!exists) {
|
|
454
|
-
db.run(
|
|
455
|
-
"INSERT INTO features (id, name, status, priority) VALUES (?, ?, ?, ?)",
|
|
456
|
-
[id, feat.name, feat.status, feat.priority]
|
|
457
|
-
);
|
|
458
|
-
featuresImported++;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
412
|
const commits = parseGitLog(projectPath);
|
|
463
|
-
if (commits.length === 0) return { sessionsCreated
|
|
413
|
+
if (commits.length === 0) return { sessionsCreated };
|
|
464
414
|
const SESSION_GAP_MS = 4 * 60 * 60 * 1e3;
|
|
465
415
|
const sessions = [];
|
|
466
416
|
let currentSession = [];
|
|
@@ -514,7 +464,286 @@ function importFromGit(db, projectPath) {
|
|
|
514
464
|
}
|
|
515
465
|
}
|
|
516
466
|
saveDb();
|
|
517
|
-
return { sessionsCreated
|
|
467
|
+
return { sessionsCreated };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// src/ingest/feature-detector.ts
|
|
471
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
472
|
+
import { join as join2 } from "path";
|
|
473
|
+
import { tmpdir } from "os";
|
|
474
|
+
import { execSync as execSync2 } from "child_process";
|
|
475
|
+
import { request as httpsRequest } from "https";
|
|
476
|
+
import { createInterface } from "readline";
|
|
477
|
+
import chalk from "chalk";
|
|
478
|
+
function run2(cmd, cwd) {
|
|
479
|
+
try {
|
|
480
|
+
return execSync2(cmd, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
481
|
+
} catch {
|
|
482
|
+
return "";
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
function collectContext(projectPath) {
|
|
486
|
+
const parts = [];
|
|
487
|
+
const gitLog = run2("git log --oneline --no-merges", projectPath);
|
|
488
|
+
if (gitLog.trim()) {
|
|
489
|
+
const lines = gitLog.trim().split("\n").slice(0, 150).join("\n");
|
|
490
|
+
parts.push(`## Git history (${lines.split("\n").length} commits)
|
|
491
|
+
${lines}`);
|
|
492
|
+
}
|
|
493
|
+
const diffStat = run2("git log --stat --no-merges --oneline -30", projectPath);
|
|
494
|
+
if (diffStat.trim()) {
|
|
495
|
+
parts.push(`## Recent commit file changes
|
|
496
|
+
${diffStat.trim().slice(0, 3e3)}`);
|
|
497
|
+
}
|
|
498
|
+
const find = run2(
|
|
499
|
+
[
|
|
500
|
+
"find . -type f",
|
|
501
|
+
"-not -path '*/node_modules/*'",
|
|
502
|
+
"-not -path '*/.git/*'",
|
|
503
|
+
"-not -path '*/dist/*'",
|
|
504
|
+
"-not -path '*/.groundctl/*'",
|
|
505
|
+
"-not -path '*/build/*'",
|
|
506
|
+
"-not -path '*/coverage/*'",
|
|
507
|
+
"-not -path '*/.venv/*'",
|
|
508
|
+
"-not -path '*/__pycache__/*'",
|
|
509
|
+
"-not -path '*/.pytest_cache/*'",
|
|
510
|
+
"-not -path '*/vendor/*'",
|
|
511
|
+
"-not -path '*/.next/*'",
|
|
512
|
+
"-not -name '*.lock'",
|
|
513
|
+
"-not -name '*.log'",
|
|
514
|
+
"-not -name '*.pyc'",
|
|
515
|
+
"| sort | head -120"
|
|
516
|
+
].join(" "),
|
|
517
|
+
projectPath
|
|
518
|
+
);
|
|
519
|
+
if (find.trim()) {
|
|
520
|
+
parts.push(`## Project file structure
|
|
521
|
+
${find.trim()}`);
|
|
522
|
+
}
|
|
523
|
+
const readmePath = join2(projectPath, "README.md");
|
|
524
|
+
if (existsSync2(readmePath)) {
|
|
525
|
+
const readme = readFileSync2(readmePath, "utf-8").slice(0, 3e3);
|
|
526
|
+
parts.push(`## README.md
|
|
527
|
+
${readme}`);
|
|
528
|
+
}
|
|
529
|
+
const psPath = join2(projectPath, "PROJECT_STATE.md");
|
|
530
|
+
if (existsSync2(psPath)) {
|
|
531
|
+
const ps = readFileSync2(psPath, "utf-8").slice(0, 2e3);
|
|
532
|
+
parts.push(`## Existing PROJECT_STATE.md
|
|
533
|
+
${ps}`);
|
|
534
|
+
}
|
|
535
|
+
return parts.join("\n\n");
|
|
536
|
+
}
|
|
537
|
+
var SYSTEM_PROMPT = "You are a product analyst. Analyze this project and identify the main product features.";
|
|
538
|
+
var USER_TEMPLATE = (context) => `Based on this git history and project structure, identify the product features with their status and priority.
|
|
539
|
+
|
|
540
|
+
Rules:
|
|
541
|
+
- Features are functional capabilities, not technical tasks
|
|
542
|
+
- Maximum 12 features
|
|
543
|
+
- status: "done" if all related commits are old and nothing is open, otherwise "open"
|
|
544
|
+
- priority: critical/high/medium/low
|
|
545
|
+
- name: short, kebab-case, human-readable (e.g. "user-auth", "data-pipeline")
|
|
546
|
+
- description: one sentence, what the feature does for the user
|
|
547
|
+
|
|
548
|
+
Respond ONLY with valid JSON, no markdown, no explanation:
|
|
549
|
+
{"features":[{"name":"...","status":"done","priority":"high","description":"..."}]}
|
|
550
|
+
|
|
551
|
+
Project context:
|
|
552
|
+
${context}`;
|
|
553
|
+
function httpsPost(opts) {
|
|
554
|
+
return new Promise((resolve3, reject) => {
|
|
555
|
+
const body = JSON.stringify({
|
|
556
|
+
model: opts.model,
|
|
557
|
+
max_tokens: opts.maxTokens ?? 1024,
|
|
558
|
+
system: opts.system,
|
|
559
|
+
messages: [{ role: "user", content: opts.userMessage }]
|
|
560
|
+
});
|
|
561
|
+
const req = httpsRequest(
|
|
562
|
+
{
|
|
563
|
+
hostname: "api.anthropic.com",
|
|
564
|
+
path: "/v1/messages",
|
|
565
|
+
method: "POST",
|
|
566
|
+
headers: {
|
|
567
|
+
"x-api-key": opts.apiKey,
|
|
568
|
+
"anthropic-version": "2023-06-01",
|
|
569
|
+
"content-type": "application/json",
|
|
570
|
+
"content-length": Buffer.byteLength(body)
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
(res) => {
|
|
574
|
+
let data = "";
|
|
575
|
+
res.on("data", (chunk) => {
|
|
576
|
+
data += chunk.toString();
|
|
577
|
+
});
|
|
578
|
+
res.on("end", () => {
|
|
579
|
+
resolve3(data);
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
);
|
|
583
|
+
req.on("error", reject);
|
|
584
|
+
req.write(body);
|
|
585
|
+
req.end();
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
function extractText(raw) {
|
|
589
|
+
const json = JSON.parse(raw);
|
|
590
|
+
if (json.error) throw new Error(`API error: ${json.error.message}`);
|
|
591
|
+
const block = (json.content ?? []).find((b) => b.type === "text");
|
|
592
|
+
if (!block) throw new Error("No text block in API response");
|
|
593
|
+
return block.text;
|
|
594
|
+
}
|
|
595
|
+
function parseFeatureJson(text) {
|
|
596
|
+
const stripped = text.replace(/^```[^\n]*\n?/, "").replace(/\n?```$/, "").trim();
|
|
597
|
+
let obj;
|
|
598
|
+
try {
|
|
599
|
+
obj = JSON.parse(stripped);
|
|
600
|
+
} catch {
|
|
601
|
+
const match = stripped.match(/\{[\s\S]*\}/);
|
|
602
|
+
if (!match) throw new Error("Could not parse JSON from model response");
|
|
603
|
+
obj = JSON.parse(match[0]);
|
|
604
|
+
}
|
|
605
|
+
if (!Array.isArray(obj.features)) throw new Error("Response missing 'features' array");
|
|
606
|
+
return obj.features.map((f) => ({
|
|
607
|
+
name: String(f.name ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""),
|
|
608
|
+
status: f.status === "done" ? "done" : "open",
|
|
609
|
+
priority: ["critical", "high", "medium", "low"].includes(f.priority) ? f.priority : "medium",
|
|
610
|
+
description: String(f.description ?? "").slice(0, 120)
|
|
611
|
+
})).filter((f) => f.name.length >= 2);
|
|
612
|
+
}
|
|
613
|
+
async function callClaude(apiKey, context) {
|
|
614
|
+
const raw = await httpsPost({
|
|
615
|
+
apiKey,
|
|
616
|
+
model: "claude-haiku-4-5-20251001",
|
|
617
|
+
system: SYSTEM_PROMPT,
|
|
618
|
+
userMessage: USER_TEMPLATE(context),
|
|
619
|
+
maxTokens: 1024
|
|
620
|
+
});
|
|
621
|
+
const text = extractText(raw);
|
|
622
|
+
return parseFeatureJson(text);
|
|
623
|
+
}
|
|
624
|
+
function renderFeatureList(features) {
|
|
625
|
+
console.log(chalk.bold(`
|
|
626
|
+
Detected ${features.length} features:
|
|
627
|
+
`));
|
|
628
|
+
for (const f of features) {
|
|
629
|
+
const statusIcon = f.status === "done" ? chalk.green("\u2713") : chalk.gray("\u25CB");
|
|
630
|
+
const prioColor = f.priority === "critical" || f.priority === "high" ? chalk.red : chalk.gray;
|
|
631
|
+
console.log(
|
|
632
|
+
` ${statusIcon} ${chalk.white(f.name.padEnd(28))}` + prioColor(`(${f.priority}, ${f.status})`.padEnd(18)) + chalk.gray(f.description)
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
console.log("");
|
|
636
|
+
}
|
|
637
|
+
function readLine(prompt) {
|
|
638
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
639
|
+
return new Promise((resolve3) => {
|
|
640
|
+
rl.question(prompt, (answer) => {
|
|
641
|
+
rl.close();
|
|
642
|
+
resolve3(answer.trim().toLowerCase());
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
async function editInEditor(features) {
|
|
647
|
+
const tmpPath = join2(tmpdir(), `groundctl-features-${Date.now()}.json`);
|
|
648
|
+
writeFileSync2(tmpPath, JSON.stringify({ features }, null, 2), "utf-8");
|
|
649
|
+
const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
|
|
650
|
+
try {
|
|
651
|
+
execSync2(`${editor} "${tmpPath}"`, { stdio: "inherit" });
|
|
652
|
+
} catch {
|
|
653
|
+
console.log(chalk.red(" Editor exited with error \u2014 using original features."));
|
|
654
|
+
return features;
|
|
655
|
+
}
|
|
656
|
+
try {
|
|
657
|
+
const edited = readFileSync2(tmpPath, "utf-8");
|
|
658
|
+
return parseFeatureJson(edited);
|
|
659
|
+
} catch (err) {
|
|
660
|
+
console.log(chalk.red(` Could not parse edited JSON: ${err.message}`));
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
function importFeatures(db, features) {
|
|
665
|
+
db.run(
|
|
666
|
+
`DELETE FROM features
|
|
667
|
+
WHERE id NOT IN (SELECT DISTINCT feature_id FROM claims)
|
|
668
|
+
AND status = 'pending'`
|
|
669
|
+
);
|
|
670
|
+
for (const f of features) {
|
|
671
|
+
const id = f.name;
|
|
672
|
+
const status = f.status === "done" ? "done" : "pending";
|
|
673
|
+
const exists = queryOne(db, "SELECT id FROM features WHERE id = ?", [id]);
|
|
674
|
+
if (!exists) {
|
|
675
|
+
db.run(
|
|
676
|
+
"INSERT INTO features (id, name, status, priority, description) VALUES (?, ?, ?, ?, ?)",
|
|
677
|
+
[id, f.name, status, f.priority, f.description]
|
|
678
|
+
);
|
|
679
|
+
} else {
|
|
680
|
+
db.run(
|
|
681
|
+
`UPDATE features
|
|
682
|
+
SET description = ?, priority = ?, updated_at = datetime('now')
|
|
683
|
+
WHERE id = ?`,
|
|
684
|
+
[f.description, f.priority, id]
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
saveDb();
|
|
689
|
+
}
|
|
690
|
+
async function detectAndImportFeatures(db, projectPath) {
|
|
691
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
692
|
+
if (!apiKey) {
|
|
693
|
+
console.log(chalk.yellow(
|
|
694
|
+
"\n Smart feature detection disabled \u2014 ANTHROPIC_API_KEY not set."
|
|
695
|
+
));
|
|
696
|
+
console.log(chalk.gray(" To enable:"));
|
|
697
|
+
console.log(chalk.gray(" export ANTHROPIC_API_KEY=sk-ant-..."));
|
|
698
|
+
console.log(chalk.gray(
|
|
699
|
+
"\n Or add features manually:\n groundctl add feature -n 'my-feature'\n"
|
|
700
|
+
));
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
console.log(chalk.gray(" Collecting project context..."));
|
|
704
|
+
const context = collectContext(projectPath);
|
|
705
|
+
console.log(chalk.gray(" Asking Claude to detect features..."));
|
|
706
|
+
let features;
|
|
707
|
+
try {
|
|
708
|
+
features = await callClaude(apiKey, context);
|
|
709
|
+
} catch (err) {
|
|
710
|
+
console.log(chalk.red(` \u2717 Feature detection failed: ${err.message}`));
|
|
711
|
+
console.log(chalk.gray(" Add features manually with: groundctl add feature -n 'my-feature'\n"));
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
if (features.length === 0) {
|
|
715
|
+
console.log(chalk.yellow(" No features detected \u2014 add them manually.\n"));
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
renderFeatureList(features);
|
|
719
|
+
let pending = features;
|
|
720
|
+
while (true) {
|
|
721
|
+
const answer = await readLine(
|
|
722
|
+
chalk.bold(" Import these features? ") + chalk.gray("[y/n/edit] ") + ""
|
|
723
|
+
);
|
|
724
|
+
if (answer === "y" || answer === "yes") {
|
|
725
|
+
importFeatures(db, pending);
|
|
726
|
+
console.log(chalk.green(`
|
|
727
|
+
\u2713 ${pending.length} features imported
|
|
728
|
+
`));
|
|
729
|
+
return true;
|
|
730
|
+
}
|
|
731
|
+
if (answer === "n" || answer === "no") {
|
|
732
|
+
console.log(chalk.gray(" Skipped \u2014 no features imported.\n"));
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
if (answer === "e" || answer === "edit") {
|
|
736
|
+
const edited = await editInEditor(pending);
|
|
737
|
+
if (edited && edited.length > 0) {
|
|
738
|
+
pending = edited;
|
|
739
|
+
renderFeatureList(pending);
|
|
740
|
+
} else {
|
|
741
|
+
console.log(chalk.yellow(" No valid features after edit \u2014 try again.\n"));
|
|
742
|
+
}
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
console.log(chalk.gray(" Please answer y, n, or edit."));
|
|
746
|
+
}
|
|
518
747
|
}
|
|
519
748
|
|
|
520
749
|
// src/commands/init.ts
|
|
@@ -554,53 +783,52 @@ echo "--- groundctl: Product state updated ---"
|
|
|
554
783
|
async function initCommand(options) {
|
|
555
784
|
const cwd = process.cwd();
|
|
556
785
|
const projectName = cwd.split("/").pop() ?? "unknown";
|
|
557
|
-
console.log(
|
|
786
|
+
console.log(chalk2.bold(`
|
|
558
787
|
groundctl init \u2014 ${projectName}
|
|
559
788
|
`));
|
|
560
|
-
console.log(
|
|
789
|
+
console.log(chalk2.gray(" Creating SQLite database..."));
|
|
561
790
|
const db = await openDb();
|
|
562
791
|
if (options.importFromGit) {
|
|
563
792
|
const isGitRepo = existsSync3(join3(cwd, ".git"));
|
|
564
793
|
if (!isGitRepo) {
|
|
565
|
-
console.log(
|
|
794
|
+
console.log(chalk2.yellow(" \u26A0 Not a git repo \u2014 skipping --import-from-git"));
|
|
566
795
|
} else {
|
|
567
|
-
console.log(
|
|
796
|
+
console.log(chalk2.gray(" Importing sessions from git history..."));
|
|
568
797
|
const result = importFromGit(db, cwd);
|
|
569
798
|
console.log(
|
|
570
|
-
|
|
571
|
-
` \u2713 Git import: ${result.sessionsCreated} sessions, ${result.featuresImported} features`
|
|
572
|
-
)
|
|
799
|
+
chalk2.green(` \u2713 Git import: ${result.sessionsCreated} sessions`)
|
|
573
800
|
);
|
|
801
|
+
await detectAndImportFeatures(db, cwd);
|
|
574
802
|
}
|
|
575
803
|
}
|
|
576
804
|
const projectState = generateProjectState(db, projectName);
|
|
577
805
|
const agentsMd = generateAgentsMd(db, projectName);
|
|
578
806
|
closeDb();
|
|
579
|
-
console.log(
|
|
807
|
+
console.log(chalk2.green(" \u2713 Database ready"));
|
|
580
808
|
const claudeHooksDir = join3(cwd, ".claude", "hooks");
|
|
581
809
|
if (!existsSync3(claudeHooksDir)) {
|
|
582
810
|
mkdirSync2(claudeHooksDir, { recursive: true });
|
|
583
811
|
}
|
|
584
|
-
|
|
812
|
+
writeFileSync3(join3(claudeHooksDir, "pre-session.sh"), PRE_SESSION_HOOK);
|
|
585
813
|
chmodSync(join3(claudeHooksDir, "pre-session.sh"), 493);
|
|
586
|
-
|
|
814
|
+
writeFileSync3(join3(claudeHooksDir, "post-session.sh"), POST_SESSION_HOOK);
|
|
587
815
|
chmodSync(join3(claudeHooksDir, "post-session.sh"), 493);
|
|
588
|
-
console.log(
|
|
816
|
+
console.log(chalk2.green(" \u2713 Claude Code hooks installed"));
|
|
589
817
|
const codexHooksDir = join3(cwd, ".codex", "hooks");
|
|
590
818
|
if (!existsSync3(codexHooksDir)) {
|
|
591
819
|
mkdirSync2(codexHooksDir, { recursive: true });
|
|
592
820
|
}
|
|
593
821
|
const codexPre = PRE_SESSION_HOOK.replace("Claude Code", "Codex");
|
|
594
822
|
const codexPost = POST_SESSION_HOOK.replace("Claude Code", "Codex").replace("claude-code", "codex");
|
|
595
|
-
|
|
823
|
+
writeFileSync3(join3(codexHooksDir, "pre-session.sh"), codexPre);
|
|
596
824
|
chmodSync(join3(codexHooksDir, "pre-session.sh"), 493);
|
|
597
|
-
|
|
825
|
+
writeFileSync3(join3(codexHooksDir, "post-session.sh"), codexPost);
|
|
598
826
|
chmodSync(join3(codexHooksDir, "post-session.sh"), 493);
|
|
599
|
-
console.log(
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
console.log(
|
|
603
|
-
console.log(
|
|
827
|
+
console.log(chalk2.green(" \u2713 Codex hooks installed"));
|
|
828
|
+
writeFileSync3(join3(cwd, "PROJECT_STATE.md"), projectState);
|
|
829
|
+
writeFileSync3(join3(cwd, "AGENTS.md"), agentsMd);
|
|
830
|
+
console.log(chalk2.green(" \u2713 PROJECT_STATE.md generated"));
|
|
831
|
+
console.log(chalk2.green(" \u2713 AGENTS.md generated"));
|
|
604
832
|
const gitignorePath = join3(cwd, ".gitignore");
|
|
605
833
|
const gitignoreEntry = "\n# groundctl local state\n.groundctl/\n";
|
|
606
834
|
if (existsSync3(gitignorePath)) {
|
|
@@ -609,68 +837,106 @@ groundctl init \u2014 ${projectName}
|
|
|
609
837
|
appendFileSync(gitignorePath, gitignoreEntry);
|
|
610
838
|
}
|
|
611
839
|
}
|
|
612
|
-
console.log(
|
|
840
|
+
console.log(chalk2.bold.green(`
|
|
613
841
|
\u2713 groundctl initialized for ${projectName}
|
|
614
842
|
`));
|
|
615
843
|
if (!options.importFromGit) {
|
|
616
|
-
console.log(
|
|
617
|
-
console.log(
|
|
618
|
-
console.log(
|
|
619
|
-
console.log(
|
|
620
|
-
console.log(
|
|
621
|
-
console.log(
|
|
844
|
+
console.log(chalk2.gray(" Next steps:"));
|
|
845
|
+
console.log(chalk2.gray(" groundctl add feature -n 'my-feature' -p high"));
|
|
846
|
+
console.log(chalk2.gray(" groundctl status"));
|
|
847
|
+
console.log(chalk2.gray(" groundctl claim my-feature"));
|
|
848
|
+
console.log(chalk2.gray("\n Or bootstrap from git history:"));
|
|
849
|
+
console.log(chalk2.gray(" groundctl init --import-from-git\n"));
|
|
622
850
|
} else {
|
|
623
|
-
console.log(
|
|
624
|
-
console.log(
|
|
625
|
-
console.log(
|
|
851
|
+
console.log(chalk2.gray(" Next steps:"));
|
|
852
|
+
console.log(chalk2.gray(" groundctl status"));
|
|
853
|
+
console.log(chalk2.gray(" groundctl next\n"));
|
|
626
854
|
}
|
|
627
855
|
}
|
|
628
856
|
|
|
629
857
|
// src/commands/status.ts
|
|
630
|
-
import
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
858
|
+
import chalk3 from "chalk";
|
|
859
|
+
var BAR_W = 14;
|
|
860
|
+
var NAME_W = 22;
|
|
861
|
+
var PROG_W = 6;
|
|
862
|
+
function progressBar(done, total, width) {
|
|
863
|
+
if (total <= 0) return chalk3.gray("\u2591".repeat(width));
|
|
864
|
+
const filled = Math.min(width, Math.round(done / total * width));
|
|
865
|
+
return chalk3.green("\u2588".repeat(filled)) + chalk3.gray("\u2591".repeat(width - filled));
|
|
866
|
+
}
|
|
867
|
+
function featureBar(status, progressDone, progressTotal) {
|
|
868
|
+
if (progressTotal != null && progressTotal > 0) {
|
|
869
|
+
return progressBar(progressDone ?? 0, progressTotal, BAR_W);
|
|
870
|
+
}
|
|
871
|
+
switch (status) {
|
|
872
|
+
case "done":
|
|
873
|
+
return progressBar(1, 1, BAR_W);
|
|
874
|
+
case "in_progress":
|
|
875
|
+
return progressBar(1, 2, BAR_W);
|
|
876
|
+
case "blocked":
|
|
877
|
+
return chalk3.red("\u2591".repeat(BAR_W));
|
|
878
|
+
default:
|
|
879
|
+
return chalk3.gray("\u2591".repeat(BAR_W));
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
function featureProgress(progressDone, progressTotal) {
|
|
883
|
+
if (progressDone != null && progressTotal != null) {
|
|
884
|
+
return `${progressDone}/${progressTotal}`;
|
|
885
|
+
}
|
|
886
|
+
return "";
|
|
887
|
+
}
|
|
888
|
+
function wrapItems(itemsCsv, maxWidth) {
|
|
889
|
+
const items = itemsCsv.split(",").map((s) => s.trim()).filter(Boolean);
|
|
890
|
+
const lines = [];
|
|
891
|
+
let current = "";
|
|
892
|
+
for (const item of items) {
|
|
893
|
+
const next = current ? `${current} \xB7 ${item}` : item;
|
|
894
|
+
if (next.length > maxWidth && current.length > 0) {
|
|
895
|
+
lines.push(current);
|
|
896
|
+
current = item;
|
|
897
|
+
} else {
|
|
898
|
+
current = next;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (current) lines.push(current);
|
|
902
|
+
return lines;
|
|
903
|
+
}
|
|
904
|
+
function timeSince(isoDate) {
|
|
905
|
+
const then = (/* @__PURE__ */ new Date(isoDate + "Z")).getTime();
|
|
906
|
+
const ms = Date.now() - then;
|
|
907
|
+
const mins = Math.floor(ms / 6e4);
|
|
908
|
+
if (mins < 60) return `${mins}m`;
|
|
909
|
+
const h = Math.floor(mins / 60);
|
|
910
|
+
const m = mins % 60;
|
|
911
|
+
return `${h}h${m > 0 ? String(m).padStart(2, "0") : ""}`;
|
|
636
912
|
}
|
|
637
913
|
async function statusCommand() {
|
|
638
914
|
const db = await openDb();
|
|
639
915
|
const projectName = process.cwd().split("/").pop() ?? "unknown";
|
|
640
|
-
const
|
|
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(
|
|
916
|
+
const features = query(
|
|
655
917
|
db,
|
|
656
|
-
`SELECT
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
const available = query(
|
|
662
|
-
db,
|
|
663
|
-
`SELECT f.id, f.name, f.priority
|
|
918
|
+
`SELECT
|
|
919
|
+
f.id, f.name, f.status, f.priority,
|
|
920
|
+
f.description, f.progress_done, f.progress_total, f.items,
|
|
921
|
+
c.session_id AS claimed_session,
|
|
922
|
+
c.claimed_at AS claimed_at
|
|
664
923
|
FROM features f
|
|
665
|
-
|
|
666
|
-
|
|
924
|
+
LEFT JOIN claims c
|
|
925
|
+
ON c.feature_id = f.id AND c.released_at IS NULL
|
|
667
926
|
ORDER BY
|
|
927
|
+
CASE f.status
|
|
928
|
+
WHEN 'in_progress' THEN 0
|
|
929
|
+
WHEN 'blocked' THEN 1
|
|
930
|
+
WHEN 'pending' THEN 2
|
|
931
|
+
WHEN 'done' THEN 3
|
|
932
|
+
END,
|
|
668
933
|
CASE f.priority
|
|
669
934
|
WHEN 'critical' THEN 0
|
|
670
|
-
WHEN 'high'
|
|
671
|
-
WHEN 'medium'
|
|
672
|
-
WHEN 'low'
|
|
673
|
-
END
|
|
935
|
+
WHEN 'high' THEN 1
|
|
936
|
+
WHEN 'medium' THEN 2
|
|
937
|
+
WHEN 'low' THEN 3
|
|
938
|
+
END,
|
|
939
|
+
f.created_at`
|
|
674
940
|
);
|
|
675
941
|
const sessionCount = queryOne(
|
|
676
942
|
db,
|
|
@@ -678,63 +944,67 @@ async function statusCommand() {
|
|
|
678
944
|
)?.count ?? 0;
|
|
679
945
|
closeDb();
|
|
680
946
|
console.log("");
|
|
681
|
-
if (
|
|
682
|
-
console.log(
|
|
947
|
+
if (features.length === 0) {
|
|
948
|
+
console.log(chalk3.bold(` ${projectName} \u2014 no features tracked yet
|
|
683
949
|
`));
|
|
684
|
-
console.log(
|
|
685
|
-
console.log(
|
|
950
|
+
console.log(chalk3.gray(" Add features with: groundctl add feature -n 'my-feature'"));
|
|
951
|
+
console.log(chalk3.gray(" Then run: groundctl status\n"));
|
|
686
952
|
return;
|
|
687
953
|
}
|
|
688
|
-
const
|
|
954
|
+
const total = features.length;
|
|
955
|
+
const done = features.filter((f) => f.status === "done").length;
|
|
956
|
+
const inProg = features.filter((f) => f.status === "in_progress").length;
|
|
957
|
+
const blocked = features.filter((f) => f.status === "blocked").length;
|
|
958
|
+
const pct = Math.round(done / total * 100);
|
|
689
959
|
console.log(
|
|
690
|
-
|
|
960
|
+
chalk3.bold(` ${projectName} \u2014 ${pct}% implemented`) + chalk3.gray(` (${sessionCount} session${sessionCount !== 1 ? "s" : ""})`)
|
|
691
961
|
);
|
|
692
962
|
console.log("");
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
);
|
|
696
|
-
if (
|
|
697
|
-
|
|
698
|
-
}
|
|
699
|
-
if (counts.blocked > 0) {
|
|
700
|
-
console.log(chalk2.red(` ${counts.blocked} blocked`));
|
|
701
|
-
}
|
|
963
|
+
const aggBar = progressBar(done, total, 20);
|
|
964
|
+
let aggSuffix = chalk3.white(` ${done}/${total} done`);
|
|
965
|
+
if (inProg > 0) aggSuffix += chalk3.yellow(` ${inProg} in progress`);
|
|
966
|
+
if (blocked > 0) aggSuffix += chalk3.red(` ${blocked} blocked`);
|
|
967
|
+
console.log(` Features ${aggBar}${aggSuffix}`);
|
|
702
968
|
console.log("");
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
969
|
+
const maxNameLen = Math.min(NAME_W, Math.max(...features.map((f) => f.name.length)));
|
|
970
|
+
const nameW = Math.max(maxNameLen, 12);
|
|
971
|
+
const contIndent = " ".repeat(4 + nameW + 1);
|
|
972
|
+
const itemsMaxW = Math.max(40, 76 - contIndent.length);
|
|
973
|
+
for (const f of features) {
|
|
974
|
+
const isDone = f.status === "done";
|
|
975
|
+
const isActive = f.status === "in_progress";
|
|
976
|
+
const isBlocked = f.status === "blocked";
|
|
977
|
+
const icon = isDone ? "\u2713" : isActive ? "\u25CF" : isBlocked ? "\u2717" : "\u25CB";
|
|
978
|
+
const iconChalk = isDone ? chalk3.green : isActive ? chalk3.yellow : isBlocked ? chalk3.red : chalk3.gray;
|
|
979
|
+
const nameRaw = f.name.slice(0, nameW).padEnd(nameW);
|
|
980
|
+
const nameChalk = isDone ? chalk3.dim : isActive ? chalk3.white : isBlocked ? chalk3.red : chalk3.gray;
|
|
981
|
+
const pd = f.progress_done ?? null;
|
|
982
|
+
const pt = f.progress_total ?? null;
|
|
983
|
+
const bar2 = featureBar(f.status, pd, pt);
|
|
984
|
+
const prog = featureProgress(pd, pt).padEnd(PROG_W);
|
|
985
|
+
const descRaw = f.description ?? "";
|
|
986
|
+
const descTrunc = descRaw.length > 38 ? descRaw.slice(0, 36) + "\u2026" : descRaw;
|
|
987
|
+
const descStr = descTrunc ? chalk3.gray(` ${descTrunc}`) : "";
|
|
988
|
+
let claimedStr = "";
|
|
989
|
+
if (isActive && f.claimed_session) {
|
|
990
|
+
const elapsed = f.claimed_at ? timeSince(f.claimed_at) : "";
|
|
991
|
+
claimedStr = chalk3.yellow(` \u2192 ${f.claimed_session}${elapsed ? ` (${elapsed})` : ""}`);
|
|
718
992
|
}
|
|
719
|
-
|
|
720
|
-
|
|
993
|
+
console.log(
|
|
994
|
+
` ${iconChalk(icon)} ${nameChalk(nameRaw)} ${bar2} ${prog}${descStr}${claimedStr}`
|
|
995
|
+
);
|
|
996
|
+
if (f.items) {
|
|
997
|
+
const lines = wrapItems(f.items, itemsMaxW);
|
|
998
|
+
for (const line of lines) {
|
|
999
|
+
console.log(chalk3.dim(`${contIndent}${line}`));
|
|
1000
|
+
}
|
|
721
1001
|
}
|
|
722
|
-
console.log("");
|
|
723
1002
|
}
|
|
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") : ""}`;
|
|
1003
|
+
console.log("");
|
|
734
1004
|
}
|
|
735
1005
|
|
|
736
1006
|
// src/commands/claim.ts
|
|
737
|
-
import
|
|
1007
|
+
import chalk4 from "chalk";
|
|
738
1008
|
import { randomUUID } from "crypto";
|
|
739
1009
|
function findFeature(db, term) {
|
|
740
1010
|
return queryOne(
|
|
@@ -771,15 +1041,15 @@ async function claimCommand(featureIdOrName, options) {
|
|
|
771
1041
|
const db = await openDb();
|
|
772
1042
|
const feature = findFeature(db, featureIdOrName);
|
|
773
1043
|
if (!feature) {
|
|
774
|
-
console.log(
|
|
1044
|
+
console.log(chalk4.red(`
|
|
775
1045
|
Feature "${featureIdOrName}" not found.
|
|
776
1046
|
`));
|
|
777
|
-
console.log(
|
|
1047
|
+
console.log(chalk4.gray(" Add it with: groundctl add feature -n '" + featureIdOrName + "'"));
|
|
778
1048
|
closeDb();
|
|
779
1049
|
process.exit(1);
|
|
780
1050
|
}
|
|
781
1051
|
if (feature.status === "done") {
|
|
782
|
-
console.log(
|
|
1052
|
+
console.log(chalk4.yellow(`
|
|
783
1053
|
Feature "${feature.name}" is already done.
|
|
784
1054
|
`));
|
|
785
1055
|
closeDb();
|
|
@@ -793,7 +1063,7 @@ async function claimCommand(featureIdOrName, options) {
|
|
|
793
1063
|
);
|
|
794
1064
|
if (existingClaim) {
|
|
795
1065
|
console.log(
|
|
796
|
-
|
|
1066
|
+
chalk4.red(`
|
|
797
1067
|
Feature "${feature.name}" is already claimed by session ${existingClaim.session_id}`)
|
|
798
1068
|
);
|
|
799
1069
|
const alternatives = query(
|
|
@@ -808,9 +1078,9 @@ async function claimCommand(featureIdOrName, options) {
|
|
|
808
1078
|
LIMIT 3`
|
|
809
1079
|
);
|
|
810
1080
|
if (alternatives.length > 0) {
|
|
811
|
-
console.log(
|
|
1081
|
+
console.log(chalk4.gray("\n Available instead:"));
|
|
812
1082
|
for (const alt of alternatives) {
|
|
813
|
-
console.log(
|
|
1083
|
+
console.log(chalk4.gray(` \u25CB ${alt.name}`));
|
|
814
1084
|
}
|
|
815
1085
|
}
|
|
816
1086
|
console.log("");
|
|
@@ -840,7 +1110,7 @@ async function claimCommand(featureIdOrName, options) {
|
|
|
840
1110
|
saveDb();
|
|
841
1111
|
closeDb();
|
|
842
1112
|
console.log(
|
|
843
|
-
|
|
1113
|
+
chalk4.green(`
|
|
844
1114
|
\u2713 Claimed "${feature.name}" \u2192 session ${sessionId}
|
|
845
1115
|
`)
|
|
846
1116
|
);
|
|
@@ -849,7 +1119,7 @@ async function completeCommand(featureIdOrName) {
|
|
|
849
1119
|
const db = await openDb();
|
|
850
1120
|
const feature = findFeature(db, featureIdOrName);
|
|
851
1121
|
if (!feature) {
|
|
852
|
-
console.log(
|
|
1122
|
+
console.log(chalk4.red(`
|
|
853
1123
|
Feature "${featureIdOrName}" not found.
|
|
854
1124
|
`));
|
|
855
1125
|
closeDb();
|
|
@@ -865,15 +1135,15 @@ async function completeCommand(featureIdOrName) {
|
|
|
865
1135
|
);
|
|
866
1136
|
saveDb();
|
|
867
1137
|
closeDb();
|
|
868
|
-
console.log(
|
|
1138
|
+
console.log(chalk4.green(`
|
|
869
1139
|
\u2713 Completed "${feature.name}"
|
|
870
1140
|
`));
|
|
871
1141
|
}
|
|
872
1142
|
|
|
873
1143
|
// src/commands/sync.ts
|
|
874
|
-
import { writeFileSync as
|
|
1144
|
+
import { writeFileSync as writeFileSync4 } from "fs";
|
|
875
1145
|
import { join as join4 } from "path";
|
|
876
|
-
import
|
|
1146
|
+
import chalk5 from "chalk";
|
|
877
1147
|
async function syncCommand(opts) {
|
|
878
1148
|
const db = await openDb();
|
|
879
1149
|
const projectName = process.cwd().split("/").pop() ?? "unknown";
|
|
@@ -881,16 +1151,16 @@ async function syncCommand(opts) {
|
|
|
881
1151
|
const agentsMd = generateAgentsMd(db, projectName);
|
|
882
1152
|
closeDb();
|
|
883
1153
|
const cwd = process.cwd();
|
|
884
|
-
|
|
885
|
-
|
|
1154
|
+
writeFileSync4(join4(cwd, "PROJECT_STATE.md"), projectState);
|
|
1155
|
+
writeFileSync4(join4(cwd, "AGENTS.md"), agentsMd);
|
|
886
1156
|
if (!opts?.silent) {
|
|
887
|
-
console.log(
|
|
888
|
-
console.log(
|
|
1157
|
+
console.log(chalk5.green("\n \u2713 PROJECT_STATE.md regenerated"));
|
|
1158
|
+
console.log(chalk5.green(" \u2713 AGENTS.md regenerated\n"));
|
|
889
1159
|
}
|
|
890
1160
|
}
|
|
891
1161
|
|
|
892
1162
|
// src/commands/next.ts
|
|
893
|
-
import
|
|
1163
|
+
import chalk6 from "chalk";
|
|
894
1164
|
async function nextCommand() {
|
|
895
1165
|
const db = await openDb();
|
|
896
1166
|
const available = query(
|
|
@@ -910,26 +1180,26 @@ async function nextCommand() {
|
|
|
910
1180
|
);
|
|
911
1181
|
closeDb();
|
|
912
1182
|
if (available.length === 0) {
|
|
913
|
-
console.log(
|
|
1183
|
+
console.log(chalk6.yellow("\n No available features to claim.\n"));
|
|
914
1184
|
return;
|
|
915
1185
|
}
|
|
916
|
-
console.log(
|
|
1186
|
+
console.log(chalk6.bold("\n Next available features:\n"));
|
|
917
1187
|
for (let i = 0; i < available.length; i++) {
|
|
918
1188
|
const feat = available[i];
|
|
919
|
-
const pColor = feat.priority === "critical" || feat.priority === "high" ?
|
|
920
|
-
const marker = i === 0 ?
|
|
1189
|
+
const pColor = feat.priority === "critical" || feat.priority === "high" ? chalk6.red : chalk6.gray;
|
|
1190
|
+
const marker = i === 0 ? chalk6.green("\u2192") : " ";
|
|
921
1191
|
console.log(` ${marker} ${feat.name} ${pColor(`(${feat.priority})`)}`);
|
|
922
1192
|
if (feat.description) {
|
|
923
|
-
console.log(
|
|
1193
|
+
console.log(chalk6.gray(` ${feat.description}`));
|
|
924
1194
|
}
|
|
925
1195
|
}
|
|
926
|
-
console.log(
|
|
1196
|
+
console.log(chalk6.gray(`
|
|
927
1197
|
Claim with: groundctl claim "${available[0].name}"
|
|
928
1198
|
`));
|
|
929
1199
|
}
|
|
930
1200
|
|
|
931
1201
|
// src/commands/log.ts
|
|
932
|
-
import
|
|
1202
|
+
import chalk7 from "chalk";
|
|
933
1203
|
async function logCommand(options) {
|
|
934
1204
|
const db = await openDb();
|
|
935
1205
|
if (options.session) {
|
|
@@ -938,18 +1208,18 @@ async function logCommand(options) {
|
|
|
938
1208
|
`%${options.session}%`
|
|
939
1209
|
]);
|
|
940
1210
|
if (!session) {
|
|
941
|
-
console.log(
|
|
1211
|
+
console.log(chalk7.red(`
|
|
942
1212
|
Session "${options.session}" not found.
|
|
943
1213
|
`));
|
|
944
1214
|
closeDb();
|
|
945
1215
|
return;
|
|
946
1216
|
}
|
|
947
|
-
console.log(
|
|
1217
|
+
console.log(chalk7.bold(`
|
|
948
1218
|
Session ${session.id}`));
|
|
949
|
-
console.log(
|
|
950
|
-
console.log(
|
|
1219
|
+
console.log(chalk7.gray(` Agent: ${session.agent}`));
|
|
1220
|
+
console.log(chalk7.gray(` Started: ${session.started_at}`));
|
|
951
1221
|
if (session.ended_at) {
|
|
952
|
-
console.log(
|
|
1222
|
+
console.log(chalk7.gray(` Ended: ${session.ended_at}`));
|
|
953
1223
|
}
|
|
954
1224
|
if (session.summary) {
|
|
955
1225
|
console.log(`
|
|
@@ -961,11 +1231,11 @@ async function logCommand(options) {
|
|
|
961
1231
|
[session.id]
|
|
962
1232
|
);
|
|
963
1233
|
if (decisions.length > 0) {
|
|
964
|
-
console.log(
|
|
1234
|
+
console.log(chalk7.bold("\n Decisions:"));
|
|
965
1235
|
for (const d of decisions) {
|
|
966
1236
|
console.log(` \u2022 ${d.description}`);
|
|
967
1237
|
if (d.rationale) {
|
|
968
|
-
console.log(
|
|
1238
|
+
console.log(chalk7.gray(` ${d.rationale}`));
|
|
969
1239
|
}
|
|
970
1240
|
}
|
|
971
1241
|
}
|
|
@@ -975,10 +1245,10 @@ async function logCommand(options) {
|
|
|
975
1245
|
[session.id]
|
|
976
1246
|
);
|
|
977
1247
|
if (files.length > 0) {
|
|
978
|
-
console.log(
|
|
1248
|
+
console.log(chalk7.bold(`
|
|
979
1249
|
Files modified (${files.length}):`));
|
|
980
1250
|
for (const f of files) {
|
|
981
|
-
const op = f.operation === "created" ?
|
|
1251
|
+
const op = f.operation === "created" ? chalk7.green("+") : f.operation === "deleted" ? chalk7.red("-") : chalk7.yellow("~");
|
|
982
1252
|
console.log(` ${op} ${f.path} (${f.lines_changed} lines)`);
|
|
983
1253
|
}
|
|
984
1254
|
}
|
|
@@ -986,18 +1256,18 @@ async function logCommand(options) {
|
|
|
986
1256
|
} else {
|
|
987
1257
|
const sessions = query(db, "SELECT * FROM sessions ORDER BY started_at DESC LIMIT 20");
|
|
988
1258
|
if (sessions.length === 0) {
|
|
989
|
-
console.log(
|
|
1259
|
+
console.log(chalk7.yellow("\n No sessions recorded yet.\n"));
|
|
990
1260
|
closeDb();
|
|
991
1261
|
return;
|
|
992
1262
|
}
|
|
993
|
-
console.log(
|
|
1263
|
+
console.log(chalk7.bold("\n Session timeline:\n"));
|
|
994
1264
|
for (const s of sessions) {
|
|
995
|
-
const status = s.ended_at ?
|
|
1265
|
+
const status = s.ended_at ? chalk7.green("done") : chalk7.yellow("active");
|
|
996
1266
|
console.log(
|
|
997
|
-
` ${
|
|
1267
|
+
` ${chalk7.bold(s.id)} ${chalk7.gray(s.started_at)} ${status} ${chalk7.gray(s.agent)}`
|
|
998
1268
|
);
|
|
999
1269
|
if (s.summary) {
|
|
1000
|
-
console.log(
|
|
1270
|
+
console.log(chalk7.gray(` ${s.summary}`));
|
|
1001
1271
|
}
|
|
1002
1272
|
}
|
|
1003
1273
|
console.log("");
|
|
@@ -1006,26 +1276,57 @@ async function logCommand(options) {
|
|
|
1006
1276
|
}
|
|
1007
1277
|
|
|
1008
1278
|
// src/commands/add.ts
|
|
1009
|
-
import
|
|
1279
|
+
import chalk8 from "chalk";
|
|
1010
1280
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1281
|
+
function parseProgress(s) {
|
|
1282
|
+
const m = s.match(/^(\d+)\/(\d+)$/);
|
|
1283
|
+
if (!m) return null;
|
|
1284
|
+
return { done: parseInt(m[1], 10), total: parseInt(m[2], 10) };
|
|
1285
|
+
}
|
|
1011
1286
|
async function addCommand(type, options) {
|
|
1012
1287
|
const db = await openDb();
|
|
1013
1288
|
if (type === "feature") {
|
|
1014
1289
|
if (!options.name) {
|
|
1015
|
-
console.log(
|
|
1290
|
+
console.log(chalk8.red("\n --name is required for features.\n"));
|
|
1016
1291
|
closeDb();
|
|
1017
1292
|
process.exit(1);
|
|
1018
1293
|
}
|
|
1019
1294
|
const id = options.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1020
1295
|
const priority = options.priority ?? "medium";
|
|
1296
|
+
let progressDone = null;
|
|
1297
|
+
let progressTotal = null;
|
|
1298
|
+
if (options.progress) {
|
|
1299
|
+
const p = parseProgress(options.progress);
|
|
1300
|
+
if (p) {
|
|
1301
|
+
progressDone = p.done;
|
|
1302
|
+
progressTotal = p.total;
|
|
1303
|
+
} else {
|
|
1304
|
+
console.log(chalk8.yellow(` \u26A0 --progress "${options.progress}" ignored (expected N/N format)`));
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
const items = options.items ? options.items.split(",").map((s) => s.trim()).filter(Boolean).join(",") : null;
|
|
1021
1308
|
db.run(
|
|
1022
|
-
|
|
1023
|
-
|
|
1309
|
+
`INSERT INTO features
|
|
1310
|
+
(id, name, priority, description, progress_done, progress_total, items)
|
|
1311
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
1312
|
+
[
|
|
1313
|
+
id,
|
|
1314
|
+
options.name,
|
|
1315
|
+
priority,
|
|
1316
|
+
options.description ?? null,
|
|
1317
|
+
progressDone,
|
|
1318
|
+
progressTotal,
|
|
1319
|
+
items
|
|
1320
|
+
]
|
|
1024
1321
|
);
|
|
1025
1322
|
saveDb();
|
|
1026
1323
|
closeDb();
|
|
1027
|
-
|
|
1028
|
-
|
|
1324
|
+
const extras = [];
|
|
1325
|
+
if (progressDone !== null) extras.push(`${progressDone}/${progressTotal}`);
|
|
1326
|
+
if (items) extras.push(`${items.split(",").length} items`);
|
|
1327
|
+
const suffix = extras.length ? chalk8.gray(` \u2014 ${extras.join(", ")}`) : "";
|
|
1328
|
+
console.log(chalk8.green(`
|
|
1329
|
+
\u2713 Feature added: ${options.name} (${priority})${suffix}
|
|
1029
1330
|
`));
|
|
1030
1331
|
} else if (type === "session") {
|
|
1031
1332
|
const id = options.name ?? randomUUID2().slice(0, 8);
|
|
@@ -1033,11 +1334,11 @@ async function addCommand(type, options) {
|
|
|
1033
1334
|
db.run("INSERT INTO sessions (id, agent) VALUES (?, ?)", [id, agent]);
|
|
1034
1335
|
saveDb();
|
|
1035
1336
|
closeDb();
|
|
1036
|
-
console.log(
|
|
1337
|
+
console.log(chalk8.green(`
|
|
1037
1338
|
\u2713 Session created: ${id} (${agent})
|
|
1038
1339
|
`));
|
|
1039
1340
|
} else {
|
|
1040
|
-
console.log(
|
|
1341
|
+
console.log(chalk8.red(`
|
|
1041
1342
|
Unknown type "${type}". Use "feature" or "session".
|
|
1042
1343
|
`));
|
|
1043
1344
|
closeDb();
|
|
@@ -1049,7 +1350,7 @@ async function addCommand(type, options) {
|
|
|
1049
1350
|
import { existsSync as existsSync4, readdirSync } from "fs";
|
|
1050
1351
|
import { join as join5, resolve } from "path";
|
|
1051
1352
|
import { homedir as homedir2 } from "os";
|
|
1052
|
-
import
|
|
1353
|
+
import chalk9 from "chalk";
|
|
1053
1354
|
|
|
1054
1355
|
// src/ingest/claude-parser.ts
|
|
1055
1356
|
import { readFileSync as readFileSync4 } from "fs";
|
|
@@ -1319,11 +1620,11 @@ async function ingestCommand(options) {
|
|
|
1319
1620
|
transcriptPath = findLatestTranscript(projectPath) ?? void 0;
|
|
1320
1621
|
}
|
|
1321
1622
|
if (!transcriptPath || !existsSync4(transcriptPath)) {
|
|
1322
|
-
console.log(
|
|
1623
|
+
console.log(chalk9.yellow("\n No transcript found. Skipping ingest.\n"));
|
|
1323
1624
|
if (!options.noSync) await syncCommand();
|
|
1324
1625
|
return;
|
|
1325
1626
|
}
|
|
1326
|
-
console.log(
|
|
1627
|
+
console.log(chalk9.gray(`
|
|
1327
1628
|
Parsing transcript: ${transcriptPath.split("/").slice(-2).join("/")}`));
|
|
1328
1629
|
const parsed = parseTranscript(transcriptPath, options.sessionId ?? "auto", projectPath);
|
|
1329
1630
|
const db = await openDb();
|
|
@@ -1373,16 +1674,16 @@ async function ingestCommand(options) {
|
|
|
1373
1674
|
saveDb();
|
|
1374
1675
|
closeDb();
|
|
1375
1676
|
console.log(
|
|
1376
|
-
|
|
1677
|
+
chalk9.green(
|
|
1377
1678
|
` \u2713 Ingested session ${sessionId}: ${newFiles} files, ${parsed.commits.length} commits, ${newDecisions} decisions`
|
|
1378
1679
|
)
|
|
1379
1680
|
);
|
|
1380
1681
|
if (parsed.decisions.length > 0 && newDecisions > 0) {
|
|
1381
|
-
console.log(
|
|
1682
|
+
console.log(chalk9.gray(`
|
|
1382
1683
|
Decisions captured:`));
|
|
1383
1684
|
for (const d of parsed.decisions.slice(0, 5)) {
|
|
1384
|
-
const conf = d.confidence === "low" ?
|
|
1385
|
-
console.log(
|
|
1685
|
+
const conf = d.confidence === "low" ? chalk9.gray(" (low confidence)") : "";
|
|
1686
|
+
console.log(chalk9.gray(` \u2022 ${d.description.slice(0, 80)}${conf}`));
|
|
1386
1687
|
}
|
|
1387
1688
|
}
|
|
1388
1689
|
if (!options.noSync) {
|
|
@@ -1392,9 +1693,9 @@ async function ingestCommand(options) {
|
|
|
1392
1693
|
}
|
|
1393
1694
|
|
|
1394
1695
|
// src/commands/report.ts
|
|
1395
|
-
import { writeFileSync as
|
|
1696
|
+
import { writeFileSync as writeFileSync5 } from "fs";
|
|
1396
1697
|
import { join as join6 } from "path";
|
|
1397
|
-
import
|
|
1698
|
+
import chalk10 from "chalk";
|
|
1398
1699
|
function formatDuration(start, end) {
|
|
1399
1700
|
if (!end) return "ongoing";
|
|
1400
1701
|
const startMs = new Date(start).getTime();
|
|
@@ -1489,7 +1790,7 @@ async function reportCommand(options) {
|
|
|
1489
1790
|
[options.session, `%${options.session}%`]
|
|
1490
1791
|
);
|
|
1491
1792
|
if (!s) {
|
|
1492
|
-
console.log(
|
|
1793
|
+
console.log(chalk10.red(`
|
|
1493
1794
|
Session "${options.session}" not found.
|
|
1494
1795
|
`));
|
|
1495
1796
|
closeDb();
|
|
@@ -1502,7 +1803,7 @@ async function reportCommand(options) {
|
|
|
1502
1803
|
"SELECT * FROM sessions ORDER BY started_at DESC LIMIT 1"
|
|
1503
1804
|
);
|
|
1504
1805
|
if (!s) {
|
|
1505
|
-
console.log(
|
|
1806
|
+
console.log(chalk10.yellow("\n No sessions found. Run groundctl init first.\n"));
|
|
1506
1807
|
closeDb();
|
|
1507
1808
|
return;
|
|
1508
1809
|
}
|
|
@@ -1544,8 +1845,8 @@ async function reportCommand(options) {
|
|
|
1544
1845
|
}
|
|
1545
1846
|
closeDb();
|
|
1546
1847
|
const outPath2 = join6(cwd, "SESSION_HISTORY.md");
|
|
1547
|
-
|
|
1548
|
-
console.log(
|
|
1848
|
+
writeFileSync5(outPath2, fullReport);
|
|
1849
|
+
console.log(chalk10.green(`
|
|
1549
1850
|
\u2713 SESSION_HISTORY.md written (${sessions.length} sessions)
|
|
1550
1851
|
`));
|
|
1551
1852
|
return;
|
|
@@ -1571,16 +1872,16 @@ async function reportCommand(options) {
|
|
|
1571
1872
|
activeClaims
|
|
1572
1873
|
);
|
|
1573
1874
|
const outPath = join6(cwd, "SESSION_REPORT.md");
|
|
1574
|
-
|
|
1575
|
-
console.log(
|
|
1875
|
+
writeFileSync5(outPath, report);
|
|
1876
|
+
console.log(chalk10.green(`
|
|
1576
1877
|
\u2713 SESSION_REPORT.md written (session ${session.id})
|
|
1577
1878
|
`));
|
|
1578
|
-
console.log(
|
|
1879
|
+
console.log(chalk10.gray(` ${files.length} files \xB7 ${decisions.length} arch log entries \xB7 ${completedFeatures.length} features completed`));
|
|
1579
1880
|
console.log("");
|
|
1580
1881
|
}
|
|
1581
1882
|
|
|
1582
1883
|
// src/commands/health.ts
|
|
1583
|
-
import
|
|
1884
|
+
import chalk11 from "chalk";
|
|
1584
1885
|
async function healthCommand() {
|
|
1585
1886
|
const db = await openDb();
|
|
1586
1887
|
const projectName = process.cwd().split("/").pop() ?? "unknown";
|
|
@@ -1631,32 +1932,32 @@ async function healthCommand() {
|
|
|
1631
1932
|
closeDb();
|
|
1632
1933
|
const totalScore = featureScore + testScore + decisionScore + claimScore + deployScore;
|
|
1633
1934
|
console.log("");
|
|
1634
|
-
console.log(
|
|
1935
|
+
console.log(chalk11.bold(` ${projectName} \u2014 Health Score: ${totalScore}/100
|
|
1635
1936
|
`));
|
|
1636
|
-
const featureColor = featurePct >= 0.7 ?
|
|
1937
|
+
const featureColor = featurePct >= 0.7 ? chalk11.green : featurePct >= 0.4 ? chalk11.yellow : chalk11.red;
|
|
1637
1938
|
const featureMark = featurePct >= 0.4 ? "\u2705" : "\u26A0\uFE0F ";
|
|
1638
1939
|
console.log(
|
|
1639
|
-
` ${featureMark} Features ${String(counts.done).padStart(2)}/${total} complete` + featureColor(` (${Math.round(featurePct * 100)}%)`) +
|
|
1940
|
+
` ${featureMark} Features ${String(counts.done).padStart(2)}/${total} complete` + featureColor(` (${Math.round(featurePct * 100)}%)`) + chalk11.gray(` +${featureScore}pts`)
|
|
1640
1941
|
);
|
|
1641
1942
|
const testMark = testFiles > 0 ? "\u2705" : "\u26A0\uFE0F ";
|
|
1642
|
-
const testColor = testFiles > 0 ?
|
|
1943
|
+
const testColor = testFiles > 0 ? chalk11.green : chalk11.red;
|
|
1643
1944
|
console.log(
|
|
1644
|
-
` ${testMark} Tests ${testColor(String(testFiles) + " test files")}` + (testFiles === 0 ?
|
|
1945
|
+
` ${testMark} Tests ${testColor(String(testFiles) + " test files")}` + (testFiles === 0 ? chalk11.red(" (-20pts)") : chalk11.gray(` +${testScore}pts`))
|
|
1645
1946
|
);
|
|
1646
1947
|
const decMark = decisionCount > 0 ? "\u2705" : "\u26A0\uFE0F ";
|
|
1647
|
-
const decColor = decisionCount > 0 ?
|
|
1948
|
+
const decColor = decisionCount > 0 ? chalk11.green : chalk11.yellow;
|
|
1648
1949
|
console.log(
|
|
1649
|
-
` ${decMark} Arch log ${decColor(decisionCount + " entries")}` +
|
|
1950
|
+
` ${decMark} Arch log ${decColor(decisionCount + " entries")}` + chalk11.gray(` +${decisionScore}pts`)
|
|
1650
1951
|
);
|
|
1651
1952
|
const claimMark = staleClaims === 0 ? "\u2705" : "\u26A0\uFE0F ";
|
|
1652
|
-
const claimColor = staleClaims === 0 ?
|
|
1953
|
+
const claimColor = staleClaims === 0 ? chalk11.green : chalk11.red;
|
|
1653
1954
|
console.log(
|
|
1654
|
-
` ${claimMark} Claims ${claimColor(staleClaims > 0 ? staleClaims + " stale (>24h)" : "0 stale")}` +
|
|
1955
|
+
` ${claimMark} Claims ${claimColor(staleClaims > 0 ? staleClaims + " stale (>24h)" : "0 stale")}` + chalk11.gray(` +${claimScore}pts`)
|
|
1655
1956
|
);
|
|
1656
1957
|
const deployMark = deployScore > 0 ? "\u2705" : "\u26A0\uFE0F ";
|
|
1657
|
-
const deployLabel = deployScore > 0 ?
|
|
1958
|
+
const deployLabel = deployScore > 0 ? chalk11.green("detected") : chalk11.gray("not detected");
|
|
1658
1959
|
console.log(
|
|
1659
|
-
` ${deployMark} Deploy ${deployLabel}` + (deployScore > 0 ?
|
|
1960
|
+
` ${deployMark} Deploy ${deployLabel}` + (deployScore > 0 ? chalk11.gray(` +${deployScore}pts`) : chalk11.gray(" +0pts"))
|
|
1660
1961
|
);
|
|
1661
1962
|
console.log("");
|
|
1662
1963
|
const recommendations = [];
|
|
@@ -1665,9 +1966,9 @@ async function healthCommand() {
|
|
|
1665
1966
|
if (decisionCount === 0) recommendations.push("Log architecture decisions during sessions so agents understand the why.");
|
|
1666
1967
|
if (featurePct < 0.5 && total > 0) recommendations.push(`${counts.pending} features pending \u2014 run groundctl next to pick one.`);
|
|
1667
1968
|
if (recommendations.length > 0) {
|
|
1668
|
-
console.log(
|
|
1969
|
+
console.log(chalk11.bold(" Recommendations:"));
|
|
1669
1970
|
for (const r of recommendations) {
|
|
1670
|
-
console.log(
|
|
1971
|
+
console.log(chalk11.yellow(` \u2192 ${r}`));
|
|
1671
1972
|
}
|
|
1672
1973
|
console.log("");
|
|
1673
1974
|
}
|
|
@@ -1678,7 +1979,7 @@ import { createServer } from "http";
|
|
|
1678
1979
|
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
1679
1980
|
import { join as join7, dirname as dirname2 } from "path";
|
|
1680
1981
|
import { exec } from "child_process";
|
|
1681
|
-
import
|
|
1982
|
+
import chalk12 from "chalk";
|
|
1682
1983
|
import initSqlJs2 from "sql.js";
|
|
1683
1984
|
function findDbPath(startDir = process.cwd()) {
|
|
1684
1985
|
let dir = startDir;
|
|
@@ -1841,9 +2142,9 @@ async function dashboardCommand(options) {
|
|
|
1841
2142
|
}
|
|
1842
2143
|
});
|
|
1843
2144
|
server.listen(port, "127.0.0.1", () => {
|
|
1844
|
-
console.log(
|
|
1845
|
-
groundctl dashboard \u2192 `) +
|
|
1846
|
-
console.log(
|
|
2145
|
+
console.log(chalk12.bold(`
|
|
2146
|
+
groundctl dashboard \u2192 `) + chalk12.blue(`http://localhost:${port}`) + "\n");
|
|
2147
|
+
console.log(chalk12.gray(" Auto-refreshes every 10s. Press Ctrl+C to stop.\n"));
|
|
1847
2148
|
exec(`open http://localhost:${port} 2>/dev/null || xdg-open http://localhost:${port} 2>/dev/null || true`);
|
|
1848
2149
|
});
|
|
1849
2150
|
await new Promise((_, reject) => {
|
|
@@ -1856,7 +2157,7 @@ import {
|
|
|
1856
2157
|
existsSync as existsSync6,
|
|
1857
2158
|
readdirSync as readdirSync2,
|
|
1858
2159
|
statSync,
|
|
1859
|
-
writeFileSync as
|
|
2160
|
+
writeFileSync as writeFileSync6,
|
|
1860
2161
|
readFileSync as readFileSync6,
|
|
1861
2162
|
mkdirSync as mkdirSync3,
|
|
1862
2163
|
watch as fsWatch
|
|
@@ -1864,7 +2165,7 @@ import {
|
|
|
1864
2165
|
import { join as join8, resolve as resolve2 } from "path";
|
|
1865
2166
|
import { homedir as homedir3 } from "os";
|
|
1866
2167
|
import { spawn } from "child_process";
|
|
1867
|
-
import
|
|
2168
|
+
import chalk13 from "chalk";
|
|
1868
2169
|
var DEBOUNCE_MS = 8e3;
|
|
1869
2170
|
var DIR_POLL_MS = 5e3;
|
|
1870
2171
|
function claudeEncode2(p) {
|
|
@@ -1896,7 +2197,7 @@ function fileSize(p) {
|
|
|
1896
2197
|
function writePidFile(groundctlDir, pid) {
|
|
1897
2198
|
try {
|
|
1898
2199
|
mkdirSync3(groundctlDir, { recursive: true });
|
|
1899
|
-
|
|
2200
|
+
writeFileSync6(join8(groundctlDir, "watch.pid"), String(pid), "utf8");
|
|
1900
2201
|
} catch {
|
|
1901
2202
|
}
|
|
1902
2203
|
}
|
|
@@ -1919,8 +2220,8 @@ function processAlive(pid) {
|
|
|
1919
2220
|
async function runIngest(transcriptPath, projectPath) {
|
|
1920
2221
|
const filename = transcriptPath.split("/").slice(-2).join("/");
|
|
1921
2222
|
console.log(
|
|
1922
|
-
|
|
1923
|
-
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] `) +
|
|
2223
|
+
chalk13.gray(`
|
|
2224
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] `) + chalk13.cyan(`Transcript stable \u2192 ingesting ${filename}`)
|
|
1924
2225
|
);
|
|
1925
2226
|
try {
|
|
1926
2227
|
const parsed = parseTranscript(transcriptPath, "auto", projectPath);
|
|
@@ -1976,11 +2277,11 @@ async function runIngest(transcriptPath, projectPath) {
|
|
|
1976
2277
|
if (parsed.commits.length > 0) parts.push(`${parsed.commits.length} commit${parsed.commits.length !== 1 ? "s" : ""}`);
|
|
1977
2278
|
if (newDecisions > 0) parts.push(`${newDecisions} decision${newDecisions !== 1 ? "s" : ""} captured`);
|
|
1978
2279
|
const summary = parts.length > 0 ? parts.join(", ") : "no new data";
|
|
1979
|
-
console.log(
|
|
2280
|
+
console.log(chalk13.green(` \u2713 Session ingested \u2014 ${summary}`));
|
|
1980
2281
|
await syncCommand({ silent: true });
|
|
1981
|
-
console.log(
|
|
2282
|
+
console.log(chalk13.gray(" \u21B3 PROJECT_STATE.md + AGENTS.md updated"));
|
|
1982
2283
|
} catch (err) {
|
|
1983
|
-
console.log(
|
|
2284
|
+
console.log(chalk13.red(` \u2717 Ingest failed: ${err.message}`));
|
|
1984
2285
|
}
|
|
1985
2286
|
}
|
|
1986
2287
|
function startWatcher(transcriptDir, projectPath) {
|
|
@@ -2028,12 +2329,12 @@ function startWatcher(transcriptDir, projectPath) {
|
|
|
2028
2329
|
schedule(fp);
|
|
2029
2330
|
}
|
|
2030
2331
|
});
|
|
2031
|
-
console.log(
|
|
2332
|
+
console.log(chalk13.bold("\n groundctl watch") + chalk13.gray(" \u2014 auto-ingest on session end\n"));
|
|
2032
2333
|
console.log(
|
|
2033
|
-
|
|
2334
|
+
chalk13.gray(" Watching: ") + chalk13.blue(transcriptDir.replace(homedir3(), "~"))
|
|
2034
2335
|
);
|
|
2035
|
-
console.log(
|
|
2036
|
-
console.log(
|
|
2336
|
+
console.log(chalk13.gray(" Stability threshold: ") + chalk13.white(`${DEBOUNCE_MS / 1e3}s`));
|
|
2337
|
+
console.log(chalk13.gray(" Press Ctrl+C to stop.\n"));
|
|
2037
2338
|
}
|
|
2038
2339
|
async function watchCommand(options) {
|
|
2039
2340
|
const projectPath = options.projectPath ? resolve2(options.projectPath) : process.cwd();
|
|
@@ -2046,29 +2347,29 @@ async function watchCommand(options) {
|
|
|
2046
2347
|
child.unref();
|
|
2047
2348
|
const groundctlDir2 = join8(projectPath, ".groundctl");
|
|
2048
2349
|
writePidFile(groundctlDir2, child.pid);
|
|
2049
|
-
console.log(
|
|
2350
|
+
console.log(chalk13.green(`
|
|
2050
2351
|
\u2713 groundctl watch running in background (PID ${child.pid})`));
|
|
2051
|
-
console.log(
|
|
2052
|
-
console.log(
|
|
2352
|
+
console.log(chalk13.gray(` PID saved to .groundctl/watch.pid`));
|
|
2353
|
+
console.log(chalk13.gray(` To stop: kill ${child.pid}
|
|
2053
2354
|
`));
|
|
2054
2355
|
process.exit(0);
|
|
2055
2356
|
}
|
|
2056
2357
|
const groundctlDir = join8(projectPath, ".groundctl");
|
|
2057
2358
|
const existingPid = readPidFile(groundctlDir);
|
|
2058
2359
|
if (existingPid && processAlive(existingPid)) {
|
|
2059
|
-
console.log(
|
|
2360
|
+
console.log(chalk13.yellow(`
|
|
2060
2361
|
\u26A0 A watcher is already running (PID ${existingPid}).`));
|
|
2061
|
-
console.log(
|
|
2362
|
+
console.log(chalk13.gray(` To stop it: kill ${existingPid}
|
|
2062
2363
|
`));
|
|
2063
2364
|
process.exit(1);
|
|
2064
2365
|
}
|
|
2065
2366
|
let transcriptDir = findTranscriptDir(projectPath);
|
|
2066
2367
|
if (!transcriptDir) {
|
|
2067
|
-
console.log(
|
|
2368
|
+
console.log(chalk13.bold("\n groundctl watch\n"));
|
|
2068
2369
|
console.log(
|
|
2069
|
-
|
|
2370
|
+
chalk13.yellow(" No Claude Code transcript directory found for this project yet.")
|
|
2070
2371
|
);
|
|
2071
|
-
console.log(
|
|
2372
|
+
console.log(chalk13.gray(" Waiting for first session to start...\n"));
|
|
2072
2373
|
await new Promise((resolve3) => {
|
|
2073
2374
|
const interval = setInterval(() => {
|
|
2074
2375
|
const dir = findTranscriptDir(projectPath);
|
|
@@ -2083,7 +2384,7 @@ async function watchCommand(options) {
|
|
|
2083
2384
|
startWatcher(transcriptDir, projectPath);
|
|
2084
2385
|
await new Promise(() => {
|
|
2085
2386
|
process.on("SIGINT", () => {
|
|
2086
|
-
console.log(
|
|
2387
|
+
console.log(chalk13.gray("\n Watcher stopped.\n"));
|
|
2087
2388
|
process.exit(0);
|
|
2088
2389
|
});
|
|
2089
2390
|
process.on("SIGTERM", () => {
|
|
@@ -2092,6 +2393,82 @@ async function watchCommand(options) {
|
|
|
2092
2393
|
});
|
|
2093
2394
|
}
|
|
2094
2395
|
|
|
2396
|
+
// src/commands/update.ts
|
|
2397
|
+
import chalk14 from "chalk";
|
|
2398
|
+
function parseProgress2(s) {
|
|
2399
|
+
const m = s.match(/^(\d+)\/(\d+)$/);
|
|
2400
|
+
if (!m) return null;
|
|
2401
|
+
return { done: parseInt(m[1], 10), total: parseInt(m[2], 10) };
|
|
2402
|
+
}
|
|
2403
|
+
async function updateCommand(type, nameOrId, options) {
|
|
2404
|
+
if (type !== "feature") {
|
|
2405
|
+
console.log(chalk14.red(`
|
|
2406
|
+
Unknown type "${type}". Use "feature".
|
|
2407
|
+
`));
|
|
2408
|
+
process.exit(1);
|
|
2409
|
+
}
|
|
2410
|
+
const db = await openDb();
|
|
2411
|
+
const feature = queryOne(
|
|
2412
|
+
db,
|
|
2413
|
+
`SELECT id, name FROM features
|
|
2414
|
+
WHERE id = ?1 OR name = ?1
|
|
2415
|
+
OR id LIKE ?2 OR name LIKE ?2
|
|
2416
|
+
ORDER BY CASE WHEN id = ?1 OR name = ?1 THEN 0 ELSE 1 END
|
|
2417
|
+
LIMIT 1`,
|
|
2418
|
+
[nameOrId, `%${nameOrId}%`]
|
|
2419
|
+
);
|
|
2420
|
+
if (!feature) {
|
|
2421
|
+
console.log(chalk14.red(`
|
|
2422
|
+
Feature "${nameOrId}" not found.
|
|
2423
|
+
`));
|
|
2424
|
+
closeDb();
|
|
2425
|
+
process.exit(1);
|
|
2426
|
+
}
|
|
2427
|
+
const sets = [];
|
|
2428
|
+
const params = [];
|
|
2429
|
+
if (options.description !== void 0) {
|
|
2430
|
+
sets.push("description = ?");
|
|
2431
|
+
params.push(options.description);
|
|
2432
|
+
}
|
|
2433
|
+
if (options.items !== void 0) {
|
|
2434
|
+
const items = options.items.split(",").map((s) => s.trim()).filter(Boolean).join(",");
|
|
2435
|
+
sets.push("items = ?");
|
|
2436
|
+
params.push(items);
|
|
2437
|
+
}
|
|
2438
|
+
if (options.progress !== void 0) {
|
|
2439
|
+
const p = parseProgress2(options.progress);
|
|
2440
|
+
if (!p) {
|
|
2441
|
+
console.log(chalk14.yellow(` \u26A0 --progress "${options.progress}" ignored (expected N/N format)
|
|
2442
|
+
`));
|
|
2443
|
+
} else {
|
|
2444
|
+
sets.push("progress_done = ?", "progress_total = ?");
|
|
2445
|
+
params.push(p.done, p.total);
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
if (options.priority !== void 0) {
|
|
2449
|
+
sets.push("priority = ?");
|
|
2450
|
+
params.push(options.priority);
|
|
2451
|
+
}
|
|
2452
|
+
if (options.status !== void 0) {
|
|
2453
|
+
sets.push("status = ?");
|
|
2454
|
+
params.push(options.status);
|
|
2455
|
+
}
|
|
2456
|
+
if (sets.length === 0) {
|
|
2457
|
+
console.log(chalk14.yellow("\n Nothing to update \u2014 pass at least one option.\n"));
|
|
2458
|
+
closeDb();
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
sets.push("updated_at = datetime('now')");
|
|
2462
|
+
params.push(feature.id);
|
|
2463
|
+
db.run(
|
|
2464
|
+
`UPDATE features SET ${sets.join(", ")} WHERE id = ?`,
|
|
2465
|
+
params
|
|
2466
|
+
);
|
|
2467
|
+
saveDb();
|
|
2468
|
+
closeDb();
|
|
2469
|
+
console.log(chalk14.green(` \u2713 Updated: ${feature.name}`));
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2095
2472
|
// src/index.ts
|
|
2096
2473
|
var require2 = createRequire(import.meta.url);
|
|
2097
2474
|
var pkg = require2("../package.json");
|
|
@@ -2104,7 +2481,7 @@ program.command("complete <feature>").description("Mark a feature as done and re
|
|
|
2104
2481
|
program.command("sync").description("Regenerate PROJECT_STATE.md and AGENTS.md from SQLite").action(syncCommand);
|
|
2105
2482
|
program.command("next").description("Show next available (unclaimed) feature").action(nextCommand);
|
|
2106
2483
|
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);
|
|
2484
|
+
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
2485
|
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
2486
|
(opts) => ingestCommand({
|
|
2110
2487
|
source: opts.source,
|
|
@@ -2123,4 +2500,13 @@ program.command("watch").description("Watch for session end and auto-ingest tran
|
|
|
2123
2500
|
projectPath: opts.projectPath
|
|
2124
2501
|
})
|
|
2125
2502
|
);
|
|
2503
|
+
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(
|
|
2504
|
+
(type, name, opts) => updateCommand(type, name, {
|
|
2505
|
+
description: opts.description,
|
|
2506
|
+
items: opts.items,
|
|
2507
|
+
progress: opts.progress,
|
|
2508
|
+
priority: opts.priority,
|
|
2509
|
+
status: opts.status
|
|
2510
|
+
})
|
|
2511
|
+
);
|
|
2126
2512
|
program.parse();
|