@markmdev/pebble 0.1.9 → 0.1.11
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
|
}
|
|
@@ -529,8 +546,10 @@ function formatIssueDetailPretty(issue, ctx) {
|
|
|
529
546
|
}
|
|
530
547
|
if (ctx.children.length > 0) {
|
|
531
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)` : "";
|
|
532
551
|
lines.push("");
|
|
533
|
-
lines.push(`Children (${closedChildren.length}/${ctx.children.length} done):`);
|
|
552
|
+
lines.push(`Children (${closedChildren.length}/${ctx.children.length} done${pendingStr}):`);
|
|
534
553
|
for (const child of ctx.children) {
|
|
535
554
|
const statusIcon = child.status === "closed" ? "\u2713" : child.status === "in_progress" ? "\u25B6" : "\u25CB";
|
|
536
555
|
lines.push(` ${statusIcon} ${child.id} - ${child.title} [${child.status}]`);
|
|
@@ -671,11 +690,94 @@ function outputMutationSuccess(id, pretty) {
|
|
|
671
690
|
console.log(JSON.stringify({ id, success: true }));
|
|
672
691
|
}
|
|
673
692
|
}
|
|
674
|
-
function outputIssueList(issues, pretty) {
|
|
693
|
+
function outputIssueList(issues, pretty, limitInfo) {
|
|
675
694
|
if (pretty) {
|
|
676
695
|
console.log(formatIssueListPretty(issues));
|
|
696
|
+
if (limitInfo?.limited) {
|
|
697
|
+
console.log(formatLimitMessage(limitInfo));
|
|
698
|
+
}
|
|
699
|
+
} else {
|
|
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
|
+
}
|
|
677
775
|
} else {
|
|
678
|
-
|
|
776
|
+
if (limitInfo?.limited) {
|
|
777
|
+
console.log(formatJson({ issues: tree, _meta: limitInfo }));
|
|
778
|
+
} else {
|
|
779
|
+
console.log(formatJson(tree));
|
|
780
|
+
}
|
|
679
781
|
}
|
|
680
782
|
}
|
|
681
783
|
function outputError(error, pretty) {
|
|
@@ -686,42 +788,55 @@ function outputError(error, pretty) {
|
|
|
686
788
|
}
|
|
687
789
|
process.exit(1);
|
|
688
790
|
}
|
|
689
|
-
function formatIssueListVerbose(issues) {
|
|
791
|
+
function formatIssueListVerbose(issues, sectionHeader) {
|
|
690
792
|
if (issues.length === 0) {
|
|
691
793
|
return "No issues found.";
|
|
692
794
|
}
|
|
693
795
|
const lines = [];
|
|
796
|
+
if (sectionHeader) {
|
|
797
|
+
lines.push(`## ${sectionHeader} (${issues.length})`);
|
|
798
|
+
lines.push("");
|
|
799
|
+
}
|
|
694
800
|
for (const info of issues) {
|
|
695
|
-
const { issue, blocking, children, verifications, blockers } = info;
|
|
696
|
-
lines.push(`${issue.id}
|
|
697
|
-
lines.push(
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
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
|
+
}
|
|
705
810
|
if (blockers && blockers.length > 0) {
|
|
706
|
-
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}`);
|
|
707
815
|
}
|
|
708
816
|
lines.push("");
|
|
709
817
|
}
|
|
710
|
-
lines.push(`Total: ${issues.length} issue(s)`);
|
|
711
818
|
return lines.join("\n");
|
|
712
819
|
}
|
|
713
|
-
function outputIssueListVerbose(issues, pretty) {
|
|
820
|
+
function outputIssueListVerbose(issues, pretty, sectionHeader, limitInfo) {
|
|
714
821
|
if (pretty) {
|
|
715
|
-
console.log(formatIssueListVerbose(issues));
|
|
822
|
+
console.log(formatIssueListVerbose(issues, sectionHeader));
|
|
823
|
+
if (limitInfo?.limited) {
|
|
824
|
+
console.log(formatLimitMessage(limitInfo));
|
|
825
|
+
}
|
|
716
826
|
} else {
|
|
717
|
-
const output = issues.map(({ issue, blocking, children, verifications, blockers }) => ({
|
|
827
|
+
const output = issues.map(({ issue, blocking, children, verifications, blockers, parent }) => ({
|
|
718
828
|
...issue,
|
|
719
829
|
blocking,
|
|
720
830
|
childrenCount: issue.type === "epic" ? children : void 0,
|
|
721
831
|
verificationsCount: verifications,
|
|
722
|
-
...blockers && { openBlockers: blockers }
|
|
832
|
+
...blockers && { openBlockers: blockers },
|
|
833
|
+
...parent && { parentInfo: parent }
|
|
723
834
|
}));
|
|
724
|
-
|
|
835
|
+
if (limitInfo?.limited) {
|
|
836
|
+
console.log(formatJson({ issues: output, _meta: limitInfo }));
|
|
837
|
+
} else {
|
|
838
|
+
console.log(formatJson(output));
|
|
839
|
+
}
|
|
725
840
|
}
|
|
726
841
|
}
|
|
727
842
|
|
|
@@ -1043,6 +1158,8 @@ Pending verifications:`);
|
|
|
1043
1158
|
for (const v of result.pendingVerifications || []) {
|
|
1044
1159
|
console.log(` \u2022 ${v.id} - ${v.title}`);
|
|
1045
1160
|
}
|
|
1161
|
+
console.log(`
|
|
1162
|
+
Run: pb verifications ${result.id}`);
|
|
1046
1163
|
} else {
|
|
1047
1164
|
console.log(`\u2713 ${result.id}`);
|
|
1048
1165
|
if (result.unblocked && result.unblocked.length > 0) {
|
|
@@ -1059,6 +1176,7 @@ Unblocked:`);
|
|
|
1059
1176
|
success: true,
|
|
1060
1177
|
status: result.status,
|
|
1061
1178
|
...result.pendingVerifications && { pendingVerifications: result.pendingVerifications },
|
|
1179
|
+
...result.pendingVerifications && { hint: `pb verifications ${result.id}` },
|
|
1062
1180
|
...result.unblocked && { unblocked: result.unblocked }
|
|
1063
1181
|
}));
|
|
1064
1182
|
}
|
|
@@ -1074,6 +1192,7 @@ Unblocked:`);
|
|
|
1074
1192
|
for (const v of result.pendingVerifications || []) {
|
|
1075
1193
|
console.log(` \u2022 ${v.id} - ${v.title}`);
|
|
1076
1194
|
}
|
|
1195
|
+
console.log(` Run: pb verifications ${result.id}`);
|
|
1077
1196
|
} else {
|
|
1078
1197
|
console.log(`\u2713 ${result.id}`);
|
|
1079
1198
|
if (result.unblocked && result.unblocked.length > 0) {
|
|
@@ -1093,6 +1212,7 @@ Unblocked:`);
|
|
|
1093
1212
|
status: r.status,
|
|
1094
1213
|
...r.error && { error: r.error },
|
|
1095
1214
|
...r.pendingVerifications && { pendingVerifications: r.pendingVerifications },
|
|
1215
|
+
...r.pendingVerifications && { hint: `pb verifications ${r.id}` },
|
|
1096
1216
|
...r.unblocked && { unblocked: r.unblocked }
|
|
1097
1217
|
}))));
|
|
1098
1218
|
}
|
|
@@ -1212,7 +1332,7 @@ function claimCommand(program2) {
|
|
|
1212
1332
|
|
|
1213
1333
|
// src/cli/commands/list.ts
|
|
1214
1334
|
function listCommand(program2) {
|
|
1215
|
-
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) => {
|
|
1216
1336
|
const pretty = program2.opts().pretty ?? false;
|
|
1217
1337
|
try {
|
|
1218
1338
|
getOrCreatePebbleDir();
|
|
@@ -1241,8 +1361,48 @@ function listCommand(program2) {
|
|
|
1241
1361
|
if (options.parent !== void 0) {
|
|
1242
1362
|
filters.parent = resolveId(options.parent);
|
|
1243
1363
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
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
|
+
}
|
|
1246
1406
|
} catch (error) {
|
|
1247
1407
|
outputError(error, pretty);
|
|
1248
1408
|
}
|
|
@@ -1273,21 +1433,48 @@ function showCommand(program2) {
|
|
|
1273
1433
|
|
|
1274
1434
|
// src/cli/commands/ready.ts
|
|
1275
1435
|
function readyCommand(program2) {
|
|
1276
|
-
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) => {
|
|
1277
1437
|
const pretty = program2.opts().pretty ?? false;
|
|
1278
1438
|
try {
|
|
1279
1439
|
getOrCreatePebbleDir();
|
|
1280
|
-
|
|
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
|
+
};
|
|
1281
1459
|
if (options.verbose) {
|
|
1282
|
-
const verboseIssues = issues.map((issue) =>
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
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);
|
|
1289
1476
|
} else {
|
|
1290
|
-
outputIssueList(issues, pretty);
|
|
1477
|
+
outputIssueList(issues, pretty, limitInfo);
|
|
1291
1478
|
}
|
|
1292
1479
|
} catch (error) {
|
|
1293
1480
|
outputError(error, pretty);
|
|
@@ -1297,26 +1484,44 @@ function readyCommand(program2) {
|
|
|
1297
1484
|
|
|
1298
1485
|
// src/cli/commands/blocked.ts
|
|
1299
1486
|
function blockedCommand(program2) {
|
|
1300
|
-
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) => {
|
|
1301
1488
|
const pretty = program2.opts().pretty ?? false;
|
|
1302
1489
|
try {
|
|
1303
1490
|
getOrCreatePebbleDir();
|
|
1304
|
-
|
|
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
|
+
};
|
|
1305
1503
|
if (options.verbose) {
|
|
1306
1504
|
const verboseIssues = issues.map((issue) => {
|
|
1307
1505
|
const allBlockers = getBlockers(issue.id);
|
|
1308
1506
|
const openBlockers = allBlockers.filter((b) => b.status !== "closed").map((b) => b.id);
|
|
1309
|
-
|
|
1507
|
+
const info = {
|
|
1310
1508
|
issue,
|
|
1311
1509
|
blocking: getBlocking(issue.id).map((i) => i.id),
|
|
1312
1510
|
children: getChildren(issue.id).length,
|
|
1313
1511
|
verifications: getVerifications(issue.id).length,
|
|
1314
1512
|
blockers: openBlockers
|
|
1315
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;
|
|
1316
1521
|
});
|
|
1317
|
-
outputIssueListVerbose(verboseIssues, pretty);
|
|
1522
|
+
outputIssueListVerbose(verboseIssues, pretty, "Blocked Issues", limitInfo);
|
|
1318
1523
|
} else {
|
|
1319
|
-
outputIssueList(issues, pretty);
|
|
1524
|
+
outputIssueList(issues, pretty, limitInfo);
|
|
1320
1525
|
}
|
|
1321
1526
|
} catch (error) {
|
|
1322
1527
|
outputError(error, pretty);
|
|
@@ -1327,39 +1532,55 @@ function blockedCommand(program2) {
|
|
|
1327
1532
|
// src/cli/commands/dep.ts
|
|
1328
1533
|
function depCommand(program2) {
|
|
1329
1534
|
const dep = program2.command("dep").description("Manage dependencies");
|
|
1330
|
-
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) => {
|
|
1331
1536
|
const pretty = program2.opts().pretty ?? false;
|
|
1332
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
|
+
}
|
|
1333
1547
|
const pebbleDir = getOrCreatePebbleDir();
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
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);
|
|
1337
1558
|
if (!issue) {
|
|
1338
|
-
throw new Error(`Issue not found: ${
|
|
1559
|
+
throw new Error(`Issue not found: ${blockedId}`);
|
|
1339
1560
|
}
|
|
1340
|
-
const blocker = getIssue(
|
|
1561
|
+
const blocker = getIssue(blockerIdResolved);
|
|
1341
1562
|
if (!blocker) {
|
|
1342
|
-
throw new Error(`Blocker issue not found: ${
|
|
1563
|
+
throw new Error(`Blocker issue not found: ${blockerIdResolved}`);
|
|
1343
1564
|
}
|
|
1344
|
-
if (
|
|
1565
|
+
if (blockedId === blockerIdResolved) {
|
|
1345
1566
|
throw new Error("Cannot add self as blocker");
|
|
1346
1567
|
}
|
|
1347
|
-
if (issue.blockedBy.includes(
|
|
1348
|
-
throw new Error(`Dependency already exists: ${
|
|
1568
|
+
if (issue.blockedBy.includes(blockerIdResolved)) {
|
|
1569
|
+
throw new Error(`Dependency already exists: ${blockedId} is blocked by ${blockerIdResolved}`);
|
|
1349
1570
|
}
|
|
1350
|
-
if (detectCycle(
|
|
1571
|
+
if (detectCycle(blockedId, blockerIdResolved)) {
|
|
1351
1572
|
throw new Error(`Adding this dependency would create a cycle`);
|
|
1352
1573
|
}
|
|
1353
1574
|
const event = {
|
|
1354
1575
|
type: "update",
|
|
1355
|
-
issueId:
|
|
1576
|
+
issueId: blockedId,
|
|
1356
1577
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1357
1578
|
data: {
|
|
1358
|
-
blockedBy: [...issue.blockedBy,
|
|
1579
|
+
blockedBy: [...issue.blockedBy, blockerIdResolved]
|
|
1359
1580
|
}
|
|
1360
1581
|
};
|
|
1361
1582
|
appendEvent(event, pebbleDir);
|
|
1362
|
-
outputMutationSuccess(
|
|
1583
|
+
outputMutationSuccess(blockedId, pretty);
|
|
1363
1584
|
} catch (error) {
|
|
1364
1585
|
outputError(error, pretty);
|
|
1365
1586
|
}
|
|
@@ -1510,7 +1731,7 @@ function depCommand(program2) {
|
|
|
1510
1731
|
outputError(error, pretty);
|
|
1511
1732
|
}
|
|
1512
1733
|
});
|
|
1513
|
-
dep.command("tree <id>").description("Show
|
|
1734
|
+
dep.command("tree <id>").description("Show issue tree (children, verifications, and full hierarchy)").action(async (id) => {
|
|
1514
1735
|
const pretty = program2.opts().pretty ?? false;
|
|
1515
1736
|
try {
|
|
1516
1737
|
const resolvedId = resolveId(id);
|
|
@@ -1520,10 +1741,9 @@ function depCommand(program2) {
|
|
|
1520
1741
|
}
|
|
1521
1742
|
const events = readEvents();
|
|
1522
1743
|
const state = computeState(events);
|
|
1523
|
-
const
|
|
1524
|
-
const tree = buildDepTree(resolvedId, visited, 0, state);
|
|
1744
|
+
const tree = buildIssueTree2(resolvedId, state);
|
|
1525
1745
|
if (pretty) {
|
|
1526
|
-
console.log(
|
|
1746
|
+
console.log(formatIssueTreePretty2(tree));
|
|
1527
1747
|
} else {
|
|
1528
1748
|
console.log(formatJson(tree));
|
|
1529
1749
|
}
|
|
@@ -1532,50 +1752,94 @@ function depCommand(program2) {
|
|
|
1532
1752
|
}
|
|
1533
1753
|
});
|
|
1534
1754
|
}
|
|
1535
|
-
function
|
|
1536
|
-
if (visited.has(issueId)) {
|
|
1537
|
-
return null;
|
|
1538
|
-
}
|
|
1539
|
-
visited.add(issueId);
|
|
1755
|
+
function buildIssueTree2(issueId, state) {
|
|
1540
1756
|
const issue = state.get(issueId);
|
|
1541
1757
|
if (!issue) {
|
|
1542
1758
|
return null;
|
|
1543
1759
|
}
|
|
1544
|
-
const
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
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
|
+
}
|
|
1549
1777
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1778
|
+
return children;
|
|
1779
|
+
};
|
|
1780
|
+
const visited = /* @__PURE__ */ new Set([issueId]);
|
|
1781
|
+
const targetChildren = buildChildren(issueId, visited);
|
|
1782
|
+
const targetNode = {
|
|
1552
1783
|
id: issue.id,
|
|
1553
1784
|
title: issue.title,
|
|
1785
|
+
type: issue.type,
|
|
1786
|
+
priority: issue.priority,
|
|
1554
1787
|
status: issue.status,
|
|
1555
|
-
|
|
1556
|
-
|
|
1788
|
+
isTarget: true,
|
|
1789
|
+
childrenCount: targetChildren.length,
|
|
1790
|
+
...targetChildren.length > 0 && { children: targetChildren }
|
|
1557
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;
|
|
1558
1824
|
}
|
|
1559
|
-
function
|
|
1825
|
+
function formatIssueTreePretty2(node) {
|
|
1560
1826
|
if (!node) {
|
|
1561
|
-
return "";
|
|
1827
|
+
return "Issue not found.";
|
|
1562
1828
|
}
|
|
1563
1829
|
const lines = [];
|
|
1564
|
-
const
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
const
|
|
1570
|
-
const
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
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);
|
|
1579
1843
|
return lines.join("\n");
|
|
1580
1844
|
}
|
|
1581
1845
|
|
|
@@ -2935,61 +3199,67 @@ function countChildren(epicId) {
|
|
|
2935
3199
|
return {
|
|
2936
3200
|
total: children.length,
|
|
2937
3201
|
done: children.filter((c) => c.status === "closed").length,
|
|
3202
|
+
pending_verification: children.filter((c) => c.status === "pending_verification").length,
|
|
2938
3203
|
in_progress: children.filter((c) => c.status === "in_progress").length,
|
|
2939
3204
|
open: children.filter((c) => c.status === "open").length,
|
|
2940
3205
|
blocked: children.filter((c) => c.status === "blocked").length
|
|
2941
3206
|
};
|
|
2942
3207
|
}
|
|
2943
|
-
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) {
|
|
2944
3220
|
if (summaries.length === 0) {
|
|
2945
3221
|
return "No epics found.";
|
|
2946
3222
|
}
|
|
2947
3223
|
const lines = [];
|
|
3224
|
+
lines.push(`## ${sectionHeader} (${summaries.length})`);
|
|
3225
|
+
lines.push("");
|
|
2948
3226
|
for (const summary of summaries) {
|
|
2949
|
-
const { children } = summary;
|
|
2950
|
-
|
|
2951
|
-
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}`);
|
|
2952
3234
|
if (summary.parent) {
|
|
2953
|
-
lines.push(`
|
|
3235
|
+
lines.push(` Parent: ${summary.parent.id} (${summary.parent.title})`);
|
|
2954
3236
|
}
|
|
2955
3237
|
if (summary.description) {
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
lines.push(` ${truncated}`);
|
|
3238
|
+
lines.push("");
|
|
3239
|
+
lines.push(` ${summary.description}`);
|
|
2959
3240
|
}
|
|
3241
|
+
lines.push("");
|
|
3242
|
+
lines.push(` Run \`pb list --parent ${summary.id}\` to see all issues.`);
|
|
3243
|
+
lines.push("");
|
|
2960
3244
|
}
|
|
2961
3245
|
return lines.join("\n");
|
|
2962
3246
|
}
|
|
2963
3247
|
function summaryCommand(program2) {
|
|
2964
|
-
program2.command("summary").description("Show epic summary with child completion status").option("--status <status>", "Filter epics by status
|
|
3248
|
+
program2.command("summary").description("Show epic summary with child completion status").option("--status <status>", "Filter epics by specific status").option("--limit <n>", "Max epics to return per section", "10").action(async (options) => {
|
|
2965
3249
|
const pretty = program2.opts().pretty ?? false;
|
|
2966
3250
|
try {
|
|
2967
3251
|
getOrCreatePebbleDir();
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
} else if (options.status !== void 0) {
|
|
2971
|
-
const status = options.status;
|
|
2972
|
-
if (!STATUSES.includes(status)) {
|
|
2973
|
-
throw new Error(`Invalid status: ${status}. Must be one of: ${STATUSES.join(", ")}`);
|
|
2974
|
-
}
|
|
2975
|
-
epics = epics.filter((e) => e.status === status);
|
|
2976
|
-
} else {
|
|
2977
|
-
epics = epics.filter((e) => e.status !== "closed");
|
|
2978
|
-
}
|
|
2979
|
-
epics.sort(
|
|
2980
|
-
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
2981
|
-
);
|
|
2982
|
-
const limit = parseInt(options.limit, 10);
|
|
2983
|
-
if (limit > 0) {
|
|
2984
|
-
epics = epics.slice(0, limit);
|
|
2985
|
-
}
|
|
2986
|
-
const summaries = epics.map((epic) => {
|
|
3252
|
+
const allEpics = getIssues({ type: "epic" });
|
|
3253
|
+
const buildSummary = (epic) => {
|
|
2987
3254
|
const summary = {
|
|
2988
3255
|
id: epic.id,
|
|
2989
3256
|
title: epic.title,
|
|
2990
3257
|
description: epic.description,
|
|
2991
3258
|
status: epic.status,
|
|
2992
|
-
|
|
3259
|
+
createdAt: epic.createdAt,
|
|
3260
|
+
updatedAt: epic.updatedAt,
|
|
3261
|
+
children: countChildren(epic.id),
|
|
3262
|
+
verifications: countVerifications(epic.id)
|
|
2993
3263
|
};
|
|
2994
3264
|
if (epic.parent) {
|
|
2995
3265
|
const parentIssue = getIssue(epic.parent);
|
|
@@ -3001,11 +3271,58 @@ function summaryCommand(program2) {
|
|
|
3001
3271
|
}
|
|
3002
3272
|
}
|
|
3003
3273
|
return summary;
|
|
3004
|
-
}
|
|
3274
|
+
};
|
|
3275
|
+
const limit = parseInt(options.limit, 10);
|
|
3276
|
+
if (options.status !== void 0) {
|
|
3277
|
+
const status = options.status;
|
|
3278
|
+
if (!STATUSES.includes(status)) {
|
|
3279
|
+
throw new Error(`Invalid status: ${status}. Must be one of: ${STATUSES.join(", ")}`);
|
|
3280
|
+
}
|
|
3281
|
+
let epics = allEpics.filter((e) => e.status === status);
|
|
3282
|
+
epics.sort(
|
|
3283
|
+
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
3284
|
+
);
|
|
3285
|
+
if (limit > 0) {
|
|
3286
|
+
epics = epics.slice(0, limit);
|
|
3287
|
+
}
|
|
3288
|
+
const summaries = epics.map(buildSummary);
|
|
3289
|
+
if (pretty) {
|
|
3290
|
+
console.log(formatSummaryPretty(summaries, `${STATUS_LABELS[status]} Epics`));
|
|
3291
|
+
} else {
|
|
3292
|
+
console.log(formatJson(summaries));
|
|
3293
|
+
}
|
|
3294
|
+
return;
|
|
3295
|
+
}
|
|
3296
|
+
const openEpics = allEpics.filter((e) => e.status !== "closed");
|
|
3297
|
+
const seventyTwoHoursAgo = Date.now() - 72 * 60 * 60 * 1e3;
|
|
3298
|
+
const closedEpics = allEpics.filter(
|
|
3299
|
+
(e) => e.status === "closed" && new Date(e.updatedAt).getTime() > seventyTwoHoursAgo
|
|
3300
|
+
);
|
|
3301
|
+
openEpics.sort(
|
|
3302
|
+
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
3303
|
+
);
|
|
3304
|
+
closedEpics.sort(
|
|
3305
|
+
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
3306
|
+
);
|
|
3307
|
+
const limitedOpen = limit > 0 ? openEpics.slice(0, limit) : openEpics;
|
|
3308
|
+
const limitedClosed = limit > 0 ? closedEpics.slice(0, limit) : closedEpics;
|
|
3309
|
+
const openSummaries = limitedOpen.map(buildSummary);
|
|
3310
|
+
const closedSummaries = limitedClosed.map(buildSummary);
|
|
3005
3311
|
if (pretty) {
|
|
3006
|
-
|
|
3312
|
+
const output = [];
|
|
3313
|
+
if (openSummaries.length > 0) {
|
|
3314
|
+
output.push(formatSummaryPretty(openSummaries, "Open Epics"));
|
|
3315
|
+
}
|
|
3316
|
+
if (closedSummaries.length > 0) {
|
|
3317
|
+
if (output.length > 0) output.push("");
|
|
3318
|
+
output.push(formatSummaryPretty(closedSummaries, "Recently Closed Epics (last 72h)"));
|
|
3319
|
+
}
|
|
3320
|
+
if (output.length === 0) {
|
|
3321
|
+
output.push("No epics found.");
|
|
3322
|
+
}
|
|
3323
|
+
console.log(output.join("\n"));
|
|
3007
3324
|
} else {
|
|
3008
|
-
console.log(formatJson(
|
|
3325
|
+
console.log(formatJson({ open: openSummaries, closed: closedSummaries }));
|
|
3009
3326
|
}
|
|
3010
3327
|
} catch (error) {
|
|
3011
3328
|
outputError(error, pretty);
|
|
@@ -3029,7 +3346,7 @@ function parseDuration(duration) {
|
|
|
3029
3346
|
};
|
|
3030
3347
|
return value * multipliers[unit];
|
|
3031
3348
|
}
|
|
3032
|
-
function
|
|
3349
|
+
function formatRelativeTime2(timestamp) {
|
|
3033
3350
|
const now = Date.now();
|
|
3034
3351
|
const then = new Date(timestamp).getTime();
|
|
3035
3352
|
const diff = now - then;
|
|
@@ -3048,7 +3365,7 @@ function formatHistoryPretty(entries) {
|
|
|
3048
3365
|
}
|
|
3049
3366
|
const lines = [];
|
|
3050
3367
|
for (const entry of entries) {
|
|
3051
|
-
const time =
|
|
3368
|
+
const time = formatRelativeTime2(entry.timestamp);
|
|
3052
3369
|
const eventLabel = entry.event.charAt(0).toUpperCase() + entry.event.slice(1);
|
|
3053
3370
|
let line = `[${time}] ${eventLabel} ${entry.issue.id} "${entry.issue.title}" (${entry.issue.type})`;
|
|
3054
3371
|
if (entry.parent) {
|
|
@@ -3173,7 +3490,7 @@ function searchCommand(program2) {
|
|
|
3173
3490
|
const bInTitle = b.title.toLowerCase().includes(lowerQuery);
|
|
3174
3491
|
if (aInTitle && !bInTitle) return -1;
|
|
3175
3492
|
if (!aInTitle && bInTitle) return 1;
|
|
3176
|
-
return new Date(b.
|
|
3493
|
+
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
3177
3494
|
});
|
|
3178
3495
|
const limit = parseInt(options.limit, 10);
|
|
3179
3496
|
if (limit > 0) {
|
|
@@ -3188,7 +3505,7 @@ function searchCommand(program2) {
|
|
|
3188
3505
|
|
|
3189
3506
|
// src/cli/commands/verifications.ts
|
|
3190
3507
|
function verificationsCommand(program2) {
|
|
3191
|
-
program2.command("verifications <id>").description("List verification issues for a given issue").action(async (id) => {
|
|
3508
|
+
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) => {
|
|
3192
3509
|
const pretty = program2.opts().pretty ?? false;
|
|
3193
3510
|
try {
|
|
3194
3511
|
getOrCreatePebbleDir();
|
|
@@ -3197,7 +3514,18 @@ function verificationsCommand(program2) {
|
|
|
3197
3514
|
if (!issue) {
|
|
3198
3515
|
throw new Error(`Issue not found: ${id}`);
|
|
3199
3516
|
}
|
|
3200
|
-
|
|
3517
|
+
let verifications = getVerifications(resolvedId);
|
|
3518
|
+
verifications.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
3519
|
+
const total = verifications.length;
|
|
3520
|
+
const limit = options.all ? 0 : options.limit ? parseInt(options.limit, 10) : 30;
|
|
3521
|
+
if (limit > 0 && verifications.length > limit) {
|
|
3522
|
+
verifications = verifications.slice(0, limit);
|
|
3523
|
+
}
|
|
3524
|
+
const limitInfo = {
|
|
3525
|
+
total,
|
|
3526
|
+
shown: verifications.length,
|
|
3527
|
+
limited: limit > 0 && total > limit
|
|
3528
|
+
};
|
|
3201
3529
|
if (pretty) {
|
|
3202
3530
|
if (verifications.length === 0) {
|
|
3203
3531
|
console.log(`No verifications for ${resolvedId}`);
|
|
@@ -3210,16 +3538,21 @@ function verificationsCommand(program2) {
|
|
|
3210
3538
|
}
|
|
3211
3539
|
console.log("");
|
|
3212
3540
|
console.log(`Total: ${verifications.length} verification(s)`);
|
|
3541
|
+
if (limitInfo.limited) {
|
|
3542
|
+
console.log(formatLimitMessage(limitInfo));
|
|
3543
|
+
}
|
|
3213
3544
|
}
|
|
3214
3545
|
} else {
|
|
3215
|
-
|
|
3546
|
+
const output = {
|
|
3216
3547
|
issueId: resolvedId,
|
|
3217
3548
|
verifications: verifications.map((v) => ({
|
|
3218
3549
|
id: v.id,
|
|
3219
3550
|
title: v.title,
|
|
3220
3551
|
status: v.status
|
|
3221
|
-
}))
|
|
3222
|
-
|
|
3552
|
+
})),
|
|
3553
|
+
...limitInfo.limited && { _meta: limitInfo }
|
|
3554
|
+
};
|
|
3555
|
+
console.log(formatJson(output));
|
|
3223
3556
|
}
|
|
3224
3557
|
} catch (error) {
|
|
3225
3558
|
outputError(error, pretty);
|
|
@@ -3255,13 +3588,6 @@ var program = new Command();
|
|
|
3255
3588
|
program.name("pebble").description("A lightweight JSONL-based issue tracker").version("0.1.0");
|
|
3256
3589
|
program.option("-P, --pretty", "Human-readable output (default: JSON)");
|
|
3257
3590
|
program.option("--json", "JSON output (this is the default, flag not needed)");
|
|
3258
|
-
program.hook("preAction", () => {
|
|
3259
|
-
const opts = program.opts();
|
|
3260
|
-
if (opts.json) {
|
|
3261
|
-
console.error("Note: --json flag is unnecessary. JSON is the default output format.");
|
|
3262
|
-
console.error("Use --pretty (-P) for human-readable output.");
|
|
3263
|
-
}
|
|
3264
|
-
});
|
|
3265
3591
|
createCommand(program);
|
|
3266
3592
|
updateCommand(program);
|
|
3267
3593
|
closeCommand(program);
|