@markmdev/pebble 0.1.8 → 0.1.10
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/cli/index.js
CHANGED
|
@@ -492,7 +492,24 @@ function getOpenBlockers(issueId) {
|
|
|
492
492
|
return issue.blockedBy.map((id) => state.get(id)).filter((i) => i !== void 0 && i.status !== "closed");
|
|
493
493
|
}
|
|
494
494
|
|
|
495
|
+
// src/shared/time.ts
|
|
496
|
+
import { formatDistanceToNow, parseISO } from "date-fns";
|
|
497
|
+
function formatRelativeTime(isoString) {
|
|
498
|
+
try {
|
|
499
|
+
const date = parseISO(isoString);
|
|
500
|
+
return formatDistanceToNow(date, { addSuffix: true });
|
|
501
|
+
} catch {
|
|
502
|
+
return isoString;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
495
506
|
// src/cli/lib/output.ts
|
|
507
|
+
function formatLimitMessage(info) {
|
|
508
|
+
if (!info.limited) return "";
|
|
509
|
+
return `
|
|
510
|
+
---
|
|
511
|
+
Showing ${info.shown} of ${info.total} issues. Use --all or --limit <n> to see more.`;
|
|
512
|
+
}
|
|
496
513
|
function formatJson(data) {
|
|
497
514
|
return JSON.stringify(data, null, 2);
|
|
498
515
|
}
|
|
@@ -512,7 +529,7 @@ function truncate(str, maxLength) {
|
|
|
512
529
|
function pad(str, width) {
|
|
513
530
|
return str.padEnd(width);
|
|
514
531
|
}
|
|
515
|
-
function
|
|
532
|
+
function formatIssueDetailPretty(issue, ctx) {
|
|
516
533
|
const lines = [];
|
|
517
534
|
lines.push(`${issue.id} - ${issue.title}`);
|
|
518
535
|
lines.push("\u2500".repeat(60));
|
|
@@ -527,13 +544,40 @@ function formatIssuePrettyWithBlocking(issue, blocking) {
|
|
|
527
544
|
lines.push("Description:");
|
|
528
545
|
lines.push(issue.description);
|
|
529
546
|
}
|
|
547
|
+
if (ctx.children.length > 0) {
|
|
548
|
+
const closedChildren = ctx.children.filter((c) => c.status === "closed");
|
|
549
|
+
const pendingChildren = ctx.children.filter((c) => c.status === "pending_verification");
|
|
550
|
+
const pendingStr = pendingChildren.length > 0 ? ` (${pendingChildren.length} pending verification)` : "";
|
|
551
|
+
lines.push("");
|
|
552
|
+
lines.push(`Children (${closedChildren.length}/${ctx.children.length} done${pendingStr}):`);
|
|
553
|
+
for (const child of ctx.children) {
|
|
554
|
+
const statusIcon = child.status === "closed" ? "\u2713" : child.status === "in_progress" ? "\u25B6" : "\u25CB";
|
|
555
|
+
lines.push(` ${statusIcon} ${child.id} - ${child.title} [${child.status}]`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
530
558
|
if (issue.blockedBy.length > 0) {
|
|
531
559
|
lines.push("");
|
|
532
560
|
lines.push(`Blocked by: ${issue.blockedBy.join(", ")}`);
|
|
533
561
|
}
|
|
534
|
-
if (blocking.length > 0) {
|
|
562
|
+
if (ctx.blocking.length > 0) {
|
|
563
|
+
lines.push("");
|
|
564
|
+
lines.push(`Blocking: ${ctx.blocking.map((i) => i.id).join(", ")}`);
|
|
565
|
+
}
|
|
566
|
+
if (ctx.verifications.length > 0) {
|
|
567
|
+
const closedVerifications = ctx.verifications.filter((v) => v.status === "closed");
|
|
535
568
|
lines.push("");
|
|
536
|
-
lines.push(`
|
|
569
|
+
lines.push(`Verifications (${closedVerifications.length}/${ctx.verifications.length} done):`);
|
|
570
|
+
for (const v of ctx.verifications) {
|
|
571
|
+
const statusIcon = v.status === "closed" ? "\u2713" : "\u25CB";
|
|
572
|
+
lines.push(` ${statusIcon} ${v.id} - ${v.title} [${v.status}]`);
|
|
573
|
+
}
|
|
574
|
+
} else if (issue.status === "pending_verification") {
|
|
575
|
+
lines.push("");
|
|
576
|
+
lines.push("Verifications: None found (status may be stale)");
|
|
577
|
+
}
|
|
578
|
+
if (ctx.related.length > 0) {
|
|
579
|
+
lines.push("");
|
|
580
|
+
lines.push(`Related: ${ctx.related.map((r) => r.id).join(", ")}`);
|
|
537
581
|
}
|
|
538
582
|
if (issue.comments.length > 0) {
|
|
539
583
|
lines.push("");
|
|
@@ -549,6 +593,20 @@ function formatIssuePrettyWithBlocking(issue, blocking) {
|
|
|
549
593
|
lines.push(`Updated: ${new Date(issue.updatedAt).toLocaleString()}`);
|
|
550
594
|
return lines.join("\n");
|
|
551
595
|
}
|
|
596
|
+
function outputIssueDetail(issue, ctx, pretty) {
|
|
597
|
+
if (pretty) {
|
|
598
|
+
console.log(formatIssueDetailPretty(issue, ctx));
|
|
599
|
+
} else {
|
|
600
|
+
const output = {
|
|
601
|
+
...issue,
|
|
602
|
+
blocking: ctx.blocking.map((i) => i.id),
|
|
603
|
+
children: ctx.children.map((i) => ({ id: i.id, title: i.title, status: i.status })),
|
|
604
|
+
verifications: ctx.verifications.map((i) => ({ id: i.id, title: i.title, status: i.status })),
|
|
605
|
+
related: ctx.related.map((i) => i.id)
|
|
606
|
+
};
|
|
607
|
+
console.log(formatJson(output));
|
|
608
|
+
}
|
|
609
|
+
}
|
|
552
610
|
function formatIssueListPretty(issues) {
|
|
553
611
|
if (issues.length === 0) {
|
|
554
612
|
return "No issues found.";
|
|
@@ -625,17 +683,6 @@ function formatErrorPretty(error) {
|
|
|
625
683
|
const message = error instanceof Error ? error.message : error;
|
|
626
684
|
return `Error: ${message}`;
|
|
627
685
|
}
|
|
628
|
-
function outputIssueWithBlocking(issue, blocking, pretty) {
|
|
629
|
-
if (pretty) {
|
|
630
|
-
console.log(formatIssuePrettyWithBlocking(issue, blocking));
|
|
631
|
-
} else {
|
|
632
|
-
const output = {
|
|
633
|
-
...issue,
|
|
634
|
-
blocking: blocking.map((i) => i.id)
|
|
635
|
-
};
|
|
636
|
-
console.log(formatJson(output));
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
686
|
function outputMutationSuccess(id, pretty) {
|
|
640
687
|
if (pretty) {
|
|
641
688
|
console.log(`\u2713 ${id}`);
|
|
@@ -643,11 +690,94 @@ function outputMutationSuccess(id, pretty) {
|
|
|
643
690
|
console.log(JSON.stringify({ id, success: true }));
|
|
644
691
|
}
|
|
645
692
|
}
|
|
646
|
-
function outputIssueList(issues, pretty) {
|
|
693
|
+
function outputIssueList(issues, pretty, limitInfo) {
|
|
647
694
|
if (pretty) {
|
|
648
695
|
console.log(formatIssueListPretty(issues));
|
|
696
|
+
if (limitInfo?.limited) {
|
|
697
|
+
console.log(formatLimitMessage(limitInfo));
|
|
698
|
+
}
|
|
649
699
|
} else {
|
|
650
|
-
|
|
700
|
+
if (limitInfo?.limited) {
|
|
701
|
+
console.log(formatJson({ issues, _meta: limitInfo }));
|
|
702
|
+
} else {
|
|
703
|
+
console.log(formatJson(issues));
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function buildIssueTree(issues) {
|
|
708
|
+
const issueMap = /* @__PURE__ */ new Map();
|
|
709
|
+
for (const issue of issues) {
|
|
710
|
+
issueMap.set(issue.id, issue);
|
|
711
|
+
}
|
|
712
|
+
const childIds = /* @__PURE__ */ new Set();
|
|
713
|
+
for (const issue of issues) {
|
|
714
|
+
if (issue.parent && issueMap.has(issue.parent)) {
|
|
715
|
+
childIds.add(issue.id);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const buildNode = (issue) => {
|
|
719
|
+
const children = issues.filter((i) => i.parent === issue.id).map(buildNode);
|
|
720
|
+
return {
|
|
721
|
+
id: issue.id,
|
|
722
|
+
title: issue.title,
|
|
723
|
+
type: issue.type,
|
|
724
|
+
priority: issue.priority,
|
|
725
|
+
status: issue.status,
|
|
726
|
+
createdAt: issue.createdAt,
|
|
727
|
+
childrenCount: children.length,
|
|
728
|
+
...children.length > 0 && { children }
|
|
729
|
+
};
|
|
730
|
+
};
|
|
731
|
+
const roots = issues.filter((i) => !childIds.has(i.id));
|
|
732
|
+
return roots.map(buildNode);
|
|
733
|
+
}
|
|
734
|
+
function formatIssueTreePretty(nodes, sectionHeader) {
|
|
735
|
+
if (nodes.length === 0) {
|
|
736
|
+
return "No issues found.";
|
|
737
|
+
}
|
|
738
|
+
const lines = [];
|
|
739
|
+
if (sectionHeader) {
|
|
740
|
+
lines.push(`## ${sectionHeader}`);
|
|
741
|
+
lines.push("");
|
|
742
|
+
}
|
|
743
|
+
const countAll = (node) => {
|
|
744
|
+
const children = node.children ?? [];
|
|
745
|
+
return 1 + children.reduce((sum, child) => sum + countAll(child), 0);
|
|
746
|
+
};
|
|
747
|
+
const totalCount = nodes.reduce((sum, node) => sum + countAll(node), 0);
|
|
748
|
+
const formatNode = (node, prefix, isLast, isRoot) => {
|
|
749
|
+
const connector = isRoot ? "" : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
|
|
750
|
+
const statusIcon = node.status === "closed" ? "\u2713" : node.status === "in_progress" ? "\u25B6" : node.status === "pending_verification" ? "\u23F3" : "\u25CB";
|
|
751
|
+
const statusText = STATUS_LABELS[node.status].toLowerCase();
|
|
752
|
+
const relativeTime = formatRelativeTime(node.createdAt);
|
|
753
|
+
lines.push(`${prefix}${connector}${statusIcon} ${node.id}: ${node.title} [${node.type}] P${node.priority} ${statusText} ${relativeTime}`);
|
|
754
|
+
const children = node.children ?? [];
|
|
755
|
+
const childPrefix = isRoot ? "" : prefix + (isLast ? " " : "\u2502 ");
|
|
756
|
+
children.forEach((child, index) => {
|
|
757
|
+
const childIsLast = index === children.length - 1;
|
|
758
|
+
formatNode(child, childPrefix, childIsLast, false);
|
|
759
|
+
});
|
|
760
|
+
};
|
|
761
|
+
nodes.forEach((node, index) => {
|
|
762
|
+
formatNode(node, "", index === nodes.length - 1, true);
|
|
763
|
+
});
|
|
764
|
+
lines.push("");
|
|
765
|
+
lines.push(`Total: ${totalCount} issue(s)`);
|
|
766
|
+
return lines.join("\n");
|
|
767
|
+
}
|
|
768
|
+
function outputIssueTree(issues, pretty, sectionHeader, limitInfo) {
|
|
769
|
+
const tree = buildIssueTree(issues);
|
|
770
|
+
if (pretty) {
|
|
771
|
+
console.log(formatIssueTreePretty(tree, sectionHeader));
|
|
772
|
+
if (limitInfo?.limited) {
|
|
773
|
+
console.log(formatLimitMessage(limitInfo));
|
|
774
|
+
}
|
|
775
|
+
} else {
|
|
776
|
+
if (limitInfo?.limited) {
|
|
777
|
+
console.log(formatJson({ issues: tree, _meta: limitInfo }));
|
|
778
|
+
} else {
|
|
779
|
+
console.log(formatJson(tree));
|
|
780
|
+
}
|
|
651
781
|
}
|
|
652
782
|
}
|
|
653
783
|
function outputError(error, pretty) {
|
|
@@ -658,42 +788,55 @@ function outputError(error, pretty) {
|
|
|
658
788
|
}
|
|
659
789
|
process.exit(1);
|
|
660
790
|
}
|
|
661
|
-
function formatIssueListVerbose(issues) {
|
|
791
|
+
function formatIssueListVerbose(issues, sectionHeader) {
|
|
662
792
|
if (issues.length === 0) {
|
|
663
793
|
return "No issues found.";
|
|
664
794
|
}
|
|
665
795
|
const lines = [];
|
|
796
|
+
if (sectionHeader) {
|
|
797
|
+
lines.push(`## ${sectionHeader} (${issues.length})`);
|
|
798
|
+
lines.push("");
|
|
799
|
+
}
|
|
666
800
|
for (const info of issues) {
|
|
667
|
-
const { issue, blocking, children, verifications, blockers } = info;
|
|
668
|
-
lines.push(`${issue.id}
|
|
669
|
-
lines.push(
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
lines.push(` Verifications: ${verifications}`);
|
|
801
|
+
const { issue, blocking, children, verifications, blockers, parent } = info;
|
|
802
|
+
lines.push(`${issue.id}: ${issue.title}`);
|
|
803
|
+
lines.push(` Type: ${formatType(issue.type)} | Priority: P${issue.priority} | Created: ${formatRelativeTime(issue.createdAt)}`);
|
|
804
|
+
if (parent) {
|
|
805
|
+
lines.push(` Epic: ${parent.id} (${parent.title})`);
|
|
806
|
+
}
|
|
807
|
+
if (blocking.length > 0) {
|
|
808
|
+
lines.push(` Blocking: ${blocking.join(", ")}`);
|
|
809
|
+
}
|
|
677
810
|
if (blockers && blockers.length > 0) {
|
|
678
|
-
lines.push(` Blocked by:
|
|
811
|
+
lines.push(` Blocked by: ${blockers.join(", ")}`);
|
|
812
|
+
}
|
|
813
|
+
if (issue.type === "epic" && children > 0) {
|
|
814
|
+
lines.push(` Children: ${children} | Verifications: ${verifications}`);
|
|
679
815
|
}
|
|
680
816
|
lines.push("");
|
|
681
817
|
}
|
|
682
|
-
lines.push(`Total: ${issues.length} issue(s)`);
|
|
683
818
|
return lines.join("\n");
|
|
684
819
|
}
|
|
685
|
-
function outputIssueListVerbose(issues, pretty) {
|
|
820
|
+
function outputIssueListVerbose(issues, pretty, sectionHeader, limitInfo) {
|
|
686
821
|
if (pretty) {
|
|
687
|
-
console.log(formatIssueListVerbose(issues));
|
|
822
|
+
console.log(formatIssueListVerbose(issues, sectionHeader));
|
|
823
|
+
if (limitInfo?.limited) {
|
|
824
|
+
console.log(formatLimitMessage(limitInfo));
|
|
825
|
+
}
|
|
688
826
|
} else {
|
|
689
|
-
const output = issues.map(({ issue, blocking, children, verifications, blockers }) => ({
|
|
827
|
+
const output = issues.map(({ issue, blocking, children, verifications, blockers, parent }) => ({
|
|
690
828
|
...issue,
|
|
691
829
|
blocking,
|
|
692
830
|
childrenCount: issue.type === "epic" ? children : void 0,
|
|
693
831
|
verificationsCount: verifications,
|
|
694
|
-
...blockers && { openBlockers: blockers }
|
|
832
|
+
...blockers && { openBlockers: blockers },
|
|
833
|
+
...parent && { parentInfo: parent }
|
|
695
834
|
}));
|
|
696
|
-
|
|
835
|
+
if (limitInfo?.limited) {
|
|
836
|
+
console.log(formatJson({ issues: output, _meta: limitInfo }));
|
|
837
|
+
} else {
|
|
838
|
+
console.log(formatJson(output));
|
|
839
|
+
}
|
|
697
840
|
}
|
|
698
841
|
}
|
|
699
842
|
|
|
@@ -1015,6 +1158,8 @@ Pending verifications:`);
|
|
|
1015
1158
|
for (const v of result.pendingVerifications || []) {
|
|
1016
1159
|
console.log(` \u2022 ${v.id} - ${v.title}`);
|
|
1017
1160
|
}
|
|
1161
|
+
console.log(`
|
|
1162
|
+
Run: pb verifications ${result.id}`);
|
|
1018
1163
|
} else {
|
|
1019
1164
|
console.log(`\u2713 ${result.id}`);
|
|
1020
1165
|
if (result.unblocked && result.unblocked.length > 0) {
|
|
@@ -1031,6 +1176,7 @@ Unblocked:`);
|
|
|
1031
1176
|
success: true,
|
|
1032
1177
|
status: result.status,
|
|
1033
1178
|
...result.pendingVerifications && { pendingVerifications: result.pendingVerifications },
|
|
1179
|
+
...result.pendingVerifications && { hint: `pb verifications ${result.id}` },
|
|
1034
1180
|
...result.unblocked && { unblocked: result.unblocked }
|
|
1035
1181
|
}));
|
|
1036
1182
|
}
|
|
@@ -1046,6 +1192,7 @@ Unblocked:`);
|
|
|
1046
1192
|
for (const v of result.pendingVerifications || []) {
|
|
1047
1193
|
console.log(` \u2022 ${v.id} - ${v.title}`);
|
|
1048
1194
|
}
|
|
1195
|
+
console.log(` Run: pb verifications ${result.id}`);
|
|
1049
1196
|
} else {
|
|
1050
1197
|
console.log(`\u2713 ${result.id}`);
|
|
1051
1198
|
if (result.unblocked && result.unblocked.length > 0) {
|
|
@@ -1065,6 +1212,7 @@ Unblocked:`);
|
|
|
1065
1212
|
status: r.status,
|
|
1066
1213
|
...r.error && { error: r.error },
|
|
1067
1214
|
...r.pendingVerifications && { pendingVerifications: r.pendingVerifications },
|
|
1215
|
+
...r.pendingVerifications && { hint: `pb verifications ${r.id}` },
|
|
1068
1216
|
...r.unblocked && { unblocked: r.unblocked }
|
|
1069
1217
|
}))));
|
|
1070
1218
|
}
|
|
@@ -1184,7 +1332,7 @@ function claimCommand(program2) {
|
|
|
1184
1332
|
|
|
1185
1333
|
// src/cli/commands/list.ts
|
|
1186
1334
|
function listCommand(program2) {
|
|
1187
|
-
program2.command("list").description("List issues").option("--status <status>", "Filter by status").option("-t, --type <type>", "Filter by type").option("--priority <priority>", "Filter by priority").option("--parent <id>", "Filter by parent epic").action(async (options) => {
|
|
1335
|
+
program2.command("list").description("List issues").option("--status <status>", "Filter by status").option("-t, --type <type>", "Filter by type").option("--priority <priority>", "Filter by priority").option("--parent <id>", "Filter by parent epic").option("-v, --verbose", "Show expanded details (parent, children, blocking, verifications)").option("--flat", "Show flat list instead of hierarchical tree").option("--limit <n>", "Max issues to return (default: 30)").option("--all", "Show all issues (no limit)").action(async (options) => {
|
|
1188
1336
|
const pretty = program2.opts().pretty ?? false;
|
|
1189
1337
|
try {
|
|
1190
1338
|
getOrCreatePebbleDir();
|
|
@@ -1213,8 +1361,48 @@ function listCommand(program2) {
|
|
|
1213
1361
|
if (options.parent !== void 0) {
|
|
1214
1362
|
filters.parent = resolveId(options.parent);
|
|
1215
1363
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1364
|
+
let issues = getIssues(filters);
|
|
1365
|
+
issues.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
1366
|
+
const total = issues.length;
|
|
1367
|
+
const limit = options.all ? 0 : options.limit ? parseInt(options.limit, 10) : 30;
|
|
1368
|
+
if (limit > 0 && issues.length > limit) {
|
|
1369
|
+
issues = issues.slice(0, limit);
|
|
1370
|
+
}
|
|
1371
|
+
const limitInfo = {
|
|
1372
|
+
total,
|
|
1373
|
+
shown: issues.length,
|
|
1374
|
+
limited: limit > 0 && total > limit
|
|
1375
|
+
};
|
|
1376
|
+
if (options.verbose) {
|
|
1377
|
+
const verboseIssues = issues.map((issue) => {
|
|
1378
|
+
const info = {
|
|
1379
|
+
issue,
|
|
1380
|
+
blocking: getBlocking(issue.id).map((i) => i.id),
|
|
1381
|
+
children: getChildren(issue.id).length,
|
|
1382
|
+
verifications: getVerifications(issue.id).length
|
|
1383
|
+
};
|
|
1384
|
+
if (issue.parent) {
|
|
1385
|
+
const parentIssue = getIssue(issue.parent);
|
|
1386
|
+
if (parentIssue) {
|
|
1387
|
+
info.parent = { id: parentIssue.id, title: parentIssue.title };
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
return info;
|
|
1391
|
+
});
|
|
1392
|
+
let sectionHeader = "Issues";
|
|
1393
|
+
if (filters.status) {
|
|
1394
|
+
sectionHeader = `${STATUS_LABELS[filters.status]} Issues`;
|
|
1395
|
+
}
|
|
1396
|
+
outputIssueListVerbose(verboseIssues, pretty, sectionHeader, limitInfo);
|
|
1397
|
+
} else if (options.flat) {
|
|
1398
|
+
outputIssueList(issues, pretty, limitInfo);
|
|
1399
|
+
} else {
|
|
1400
|
+
let sectionHeader = "Issues";
|
|
1401
|
+
if (filters.status) {
|
|
1402
|
+
sectionHeader = `${STATUS_LABELS[filters.status]} Issues`;
|
|
1403
|
+
}
|
|
1404
|
+
outputIssueTree(issues, pretty, sectionHeader, limitInfo);
|
|
1405
|
+
}
|
|
1218
1406
|
} catch (error) {
|
|
1219
1407
|
outputError(error, pretty);
|
|
1220
1408
|
}
|
|
@@ -1233,7 +1421,10 @@ function showCommand(program2) {
|
|
|
1233
1421
|
throw new Error(`Issue not found: ${id}`);
|
|
1234
1422
|
}
|
|
1235
1423
|
const blocking = getBlocking(resolvedId);
|
|
1236
|
-
|
|
1424
|
+
const children = issue.type === "epic" ? getChildren(resolvedId) : [];
|
|
1425
|
+
const verifications = getVerifications(resolvedId);
|
|
1426
|
+
const related = getRelated(resolvedId);
|
|
1427
|
+
outputIssueDetail(issue, { blocking, children, verifications, related }, pretty);
|
|
1237
1428
|
} catch (error) {
|
|
1238
1429
|
outputError(error, pretty);
|
|
1239
1430
|
}
|
|
@@ -1242,21 +1433,48 @@ function showCommand(program2) {
|
|
|
1242
1433
|
|
|
1243
1434
|
// src/cli/commands/ready.ts
|
|
1244
1435
|
function readyCommand(program2) {
|
|
1245
|
-
program2.command("ready").description("Show issues ready for work (no open blockers)").option("-v, --verbose", "Show expanded details (parent, children, blocking, verifications)").action(async (options) => {
|
|
1436
|
+
program2.command("ready").description("Show issues ready for work (no open blockers)").option("-v, --verbose", "Show expanded details (parent, children, blocking, verifications)").option("-t, --type <type>", "Filter by type: task, bug, epic, verification").option("--limit <n>", "Max issues to return (default: 30)").option("--all", "Show all issues (no limit)").action(async (options) => {
|
|
1246
1437
|
const pretty = program2.opts().pretty ?? false;
|
|
1247
1438
|
try {
|
|
1248
1439
|
getOrCreatePebbleDir();
|
|
1249
|
-
|
|
1440
|
+
let issues = getReady();
|
|
1441
|
+
if (options.type) {
|
|
1442
|
+
const typeFilter = options.type.toLowerCase();
|
|
1443
|
+
if (!ISSUE_TYPES.includes(typeFilter)) {
|
|
1444
|
+
throw new Error(`Invalid type: ${options.type}. Must be one of: ${ISSUE_TYPES.join(", ")}`);
|
|
1445
|
+
}
|
|
1446
|
+
issues = issues.filter((i) => i.type === typeFilter);
|
|
1447
|
+
}
|
|
1448
|
+
issues.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
1449
|
+
const total = issues.length;
|
|
1450
|
+
const limit = options.all ? 0 : options.limit ? parseInt(options.limit, 10) : 30;
|
|
1451
|
+
if (limit > 0 && issues.length > limit) {
|
|
1452
|
+
issues = issues.slice(0, limit);
|
|
1453
|
+
}
|
|
1454
|
+
const limitInfo = {
|
|
1455
|
+
total,
|
|
1456
|
+
shown: issues.length,
|
|
1457
|
+
limited: limit > 0 && total > limit
|
|
1458
|
+
};
|
|
1250
1459
|
if (options.verbose) {
|
|
1251
|
-
const verboseIssues = issues.map((issue) =>
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1460
|
+
const verboseIssues = issues.map((issue) => {
|
|
1461
|
+
const info = {
|
|
1462
|
+
issue,
|
|
1463
|
+
blocking: getBlocking(issue.id).map((i) => i.id),
|
|
1464
|
+
children: getChildren(issue.id).length,
|
|
1465
|
+
verifications: getVerifications(issue.id).length
|
|
1466
|
+
};
|
|
1467
|
+
if (issue.parent) {
|
|
1468
|
+
const parentIssue = getIssue(issue.parent);
|
|
1469
|
+
if (parentIssue) {
|
|
1470
|
+
info.parent = { id: parentIssue.id, title: parentIssue.title };
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
return info;
|
|
1474
|
+
});
|
|
1475
|
+
outputIssueListVerbose(verboseIssues, pretty, "Ready Issues", limitInfo);
|
|
1258
1476
|
} else {
|
|
1259
|
-
outputIssueList(issues, pretty);
|
|
1477
|
+
outputIssueList(issues, pretty, limitInfo);
|
|
1260
1478
|
}
|
|
1261
1479
|
} catch (error) {
|
|
1262
1480
|
outputError(error, pretty);
|
|
@@ -1266,26 +1484,44 @@ function readyCommand(program2) {
|
|
|
1266
1484
|
|
|
1267
1485
|
// src/cli/commands/blocked.ts
|
|
1268
1486
|
function blockedCommand(program2) {
|
|
1269
|
-
program2.command("blocked").description("Show blocked issues (have open blockers)").option("-v, --verbose", "Show expanded details including WHY each issue is blocked").action(async (options) => {
|
|
1487
|
+
program2.command("blocked").description("Show blocked issues (have open blockers)").option("-v, --verbose", "Show expanded details including WHY each issue is blocked").option("--limit <n>", "Max issues to return (default: 30)").option("--all", "Show all issues (no limit)").action(async (options) => {
|
|
1270
1488
|
const pretty = program2.opts().pretty ?? false;
|
|
1271
1489
|
try {
|
|
1272
1490
|
getOrCreatePebbleDir();
|
|
1273
|
-
|
|
1491
|
+
let issues = getBlocked();
|
|
1492
|
+
issues.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
1493
|
+
const total = issues.length;
|
|
1494
|
+
const limit = options.all ? 0 : options.limit ? parseInt(options.limit, 10) : 30;
|
|
1495
|
+
if (limit > 0 && issues.length > limit) {
|
|
1496
|
+
issues = issues.slice(0, limit);
|
|
1497
|
+
}
|
|
1498
|
+
const limitInfo = {
|
|
1499
|
+
total,
|
|
1500
|
+
shown: issues.length,
|
|
1501
|
+
limited: limit > 0 && total > limit
|
|
1502
|
+
};
|
|
1274
1503
|
if (options.verbose) {
|
|
1275
1504
|
const verboseIssues = issues.map((issue) => {
|
|
1276
1505
|
const allBlockers = getBlockers(issue.id);
|
|
1277
1506
|
const openBlockers = allBlockers.filter((b) => b.status !== "closed").map((b) => b.id);
|
|
1278
|
-
|
|
1507
|
+
const info = {
|
|
1279
1508
|
issue,
|
|
1280
1509
|
blocking: getBlocking(issue.id).map((i) => i.id),
|
|
1281
1510
|
children: getChildren(issue.id).length,
|
|
1282
1511
|
verifications: getVerifications(issue.id).length,
|
|
1283
1512
|
blockers: openBlockers
|
|
1284
1513
|
};
|
|
1514
|
+
if (issue.parent) {
|
|
1515
|
+
const parentIssue = getIssue(issue.parent);
|
|
1516
|
+
if (parentIssue) {
|
|
1517
|
+
info.parent = { id: parentIssue.id, title: parentIssue.title };
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
return info;
|
|
1285
1521
|
});
|
|
1286
|
-
outputIssueListVerbose(verboseIssues, pretty);
|
|
1522
|
+
outputIssueListVerbose(verboseIssues, pretty, "Blocked Issues", limitInfo);
|
|
1287
1523
|
} else {
|
|
1288
|
-
outputIssueList(issues, pretty);
|
|
1524
|
+
outputIssueList(issues, pretty, limitInfo);
|
|
1289
1525
|
}
|
|
1290
1526
|
} catch (error) {
|
|
1291
1527
|
outputError(error, pretty);
|
|
@@ -1296,39 +1532,55 @@ function blockedCommand(program2) {
|
|
|
1296
1532
|
// src/cli/commands/dep.ts
|
|
1297
1533
|
function depCommand(program2) {
|
|
1298
1534
|
const dep = program2.command("dep").description("Manage dependencies");
|
|
1299
|
-
dep.command("add <id>
|
|
1535
|
+
dep.command("add <id> [blockerId]").description("Add a blocking dependency. Use --needs or --blocks for self-documenting syntax.").option("--needs <id>", "Issue that must be completed first (first arg needs this)").option("--blocks <id>", "Issue that this blocks (first arg blocks this)").action(async (id, blockerId, options) => {
|
|
1300
1536
|
const pretty = program2.opts().pretty ?? false;
|
|
1301
1537
|
try {
|
|
1538
|
+
if (options.needs && options.blocks) {
|
|
1539
|
+
throw new Error("Cannot use both --needs and --blocks");
|
|
1540
|
+
}
|
|
1541
|
+
if (blockerId && (options.needs || options.blocks)) {
|
|
1542
|
+
throw new Error("Cannot combine positional blockerId with --needs or --blocks");
|
|
1543
|
+
}
|
|
1544
|
+
if (!blockerId && !options.needs && !options.blocks) {
|
|
1545
|
+
throw new Error("Must provide blockerId, --needs <id>, or --blocks <id>");
|
|
1546
|
+
}
|
|
1302
1547
|
const pebbleDir = getOrCreatePebbleDir();
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1548
|
+
let blockedId;
|
|
1549
|
+
let blockerIdResolved;
|
|
1550
|
+
if (options.blocks) {
|
|
1551
|
+
blockedId = resolveId(options.blocks);
|
|
1552
|
+
blockerIdResolved = resolveId(id);
|
|
1553
|
+
} else {
|
|
1554
|
+
blockedId = resolveId(id);
|
|
1555
|
+
blockerIdResolved = resolveId(options.needs || blockerId);
|
|
1556
|
+
}
|
|
1557
|
+
const issue = getIssue(blockedId);
|
|
1306
1558
|
if (!issue) {
|
|
1307
|
-
throw new Error(`Issue not found: ${
|
|
1559
|
+
throw new Error(`Issue not found: ${blockedId}`);
|
|
1308
1560
|
}
|
|
1309
|
-
const blocker = getIssue(
|
|
1561
|
+
const blocker = getIssue(blockerIdResolved);
|
|
1310
1562
|
if (!blocker) {
|
|
1311
|
-
throw new Error(`Blocker issue not found: ${
|
|
1563
|
+
throw new Error(`Blocker issue not found: ${blockerIdResolved}`);
|
|
1312
1564
|
}
|
|
1313
|
-
if (
|
|
1565
|
+
if (blockedId === blockerIdResolved) {
|
|
1314
1566
|
throw new Error("Cannot add self as blocker");
|
|
1315
1567
|
}
|
|
1316
|
-
if (issue.blockedBy.includes(
|
|
1317
|
-
throw new Error(`Dependency already exists: ${
|
|
1568
|
+
if (issue.blockedBy.includes(blockerIdResolved)) {
|
|
1569
|
+
throw new Error(`Dependency already exists: ${blockedId} is blocked by ${blockerIdResolved}`);
|
|
1318
1570
|
}
|
|
1319
|
-
if (detectCycle(
|
|
1571
|
+
if (detectCycle(blockedId, blockerIdResolved)) {
|
|
1320
1572
|
throw new Error(`Adding this dependency would create a cycle`);
|
|
1321
1573
|
}
|
|
1322
1574
|
const event = {
|
|
1323
1575
|
type: "update",
|
|
1324
|
-
issueId:
|
|
1576
|
+
issueId: blockedId,
|
|
1325
1577
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1326
1578
|
data: {
|
|
1327
|
-
blockedBy: [...issue.blockedBy,
|
|
1579
|
+
blockedBy: [...issue.blockedBy, blockerIdResolved]
|
|
1328
1580
|
}
|
|
1329
1581
|
};
|
|
1330
1582
|
appendEvent(event, pebbleDir);
|
|
1331
|
-
outputMutationSuccess(
|
|
1583
|
+
outputMutationSuccess(blockedId, pretty);
|
|
1332
1584
|
} catch (error) {
|
|
1333
1585
|
outputError(error, pretty);
|
|
1334
1586
|
}
|
|
@@ -1479,7 +1731,7 @@ function depCommand(program2) {
|
|
|
1479
1731
|
outputError(error, pretty);
|
|
1480
1732
|
}
|
|
1481
1733
|
});
|
|
1482
|
-
dep.command("tree <id>").description("Show
|
|
1734
|
+
dep.command("tree <id>").description("Show issue tree (children, verifications, and full hierarchy)").action(async (id) => {
|
|
1483
1735
|
const pretty = program2.opts().pretty ?? false;
|
|
1484
1736
|
try {
|
|
1485
1737
|
const resolvedId = resolveId(id);
|
|
@@ -1489,10 +1741,9 @@ function depCommand(program2) {
|
|
|
1489
1741
|
}
|
|
1490
1742
|
const events = readEvents();
|
|
1491
1743
|
const state = computeState(events);
|
|
1492
|
-
const
|
|
1493
|
-
const tree = buildDepTree(resolvedId, visited, 0, state);
|
|
1744
|
+
const tree = buildIssueTree2(resolvedId, state);
|
|
1494
1745
|
if (pretty) {
|
|
1495
|
-
console.log(
|
|
1746
|
+
console.log(formatIssueTreePretty2(tree));
|
|
1496
1747
|
} else {
|
|
1497
1748
|
console.log(formatJson(tree));
|
|
1498
1749
|
}
|
|
@@ -1501,50 +1752,94 @@ function depCommand(program2) {
|
|
|
1501
1752
|
}
|
|
1502
1753
|
});
|
|
1503
1754
|
}
|
|
1504
|
-
function
|
|
1505
|
-
if (visited.has(issueId)) {
|
|
1506
|
-
return null;
|
|
1507
|
-
}
|
|
1508
|
-
visited.add(issueId);
|
|
1755
|
+
function buildIssueTree2(issueId, state) {
|
|
1509
1756
|
const issue = state.get(issueId);
|
|
1510
1757
|
if (!issue) {
|
|
1511
1758
|
return null;
|
|
1512
1759
|
}
|
|
1513
|
-
const
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1760
|
+
const buildChildren = (id, visited2) => {
|
|
1761
|
+
const children = [];
|
|
1762
|
+
for (const [, i] of state) {
|
|
1763
|
+
if ((i.parent === id || i.verifies === id) && !visited2.has(i.id)) {
|
|
1764
|
+
visited2.add(i.id);
|
|
1765
|
+
const nodeChildren = buildChildren(i.id, visited2);
|
|
1766
|
+
children.push({
|
|
1767
|
+
id: i.id,
|
|
1768
|
+
title: i.title,
|
|
1769
|
+
type: i.type,
|
|
1770
|
+
priority: i.priority,
|
|
1771
|
+
status: i.status,
|
|
1772
|
+
isTarget: i.id === issueId,
|
|
1773
|
+
childrenCount: nodeChildren.length,
|
|
1774
|
+
...nodeChildren.length > 0 && { children: nodeChildren }
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1518
1777
|
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1778
|
+
return children;
|
|
1779
|
+
};
|
|
1780
|
+
const visited = /* @__PURE__ */ new Set([issueId]);
|
|
1781
|
+
const targetChildren = buildChildren(issueId, visited);
|
|
1782
|
+
const targetNode = {
|
|
1521
1783
|
id: issue.id,
|
|
1522
1784
|
title: issue.title,
|
|
1785
|
+
type: issue.type,
|
|
1786
|
+
priority: issue.priority,
|
|
1523
1787
|
status: issue.status,
|
|
1524
|
-
|
|
1525
|
-
|
|
1788
|
+
isTarget: true,
|
|
1789
|
+
childrenCount: targetChildren.length,
|
|
1790
|
+
...targetChildren.length > 0 && { children: targetChildren }
|
|
1526
1791
|
};
|
|
1792
|
+
let currentNode = targetNode;
|
|
1793
|
+
let currentIssue = issue;
|
|
1794
|
+
while (currentIssue.parent) {
|
|
1795
|
+
const parentIssue = state.get(currentIssue.parent);
|
|
1796
|
+
if (!parentIssue) break;
|
|
1797
|
+
const siblings = [];
|
|
1798
|
+
for (const [, i] of state) {
|
|
1799
|
+
if ((i.parent === parentIssue.id || i.verifies === parentIssue.id) && i.id !== currentIssue.id) {
|
|
1800
|
+
siblings.push({
|
|
1801
|
+
id: i.id,
|
|
1802
|
+
title: i.title,
|
|
1803
|
+
type: i.type,
|
|
1804
|
+
priority: i.priority,
|
|
1805
|
+
status: i.status,
|
|
1806
|
+
childrenCount: 0
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
const parentNodeChildren = [currentNode, ...siblings];
|
|
1811
|
+
const parentNode = {
|
|
1812
|
+
id: parentIssue.id,
|
|
1813
|
+
title: parentIssue.title,
|
|
1814
|
+
type: parentIssue.type,
|
|
1815
|
+
priority: parentIssue.priority,
|
|
1816
|
+
status: parentIssue.status,
|
|
1817
|
+
childrenCount: parentNodeChildren.length,
|
|
1818
|
+
children: parentNodeChildren
|
|
1819
|
+
};
|
|
1820
|
+
currentNode = parentNode;
|
|
1821
|
+
currentIssue = parentIssue;
|
|
1822
|
+
}
|
|
1823
|
+
return currentNode;
|
|
1527
1824
|
}
|
|
1528
|
-
function
|
|
1825
|
+
function formatIssueTreePretty2(node) {
|
|
1529
1826
|
if (!node) {
|
|
1530
|
-
return "";
|
|
1827
|
+
return "Issue not found.";
|
|
1531
1828
|
}
|
|
1532
1829
|
const lines = [];
|
|
1533
|
-
const
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
const
|
|
1539
|
-
const
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1830
|
+
const formatNode = (n, prefix, isLast, isRoot) => {
|
|
1831
|
+
const connector = isRoot ? "" : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
|
|
1832
|
+
const statusIcon = n.status === "closed" ? "\u2713" : n.status === "in_progress" ? "\u25B6" : n.status === "pending_verification" ? "\u23F3" : "\u25CB";
|
|
1833
|
+
const marker = n.isTarget ? " \u25C0" : "";
|
|
1834
|
+
lines.push(`${prefix}${connector}${statusIcon} ${n.id}: ${n.title} [${n.type}] P${n.priority}${marker}`);
|
|
1835
|
+
const children = n.children ?? [];
|
|
1836
|
+
const childPrefix = isRoot ? "" : prefix + (isLast ? " " : "\u2502 ");
|
|
1837
|
+
children.forEach((child, index) => {
|
|
1838
|
+
const childIsLast = index === children.length - 1;
|
|
1839
|
+
formatNode(child, childPrefix, childIsLast, false);
|
|
1840
|
+
});
|
|
1841
|
+
};
|
|
1842
|
+
formatNode(node, "", true, true);
|
|
1548
1843
|
return lines.join("\n");
|
|
1549
1844
|
}
|
|
1550
1845
|
|
|
@@ -2904,28 +3199,48 @@ function countChildren(epicId) {
|
|
|
2904
3199
|
return {
|
|
2905
3200
|
total: children.length,
|
|
2906
3201
|
done: children.filter((c) => c.status === "closed").length,
|
|
3202
|
+
pending_verification: children.filter((c) => c.status === "pending_verification").length,
|
|
2907
3203
|
in_progress: children.filter((c) => c.status === "in_progress").length,
|
|
2908
3204
|
open: children.filter((c) => c.status === "open").length,
|
|
2909
3205
|
blocked: children.filter((c) => c.status === "blocked").length
|
|
2910
3206
|
};
|
|
2911
3207
|
}
|
|
2912
|
-
function
|
|
3208
|
+
function countVerifications(epicId) {
|
|
3209
|
+
const children = getChildren(epicId);
|
|
3210
|
+
let total = 0;
|
|
3211
|
+
let done = 0;
|
|
3212
|
+
for (const child of children) {
|
|
3213
|
+
const verifications = getVerifications(child.id);
|
|
3214
|
+
total += verifications.length;
|
|
3215
|
+
done += verifications.filter((v) => v.status === "closed").length;
|
|
3216
|
+
}
|
|
3217
|
+
return { total, done };
|
|
3218
|
+
}
|
|
3219
|
+
function formatSummaryPretty(summaries, sectionHeader) {
|
|
2913
3220
|
if (summaries.length === 0) {
|
|
2914
3221
|
return "No epics found.";
|
|
2915
3222
|
}
|
|
2916
3223
|
const lines = [];
|
|
3224
|
+
lines.push(`## ${sectionHeader} (${summaries.length})`);
|
|
3225
|
+
lines.push("");
|
|
2917
3226
|
for (const summary of summaries) {
|
|
2918
|
-
const { children } = summary;
|
|
2919
|
-
|
|
2920
|
-
lines.push(
|
|
3227
|
+
const { children, verifications } = summary;
|
|
3228
|
+
lines.push(`${summary.id}: ${summary.title}`);
|
|
3229
|
+
lines.push(` Created: ${formatRelativeTime(summary.createdAt)} | Updated: ${formatRelativeTime(summary.updatedAt)}`);
|
|
3230
|
+
const pendingStr = children.pending_verification > 0 ? ` (${children.pending_verification} pending verification)` : "";
|
|
3231
|
+
const issueCount = `Issues: ${children.done}/${children.total} done${pendingStr}`;
|
|
3232
|
+
const verifCount = `Verifications: ${verifications.done}/${verifications.total} done`;
|
|
3233
|
+
lines.push(` ${issueCount} | ${verifCount}`);
|
|
2921
3234
|
if (summary.parent) {
|
|
2922
|
-
lines.push(`
|
|
3235
|
+
lines.push(` Parent: ${summary.parent.id} (${summary.parent.title})`);
|
|
2923
3236
|
}
|
|
2924
3237
|
if (summary.description) {
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
lines.push(` ${truncated}`);
|
|
3238
|
+
lines.push("");
|
|
3239
|
+
lines.push(` ${summary.description}`);
|
|
2928
3240
|
}
|
|
3241
|
+
lines.push("");
|
|
3242
|
+
lines.push(` Run \`pb list --parent ${summary.id}\` to see all issues.`);
|
|
3243
|
+
lines.push("");
|
|
2929
3244
|
}
|
|
2930
3245
|
return lines.join("\n");
|
|
2931
3246
|
}
|
|
@@ -2934,31 +3249,17 @@ function summaryCommand(program2) {
|
|
|
2934
3249
|
const pretty = program2.opts().pretty ?? false;
|
|
2935
3250
|
try {
|
|
2936
3251
|
getOrCreatePebbleDir();
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
} else if (options.status !== void 0) {
|
|
2940
|
-
const status = options.status;
|
|
2941
|
-
if (!STATUSES.includes(status)) {
|
|
2942
|
-
throw new Error(`Invalid status: ${status}. Must be one of: ${STATUSES.join(", ")}`);
|
|
2943
|
-
}
|
|
2944
|
-
epics = epics.filter((e) => e.status === status);
|
|
2945
|
-
} else {
|
|
2946
|
-
epics = epics.filter((e) => e.status !== "closed");
|
|
2947
|
-
}
|
|
2948
|
-
epics.sort(
|
|
2949
|
-
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
2950
|
-
);
|
|
2951
|
-
const limit = parseInt(options.limit, 10);
|
|
2952
|
-
if (limit > 0) {
|
|
2953
|
-
epics = epics.slice(0, limit);
|
|
2954
|
-
}
|
|
2955
|
-
const summaries = epics.map((epic) => {
|
|
3252
|
+
const allEpics = getIssues({ type: "epic" });
|
|
3253
|
+
const buildSummary = (epic) => {
|
|
2956
3254
|
const summary = {
|
|
2957
3255
|
id: epic.id,
|
|
2958
3256
|
title: epic.title,
|
|
2959
3257
|
description: epic.description,
|
|
2960
3258
|
status: epic.status,
|
|
2961
|
-
|
|
3259
|
+
createdAt: epic.createdAt,
|
|
3260
|
+
updatedAt: epic.updatedAt,
|
|
3261
|
+
children: countChildren(epic.id),
|
|
3262
|
+
verifications: countVerifications(epic.id)
|
|
2962
3263
|
};
|
|
2963
3264
|
if (epic.parent) {
|
|
2964
3265
|
const parentIssue = getIssue(epic.parent);
|
|
@@ -2970,9 +3271,60 @@ function summaryCommand(program2) {
|
|
|
2970
3271
|
}
|
|
2971
3272
|
}
|
|
2972
3273
|
return summary;
|
|
2973
|
-
}
|
|
3274
|
+
};
|
|
3275
|
+
const limit = parseInt(options.limit, 10);
|
|
3276
|
+
if (options.includeClosed) {
|
|
3277
|
+
const openEpics = allEpics.filter((e) => e.status !== "closed");
|
|
3278
|
+
const seventyTwoHoursAgo = Date.now() - 72 * 60 * 60 * 1e3;
|
|
3279
|
+
const closedEpics = allEpics.filter(
|
|
3280
|
+
(e) => e.status === "closed" && new Date(e.updatedAt).getTime() > seventyTwoHoursAgo
|
|
3281
|
+
);
|
|
3282
|
+
openEpics.sort(
|
|
3283
|
+
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
3284
|
+
);
|
|
3285
|
+
closedEpics.sort(
|
|
3286
|
+
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
3287
|
+
);
|
|
3288
|
+
const limitedOpen = limit > 0 ? openEpics.slice(0, limit) : openEpics;
|
|
3289
|
+
const limitedClosed = limit > 0 ? closedEpics.slice(0, limit) : closedEpics;
|
|
3290
|
+
const openSummaries = limitedOpen.map(buildSummary);
|
|
3291
|
+
const closedSummaries = limitedClosed.map(buildSummary);
|
|
3292
|
+
if (pretty) {
|
|
3293
|
+
const output = [];
|
|
3294
|
+
if (openSummaries.length > 0) {
|
|
3295
|
+
output.push(formatSummaryPretty(openSummaries, "Open Epics"));
|
|
3296
|
+
}
|
|
3297
|
+
if (closedSummaries.length > 0) {
|
|
3298
|
+
if (output.length > 0) output.push("");
|
|
3299
|
+
output.push(formatSummaryPretty(closedSummaries, "Recently Closed Epics (last 72h)"));
|
|
3300
|
+
}
|
|
3301
|
+
console.log(output.join("\n"));
|
|
3302
|
+
} else {
|
|
3303
|
+
console.log(formatJson({ open: openSummaries, closed: closedSummaries }));
|
|
3304
|
+
}
|
|
3305
|
+
return;
|
|
3306
|
+
}
|
|
3307
|
+
let epics = allEpics;
|
|
3308
|
+
let sectionHeader = "Open Epics";
|
|
3309
|
+
if (options.status !== void 0) {
|
|
3310
|
+
const status = options.status;
|
|
3311
|
+
if (!STATUSES.includes(status)) {
|
|
3312
|
+
throw new Error(`Invalid status: ${status}. Must be one of: ${STATUSES.join(", ")}`);
|
|
3313
|
+
}
|
|
3314
|
+
epics = epics.filter((e) => e.status === status);
|
|
3315
|
+
sectionHeader = `${STATUS_LABELS[status]} Epics`;
|
|
3316
|
+
} else {
|
|
3317
|
+
epics = epics.filter((e) => e.status !== "closed");
|
|
3318
|
+
}
|
|
3319
|
+
epics.sort(
|
|
3320
|
+
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
3321
|
+
);
|
|
3322
|
+
if (limit > 0) {
|
|
3323
|
+
epics = epics.slice(0, limit);
|
|
3324
|
+
}
|
|
3325
|
+
const summaries = epics.map(buildSummary);
|
|
2974
3326
|
if (pretty) {
|
|
2975
|
-
console.log(formatSummaryPretty(summaries));
|
|
3327
|
+
console.log(formatSummaryPretty(summaries, sectionHeader));
|
|
2976
3328
|
} else {
|
|
2977
3329
|
console.log(formatJson(summaries));
|
|
2978
3330
|
}
|
|
@@ -2998,7 +3350,7 @@ function parseDuration(duration) {
|
|
|
2998
3350
|
};
|
|
2999
3351
|
return value * multipliers[unit];
|
|
3000
3352
|
}
|
|
3001
|
-
function
|
|
3353
|
+
function formatRelativeTime2(timestamp) {
|
|
3002
3354
|
const now = Date.now();
|
|
3003
3355
|
const then = new Date(timestamp).getTime();
|
|
3004
3356
|
const diff = now - then;
|
|
@@ -3017,7 +3369,7 @@ function formatHistoryPretty(entries) {
|
|
|
3017
3369
|
}
|
|
3018
3370
|
const lines = [];
|
|
3019
3371
|
for (const entry of entries) {
|
|
3020
|
-
const time =
|
|
3372
|
+
const time = formatRelativeTime2(entry.timestamp);
|
|
3021
3373
|
const eventLabel = entry.event.charAt(0).toUpperCase() + entry.event.slice(1);
|
|
3022
3374
|
let line = `[${time}] ${eventLabel} ${entry.issue.id} "${entry.issue.title}" (${entry.issue.type})`;
|
|
3023
3375
|
if (entry.parent) {
|
|
@@ -3142,7 +3494,7 @@ function searchCommand(program2) {
|
|
|
3142
3494
|
const bInTitle = b.title.toLowerCase().includes(lowerQuery);
|
|
3143
3495
|
if (aInTitle && !bInTitle) return -1;
|
|
3144
3496
|
if (!aInTitle && bInTitle) return 1;
|
|
3145
|
-
return new Date(b.
|
|
3497
|
+
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
3146
3498
|
});
|
|
3147
3499
|
const limit = parseInt(options.limit, 10);
|
|
3148
3500
|
if (limit > 0) {
|
|
@@ -3157,7 +3509,7 @@ function searchCommand(program2) {
|
|
|
3157
3509
|
|
|
3158
3510
|
// src/cli/commands/verifications.ts
|
|
3159
3511
|
function verificationsCommand(program2) {
|
|
3160
|
-
program2.command("verifications <id>").description("List verification issues for a given issue").action(async (id) => {
|
|
3512
|
+
program2.command("verifications <id>").description("List verification issues for a given issue").option("--limit <n>", "Max verifications to return (default: 30)").option("--all", "Show all verifications (no limit)").action(async (id, options) => {
|
|
3161
3513
|
const pretty = program2.opts().pretty ?? false;
|
|
3162
3514
|
try {
|
|
3163
3515
|
getOrCreatePebbleDir();
|
|
@@ -3166,7 +3518,18 @@ function verificationsCommand(program2) {
|
|
|
3166
3518
|
if (!issue) {
|
|
3167
3519
|
throw new Error(`Issue not found: ${id}`);
|
|
3168
3520
|
}
|
|
3169
|
-
|
|
3521
|
+
let verifications = getVerifications(resolvedId);
|
|
3522
|
+
verifications.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
3523
|
+
const total = verifications.length;
|
|
3524
|
+
const limit = options.all ? 0 : options.limit ? parseInt(options.limit, 10) : 30;
|
|
3525
|
+
if (limit > 0 && verifications.length > limit) {
|
|
3526
|
+
verifications = verifications.slice(0, limit);
|
|
3527
|
+
}
|
|
3528
|
+
const limitInfo = {
|
|
3529
|
+
total,
|
|
3530
|
+
shown: verifications.length,
|
|
3531
|
+
limited: limit > 0 && total > limit
|
|
3532
|
+
};
|
|
3170
3533
|
if (pretty) {
|
|
3171
3534
|
if (verifications.length === 0) {
|
|
3172
3535
|
console.log(`No verifications for ${resolvedId}`);
|
|
@@ -3179,16 +3542,21 @@ function verificationsCommand(program2) {
|
|
|
3179
3542
|
}
|
|
3180
3543
|
console.log("");
|
|
3181
3544
|
console.log(`Total: ${verifications.length} verification(s)`);
|
|
3545
|
+
if (limitInfo.limited) {
|
|
3546
|
+
console.log(formatLimitMessage(limitInfo));
|
|
3547
|
+
}
|
|
3182
3548
|
}
|
|
3183
3549
|
} else {
|
|
3184
|
-
|
|
3550
|
+
const output = {
|
|
3185
3551
|
issueId: resolvedId,
|
|
3186
3552
|
verifications: verifications.map((v) => ({
|
|
3187
3553
|
id: v.id,
|
|
3188
3554
|
title: v.title,
|
|
3189
3555
|
status: v.status
|
|
3190
|
-
}))
|
|
3191
|
-
|
|
3556
|
+
})),
|
|
3557
|
+
...limitInfo.limited && { _meta: limitInfo }
|
|
3558
|
+
};
|
|
3559
|
+
console.log(formatJson(output));
|
|
3192
3560
|
}
|
|
3193
3561
|
} catch (error) {
|
|
3194
3562
|
outputError(error, pretty);
|
|
@@ -3223,6 +3591,7 @@ function initCommand(program2) {
|
|
|
3223
3591
|
var program = new Command();
|
|
3224
3592
|
program.name("pebble").description("A lightweight JSONL-based issue tracker").version("0.1.0");
|
|
3225
3593
|
program.option("-P, --pretty", "Human-readable output (default: JSON)");
|
|
3594
|
+
program.option("--json", "JSON output (this is the default, flag not needed)");
|
|
3226
3595
|
createCommand(program);
|
|
3227
3596
|
updateCommand(program);
|
|
3228
3597
|
closeCommand(program);
|