@markmdev/pebble 0.1.5 → 0.1.7
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 +375 -26
- package/dist/cli/index.js.map +1 -1
- package/dist/ui/assets/index-CAcD3W8c.css +1 -0
- package/dist/ui/assets/index-D7K46Jfk.js +317 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-BwKh2bk7.js +0 -343
- package/dist/ui/assets/index-R8Oj47ay.css +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { Command } from "commander";
|
|
|
12
12
|
// src/shared/types.ts
|
|
13
13
|
var ISSUE_TYPES = ["task", "bug", "epic", "verification"];
|
|
14
14
|
var PRIORITIES = [0, 1, 2, 3, 4];
|
|
15
|
-
var STATUSES = ["open", "in_progress", "blocked", "closed"];
|
|
15
|
+
var STATUSES = ["open", "in_progress", "blocked", "pending_verification", "closed"];
|
|
16
16
|
var EVENT_TYPES = ["create", "update", "close", "reopen", "comment"];
|
|
17
17
|
var PRIORITY_LABELS = {
|
|
18
18
|
0: "critical",
|
|
@@ -25,6 +25,7 @@ var STATUS_LABELS = {
|
|
|
25
25
|
open: "Open",
|
|
26
26
|
in_progress: "In Progress",
|
|
27
27
|
blocked: "Blocked",
|
|
28
|
+
pending_verification: "Pending Verification",
|
|
28
29
|
closed: "Closed"
|
|
29
30
|
};
|
|
30
31
|
var TYPE_LABELS = {
|
|
@@ -149,7 +150,7 @@ function getConfigPath(pebbleDir) {
|
|
|
149
150
|
function getConfig(pebbleDir) {
|
|
150
151
|
const configPath = getConfigPath(pebbleDir);
|
|
151
152
|
if (!fs.existsSync(configPath)) {
|
|
152
|
-
throw new Error("
|
|
153
|
+
throw new Error("No .pebble directory found. Run 'pb init' to initialize.");
|
|
153
154
|
}
|
|
154
155
|
const content = fs.readFileSync(configPath, "utf-8");
|
|
155
156
|
return JSON.parse(content);
|
|
@@ -183,6 +184,7 @@ function computeState(events) {
|
|
|
183
184
|
description: createEvent.data.description,
|
|
184
185
|
parent: createEvent.data.parent,
|
|
185
186
|
blockedBy: [],
|
|
187
|
+
relatedTo: [],
|
|
186
188
|
verifies: createEvent.data.verifies,
|
|
187
189
|
comments: [],
|
|
188
190
|
createdAt: event.timestamp,
|
|
@@ -211,11 +213,14 @@ function computeState(events) {
|
|
|
211
213
|
issue.description = updateEvent.data.description;
|
|
212
214
|
}
|
|
213
215
|
if (updateEvent.data.parent !== void 0) {
|
|
214
|
-
issue.parent = updateEvent.data.parent;
|
|
216
|
+
issue.parent = updateEvent.data.parent || void 0;
|
|
215
217
|
}
|
|
216
218
|
if (updateEvent.data.blockedBy !== void 0) {
|
|
217
219
|
issue.blockedBy = updateEvent.data.blockedBy;
|
|
218
220
|
}
|
|
221
|
+
if (updateEvent.data.relatedTo !== void 0) {
|
|
222
|
+
issue.relatedTo = updateEvent.data.relatedTo;
|
|
223
|
+
}
|
|
219
224
|
issue.updatedAt = event.timestamp;
|
|
220
225
|
}
|
|
221
226
|
break;
|
|
@@ -315,7 +320,7 @@ function getReady() {
|
|
|
315
320
|
const state = computeState(events);
|
|
316
321
|
const issues = Array.from(state.values());
|
|
317
322
|
return issues.filter((issue) => {
|
|
318
|
-
if (issue.status === "closed") {
|
|
323
|
+
if (issue.status === "closed" || issue.status === "pending_verification") {
|
|
319
324
|
return false;
|
|
320
325
|
}
|
|
321
326
|
for (const blockerId of issue.blockedBy) {
|
|
@@ -456,6 +461,36 @@ function getNewlyUnblocked(closedIssueId) {
|
|
|
456
461
|
}
|
|
457
462
|
return result;
|
|
458
463
|
}
|
|
464
|
+
function getRelated(issueId) {
|
|
465
|
+
const issue = getIssue(issueId);
|
|
466
|
+
if (!issue) {
|
|
467
|
+
return [];
|
|
468
|
+
}
|
|
469
|
+
const events = readEvents();
|
|
470
|
+
const state = computeState(events);
|
|
471
|
+
return issue.relatedTo.map((id) => state.get(id)).filter((i) => i !== void 0);
|
|
472
|
+
}
|
|
473
|
+
function hasOpenBlockersById(issueId) {
|
|
474
|
+
const issue = getIssue(issueId);
|
|
475
|
+
if (!issue) {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
const events = readEvents();
|
|
479
|
+
const state = computeState(events);
|
|
480
|
+
return issue.blockedBy.some((blockerId) => {
|
|
481
|
+
const blocker = state.get(blockerId);
|
|
482
|
+
return blocker && blocker.status !== "closed";
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
function getOpenBlockers(issueId) {
|
|
486
|
+
const issue = getIssue(issueId);
|
|
487
|
+
if (!issue) {
|
|
488
|
+
return [];
|
|
489
|
+
}
|
|
490
|
+
const events = readEvents();
|
|
491
|
+
const state = computeState(events);
|
|
492
|
+
return issue.blockedBy.map((id) => state.get(id)).filter((i) => i !== void 0 && i.status !== "closed");
|
|
493
|
+
}
|
|
459
494
|
|
|
460
495
|
// src/cli/lib/output.ts
|
|
461
496
|
function formatJson(data) {
|
|
@@ -547,7 +582,7 @@ function formatIssueListPretty(issues) {
|
|
|
547
582
|
lines.push(`Total: ${issues.length} issue(s)`);
|
|
548
583
|
return lines.join("\n");
|
|
549
584
|
}
|
|
550
|
-
function formatDepsPretty(issueId, blockedBy, blocking) {
|
|
585
|
+
function formatDepsPretty(issueId, blockedBy, blocking, related = []) {
|
|
551
586
|
const lines = [];
|
|
552
587
|
lines.push(`Dependencies for ${issueId}`);
|
|
553
588
|
lines.push("\u2500".repeat(40));
|
|
@@ -570,6 +605,16 @@ function formatDepsPretty(issueId, blockedBy, blocking) {
|
|
|
570
605
|
lines.push(` \u25CB ${issue.id} - ${truncate(issue.title, 30)}`);
|
|
571
606
|
}
|
|
572
607
|
}
|
|
608
|
+
lines.push("");
|
|
609
|
+
lines.push("Related:");
|
|
610
|
+
if (related.length === 0) {
|
|
611
|
+
lines.push(" (none)");
|
|
612
|
+
} else {
|
|
613
|
+
for (const issue of related) {
|
|
614
|
+
const status = issue.status === "closed" ? "\u2713" : "\u25CB";
|
|
615
|
+
lines.push(` ${status} ${issue.id} - ${truncate(issue.title, 30)}`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
573
618
|
return lines.join("\n");
|
|
574
619
|
}
|
|
575
620
|
function formatError(error) {
|
|
@@ -613,10 +658,48 @@ function outputError(error, pretty) {
|
|
|
613
658
|
}
|
|
614
659
|
process.exit(1);
|
|
615
660
|
}
|
|
661
|
+
function formatIssueListVerbose(issues) {
|
|
662
|
+
if (issues.length === 0) {
|
|
663
|
+
return "No issues found.";
|
|
664
|
+
}
|
|
665
|
+
const lines = [];
|
|
666
|
+
for (const info of issues) {
|
|
667
|
+
const { issue, blocking, children, verifications, blockers } = info;
|
|
668
|
+
lines.push(`${issue.id} - ${issue.title}`);
|
|
669
|
+
lines.push("\u2500".repeat(60));
|
|
670
|
+
lines.push(` Type: ${formatType(issue.type)}`);
|
|
671
|
+
lines.push(` Priority: P${issue.priority}`);
|
|
672
|
+
lines.push(` Status: ${issue.status}`);
|
|
673
|
+
lines.push(` Parent: ${issue.parent || "-"}`);
|
|
674
|
+
lines.push(` Children: ${issue.type === "epic" ? children : "-"}`);
|
|
675
|
+
lines.push(` Blocking: ${blocking.length > 0 ? blocking.join(", ") : "[]"}`);
|
|
676
|
+
lines.push(` Verifications: ${verifications}`);
|
|
677
|
+
if (blockers && blockers.length > 0) {
|
|
678
|
+
lines.push(` Blocked by: ${blockers.join(", ")}`);
|
|
679
|
+
}
|
|
680
|
+
lines.push("");
|
|
681
|
+
}
|
|
682
|
+
lines.push(`Total: ${issues.length} issue(s)`);
|
|
683
|
+
return lines.join("\n");
|
|
684
|
+
}
|
|
685
|
+
function outputIssueListVerbose(issues, pretty) {
|
|
686
|
+
if (pretty) {
|
|
687
|
+
console.log(formatIssueListVerbose(issues));
|
|
688
|
+
} else {
|
|
689
|
+
const output = issues.map(({ issue, blocking, children, verifications, blockers }) => ({
|
|
690
|
+
...issue,
|
|
691
|
+
blocking,
|
|
692
|
+
childrenCount: issue.type === "epic" ? children : void 0,
|
|
693
|
+
verificationsCount: verifications,
|
|
694
|
+
...blockers && { openBlockers: blockers }
|
|
695
|
+
}));
|
|
696
|
+
console.log(formatJson(output));
|
|
697
|
+
}
|
|
698
|
+
}
|
|
616
699
|
|
|
617
700
|
// src/cli/commands/create.ts
|
|
618
701
|
function createCommand(program2) {
|
|
619
|
-
program2.command("create <title>").description("Create a new issue").option("-t, --type <type>", "Issue type (task, bug, epic, verification)", "task").option("-p, --priority <priority>", "Priority (0-4)", "2").option("-d, --description <desc>", "Description").option("--parent <id>", "Parent epic ID").option("--verifies <id>", "ID of issue this verifies (sets type to verification)").action(async (title, options) => {
|
|
702
|
+
program2.command("create <title>").description("Create a new issue").option("-t, --type <type>", "Issue type (task, bug, epic, verification)", "task").option("-p, --priority <priority>", "Priority (0-4)", "2").option("-d, --description <desc>", "Description").option("--parent <id>", "Parent epic ID").option("--verifies <id>", "ID of issue this verifies (sets type to verification)").option("--blocked-by <ids>", "Comma-separated IDs of issues that block this one").option("--blocks <ids>", "Comma-separated IDs of issues this one will block").action(async (title, options) => {
|
|
620
703
|
const pretty = program2.opts().pretty ?? false;
|
|
621
704
|
try {
|
|
622
705
|
let type = options.type;
|
|
@@ -657,6 +740,33 @@ function createCommand(program2) {
|
|
|
657
740
|
throw new Error(`Target issue not found: ${options.verifies}`);
|
|
658
741
|
}
|
|
659
742
|
}
|
|
743
|
+
const blockedByIds = [];
|
|
744
|
+
if (options.blockedBy) {
|
|
745
|
+
const ids = options.blockedBy.split(",").map((s) => s.trim()).filter(Boolean);
|
|
746
|
+
for (const rawId of ids) {
|
|
747
|
+
const resolvedId = resolveId(rawId);
|
|
748
|
+
const blocker = getIssue(resolvedId);
|
|
749
|
+
if (!blocker) {
|
|
750
|
+
throw new Error(`Blocker issue not found: ${rawId}`);
|
|
751
|
+
}
|
|
752
|
+
if (blocker.status === "closed") {
|
|
753
|
+
throw new Error(`Cannot be blocked by closed issue: ${resolvedId}`);
|
|
754
|
+
}
|
|
755
|
+
blockedByIds.push(resolvedId);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const blocksIds = [];
|
|
759
|
+
if (options.blocks) {
|
|
760
|
+
const ids = options.blocks.split(",").map((s) => s.trim()).filter(Boolean);
|
|
761
|
+
for (const rawId of ids) {
|
|
762
|
+
const resolvedId = resolveId(rawId);
|
|
763
|
+
const blocked = getIssue(resolvedId);
|
|
764
|
+
if (!blocked) {
|
|
765
|
+
throw new Error(`Issue to block not found: ${rawId}`);
|
|
766
|
+
}
|
|
767
|
+
blocksIds.push(resolvedId);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
660
770
|
const id = generateId(config.prefix);
|
|
661
771
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
662
772
|
const event = {
|
|
@@ -673,6 +783,26 @@ function createCommand(program2) {
|
|
|
673
783
|
}
|
|
674
784
|
};
|
|
675
785
|
appendEvent(event, pebbleDir);
|
|
786
|
+
if (blockedByIds.length > 0) {
|
|
787
|
+
const depEvent = {
|
|
788
|
+
type: "update",
|
|
789
|
+
issueId: id,
|
|
790
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
791
|
+
data: { blockedBy: blockedByIds }
|
|
792
|
+
};
|
|
793
|
+
appendEvent(depEvent, pebbleDir);
|
|
794
|
+
}
|
|
795
|
+
for (const targetId of blocksIds) {
|
|
796
|
+
const target = getIssue(targetId);
|
|
797
|
+
const existingBlockers = target?.blockedBy || [];
|
|
798
|
+
const depEvent = {
|
|
799
|
+
type: "update",
|
|
800
|
+
issueId: targetId,
|
|
801
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
802
|
+
data: { blockedBy: [...existingBlockers, id] }
|
|
803
|
+
};
|
|
804
|
+
appendEvent(depEvent, pebbleDir);
|
|
805
|
+
}
|
|
676
806
|
outputMutationSuccess(id, pretty);
|
|
677
807
|
} catch (error) {
|
|
678
808
|
outputError(error, pretty);
|
|
@@ -682,7 +812,7 @@ function createCommand(program2) {
|
|
|
682
812
|
|
|
683
813
|
// src/cli/commands/update.ts
|
|
684
814
|
function updateCommand(program2) {
|
|
685
|
-
program2.command("update <ids...>").description("Update issues. Supports multiple IDs.").option("--status <status>", "Status (open, in_progress, blocked, closed)").option("--priority <priority>", "Priority (0-4)").option("--title <title>", "Title").option("--description <desc>", "Description").action(async (ids, options) => {
|
|
815
|
+
program2.command("update <ids...>").description("Update issues. Supports multiple IDs.").option("--status <status>", "Status (open, in_progress, blocked, closed)").option("--priority <priority>", "Priority (0-4)").option("--title <title>", "Title").option("--description <desc>", "Description").option("--parent <id>", 'Parent epic ID (use "null" to remove parent)').action(async (ids, options) => {
|
|
686
816
|
const pretty = program2.opts().pretty ?? false;
|
|
687
817
|
try {
|
|
688
818
|
const pebbleDir = getOrCreatePebbleDir();
|
|
@@ -716,8 +846,27 @@ function updateCommand(program2) {
|
|
|
716
846
|
data.description = options.description;
|
|
717
847
|
hasChanges = true;
|
|
718
848
|
}
|
|
849
|
+
if (options.parent !== void 0) {
|
|
850
|
+
if (options.parent.toLowerCase() === "null") {
|
|
851
|
+
data.parent = "";
|
|
852
|
+
} else {
|
|
853
|
+
const parentId = resolveId(options.parent);
|
|
854
|
+
const parentIssue = getIssue(parentId);
|
|
855
|
+
if (!parentIssue) {
|
|
856
|
+
throw new Error(`Parent issue not found: ${options.parent}`);
|
|
857
|
+
}
|
|
858
|
+
if (parentIssue.type !== "epic") {
|
|
859
|
+
throw new Error(`Parent must be an epic. ${parentId} is a ${parentIssue.type}`);
|
|
860
|
+
}
|
|
861
|
+
if (parentIssue.status === "closed") {
|
|
862
|
+
throw new Error(`Cannot set parent to closed epic: ${parentId}`);
|
|
863
|
+
}
|
|
864
|
+
data.parent = parentId;
|
|
865
|
+
}
|
|
866
|
+
hasChanges = true;
|
|
867
|
+
}
|
|
719
868
|
if (!hasChanges) {
|
|
720
|
-
throw new Error("No changes specified. Use --status, --priority, --title, or --
|
|
869
|
+
throw new Error("No changes specified. Use --status, --priority, --title, --description, or --parent");
|
|
721
870
|
}
|
|
722
871
|
const results = [];
|
|
723
872
|
for (const id of allIds) {
|
|
@@ -728,6 +877,12 @@ function updateCommand(program2) {
|
|
|
728
877
|
results.push({ id, success: false, error: `Issue not found: ${id}` });
|
|
729
878
|
continue;
|
|
730
879
|
}
|
|
880
|
+
if (data.status === "in_progress" && hasOpenBlockersById(resolvedId)) {
|
|
881
|
+
const blockers = getOpenBlockers(resolvedId);
|
|
882
|
+
const blockerIds = blockers.map((b) => b.id).join(", ");
|
|
883
|
+
results.push({ id: resolvedId, success: false, error: `Cannot set to in_progress - blocked by: ${blockerIds}` });
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
731
886
|
const event = {
|
|
732
887
|
type: "update",
|
|
733
888
|
issueId: resolvedId,
|
|
@@ -810,6 +965,25 @@ function closeCommand(program2) {
|
|
|
810
965
|
};
|
|
811
966
|
appendEvent(commentEvent, pebbleDir);
|
|
812
967
|
}
|
|
968
|
+
const pendingVerifications = getVerifications(resolvedId).filter((v) => v.status !== "closed");
|
|
969
|
+
if (pendingVerifications.length > 0) {
|
|
970
|
+
const updateEvent = {
|
|
971
|
+
type: "update",
|
|
972
|
+
issueId: resolvedId,
|
|
973
|
+
timestamp,
|
|
974
|
+
data: {
|
|
975
|
+
status: "pending_verification"
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
appendEvent(updateEvent, pebbleDir);
|
|
979
|
+
results.push({
|
|
980
|
+
id: resolvedId,
|
|
981
|
+
success: true,
|
|
982
|
+
status: "pending_verification",
|
|
983
|
+
pendingVerifications: pendingVerifications.map((v) => ({ id: v.id, title: v.title }))
|
|
984
|
+
});
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
813
987
|
const closeEvent = {
|
|
814
988
|
type: "close",
|
|
815
989
|
issueId: resolvedId,
|
|
@@ -823,6 +997,7 @@ function closeCommand(program2) {
|
|
|
823
997
|
results.push({
|
|
824
998
|
id: resolvedId,
|
|
825
999
|
success: true,
|
|
1000
|
+
status: "closed",
|
|
826
1001
|
unblocked: unblocked.length > 0 ? unblocked.map((i) => ({ id: i.id, title: i.title })) : void 0
|
|
827
1002
|
});
|
|
828
1003
|
} catch (error) {
|
|
@@ -833,18 +1008,29 @@ function closeCommand(program2) {
|
|
|
833
1008
|
const result = results[0];
|
|
834
1009
|
if (result.success) {
|
|
835
1010
|
if (pretty) {
|
|
836
|
-
|
|
837
|
-
|
|
1011
|
+
if (result.status === "pending_verification") {
|
|
1012
|
+
console.log(`\u23F3 ${result.id} \u2192 pending_verification`);
|
|
838
1013
|
console.log(`
|
|
1014
|
+
Pending verifications:`);
|
|
1015
|
+
for (const v of result.pendingVerifications || []) {
|
|
1016
|
+
console.log(` \u2022 ${v.id} - ${v.title}`);
|
|
1017
|
+
}
|
|
1018
|
+
} else {
|
|
1019
|
+
console.log(`\u2713 ${result.id}`);
|
|
1020
|
+
if (result.unblocked && result.unblocked.length > 0) {
|
|
1021
|
+
console.log(`
|
|
839
1022
|
Unblocked:`);
|
|
840
|
-
|
|
841
|
-
|
|
1023
|
+
for (const u of result.unblocked) {
|
|
1024
|
+
console.log(` \u2192 ${u.id} - ${u.title}`);
|
|
1025
|
+
}
|
|
842
1026
|
}
|
|
843
1027
|
}
|
|
844
1028
|
} else {
|
|
845
1029
|
console.log(formatJson({
|
|
846
1030
|
id: result.id,
|
|
847
1031
|
success: true,
|
|
1032
|
+
status: result.status,
|
|
1033
|
+
...result.pendingVerifications && { pendingVerifications: result.pendingVerifications },
|
|
848
1034
|
...result.unblocked && { unblocked: result.unblocked }
|
|
849
1035
|
}));
|
|
850
1036
|
}
|
|
@@ -855,10 +1041,17 @@ Unblocked:`);
|
|
|
855
1041
|
if (pretty) {
|
|
856
1042
|
for (const result of results) {
|
|
857
1043
|
if (result.success) {
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
for (const
|
|
861
|
-
console.log(` \
|
|
1044
|
+
if (result.status === "pending_verification") {
|
|
1045
|
+
console.log(`\u23F3 ${result.id} \u2192 pending_verification`);
|
|
1046
|
+
for (const v of result.pendingVerifications || []) {
|
|
1047
|
+
console.log(` \u2022 ${v.id} - ${v.title}`);
|
|
1048
|
+
}
|
|
1049
|
+
} else {
|
|
1050
|
+
console.log(`\u2713 ${result.id}`);
|
|
1051
|
+
if (result.unblocked && result.unblocked.length > 0) {
|
|
1052
|
+
for (const u of result.unblocked) {
|
|
1053
|
+
console.log(` \u2192 ${u.id} - ${u.title}`);
|
|
1054
|
+
}
|
|
862
1055
|
}
|
|
863
1056
|
}
|
|
864
1057
|
} else {
|
|
@@ -869,7 +1062,9 @@ Unblocked:`);
|
|
|
869
1062
|
console.log(formatJson(results.map((r) => ({
|
|
870
1063
|
id: r.id,
|
|
871
1064
|
success: r.success,
|
|
1065
|
+
status: r.status,
|
|
872
1066
|
...r.error && { error: r.error },
|
|
1067
|
+
...r.pendingVerifications && { pendingVerifications: r.pendingVerifications },
|
|
873
1068
|
...r.unblocked && { unblocked: r.unblocked }
|
|
874
1069
|
}))));
|
|
875
1070
|
}
|
|
@@ -937,6 +1132,12 @@ function claimCommand(program2) {
|
|
|
937
1132
|
results.push({ id: resolvedId, success: false, error: `Cannot claim closed issue: ${resolvedId}` });
|
|
938
1133
|
continue;
|
|
939
1134
|
}
|
|
1135
|
+
if (hasOpenBlockersById(resolvedId)) {
|
|
1136
|
+
const blockers = getOpenBlockers(resolvedId);
|
|
1137
|
+
const blockerIds = blockers.map((b) => b.id).join(", ");
|
|
1138
|
+
results.push({ id: resolvedId, success: false, error: `Cannot claim blocked issue. Blocked by: ${blockerIds}` });
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
940
1141
|
const event = {
|
|
941
1142
|
type: "update",
|
|
942
1143
|
issueId: resolvedId,
|
|
@@ -1041,12 +1242,22 @@ function showCommand(program2) {
|
|
|
1041
1242
|
|
|
1042
1243
|
// src/cli/commands/ready.ts
|
|
1043
1244
|
function readyCommand(program2) {
|
|
1044
|
-
program2.command("ready").description("Show issues ready for work (no open blockers)").action(async () => {
|
|
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) => {
|
|
1045
1246
|
const pretty = program2.opts().pretty ?? false;
|
|
1046
1247
|
try {
|
|
1047
1248
|
getOrCreatePebbleDir();
|
|
1048
1249
|
const issues = getReady();
|
|
1049
|
-
|
|
1250
|
+
if (options.verbose) {
|
|
1251
|
+
const verboseIssues = issues.map((issue) => ({
|
|
1252
|
+
issue,
|
|
1253
|
+
blocking: getBlocking(issue.id).map((i) => i.id),
|
|
1254
|
+
children: getChildren(issue.id).length,
|
|
1255
|
+
verifications: getVerifications(issue.id).length
|
|
1256
|
+
}));
|
|
1257
|
+
outputIssueListVerbose(verboseIssues, pretty);
|
|
1258
|
+
} else {
|
|
1259
|
+
outputIssueList(issues, pretty);
|
|
1260
|
+
}
|
|
1050
1261
|
} catch (error) {
|
|
1051
1262
|
outputError(error, pretty);
|
|
1052
1263
|
}
|
|
@@ -1055,12 +1266,27 @@ function readyCommand(program2) {
|
|
|
1055
1266
|
|
|
1056
1267
|
// src/cli/commands/blocked.ts
|
|
1057
1268
|
function blockedCommand(program2) {
|
|
1058
|
-
program2.command("blocked").description("Show blocked issues (have open blockers)").action(async () => {
|
|
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) => {
|
|
1059
1270
|
const pretty = program2.opts().pretty ?? false;
|
|
1060
1271
|
try {
|
|
1061
1272
|
getOrCreatePebbleDir();
|
|
1062
1273
|
const issues = getBlocked();
|
|
1063
|
-
|
|
1274
|
+
if (options.verbose) {
|
|
1275
|
+
const verboseIssues = issues.map((issue) => {
|
|
1276
|
+
const allBlockers = getBlockers(issue.id);
|
|
1277
|
+
const openBlockers = allBlockers.filter((b) => b.status !== "closed").map((b) => b.id);
|
|
1278
|
+
return {
|
|
1279
|
+
issue,
|
|
1280
|
+
blocking: getBlocking(issue.id).map((i) => i.id),
|
|
1281
|
+
children: getChildren(issue.id).length,
|
|
1282
|
+
verifications: getVerifications(issue.id).length,
|
|
1283
|
+
blockers: openBlockers
|
|
1284
|
+
};
|
|
1285
|
+
});
|
|
1286
|
+
outputIssueListVerbose(verboseIssues, pretty);
|
|
1287
|
+
} else {
|
|
1288
|
+
outputIssueList(issues, pretty);
|
|
1289
|
+
}
|
|
1064
1290
|
} catch (error) {
|
|
1065
1291
|
outputError(error, pretty);
|
|
1066
1292
|
}
|
|
@@ -1135,6 +1361,99 @@ function depCommand(program2) {
|
|
|
1135
1361
|
outputError(error, pretty);
|
|
1136
1362
|
}
|
|
1137
1363
|
});
|
|
1364
|
+
dep.command("relate <id1> <id2>").description("Add a bidirectional related link between two issues").action(async (id1, id2) => {
|
|
1365
|
+
const pretty = program2.opts().pretty ?? false;
|
|
1366
|
+
try {
|
|
1367
|
+
const pebbleDir = getOrCreatePebbleDir();
|
|
1368
|
+
const resolvedId1 = resolveId(id1);
|
|
1369
|
+
const resolvedId2 = resolveId(id2);
|
|
1370
|
+
const issue1 = getIssue(resolvedId1);
|
|
1371
|
+
if (!issue1) {
|
|
1372
|
+
throw new Error(`Issue not found: ${id1}`);
|
|
1373
|
+
}
|
|
1374
|
+
const issue2 = getIssue(resolvedId2);
|
|
1375
|
+
if (!issue2) {
|
|
1376
|
+
throw new Error(`Issue not found: ${id2}`);
|
|
1377
|
+
}
|
|
1378
|
+
if (resolvedId1 === resolvedId2) {
|
|
1379
|
+
throw new Error("Cannot relate issue to itself");
|
|
1380
|
+
}
|
|
1381
|
+
if (issue1.relatedTo.includes(resolvedId2)) {
|
|
1382
|
+
throw new Error(`Issues are already related: ${resolvedId1} \u2194 ${resolvedId2}`);
|
|
1383
|
+
}
|
|
1384
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1385
|
+
const event1 = {
|
|
1386
|
+
type: "update",
|
|
1387
|
+
issueId: resolvedId1,
|
|
1388
|
+
timestamp,
|
|
1389
|
+
data: {
|
|
1390
|
+
relatedTo: [...issue1.relatedTo, resolvedId2]
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
const event2 = {
|
|
1394
|
+
type: "update",
|
|
1395
|
+
issueId: resolvedId2,
|
|
1396
|
+
timestamp,
|
|
1397
|
+
data: {
|
|
1398
|
+
relatedTo: [...issue2.relatedTo, resolvedId1]
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
appendEvent(event1, pebbleDir);
|
|
1402
|
+
appendEvent(event2, pebbleDir);
|
|
1403
|
+
if (pretty) {
|
|
1404
|
+
console.log(`\u2713 ${resolvedId1} \u2194 ${resolvedId2}`);
|
|
1405
|
+
} else {
|
|
1406
|
+
console.log(formatJson({ id1: resolvedId1, id2: resolvedId2, related: true }));
|
|
1407
|
+
}
|
|
1408
|
+
} catch (error) {
|
|
1409
|
+
outputError(error, pretty);
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
dep.command("unrelate <id1> <id2>").description("Remove a bidirectional related link between two issues").action(async (id1, id2) => {
|
|
1413
|
+
const pretty = program2.opts().pretty ?? false;
|
|
1414
|
+
try {
|
|
1415
|
+
const pebbleDir = getOrCreatePebbleDir();
|
|
1416
|
+
const resolvedId1 = resolveId(id1);
|
|
1417
|
+
const resolvedId2 = resolveId(id2);
|
|
1418
|
+
const issue1 = getIssue(resolvedId1);
|
|
1419
|
+
if (!issue1) {
|
|
1420
|
+
throw new Error(`Issue not found: ${id1}`);
|
|
1421
|
+
}
|
|
1422
|
+
const issue2 = getIssue(resolvedId2);
|
|
1423
|
+
if (!issue2) {
|
|
1424
|
+
throw new Error(`Issue not found: ${id2}`);
|
|
1425
|
+
}
|
|
1426
|
+
if (!issue1.relatedTo.includes(resolvedId2)) {
|
|
1427
|
+
throw new Error(`Issues are not related: ${resolvedId1} \u2194 ${resolvedId2}`);
|
|
1428
|
+
}
|
|
1429
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1430
|
+
const event1 = {
|
|
1431
|
+
type: "update",
|
|
1432
|
+
issueId: resolvedId1,
|
|
1433
|
+
timestamp,
|
|
1434
|
+
data: {
|
|
1435
|
+
relatedTo: issue1.relatedTo.filter((id) => id !== resolvedId2)
|
|
1436
|
+
}
|
|
1437
|
+
};
|
|
1438
|
+
const event2 = {
|
|
1439
|
+
type: "update",
|
|
1440
|
+
issueId: resolvedId2,
|
|
1441
|
+
timestamp,
|
|
1442
|
+
data: {
|
|
1443
|
+
relatedTo: issue2.relatedTo.filter((id) => id !== resolvedId1)
|
|
1444
|
+
}
|
|
1445
|
+
};
|
|
1446
|
+
appendEvent(event1, pebbleDir);
|
|
1447
|
+
appendEvent(event2, pebbleDir);
|
|
1448
|
+
if (pretty) {
|
|
1449
|
+
console.log(`\u2713 ${resolvedId1} \u21AE ${resolvedId2}`);
|
|
1450
|
+
} else {
|
|
1451
|
+
console.log(formatJson({ id1: resolvedId1, id2: resolvedId2, related: false }));
|
|
1452
|
+
}
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
outputError(error, pretty);
|
|
1455
|
+
}
|
|
1456
|
+
});
|
|
1138
1457
|
dep.command("list <id>").description("List dependencies for an issue").action(async (id) => {
|
|
1139
1458
|
const pretty = program2.opts().pretty ?? false;
|
|
1140
1459
|
try {
|
|
@@ -1145,13 +1464,15 @@ function depCommand(program2) {
|
|
|
1145
1464
|
}
|
|
1146
1465
|
const blockedBy = getBlockers(resolvedId);
|
|
1147
1466
|
const blocking = getBlocking(resolvedId);
|
|
1467
|
+
const related = getRelated(resolvedId);
|
|
1148
1468
|
if (pretty) {
|
|
1149
|
-
console.log(formatDepsPretty(resolvedId, blockedBy, blocking));
|
|
1469
|
+
console.log(formatDepsPretty(resolvedId, blockedBy, blocking, related));
|
|
1150
1470
|
} else {
|
|
1151
1471
|
console.log(formatJson({
|
|
1152
1472
|
issueId: resolvedId,
|
|
1153
1473
|
blockedBy: blockedBy.map((i) => ({ id: i.id, title: i.title, status: i.status })),
|
|
1154
|
-
blocking: blocking.map((i) => ({ id: i.id, title: i.title, status: i.status }))
|
|
1474
|
+
blocking: blocking.map((i) => ({ id: i.id, title: i.title, status: i.status })),
|
|
1475
|
+
related: related.map((i) => ({ id: i.id, title: i.title, status: i.status }))
|
|
1155
1476
|
}));
|
|
1156
1477
|
}
|
|
1157
1478
|
} catch (error) {
|
|
@@ -1781,7 +2102,7 @@ data: ${message}
|
|
|
1781
2102
|
return;
|
|
1782
2103
|
}
|
|
1783
2104
|
if (updates.status) {
|
|
1784
|
-
const validStatuses = ["open", "in_progress", "blocked"];
|
|
2105
|
+
const validStatuses = ["open", "in_progress", "blocked", "pending_verification"];
|
|
1785
2106
|
if (!validStatuses.includes(updates.status)) {
|
|
1786
2107
|
res.status(400).json({
|
|
1787
2108
|
error: `Invalid status: ${updates.status}. Use close endpoint to close issues.`
|
|
@@ -2439,15 +2760,19 @@ function generateSuffix() {
|
|
|
2439
2760
|
import * as fs4 from "fs";
|
|
2440
2761
|
import * as path4 from "path";
|
|
2441
2762
|
function mergeEvents(filePaths) {
|
|
2442
|
-
const
|
|
2763
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2443
2764
|
for (const filePath of filePaths) {
|
|
2444
2765
|
const events = readEventsFromFile(filePath);
|
|
2445
2766
|
for (const event of events) {
|
|
2446
|
-
|
|
2767
|
+
const key = `${event.issueId}-${event.timestamp}-${event.type}`;
|
|
2768
|
+
if (!seen.has(key)) {
|
|
2769
|
+
seen.set(key, event);
|
|
2770
|
+
}
|
|
2447
2771
|
}
|
|
2448
2772
|
}
|
|
2773
|
+
const allEvents = Array.from(seen.values());
|
|
2449
2774
|
allEvents.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
2450
|
-
return allEvents
|
|
2775
|
+
return allEvents;
|
|
2451
2776
|
}
|
|
2452
2777
|
function mergeIssues(filePaths) {
|
|
2453
2778
|
const merged = /* @__PURE__ */ new Map();
|
|
@@ -2808,6 +3133,29 @@ function verificationsCommand(program2) {
|
|
|
2808
3133
|
});
|
|
2809
3134
|
}
|
|
2810
3135
|
|
|
3136
|
+
// src/cli/commands/init.ts
|
|
3137
|
+
import * as path5 from "path";
|
|
3138
|
+
function initCommand(program2) {
|
|
3139
|
+
program2.command("init").description("Initialize a new .pebble directory in the current directory").option("--force", "Re-initialize even if .pebble already exists").action((options) => {
|
|
3140
|
+
const existing = discoverPebbleDir();
|
|
3141
|
+
if (existing && !options.force) {
|
|
3142
|
+
console.error(JSON.stringify({
|
|
3143
|
+
error: "Already initialized",
|
|
3144
|
+
path: existing,
|
|
3145
|
+
hint: "Use --force to re-initialize"
|
|
3146
|
+
}));
|
|
3147
|
+
process.exit(1);
|
|
3148
|
+
}
|
|
3149
|
+
const pebbleDir = ensurePebbleDir(process.cwd());
|
|
3150
|
+
console.log(JSON.stringify({
|
|
3151
|
+
initialized: true,
|
|
3152
|
+
path: pebbleDir,
|
|
3153
|
+
configPath: path5.join(pebbleDir, "config.json"),
|
|
3154
|
+
issuesPath: path5.join(pebbleDir, "issues.jsonl")
|
|
3155
|
+
}));
|
|
3156
|
+
});
|
|
3157
|
+
}
|
|
3158
|
+
|
|
2811
3159
|
// src/cli/index.ts
|
|
2812
3160
|
var program = new Command();
|
|
2813
3161
|
program.name("pebble").description("A lightweight JSONL-based issue tracker").version("0.1.0");
|
|
@@ -2831,5 +3179,6 @@ summaryCommand(program);
|
|
|
2831
3179
|
historyCommand(program);
|
|
2832
3180
|
searchCommand(program);
|
|
2833
3181
|
verificationsCommand(program);
|
|
3182
|
+
initCommand(program);
|
|
2834
3183
|
program.parse();
|
|
2835
3184
|
//# sourceMappingURL=index.js.map
|