@groundctl/cli 0.3.2 → 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 +433 -214
- 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";
|
|
@@ -352,8 +352,6 @@ function generateAgentsMd(db, projectName) {
|
|
|
352
352
|
|
|
353
353
|
// src/ingest/git-import.ts
|
|
354
354
|
import { execSync } from "child_process";
|
|
355
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
356
|
-
import { join as join2 } from "path";
|
|
357
355
|
function run(cmd, cwd) {
|
|
358
356
|
try {
|
|
359
357
|
return execSync(cmd, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -409,67 +407,10 @@ function parseGitLog(cwd) {
|
|
|
409
407
|
}
|
|
410
408
|
return commits.reverse();
|
|
411
409
|
}
|
|
412
|
-
function parseProjectStateMd(content) {
|
|
413
|
-
const features = [];
|
|
414
|
-
const lines = content.split("\n");
|
|
415
|
-
let section = "";
|
|
416
|
-
for (const line of lines) {
|
|
417
|
-
const trimmed = line.trim();
|
|
418
|
-
if (trimmed.startsWith("## ")) {
|
|
419
|
-
section = trimmed.toLowerCase();
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
|
-
if (section.includes("decision") || section.includes("session") || section.includes("debt") || section.includes("note")) continue;
|
|
423
|
-
if (!trimmed.startsWith("- ") && !trimmed.startsWith("* ")) continue;
|
|
424
|
-
const item = trimmed.slice(2).trim();
|
|
425
|
-
if (!item || item.length < 3) continue;
|
|
426
|
-
const name = item.split("(")[0].split("\u2192")[0].split("\u2014")[0].trim();
|
|
427
|
-
if (!name || name.length < 3 || name.length > 80) continue;
|
|
428
|
-
if (/^\d{4}-\d{2}-\d{2}/.test(name)) continue;
|
|
429
|
-
if (name.split(" ").length > 8) continue;
|
|
430
|
-
let status = "pending";
|
|
431
|
-
let priority = "medium";
|
|
432
|
-
if (section.includes("built") || section.includes("done") || section.includes("complete")) {
|
|
433
|
-
status = "done";
|
|
434
|
-
} else if (section.includes("claimed") || section.includes("in progress") || section.includes("current")) {
|
|
435
|
-
status = "in_progress";
|
|
436
|
-
} else if (section.includes("available") || section.includes("next")) {
|
|
437
|
-
status = "pending";
|
|
438
|
-
} else if (section.includes("blocked")) {
|
|
439
|
-
status = "blocked";
|
|
440
|
-
}
|
|
441
|
-
if (/priority:\s*critical|critical\)/i.test(item)) priority = "critical";
|
|
442
|
-
else if (/priority:\s*high|high\)/i.test(item)) priority = "high";
|
|
443
|
-
else if (/priority:\s*low|low\)/i.test(item)) priority = "low";
|
|
444
|
-
features.push({ name, status, priority });
|
|
445
|
-
}
|
|
446
|
-
return features;
|
|
447
|
-
}
|
|
448
|
-
function featureIdFromName(name) {
|
|
449
|
-
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
|
|
450
|
-
}
|
|
451
410
|
function importFromGit(db, projectPath) {
|
|
452
411
|
let sessionsCreated = 0;
|
|
453
|
-
let featuresImported = 0;
|
|
454
|
-
const psMdPath = join2(projectPath, "PROJECT_STATE.md");
|
|
455
|
-
if (existsSync2(psMdPath)) {
|
|
456
|
-
const content = readFileSync2(psMdPath, "utf-8");
|
|
457
|
-
const features = parseProjectStateMd(content);
|
|
458
|
-
for (const feat of features) {
|
|
459
|
-
const id = featureIdFromName(feat.name);
|
|
460
|
-
if (!id) continue;
|
|
461
|
-
const exists = queryOne(db, "SELECT id FROM features WHERE id = ?", [id]);
|
|
462
|
-
if (!exists) {
|
|
463
|
-
db.run(
|
|
464
|
-
"INSERT INTO features (id, name, status, priority) VALUES (?, ?, ?, ?)",
|
|
465
|
-
[id, feat.name, feat.status, feat.priority]
|
|
466
|
-
);
|
|
467
|
-
featuresImported++;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
412
|
const commits = parseGitLog(projectPath);
|
|
472
|
-
if (commits.length === 0) return { sessionsCreated
|
|
413
|
+
if (commits.length === 0) return { sessionsCreated };
|
|
473
414
|
const SESSION_GAP_MS = 4 * 60 * 60 * 1e3;
|
|
474
415
|
const sessions = [];
|
|
475
416
|
let currentSession = [];
|
|
@@ -523,7 +464,286 @@ function importFromGit(db, projectPath) {
|
|
|
523
464
|
}
|
|
524
465
|
}
|
|
525
466
|
saveDb();
|
|
526
|
-
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
|
+
}
|
|
527
747
|
}
|
|
528
748
|
|
|
529
749
|
// src/commands/init.ts
|
|
@@ -563,53 +783,52 @@ echo "--- groundctl: Product state updated ---"
|
|
|
563
783
|
async function initCommand(options) {
|
|
564
784
|
const cwd = process.cwd();
|
|
565
785
|
const projectName = cwd.split("/").pop() ?? "unknown";
|
|
566
|
-
console.log(
|
|
786
|
+
console.log(chalk2.bold(`
|
|
567
787
|
groundctl init \u2014 ${projectName}
|
|
568
788
|
`));
|
|
569
|
-
console.log(
|
|
789
|
+
console.log(chalk2.gray(" Creating SQLite database..."));
|
|
570
790
|
const db = await openDb();
|
|
571
791
|
if (options.importFromGit) {
|
|
572
792
|
const isGitRepo = existsSync3(join3(cwd, ".git"));
|
|
573
793
|
if (!isGitRepo) {
|
|
574
|
-
console.log(
|
|
794
|
+
console.log(chalk2.yellow(" \u26A0 Not a git repo \u2014 skipping --import-from-git"));
|
|
575
795
|
} else {
|
|
576
|
-
console.log(
|
|
796
|
+
console.log(chalk2.gray(" Importing sessions from git history..."));
|
|
577
797
|
const result = importFromGit(db, cwd);
|
|
578
798
|
console.log(
|
|
579
|
-
|
|
580
|
-
` \u2713 Git import: ${result.sessionsCreated} sessions, ${result.featuresImported} features`
|
|
581
|
-
)
|
|
799
|
+
chalk2.green(` \u2713 Git import: ${result.sessionsCreated} sessions`)
|
|
582
800
|
);
|
|
801
|
+
await detectAndImportFeatures(db, cwd);
|
|
583
802
|
}
|
|
584
803
|
}
|
|
585
804
|
const projectState = generateProjectState(db, projectName);
|
|
586
805
|
const agentsMd = generateAgentsMd(db, projectName);
|
|
587
806
|
closeDb();
|
|
588
|
-
console.log(
|
|
807
|
+
console.log(chalk2.green(" \u2713 Database ready"));
|
|
589
808
|
const claudeHooksDir = join3(cwd, ".claude", "hooks");
|
|
590
809
|
if (!existsSync3(claudeHooksDir)) {
|
|
591
810
|
mkdirSync2(claudeHooksDir, { recursive: true });
|
|
592
811
|
}
|
|
593
|
-
|
|
812
|
+
writeFileSync3(join3(claudeHooksDir, "pre-session.sh"), PRE_SESSION_HOOK);
|
|
594
813
|
chmodSync(join3(claudeHooksDir, "pre-session.sh"), 493);
|
|
595
|
-
|
|
814
|
+
writeFileSync3(join3(claudeHooksDir, "post-session.sh"), POST_SESSION_HOOK);
|
|
596
815
|
chmodSync(join3(claudeHooksDir, "post-session.sh"), 493);
|
|
597
|
-
console.log(
|
|
816
|
+
console.log(chalk2.green(" \u2713 Claude Code hooks installed"));
|
|
598
817
|
const codexHooksDir = join3(cwd, ".codex", "hooks");
|
|
599
818
|
if (!existsSync3(codexHooksDir)) {
|
|
600
819
|
mkdirSync2(codexHooksDir, { recursive: true });
|
|
601
820
|
}
|
|
602
821
|
const codexPre = PRE_SESSION_HOOK.replace("Claude Code", "Codex");
|
|
603
822
|
const codexPost = POST_SESSION_HOOK.replace("Claude Code", "Codex").replace("claude-code", "codex");
|
|
604
|
-
|
|
823
|
+
writeFileSync3(join3(codexHooksDir, "pre-session.sh"), codexPre);
|
|
605
824
|
chmodSync(join3(codexHooksDir, "pre-session.sh"), 493);
|
|
606
|
-
|
|
825
|
+
writeFileSync3(join3(codexHooksDir, "post-session.sh"), codexPost);
|
|
607
826
|
chmodSync(join3(codexHooksDir, "post-session.sh"), 493);
|
|
608
|
-
console.log(
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
console.log(
|
|
612
|
-
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"));
|
|
613
832
|
const gitignorePath = join3(cwd, ".gitignore");
|
|
614
833
|
const gitignoreEntry = "\n# groundctl local state\n.groundctl/\n";
|
|
615
834
|
if (existsSync3(gitignorePath)) {
|
|
@@ -618,32 +837,32 @@ groundctl init \u2014 ${projectName}
|
|
|
618
837
|
appendFileSync(gitignorePath, gitignoreEntry);
|
|
619
838
|
}
|
|
620
839
|
}
|
|
621
|
-
console.log(
|
|
840
|
+
console.log(chalk2.bold.green(`
|
|
622
841
|
\u2713 groundctl initialized for ${projectName}
|
|
623
842
|
`));
|
|
624
843
|
if (!options.importFromGit) {
|
|
625
|
-
console.log(
|
|
626
|
-
console.log(
|
|
627
|
-
console.log(
|
|
628
|
-
console.log(
|
|
629
|
-
console.log(
|
|
630
|
-
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"));
|
|
631
850
|
} else {
|
|
632
|
-
console.log(
|
|
633
|
-
console.log(
|
|
634
|
-
console.log(
|
|
851
|
+
console.log(chalk2.gray(" Next steps:"));
|
|
852
|
+
console.log(chalk2.gray(" groundctl status"));
|
|
853
|
+
console.log(chalk2.gray(" groundctl next\n"));
|
|
635
854
|
}
|
|
636
855
|
}
|
|
637
856
|
|
|
638
857
|
// src/commands/status.ts
|
|
639
|
-
import
|
|
858
|
+
import chalk3 from "chalk";
|
|
640
859
|
var BAR_W = 14;
|
|
641
860
|
var NAME_W = 22;
|
|
642
861
|
var PROG_W = 6;
|
|
643
862
|
function progressBar(done, total, width) {
|
|
644
|
-
if (total <= 0) return
|
|
863
|
+
if (total <= 0) return chalk3.gray("\u2591".repeat(width));
|
|
645
864
|
const filled = Math.min(width, Math.round(done / total * width));
|
|
646
|
-
return
|
|
865
|
+
return chalk3.green("\u2588".repeat(filled)) + chalk3.gray("\u2591".repeat(width - filled));
|
|
647
866
|
}
|
|
648
867
|
function featureBar(status, progressDone, progressTotal) {
|
|
649
868
|
if (progressTotal != null && progressTotal > 0) {
|
|
@@ -655,9 +874,9 @@ function featureBar(status, progressDone, progressTotal) {
|
|
|
655
874
|
case "in_progress":
|
|
656
875
|
return progressBar(1, 2, BAR_W);
|
|
657
876
|
case "blocked":
|
|
658
|
-
return
|
|
877
|
+
return chalk3.red("\u2591".repeat(BAR_W));
|
|
659
878
|
default:
|
|
660
|
-
return
|
|
879
|
+
return chalk3.gray("\u2591".repeat(BAR_W));
|
|
661
880
|
}
|
|
662
881
|
}
|
|
663
882
|
function featureProgress(progressDone, progressTotal) {
|
|
@@ -726,10 +945,10 @@ async function statusCommand() {
|
|
|
726
945
|
closeDb();
|
|
727
946
|
console.log("");
|
|
728
947
|
if (features.length === 0) {
|
|
729
|
-
console.log(
|
|
948
|
+
console.log(chalk3.bold(` ${projectName} \u2014 no features tracked yet
|
|
730
949
|
`));
|
|
731
|
-
console.log(
|
|
732
|
-
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"));
|
|
733
952
|
return;
|
|
734
953
|
}
|
|
735
954
|
const total = features.length;
|
|
@@ -738,13 +957,13 @@ async function statusCommand() {
|
|
|
738
957
|
const blocked = features.filter((f) => f.status === "blocked").length;
|
|
739
958
|
const pct = Math.round(done / total * 100);
|
|
740
959
|
console.log(
|
|
741
|
-
|
|
960
|
+
chalk3.bold(` ${projectName} \u2014 ${pct}% implemented`) + chalk3.gray(` (${sessionCount} session${sessionCount !== 1 ? "s" : ""})`)
|
|
742
961
|
);
|
|
743
962
|
console.log("");
|
|
744
963
|
const aggBar = progressBar(done, total, 20);
|
|
745
|
-
let aggSuffix =
|
|
746
|
-
if (inProg > 0) aggSuffix +=
|
|
747
|
-
if (blocked > 0) aggSuffix +=
|
|
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`);
|
|
748
967
|
console.log(` Features ${aggBar}${aggSuffix}`);
|
|
749
968
|
console.log("");
|
|
750
969
|
const maxNameLen = Math.min(NAME_W, Math.max(...features.map((f) => f.name.length)));
|
|
@@ -756,20 +975,20 @@ async function statusCommand() {
|
|
|
756
975
|
const isActive = f.status === "in_progress";
|
|
757
976
|
const isBlocked = f.status === "blocked";
|
|
758
977
|
const icon = isDone ? "\u2713" : isActive ? "\u25CF" : isBlocked ? "\u2717" : "\u25CB";
|
|
759
|
-
const iconChalk = isDone ?
|
|
978
|
+
const iconChalk = isDone ? chalk3.green : isActive ? chalk3.yellow : isBlocked ? chalk3.red : chalk3.gray;
|
|
760
979
|
const nameRaw = f.name.slice(0, nameW).padEnd(nameW);
|
|
761
|
-
const nameChalk = isDone ?
|
|
980
|
+
const nameChalk = isDone ? chalk3.dim : isActive ? chalk3.white : isBlocked ? chalk3.red : chalk3.gray;
|
|
762
981
|
const pd = f.progress_done ?? null;
|
|
763
982
|
const pt = f.progress_total ?? null;
|
|
764
983
|
const bar2 = featureBar(f.status, pd, pt);
|
|
765
984
|
const prog = featureProgress(pd, pt).padEnd(PROG_W);
|
|
766
985
|
const descRaw = f.description ?? "";
|
|
767
986
|
const descTrunc = descRaw.length > 38 ? descRaw.slice(0, 36) + "\u2026" : descRaw;
|
|
768
|
-
const descStr = descTrunc ?
|
|
987
|
+
const descStr = descTrunc ? chalk3.gray(` ${descTrunc}`) : "";
|
|
769
988
|
let claimedStr = "";
|
|
770
989
|
if (isActive && f.claimed_session) {
|
|
771
990
|
const elapsed = f.claimed_at ? timeSince(f.claimed_at) : "";
|
|
772
|
-
claimedStr =
|
|
991
|
+
claimedStr = chalk3.yellow(` \u2192 ${f.claimed_session}${elapsed ? ` (${elapsed})` : ""}`);
|
|
773
992
|
}
|
|
774
993
|
console.log(
|
|
775
994
|
` ${iconChalk(icon)} ${nameChalk(nameRaw)} ${bar2} ${prog}${descStr}${claimedStr}`
|
|
@@ -777,7 +996,7 @@ async function statusCommand() {
|
|
|
777
996
|
if (f.items) {
|
|
778
997
|
const lines = wrapItems(f.items, itemsMaxW);
|
|
779
998
|
for (const line of lines) {
|
|
780
|
-
console.log(
|
|
999
|
+
console.log(chalk3.dim(`${contIndent}${line}`));
|
|
781
1000
|
}
|
|
782
1001
|
}
|
|
783
1002
|
}
|
|
@@ -785,7 +1004,7 @@ async function statusCommand() {
|
|
|
785
1004
|
}
|
|
786
1005
|
|
|
787
1006
|
// src/commands/claim.ts
|
|
788
|
-
import
|
|
1007
|
+
import chalk4 from "chalk";
|
|
789
1008
|
import { randomUUID } from "crypto";
|
|
790
1009
|
function findFeature(db, term) {
|
|
791
1010
|
return queryOne(
|
|
@@ -822,15 +1041,15 @@ async function claimCommand(featureIdOrName, options) {
|
|
|
822
1041
|
const db = await openDb();
|
|
823
1042
|
const feature = findFeature(db, featureIdOrName);
|
|
824
1043
|
if (!feature) {
|
|
825
|
-
console.log(
|
|
1044
|
+
console.log(chalk4.red(`
|
|
826
1045
|
Feature "${featureIdOrName}" not found.
|
|
827
1046
|
`));
|
|
828
|
-
console.log(
|
|
1047
|
+
console.log(chalk4.gray(" Add it with: groundctl add feature -n '" + featureIdOrName + "'"));
|
|
829
1048
|
closeDb();
|
|
830
1049
|
process.exit(1);
|
|
831
1050
|
}
|
|
832
1051
|
if (feature.status === "done") {
|
|
833
|
-
console.log(
|
|
1052
|
+
console.log(chalk4.yellow(`
|
|
834
1053
|
Feature "${feature.name}" is already done.
|
|
835
1054
|
`));
|
|
836
1055
|
closeDb();
|
|
@@ -844,7 +1063,7 @@ async function claimCommand(featureIdOrName, options) {
|
|
|
844
1063
|
);
|
|
845
1064
|
if (existingClaim) {
|
|
846
1065
|
console.log(
|
|
847
|
-
|
|
1066
|
+
chalk4.red(`
|
|
848
1067
|
Feature "${feature.name}" is already claimed by session ${existingClaim.session_id}`)
|
|
849
1068
|
);
|
|
850
1069
|
const alternatives = query(
|
|
@@ -859,9 +1078,9 @@ async function claimCommand(featureIdOrName, options) {
|
|
|
859
1078
|
LIMIT 3`
|
|
860
1079
|
);
|
|
861
1080
|
if (alternatives.length > 0) {
|
|
862
|
-
console.log(
|
|
1081
|
+
console.log(chalk4.gray("\n Available instead:"));
|
|
863
1082
|
for (const alt of alternatives) {
|
|
864
|
-
console.log(
|
|
1083
|
+
console.log(chalk4.gray(` \u25CB ${alt.name}`));
|
|
865
1084
|
}
|
|
866
1085
|
}
|
|
867
1086
|
console.log("");
|
|
@@ -891,7 +1110,7 @@ async function claimCommand(featureIdOrName, options) {
|
|
|
891
1110
|
saveDb();
|
|
892
1111
|
closeDb();
|
|
893
1112
|
console.log(
|
|
894
|
-
|
|
1113
|
+
chalk4.green(`
|
|
895
1114
|
\u2713 Claimed "${feature.name}" \u2192 session ${sessionId}
|
|
896
1115
|
`)
|
|
897
1116
|
);
|
|
@@ -900,7 +1119,7 @@ async function completeCommand(featureIdOrName) {
|
|
|
900
1119
|
const db = await openDb();
|
|
901
1120
|
const feature = findFeature(db, featureIdOrName);
|
|
902
1121
|
if (!feature) {
|
|
903
|
-
console.log(
|
|
1122
|
+
console.log(chalk4.red(`
|
|
904
1123
|
Feature "${featureIdOrName}" not found.
|
|
905
1124
|
`));
|
|
906
1125
|
closeDb();
|
|
@@ -916,15 +1135,15 @@ async function completeCommand(featureIdOrName) {
|
|
|
916
1135
|
);
|
|
917
1136
|
saveDb();
|
|
918
1137
|
closeDb();
|
|
919
|
-
console.log(
|
|
1138
|
+
console.log(chalk4.green(`
|
|
920
1139
|
\u2713 Completed "${feature.name}"
|
|
921
1140
|
`));
|
|
922
1141
|
}
|
|
923
1142
|
|
|
924
1143
|
// src/commands/sync.ts
|
|
925
|
-
import { writeFileSync as
|
|
1144
|
+
import { writeFileSync as writeFileSync4 } from "fs";
|
|
926
1145
|
import { join as join4 } from "path";
|
|
927
|
-
import
|
|
1146
|
+
import chalk5 from "chalk";
|
|
928
1147
|
async function syncCommand(opts) {
|
|
929
1148
|
const db = await openDb();
|
|
930
1149
|
const projectName = process.cwd().split("/").pop() ?? "unknown";
|
|
@@ -932,16 +1151,16 @@ async function syncCommand(opts) {
|
|
|
932
1151
|
const agentsMd = generateAgentsMd(db, projectName);
|
|
933
1152
|
closeDb();
|
|
934
1153
|
const cwd = process.cwd();
|
|
935
|
-
|
|
936
|
-
|
|
1154
|
+
writeFileSync4(join4(cwd, "PROJECT_STATE.md"), projectState);
|
|
1155
|
+
writeFileSync4(join4(cwd, "AGENTS.md"), agentsMd);
|
|
937
1156
|
if (!opts?.silent) {
|
|
938
|
-
console.log(
|
|
939
|
-
console.log(
|
|
1157
|
+
console.log(chalk5.green("\n \u2713 PROJECT_STATE.md regenerated"));
|
|
1158
|
+
console.log(chalk5.green(" \u2713 AGENTS.md regenerated\n"));
|
|
940
1159
|
}
|
|
941
1160
|
}
|
|
942
1161
|
|
|
943
1162
|
// src/commands/next.ts
|
|
944
|
-
import
|
|
1163
|
+
import chalk6 from "chalk";
|
|
945
1164
|
async function nextCommand() {
|
|
946
1165
|
const db = await openDb();
|
|
947
1166
|
const available = query(
|
|
@@ -961,26 +1180,26 @@ async function nextCommand() {
|
|
|
961
1180
|
);
|
|
962
1181
|
closeDb();
|
|
963
1182
|
if (available.length === 0) {
|
|
964
|
-
console.log(
|
|
1183
|
+
console.log(chalk6.yellow("\n No available features to claim.\n"));
|
|
965
1184
|
return;
|
|
966
1185
|
}
|
|
967
|
-
console.log(
|
|
1186
|
+
console.log(chalk6.bold("\n Next available features:\n"));
|
|
968
1187
|
for (let i = 0; i < available.length; i++) {
|
|
969
1188
|
const feat = available[i];
|
|
970
|
-
const pColor = feat.priority === "critical" || feat.priority === "high" ?
|
|
971
|
-
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") : " ";
|
|
972
1191
|
console.log(` ${marker} ${feat.name} ${pColor(`(${feat.priority})`)}`);
|
|
973
1192
|
if (feat.description) {
|
|
974
|
-
console.log(
|
|
1193
|
+
console.log(chalk6.gray(` ${feat.description}`));
|
|
975
1194
|
}
|
|
976
1195
|
}
|
|
977
|
-
console.log(
|
|
1196
|
+
console.log(chalk6.gray(`
|
|
978
1197
|
Claim with: groundctl claim "${available[0].name}"
|
|
979
1198
|
`));
|
|
980
1199
|
}
|
|
981
1200
|
|
|
982
1201
|
// src/commands/log.ts
|
|
983
|
-
import
|
|
1202
|
+
import chalk7 from "chalk";
|
|
984
1203
|
async function logCommand(options) {
|
|
985
1204
|
const db = await openDb();
|
|
986
1205
|
if (options.session) {
|
|
@@ -989,18 +1208,18 @@ async function logCommand(options) {
|
|
|
989
1208
|
`%${options.session}%`
|
|
990
1209
|
]);
|
|
991
1210
|
if (!session) {
|
|
992
|
-
console.log(
|
|
1211
|
+
console.log(chalk7.red(`
|
|
993
1212
|
Session "${options.session}" not found.
|
|
994
1213
|
`));
|
|
995
1214
|
closeDb();
|
|
996
1215
|
return;
|
|
997
1216
|
}
|
|
998
|
-
console.log(
|
|
1217
|
+
console.log(chalk7.bold(`
|
|
999
1218
|
Session ${session.id}`));
|
|
1000
|
-
console.log(
|
|
1001
|
-
console.log(
|
|
1219
|
+
console.log(chalk7.gray(` Agent: ${session.agent}`));
|
|
1220
|
+
console.log(chalk7.gray(` Started: ${session.started_at}`));
|
|
1002
1221
|
if (session.ended_at) {
|
|
1003
|
-
console.log(
|
|
1222
|
+
console.log(chalk7.gray(` Ended: ${session.ended_at}`));
|
|
1004
1223
|
}
|
|
1005
1224
|
if (session.summary) {
|
|
1006
1225
|
console.log(`
|
|
@@ -1012,11 +1231,11 @@ async function logCommand(options) {
|
|
|
1012
1231
|
[session.id]
|
|
1013
1232
|
);
|
|
1014
1233
|
if (decisions.length > 0) {
|
|
1015
|
-
console.log(
|
|
1234
|
+
console.log(chalk7.bold("\n Decisions:"));
|
|
1016
1235
|
for (const d of decisions) {
|
|
1017
1236
|
console.log(` \u2022 ${d.description}`);
|
|
1018
1237
|
if (d.rationale) {
|
|
1019
|
-
console.log(
|
|
1238
|
+
console.log(chalk7.gray(` ${d.rationale}`));
|
|
1020
1239
|
}
|
|
1021
1240
|
}
|
|
1022
1241
|
}
|
|
@@ -1026,10 +1245,10 @@ async function logCommand(options) {
|
|
|
1026
1245
|
[session.id]
|
|
1027
1246
|
);
|
|
1028
1247
|
if (files.length > 0) {
|
|
1029
|
-
console.log(
|
|
1248
|
+
console.log(chalk7.bold(`
|
|
1030
1249
|
Files modified (${files.length}):`));
|
|
1031
1250
|
for (const f of files) {
|
|
1032
|
-
const op = f.operation === "created" ?
|
|
1251
|
+
const op = f.operation === "created" ? chalk7.green("+") : f.operation === "deleted" ? chalk7.red("-") : chalk7.yellow("~");
|
|
1033
1252
|
console.log(` ${op} ${f.path} (${f.lines_changed} lines)`);
|
|
1034
1253
|
}
|
|
1035
1254
|
}
|
|
@@ -1037,18 +1256,18 @@ async function logCommand(options) {
|
|
|
1037
1256
|
} else {
|
|
1038
1257
|
const sessions = query(db, "SELECT * FROM sessions ORDER BY started_at DESC LIMIT 20");
|
|
1039
1258
|
if (sessions.length === 0) {
|
|
1040
|
-
console.log(
|
|
1259
|
+
console.log(chalk7.yellow("\n No sessions recorded yet.\n"));
|
|
1041
1260
|
closeDb();
|
|
1042
1261
|
return;
|
|
1043
1262
|
}
|
|
1044
|
-
console.log(
|
|
1263
|
+
console.log(chalk7.bold("\n Session timeline:\n"));
|
|
1045
1264
|
for (const s of sessions) {
|
|
1046
|
-
const status = s.ended_at ?
|
|
1265
|
+
const status = s.ended_at ? chalk7.green("done") : chalk7.yellow("active");
|
|
1047
1266
|
console.log(
|
|
1048
|
-
` ${
|
|
1267
|
+
` ${chalk7.bold(s.id)} ${chalk7.gray(s.started_at)} ${status} ${chalk7.gray(s.agent)}`
|
|
1049
1268
|
);
|
|
1050
1269
|
if (s.summary) {
|
|
1051
|
-
console.log(
|
|
1270
|
+
console.log(chalk7.gray(` ${s.summary}`));
|
|
1052
1271
|
}
|
|
1053
1272
|
}
|
|
1054
1273
|
console.log("");
|
|
@@ -1057,7 +1276,7 @@ async function logCommand(options) {
|
|
|
1057
1276
|
}
|
|
1058
1277
|
|
|
1059
1278
|
// src/commands/add.ts
|
|
1060
|
-
import
|
|
1279
|
+
import chalk8 from "chalk";
|
|
1061
1280
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1062
1281
|
function parseProgress(s) {
|
|
1063
1282
|
const m = s.match(/^(\d+)\/(\d+)$/);
|
|
@@ -1068,7 +1287,7 @@ async function addCommand(type, options) {
|
|
|
1068
1287
|
const db = await openDb();
|
|
1069
1288
|
if (type === "feature") {
|
|
1070
1289
|
if (!options.name) {
|
|
1071
|
-
console.log(
|
|
1290
|
+
console.log(chalk8.red("\n --name is required for features.\n"));
|
|
1072
1291
|
closeDb();
|
|
1073
1292
|
process.exit(1);
|
|
1074
1293
|
}
|
|
@@ -1082,7 +1301,7 @@ async function addCommand(type, options) {
|
|
|
1082
1301
|
progressDone = p.done;
|
|
1083
1302
|
progressTotal = p.total;
|
|
1084
1303
|
} else {
|
|
1085
|
-
console.log(
|
|
1304
|
+
console.log(chalk8.yellow(` \u26A0 --progress "${options.progress}" ignored (expected N/N format)`));
|
|
1086
1305
|
}
|
|
1087
1306
|
}
|
|
1088
1307
|
const items = options.items ? options.items.split(",").map((s) => s.trim()).filter(Boolean).join(",") : null;
|
|
@@ -1105,8 +1324,8 @@ async function addCommand(type, options) {
|
|
|
1105
1324
|
const extras = [];
|
|
1106
1325
|
if (progressDone !== null) extras.push(`${progressDone}/${progressTotal}`);
|
|
1107
1326
|
if (items) extras.push(`${items.split(",").length} items`);
|
|
1108
|
-
const suffix = extras.length ?
|
|
1109
|
-
console.log(
|
|
1327
|
+
const suffix = extras.length ? chalk8.gray(` \u2014 ${extras.join(", ")}`) : "";
|
|
1328
|
+
console.log(chalk8.green(`
|
|
1110
1329
|
\u2713 Feature added: ${options.name} (${priority})${suffix}
|
|
1111
1330
|
`));
|
|
1112
1331
|
} else if (type === "session") {
|
|
@@ -1115,11 +1334,11 @@ async function addCommand(type, options) {
|
|
|
1115
1334
|
db.run("INSERT INTO sessions (id, agent) VALUES (?, ?)", [id, agent]);
|
|
1116
1335
|
saveDb();
|
|
1117
1336
|
closeDb();
|
|
1118
|
-
console.log(
|
|
1337
|
+
console.log(chalk8.green(`
|
|
1119
1338
|
\u2713 Session created: ${id} (${agent})
|
|
1120
1339
|
`));
|
|
1121
1340
|
} else {
|
|
1122
|
-
console.log(
|
|
1341
|
+
console.log(chalk8.red(`
|
|
1123
1342
|
Unknown type "${type}". Use "feature" or "session".
|
|
1124
1343
|
`));
|
|
1125
1344
|
closeDb();
|
|
@@ -1131,7 +1350,7 @@ async function addCommand(type, options) {
|
|
|
1131
1350
|
import { existsSync as existsSync4, readdirSync } from "fs";
|
|
1132
1351
|
import { join as join5, resolve } from "path";
|
|
1133
1352
|
import { homedir as homedir2 } from "os";
|
|
1134
|
-
import
|
|
1353
|
+
import chalk9 from "chalk";
|
|
1135
1354
|
|
|
1136
1355
|
// src/ingest/claude-parser.ts
|
|
1137
1356
|
import { readFileSync as readFileSync4 } from "fs";
|
|
@@ -1401,11 +1620,11 @@ async function ingestCommand(options) {
|
|
|
1401
1620
|
transcriptPath = findLatestTranscript(projectPath) ?? void 0;
|
|
1402
1621
|
}
|
|
1403
1622
|
if (!transcriptPath || !existsSync4(transcriptPath)) {
|
|
1404
|
-
console.log(
|
|
1623
|
+
console.log(chalk9.yellow("\n No transcript found. Skipping ingest.\n"));
|
|
1405
1624
|
if (!options.noSync) await syncCommand();
|
|
1406
1625
|
return;
|
|
1407
1626
|
}
|
|
1408
|
-
console.log(
|
|
1627
|
+
console.log(chalk9.gray(`
|
|
1409
1628
|
Parsing transcript: ${transcriptPath.split("/").slice(-2).join("/")}`));
|
|
1410
1629
|
const parsed = parseTranscript(transcriptPath, options.sessionId ?? "auto", projectPath);
|
|
1411
1630
|
const db = await openDb();
|
|
@@ -1455,16 +1674,16 @@ async function ingestCommand(options) {
|
|
|
1455
1674
|
saveDb();
|
|
1456
1675
|
closeDb();
|
|
1457
1676
|
console.log(
|
|
1458
|
-
|
|
1677
|
+
chalk9.green(
|
|
1459
1678
|
` \u2713 Ingested session ${sessionId}: ${newFiles} files, ${parsed.commits.length} commits, ${newDecisions} decisions`
|
|
1460
1679
|
)
|
|
1461
1680
|
);
|
|
1462
1681
|
if (parsed.decisions.length > 0 && newDecisions > 0) {
|
|
1463
|
-
console.log(
|
|
1682
|
+
console.log(chalk9.gray(`
|
|
1464
1683
|
Decisions captured:`));
|
|
1465
1684
|
for (const d of parsed.decisions.slice(0, 5)) {
|
|
1466
|
-
const conf = d.confidence === "low" ?
|
|
1467
|
-
console.log(
|
|
1685
|
+
const conf = d.confidence === "low" ? chalk9.gray(" (low confidence)") : "";
|
|
1686
|
+
console.log(chalk9.gray(` \u2022 ${d.description.slice(0, 80)}${conf}`));
|
|
1468
1687
|
}
|
|
1469
1688
|
}
|
|
1470
1689
|
if (!options.noSync) {
|
|
@@ -1474,9 +1693,9 @@ async function ingestCommand(options) {
|
|
|
1474
1693
|
}
|
|
1475
1694
|
|
|
1476
1695
|
// src/commands/report.ts
|
|
1477
|
-
import { writeFileSync as
|
|
1696
|
+
import { writeFileSync as writeFileSync5 } from "fs";
|
|
1478
1697
|
import { join as join6 } from "path";
|
|
1479
|
-
import
|
|
1698
|
+
import chalk10 from "chalk";
|
|
1480
1699
|
function formatDuration(start, end) {
|
|
1481
1700
|
if (!end) return "ongoing";
|
|
1482
1701
|
const startMs = new Date(start).getTime();
|
|
@@ -1571,7 +1790,7 @@ async function reportCommand(options) {
|
|
|
1571
1790
|
[options.session, `%${options.session}%`]
|
|
1572
1791
|
);
|
|
1573
1792
|
if (!s) {
|
|
1574
|
-
console.log(
|
|
1793
|
+
console.log(chalk10.red(`
|
|
1575
1794
|
Session "${options.session}" not found.
|
|
1576
1795
|
`));
|
|
1577
1796
|
closeDb();
|
|
@@ -1584,7 +1803,7 @@ async function reportCommand(options) {
|
|
|
1584
1803
|
"SELECT * FROM sessions ORDER BY started_at DESC LIMIT 1"
|
|
1585
1804
|
);
|
|
1586
1805
|
if (!s) {
|
|
1587
|
-
console.log(
|
|
1806
|
+
console.log(chalk10.yellow("\n No sessions found. Run groundctl init first.\n"));
|
|
1588
1807
|
closeDb();
|
|
1589
1808
|
return;
|
|
1590
1809
|
}
|
|
@@ -1626,8 +1845,8 @@ async function reportCommand(options) {
|
|
|
1626
1845
|
}
|
|
1627
1846
|
closeDb();
|
|
1628
1847
|
const outPath2 = join6(cwd, "SESSION_HISTORY.md");
|
|
1629
|
-
|
|
1630
|
-
console.log(
|
|
1848
|
+
writeFileSync5(outPath2, fullReport);
|
|
1849
|
+
console.log(chalk10.green(`
|
|
1631
1850
|
\u2713 SESSION_HISTORY.md written (${sessions.length} sessions)
|
|
1632
1851
|
`));
|
|
1633
1852
|
return;
|
|
@@ -1653,16 +1872,16 @@ async function reportCommand(options) {
|
|
|
1653
1872
|
activeClaims
|
|
1654
1873
|
);
|
|
1655
1874
|
const outPath = join6(cwd, "SESSION_REPORT.md");
|
|
1656
|
-
|
|
1657
|
-
console.log(
|
|
1875
|
+
writeFileSync5(outPath, report);
|
|
1876
|
+
console.log(chalk10.green(`
|
|
1658
1877
|
\u2713 SESSION_REPORT.md written (session ${session.id})
|
|
1659
1878
|
`));
|
|
1660
|
-
console.log(
|
|
1879
|
+
console.log(chalk10.gray(` ${files.length} files \xB7 ${decisions.length} arch log entries \xB7 ${completedFeatures.length} features completed`));
|
|
1661
1880
|
console.log("");
|
|
1662
1881
|
}
|
|
1663
1882
|
|
|
1664
1883
|
// src/commands/health.ts
|
|
1665
|
-
import
|
|
1884
|
+
import chalk11 from "chalk";
|
|
1666
1885
|
async function healthCommand() {
|
|
1667
1886
|
const db = await openDb();
|
|
1668
1887
|
const projectName = process.cwd().split("/").pop() ?? "unknown";
|
|
@@ -1713,32 +1932,32 @@ async function healthCommand() {
|
|
|
1713
1932
|
closeDb();
|
|
1714
1933
|
const totalScore = featureScore + testScore + decisionScore + claimScore + deployScore;
|
|
1715
1934
|
console.log("");
|
|
1716
|
-
console.log(
|
|
1935
|
+
console.log(chalk11.bold(` ${projectName} \u2014 Health Score: ${totalScore}/100
|
|
1717
1936
|
`));
|
|
1718
|
-
const featureColor = featurePct >= 0.7 ?
|
|
1937
|
+
const featureColor = featurePct >= 0.7 ? chalk11.green : featurePct >= 0.4 ? chalk11.yellow : chalk11.red;
|
|
1719
1938
|
const featureMark = featurePct >= 0.4 ? "\u2705" : "\u26A0\uFE0F ";
|
|
1720
1939
|
console.log(
|
|
1721
|
-
` ${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`)
|
|
1722
1941
|
);
|
|
1723
1942
|
const testMark = testFiles > 0 ? "\u2705" : "\u26A0\uFE0F ";
|
|
1724
|
-
const testColor = testFiles > 0 ?
|
|
1943
|
+
const testColor = testFiles > 0 ? chalk11.green : chalk11.red;
|
|
1725
1944
|
console.log(
|
|
1726
|
-
` ${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`))
|
|
1727
1946
|
);
|
|
1728
1947
|
const decMark = decisionCount > 0 ? "\u2705" : "\u26A0\uFE0F ";
|
|
1729
|
-
const decColor = decisionCount > 0 ?
|
|
1948
|
+
const decColor = decisionCount > 0 ? chalk11.green : chalk11.yellow;
|
|
1730
1949
|
console.log(
|
|
1731
|
-
` ${decMark} Arch log ${decColor(decisionCount + " entries")}` +
|
|
1950
|
+
` ${decMark} Arch log ${decColor(decisionCount + " entries")}` + chalk11.gray(` +${decisionScore}pts`)
|
|
1732
1951
|
);
|
|
1733
1952
|
const claimMark = staleClaims === 0 ? "\u2705" : "\u26A0\uFE0F ";
|
|
1734
|
-
const claimColor = staleClaims === 0 ?
|
|
1953
|
+
const claimColor = staleClaims === 0 ? chalk11.green : chalk11.red;
|
|
1735
1954
|
console.log(
|
|
1736
|
-
` ${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`)
|
|
1737
1956
|
);
|
|
1738
1957
|
const deployMark = deployScore > 0 ? "\u2705" : "\u26A0\uFE0F ";
|
|
1739
|
-
const deployLabel = deployScore > 0 ?
|
|
1958
|
+
const deployLabel = deployScore > 0 ? chalk11.green("detected") : chalk11.gray("not detected");
|
|
1740
1959
|
console.log(
|
|
1741
|
-
` ${deployMark} Deploy ${deployLabel}` + (deployScore > 0 ?
|
|
1960
|
+
` ${deployMark} Deploy ${deployLabel}` + (deployScore > 0 ? chalk11.gray(` +${deployScore}pts`) : chalk11.gray(" +0pts"))
|
|
1742
1961
|
);
|
|
1743
1962
|
console.log("");
|
|
1744
1963
|
const recommendations = [];
|
|
@@ -1747,9 +1966,9 @@ async function healthCommand() {
|
|
|
1747
1966
|
if (decisionCount === 0) recommendations.push("Log architecture decisions during sessions so agents understand the why.");
|
|
1748
1967
|
if (featurePct < 0.5 && total > 0) recommendations.push(`${counts.pending} features pending \u2014 run groundctl next to pick one.`);
|
|
1749
1968
|
if (recommendations.length > 0) {
|
|
1750
|
-
console.log(
|
|
1969
|
+
console.log(chalk11.bold(" Recommendations:"));
|
|
1751
1970
|
for (const r of recommendations) {
|
|
1752
|
-
console.log(
|
|
1971
|
+
console.log(chalk11.yellow(` \u2192 ${r}`));
|
|
1753
1972
|
}
|
|
1754
1973
|
console.log("");
|
|
1755
1974
|
}
|
|
@@ -1760,7 +1979,7 @@ import { createServer } from "http";
|
|
|
1760
1979
|
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
1761
1980
|
import { join as join7, dirname as dirname2 } from "path";
|
|
1762
1981
|
import { exec } from "child_process";
|
|
1763
|
-
import
|
|
1982
|
+
import chalk12 from "chalk";
|
|
1764
1983
|
import initSqlJs2 from "sql.js";
|
|
1765
1984
|
function findDbPath(startDir = process.cwd()) {
|
|
1766
1985
|
let dir = startDir;
|
|
@@ -1923,9 +2142,9 @@ async function dashboardCommand(options) {
|
|
|
1923
2142
|
}
|
|
1924
2143
|
});
|
|
1925
2144
|
server.listen(port, "127.0.0.1", () => {
|
|
1926
|
-
console.log(
|
|
1927
|
-
groundctl dashboard \u2192 `) +
|
|
1928
|
-
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"));
|
|
1929
2148
|
exec(`open http://localhost:${port} 2>/dev/null || xdg-open http://localhost:${port} 2>/dev/null || true`);
|
|
1930
2149
|
});
|
|
1931
2150
|
await new Promise((_, reject) => {
|
|
@@ -1938,7 +2157,7 @@ import {
|
|
|
1938
2157
|
existsSync as existsSync6,
|
|
1939
2158
|
readdirSync as readdirSync2,
|
|
1940
2159
|
statSync,
|
|
1941
|
-
writeFileSync as
|
|
2160
|
+
writeFileSync as writeFileSync6,
|
|
1942
2161
|
readFileSync as readFileSync6,
|
|
1943
2162
|
mkdirSync as mkdirSync3,
|
|
1944
2163
|
watch as fsWatch
|
|
@@ -1946,7 +2165,7 @@ import {
|
|
|
1946
2165
|
import { join as join8, resolve as resolve2 } from "path";
|
|
1947
2166
|
import { homedir as homedir3 } from "os";
|
|
1948
2167
|
import { spawn } from "child_process";
|
|
1949
|
-
import
|
|
2168
|
+
import chalk13 from "chalk";
|
|
1950
2169
|
var DEBOUNCE_MS = 8e3;
|
|
1951
2170
|
var DIR_POLL_MS = 5e3;
|
|
1952
2171
|
function claudeEncode2(p) {
|
|
@@ -1978,7 +2197,7 @@ function fileSize(p) {
|
|
|
1978
2197
|
function writePidFile(groundctlDir, pid) {
|
|
1979
2198
|
try {
|
|
1980
2199
|
mkdirSync3(groundctlDir, { recursive: true });
|
|
1981
|
-
|
|
2200
|
+
writeFileSync6(join8(groundctlDir, "watch.pid"), String(pid), "utf8");
|
|
1982
2201
|
} catch {
|
|
1983
2202
|
}
|
|
1984
2203
|
}
|
|
@@ -2001,8 +2220,8 @@ function processAlive(pid) {
|
|
|
2001
2220
|
async function runIngest(transcriptPath, projectPath) {
|
|
2002
2221
|
const filename = transcriptPath.split("/").slice(-2).join("/");
|
|
2003
2222
|
console.log(
|
|
2004
|
-
|
|
2005
|
-
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] `) +
|
|
2223
|
+
chalk13.gray(`
|
|
2224
|
+
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] `) + chalk13.cyan(`Transcript stable \u2192 ingesting ${filename}`)
|
|
2006
2225
|
);
|
|
2007
2226
|
try {
|
|
2008
2227
|
const parsed = parseTranscript(transcriptPath, "auto", projectPath);
|
|
@@ -2058,11 +2277,11 @@ async function runIngest(transcriptPath, projectPath) {
|
|
|
2058
2277
|
if (parsed.commits.length > 0) parts.push(`${parsed.commits.length} commit${parsed.commits.length !== 1 ? "s" : ""}`);
|
|
2059
2278
|
if (newDecisions > 0) parts.push(`${newDecisions} decision${newDecisions !== 1 ? "s" : ""} captured`);
|
|
2060
2279
|
const summary = parts.length > 0 ? parts.join(", ") : "no new data";
|
|
2061
|
-
console.log(
|
|
2280
|
+
console.log(chalk13.green(` \u2713 Session ingested \u2014 ${summary}`));
|
|
2062
2281
|
await syncCommand({ silent: true });
|
|
2063
|
-
console.log(
|
|
2282
|
+
console.log(chalk13.gray(" \u21B3 PROJECT_STATE.md + AGENTS.md updated"));
|
|
2064
2283
|
} catch (err) {
|
|
2065
|
-
console.log(
|
|
2284
|
+
console.log(chalk13.red(` \u2717 Ingest failed: ${err.message}`));
|
|
2066
2285
|
}
|
|
2067
2286
|
}
|
|
2068
2287
|
function startWatcher(transcriptDir, projectPath) {
|
|
@@ -2110,12 +2329,12 @@ function startWatcher(transcriptDir, projectPath) {
|
|
|
2110
2329
|
schedule(fp);
|
|
2111
2330
|
}
|
|
2112
2331
|
});
|
|
2113
|
-
console.log(
|
|
2332
|
+
console.log(chalk13.bold("\n groundctl watch") + chalk13.gray(" \u2014 auto-ingest on session end\n"));
|
|
2114
2333
|
console.log(
|
|
2115
|
-
|
|
2334
|
+
chalk13.gray(" Watching: ") + chalk13.blue(transcriptDir.replace(homedir3(), "~"))
|
|
2116
2335
|
);
|
|
2117
|
-
console.log(
|
|
2118
|
-
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"));
|
|
2119
2338
|
}
|
|
2120
2339
|
async function watchCommand(options) {
|
|
2121
2340
|
const projectPath = options.projectPath ? resolve2(options.projectPath) : process.cwd();
|
|
@@ -2128,29 +2347,29 @@ async function watchCommand(options) {
|
|
|
2128
2347
|
child.unref();
|
|
2129
2348
|
const groundctlDir2 = join8(projectPath, ".groundctl");
|
|
2130
2349
|
writePidFile(groundctlDir2, child.pid);
|
|
2131
|
-
console.log(
|
|
2350
|
+
console.log(chalk13.green(`
|
|
2132
2351
|
\u2713 groundctl watch running in background (PID ${child.pid})`));
|
|
2133
|
-
console.log(
|
|
2134
|
-
console.log(
|
|
2352
|
+
console.log(chalk13.gray(` PID saved to .groundctl/watch.pid`));
|
|
2353
|
+
console.log(chalk13.gray(` To stop: kill ${child.pid}
|
|
2135
2354
|
`));
|
|
2136
2355
|
process.exit(0);
|
|
2137
2356
|
}
|
|
2138
2357
|
const groundctlDir = join8(projectPath, ".groundctl");
|
|
2139
2358
|
const existingPid = readPidFile(groundctlDir);
|
|
2140
2359
|
if (existingPid && processAlive(existingPid)) {
|
|
2141
|
-
console.log(
|
|
2360
|
+
console.log(chalk13.yellow(`
|
|
2142
2361
|
\u26A0 A watcher is already running (PID ${existingPid}).`));
|
|
2143
|
-
console.log(
|
|
2362
|
+
console.log(chalk13.gray(` To stop it: kill ${existingPid}
|
|
2144
2363
|
`));
|
|
2145
2364
|
process.exit(1);
|
|
2146
2365
|
}
|
|
2147
2366
|
let transcriptDir = findTranscriptDir(projectPath);
|
|
2148
2367
|
if (!transcriptDir) {
|
|
2149
|
-
console.log(
|
|
2368
|
+
console.log(chalk13.bold("\n groundctl watch\n"));
|
|
2150
2369
|
console.log(
|
|
2151
|
-
|
|
2370
|
+
chalk13.yellow(" No Claude Code transcript directory found for this project yet.")
|
|
2152
2371
|
);
|
|
2153
|
-
console.log(
|
|
2372
|
+
console.log(chalk13.gray(" Waiting for first session to start...\n"));
|
|
2154
2373
|
await new Promise((resolve3) => {
|
|
2155
2374
|
const interval = setInterval(() => {
|
|
2156
2375
|
const dir = findTranscriptDir(projectPath);
|
|
@@ -2165,7 +2384,7 @@ async function watchCommand(options) {
|
|
|
2165
2384
|
startWatcher(transcriptDir, projectPath);
|
|
2166
2385
|
await new Promise(() => {
|
|
2167
2386
|
process.on("SIGINT", () => {
|
|
2168
|
-
console.log(
|
|
2387
|
+
console.log(chalk13.gray("\n Watcher stopped.\n"));
|
|
2169
2388
|
process.exit(0);
|
|
2170
2389
|
});
|
|
2171
2390
|
process.on("SIGTERM", () => {
|
|
@@ -2175,7 +2394,7 @@ async function watchCommand(options) {
|
|
|
2175
2394
|
}
|
|
2176
2395
|
|
|
2177
2396
|
// src/commands/update.ts
|
|
2178
|
-
import
|
|
2397
|
+
import chalk14 from "chalk";
|
|
2179
2398
|
function parseProgress2(s) {
|
|
2180
2399
|
const m = s.match(/^(\d+)\/(\d+)$/);
|
|
2181
2400
|
if (!m) return null;
|
|
@@ -2183,7 +2402,7 @@ function parseProgress2(s) {
|
|
|
2183
2402
|
}
|
|
2184
2403
|
async function updateCommand(type, nameOrId, options) {
|
|
2185
2404
|
if (type !== "feature") {
|
|
2186
|
-
console.log(
|
|
2405
|
+
console.log(chalk14.red(`
|
|
2187
2406
|
Unknown type "${type}". Use "feature".
|
|
2188
2407
|
`));
|
|
2189
2408
|
process.exit(1);
|
|
@@ -2199,7 +2418,7 @@ async function updateCommand(type, nameOrId, options) {
|
|
|
2199
2418
|
[nameOrId, `%${nameOrId}%`]
|
|
2200
2419
|
);
|
|
2201
2420
|
if (!feature) {
|
|
2202
|
-
console.log(
|
|
2421
|
+
console.log(chalk14.red(`
|
|
2203
2422
|
Feature "${nameOrId}" not found.
|
|
2204
2423
|
`));
|
|
2205
2424
|
closeDb();
|
|
@@ -2219,7 +2438,7 @@ async function updateCommand(type, nameOrId, options) {
|
|
|
2219
2438
|
if (options.progress !== void 0) {
|
|
2220
2439
|
const p = parseProgress2(options.progress);
|
|
2221
2440
|
if (!p) {
|
|
2222
|
-
console.log(
|
|
2441
|
+
console.log(chalk14.yellow(` \u26A0 --progress "${options.progress}" ignored (expected N/N format)
|
|
2223
2442
|
`));
|
|
2224
2443
|
} else {
|
|
2225
2444
|
sets.push("progress_done = ?", "progress_total = ?");
|
|
@@ -2235,7 +2454,7 @@ async function updateCommand(type, nameOrId, options) {
|
|
|
2235
2454
|
params.push(options.status);
|
|
2236
2455
|
}
|
|
2237
2456
|
if (sets.length === 0) {
|
|
2238
|
-
console.log(
|
|
2457
|
+
console.log(chalk14.yellow("\n Nothing to update \u2014 pass at least one option.\n"));
|
|
2239
2458
|
closeDb();
|
|
2240
2459
|
return;
|
|
2241
2460
|
}
|
|
@@ -2247,7 +2466,7 @@ async function updateCommand(type, nameOrId, options) {
|
|
|
2247
2466
|
);
|
|
2248
2467
|
saveDb();
|
|
2249
2468
|
closeDb();
|
|
2250
|
-
console.log(
|
|
2469
|
+
console.log(chalk14.green(` \u2713 Updated: ${feature.name}`));
|
|
2251
2470
|
}
|
|
2252
2471
|
|
|
2253
2472
|
// src/index.ts
|