@markmdev/pebble 0.1.20 → 0.1.22
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
|
@@ -566,10 +566,12 @@ function getBlocking(issueId) {
|
|
|
566
566
|
(issue) => issue.blockedBy.includes(issueId)
|
|
567
567
|
);
|
|
568
568
|
}
|
|
569
|
-
function getChildren(epicId) {
|
|
569
|
+
function getChildren(epicId, includeDeleted = false) {
|
|
570
570
|
const events = readEvents();
|
|
571
571
|
const state = computeState(events);
|
|
572
|
-
return Array.from(state.values()).filter(
|
|
572
|
+
return Array.from(state.values()).filter(
|
|
573
|
+
(issue) => issue.parent === epicId && (includeDeleted || !issue.deleted)
|
|
574
|
+
);
|
|
573
575
|
}
|
|
574
576
|
function getVerifications(issueId) {
|
|
575
577
|
const events = readEvents();
|
|
@@ -656,6 +658,73 @@ function getAncestryChain(issueId, state) {
|
|
|
656
658
|
}
|
|
657
659
|
return chain;
|
|
658
660
|
}
|
|
661
|
+
function getAncestryBlocker(issueId, state) {
|
|
662
|
+
let current = state.get(issueId);
|
|
663
|
+
while (current?.parent) {
|
|
664
|
+
const parent = state.get(current.parent);
|
|
665
|
+
if (!parent) break;
|
|
666
|
+
const openBlockers = parent.blockedBy.map((id) => state.get(id)).filter((i) => i !== void 0 && i.status !== "closed" && !i.deleted);
|
|
667
|
+
if (openBlockers.length > 0) {
|
|
668
|
+
return { blockedAncestor: parent, blockers: openBlockers };
|
|
669
|
+
}
|
|
670
|
+
current = parent;
|
|
671
|
+
}
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
function claimWithCascade(issueId, pebbleDir) {
|
|
675
|
+
const events = readEvents();
|
|
676
|
+
const state = computeState(events);
|
|
677
|
+
const issue = state.get(issueId);
|
|
678
|
+
if (!issue) {
|
|
679
|
+
return { success: false, error: `Issue not found: ${issueId}` };
|
|
680
|
+
}
|
|
681
|
+
if (issue.deleted) {
|
|
682
|
+
return { success: false, error: `Cannot claim deleted issue: ${issueId}` };
|
|
683
|
+
}
|
|
684
|
+
if (issue.status === "closed") {
|
|
685
|
+
return { success: false, error: `Cannot claim closed issue: ${issueId}` };
|
|
686
|
+
}
|
|
687
|
+
const openBlockers = issue.blockedBy.map((id) => state.get(id)).filter((i) => i !== void 0 && i.status !== "closed" && !i.deleted);
|
|
688
|
+
if (openBlockers.length > 0) {
|
|
689
|
+
const blockerIds = openBlockers.map((b) => b.id).join(", ");
|
|
690
|
+
return {
|
|
691
|
+
success: false,
|
|
692
|
+
error: `Cannot claim blocked issue. Blocked by: ${blockerIds}`
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
const ancestryBlocker = getAncestryBlocker(issueId, state);
|
|
696
|
+
if (ancestryBlocker) {
|
|
697
|
+
const blockerIds = ancestryBlocker.blockers.map((b) => b.id).join(", ");
|
|
698
|
+
return {
|
|
699
|
+
success: false,
|
|
700
|
+
error: `Parent ${ancestryBlocker.blockedAncestor.id} is blocked by: ${blockerIds}`
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
const toClaim = [];
|
|
704
|
+
if (issue.status !== "in_progress") {
|
|
705
|
+
toClaim.push(issueId);
|
|
706
|
+
}
|
|
707
|
+
let current = issue;
|
|
708
|
+
while (current.parent) {
|
|
709
|
+
const parent = state.get(current.parent);
|
|
710
|
+
if (!parent) break;
|
|
711
|
+
if (parent.status === "open") {
|
|
712
|
+
toClaim.push(parent.id);
|
|
713
|
+
}
|
|
714
|
+
current = parent;
|
|
715
|
+
}
|
|
716
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
717
|
+
for (const id of toClaim) {
|
|
718
|
+
const event = {
|
|
719
|
+
type: "update",
|
|
720
|
+
issueId: id,
|
|
721
|
+
timestamp,
|
|
722
|
+
data: { status: "in_progress" }
|
|
723
|
+
};
|
|
724
|
+
appendEvent(event, pebbleDir);
|
|
725
|
+
}
|
|
726
|
+
return { success: true, claimedIds: toClaim };
|
|
727
|
+
}
|
|
659
728
|
function getDescendants(issueId, state) {
|
|
660
729
|
const issueState = state ?? getComputedState();
|
|
661
730
|
const descendants = [];
|
|
@@ -1148,6 +1217,15 @@ function createCommand(program2) {
|
|
|
1148
1217
|
}
|
|
1149
1218
|
};
|
|
1150
1219
|
appendEvent(event, pebbleDir);
|
|
1220
|
+
if (parentId) {
|
|
1221
|
+
const parentUpdateEvent = {
|
|
1222
|
+
type: "update",
|
|
1223
|
+
issueId: parentId,
|
|
1224
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1225
|
+
data: {}
|
|
1226
|
+
};
|
|
1227
|
+
appendEvent(parentUpdateEvent, pebbleDir);
|
|
1228
|
+
}
|
|
1151
1229
|
if (blockedByIds.length > 0) {
|
|
1152
1230
|
const depEvent = {
|
|
1153
1231
|
type: "update",
|
|
@@ -1322,8 +1400,8 @@ function closeCommand(program2) {
|
|
|
1322
1400
|
results.push({ id: resolvedId, success: false, error: `Issue is already closed: ${resolvedId}` });
|
|
1323
1401
|
continue;
|
|
1324
1402
|
}
|
|
1325
|
-
if (
|
|
1326
|
-
results.push({ id: resolvedId, success: false, error: `Cannot close
|
|
1403
|
+
if (hasOpenChildren(resolvedId)) {
|
|
1404
|
+
results.push({ id: resolvedId, success: false, error: `Cannot close issue with open children: ${resolvedId}` });
|
|
1327
1405
|
continue;
|
|
1328
1406
|
}
|
|
1329
1407
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1729,7 +1807,7 @@ function restoreCommand(program2) {
|
|
|
1729
1807
|
|
|
1730
1808
|
// src/cli/commands/claim.ts
|
|
1731
1809
|
function claimCommand(program2) {
|
|
1732
|
-
program2.command("claim <ids...>").description("Claim issues (set status to in_progress).
|
|
1810
|
+
program2.command("claim <ids...>").description("Claim issues (set status to in_progress). Cascades to open parent issues.").action(async (ids) => {
|
|
1733
1811
|
const pretty = program2.opts().pretty ?? false;
|
|
1734
1812
|
try {
|
|
1735
1813
|
const pebbleDir = getOrCreatePebbleDir();
|
|
@@ -1741,35 +1819,20 @@ function claimCommand(program2) {
|
|
|
1741
1819
|
for (const id of allIds) {
|
|
1742
1820
|
try {
|
|
1743
1821
|
const resolvedId = resolveId(id);
|
|
1744
|
-
const
|
|
1745
|
-
if (
|
|
1746
|
-
results.push({
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
if (hasOpenBlockersById(resolvedId)) {
|
|
1758
|
-
const blockers = getOpenBlockers(resolvedId);
|
|
1759
|
-
const blockerIds = blockers.map((b) => b.id).join(", ");
|
|
1760
|
-
results.push({ id: resolvedId, success: false, error: `Cannot claim blocked issue. Blocked by: ${blockerIds}` });
|
|
1761
|
-
continue;
|
|
1822
|
+
const result = claimWithCascade(resolvedId, pebbleDir);
|
|
1823
|
+
if (result.success) {
|
|
1824
|
+
results.push({
|
|
1825
|
+
id: resolvedId,
|
|
1826
|
+
success: true,
|
|
1827
|
+
claimedIds: result.claimedIds
|
|
1828
|
+
});
|
|
1829
|
+
} else {
|
|
1830
|
+
results.push({
|
|
1831
|
+
id: resolvedId,
|
|
1832
|
+
success: false,
|
|
1833
|
+
error: result.error
|
|
1834
|
+
});
|
|
1762
1835
|
}
|
|
1763
|
-
const event = {
|
|
1764
|
-
type: "update",
|
|
1765
|
-
issueId: resolvedId,
|
|
1766
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1767
|
-
data: {
|
|
1768
|
-
status: "in_progress"
|
|
1769
|
-
}
|
|
1770
|
-
};
|
|
1771
|
-
appendEvent(event, pebbleDir);
|
|
1772
|
-
results.push({ id: resolvedId, success: true });
|
|
1773
1836
|
} catch (error) {
|
|
1774
1837
|
results.push({ id, success: false, error: error.message });
|
|
1775
1838
|
}
|
|
@@ -1777,7 +1840,20 @@ function claimCommand(program2) {
|
|
|
1777
1840
|
if (allIds.length === 1) {
|
|
1778
1841
|
const result = results[0];
|
|
1779
1842
|
if (result.success) {
|
|
1780
|
-
|
|
1843
|
+
if (pretty) {
|
|
1844
|
+
const cascaded = result.claimedIds?.filter((cid) => cid !== result.id) ?? [];
|
|
1845
|
+
if (cascaded.length > 0) {
|
|
1846
|
+
console.log(`\u2713 ${result.id} (also claimed: ${cascaded.join(", ")})`);
|
|
1847
|
+
} else {
|
|
1848
|
+
console.log(`\u2713 ${result.id}`);
|
|
1849
|
+
}
|
|
1850
|
+
} else {
|
|
1851
|
+
console.log(formatJson({
|
|
1852
|
+
id: result.id,
|
|
1853
|
+
success: true,
|
|
1854
|
+
claimedIds: result.claimedIds
|
|
1855
|
+
}));
|
|
1856
|
+
}
|
|
1781
1857
|
} else {
|
|
1782
1858
|
throw new Error(result.error || "Unknown error");
|
|
1783
1859
|
}
|
|
@@ -1785,7 +1861,12 @@ function claimCommand(program2) {
|
|
|
1785
1861
|
if (pretty) {
|
|
1786
1862
|
for (const result of results) {
|
|
1787
1863
|
if (result.success) {
|
|
1788
|
-
|
|
1864
|
+
const cascaded = result.claimedIds?.filter((cid) => cid !== result.id) ?? [];
|
|
1865
|
+
if (cascaded.length > 0) {
|
|
1866
|
+
console.log(`\u2713 ${result.id} (also claimed: ${cascaded.join(", ")})`);
|
|
1867
|
+
} else {
|
|
1868
|
+
console.log(`\u2713 ${result.id}`);
|
|
1869
|
+
}
|
|
1789
1870
|
} else {
|
|
1790
1871
|
console.log(`\u2717 ${result.id}: ${result.error}`);
|
|
1791
1872
|
}
|
|
@@ -1794,6 +1875,7 @@ function claimCommand(program2) {
|
|
|
1794
1875
|
console.log(formatJson(results.map((r) => ({
|
|
1795
1876
|
id: r.id,
|
|
1796
1877
|
success: r.success,
|
|
1878
|
+
...r.claimedIds && { claimedIds: r.claimedIds },
|
|
1797
1879
|
...r.error && { error: r.error }
|
|
1798
1880
|
}))));
|
|
1799
1881
|
}
|
|
@@ -2805,6 +2887,15 @@ data: ${message}
|
|
|
2805
2887
|
}
|
|
2806
2888
|
};
|
|
2807
2889
|
appendEvent(event, pebbleDir);
|
|
2890
|
+
if (parent) {
|
|
2891
|
+
const parentUpdateEvent = {
|
|
2892
|
+
type: "update",
|
|
2893
|
+
issueId: parent,
|
|
2894
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2895
|
+
data: {}
|
|
2896
|
+
};
|
|
2897
|
+
appendEvent(parentUpdateEvent, pebbleDir);
|
|
2898
|
+
}
|
|
2808
2899
|
const issue = getIssue(issueId);
|
|
2809
2900
|
const result = parentReopened ? { ...issue, _parentReopened: parentReopened } : issue;
|
|
2810
2901
|
res.status(201).json(result);
|
|
@@ -2833,8 +2924,8 @@ data: ${message}
|
|
|
2833
2924
|
results.push({ id: issueId, success: true });
|
|
2834
2925
|
continue;
|
|
2835
2926
|
}
|
|
2836
|
-
if (
|
|
2837
|
-
results.push({ id: issueId, success: false, error: "Cannot close
|
|
2927
|
+
if (hasOpenChildren(issueId)) {
|
|
2928
|
+
results.push({ id: issueId, success: false, error: "Cannot close issue with open children" });
|
|
2838
2929
|
continue;
|
|
2839
2930
|
}
|
|
2840
2931
|
const pendingVerifications = getVerifications(issueId).filter((v) => v.status !== "closed");
|
|
@@ -2975,11 +3066,47 @@ data: ${message}
|
|
|
2975
3066
|
}
|
|
2976
3067
|
updates.priority = priority;
|
|
2977
3068
|
}
|
|
3069
|
+
const cascadeClaim = [];
|
|
2978
3070
|
if (status !== void 0) {
|
|
2979
3071
|
if (!STATUSES.includes(status)) {
|
|
2980
3072
|
res.status(400).json({ error: `Invalid status. Must be one of: ${STATUSES.join(", ")}` });
|
|
2981
3073
|
return;
|
|
2982
3074
|
}
|
|
3075
|
+
if (status === "in_progress" && issue.status !== "in_progress") {
|
|
3076
|
+
const state = isMultiWorktree() ? computeState(issueFiles.flatMap((f) => readEventsFromFile(f))) : getComputedState();
|
|
3077
|
+
const openBlockers = issue.blockedBy.map((id) => state.get(id)).filter((i) => i !== void 0 && i.status !== "closed" && !i.deleted);
|
|
3078
|
+
if (openBlockers.length > 0) {
|
|
3079
|
+
const blockerIds = openBlockers.map((b) => b.id).join(", ");
|
|
3080
|
+
res.status(400).json({
|
|
3081
|
+
error: `Cannot claim blocked issue. Blocked by: ${blockerIds}`
|
|
3082
|
+
});
|
|
3083
|
+
return;
|
|
3084
|
+
}
|
|
3085
|
+
const ancestryBlocker = getAncestryBlocker(issueId, state);
|
|
3086
|
+
if (ancestryBlocker) {
|
|
3087
|
+
const blockerIds = ancestryBlocker.blockers.map((b) => b.id).join(", ");
|
|
3088
|
+
res.status(400).json({
|
|
3089
|
+
error: `Parent ${ancestryBlocker.blockedAncestor.id} is blocked by: ${blockerIds}`
|
|
3090
|
+
});
|
|
3091
|
+
return;
|
|
3092
|
+
}
|
|
3093
|
+
let current = issue;
|
|
3094
|
+
while (current.parent) {
|
|
3095
|
+
const parent2 = state.get(current.parent);
|
|
3096
|
+
if (!parent2) break;
|
|
3097
|
+
if (parent2.status === "open") {
|
|
3098
|
+
let parentTargetFile = targetFile;
|
|
3099
|
+
if (isMultiWorktree()) {
|
|
3100
|
+
const parentFound = findIssueInSources(parent2.id, issueFiles);
|
|
3101
|
+
if (parentFound) {
|
|
3102
|
+
parentTargetFile = parentFound.targetFile;
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
cascadeClaim.push({ id: parent2.id, targetFile: parentTargetFile });
|
|
3106
|
+
}
|
|
3107
|
+
current = parent2;
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
2983
3110
|
updates.status = status;
|
|
2984
3111
|
}
|
|
2985
3112
|
if (description !== void 0) {
|
|
@@ -3045,11 +3172,32 @@ data: ${message}
|
|
|
3045
3172
|
data: updates
|
|
3046
3173
|
};
|
|
3047
3174
|
appendEventToFile(event, targetFile);
|
|
3175
|
+
const cascadeClaimedIds = [];
|
|
3176
|
+
for (const { id, targetFile: parentTargetFile } of cascadeClaim) {
|
|
3177
|
+
const cascadeEvent = {
|
|
3178
|
+
type: "update",
|
|
3179
|
+
issueId: id,
|
|
3180
|
+
timestamp,
|
|
3181
|
+
data: { status: "in_progress" }
|
|
3182
|
+
};
|
|
3183
|
+
appendEventToFile(cascadeEvent, parentTargetFile);
|
|
3184
|
+
cascadeClaimedIds.push(id);
|
|
3185
|
+
}
|
|
3048
3186
|
if (isMultiWorktree()) {
|
|
3049
3187
|
const updated = findIssueInSources(issueId, issueFiles);
|
|
3050
|
-
|
|
3188
|
+
const baseIssue = updated?.issue || { ...issue, ...updates, updatedAt: timestamp };
|
|
3189
|
+
if (cascadeClaimedIds.length > 0) {
|
|
3190
|
+
res.json({ ...baseIssue, _cascadeClaimed: cascadeClaimedIds });
|
|
3191
|
+
} else {
|
|
3192
|
+
res.json(baseIssue);
|
|
3193
|
+
}
|
|
3051
3194
|
} else {
|
|
3052
|
-
|
|
3195
|
+
const updatedIssue = getIssue(issueId);
|
|
3196
|
+
if (cascadeClaimedIds.length > 0) {
|
|
3197
|
+
res.json({ ...updatedIssue, _cascadeClaimed: cascadeClaimedIds });
|
|
3198
|
+
} else {
|
|
3199
|
+
res.json(updatedIssue);
|
|
3200
|
+
}
|
|
3053
3201
|
}
|
|
3054
3202
|
} catch (error) {
|
|
3055
3203
|
res.status(500).json({ error: error.message });
|
|
@@ -3084,8 +3232,8 @@ data: ${message}
|
|
|
3084
3232
|
res.status(400).json({ error: "Issue is already closed" });
|
|
3085
3233
|
return;
|
|
3086
3234
|
}
|
|
3087
|
-
if (!isMultiWorktree() &&
|
|
3088
|
-
res.status(400).json({ error: "Cannot close
|
|
3235
|
+
if (!isMultiWorktree() && hasOpenChildren(issueId)) {
|
|
3236
|
+
res.status(400).json({ error: "Cannot close issue with open children" });
|
|
3089
3237
|
return;
|
|
3090
3238
|
}
|
|
3091
3239
|
const { reason } = req.body;
|
|
@@ -3898,6 +4046,21 @@ function formatInProgressPretty(issues) {
|
|
|
3898
4046
|
}
|
|
3899
4047
|
return lines.join("\n");
|
|
3900
4048
|
}
|
|
4049
|
+
function formatClosedIssuesPretty(issues) {
|
|
4050
|
+
if (issues.length === 0) return "";
|
|
4051
|
+
const lines = [];
|
|
4052
|
+
lines.push(`## Recently Closed (${issues.length})`);
|
|
4053
|
+
lines.push("");
|
|
4054
|
+
for (const issue of issues) {
|
|
4055
|
+
lines.push(`\u2713 ${issue.id}: ${issue.title} [${issue.type}]`);
|
|
4056
|
+
lines.push(` Closed: ${formatRelativeTime(issue.closedAt)}${issue.lastSource ? ` | Source: ${issue.lastSource}` : ""}`);
|
|
4057
|
+
if (issue.parent) {
|
|
4058
|
+
lines.push(` Parent: ${issue.parent.title}`);
|
|
4059
|
+
}
|
|
4060
|
+
lines.push("");
|
|
4061
|
+
}
|
|
4062
|
+
return lines.join("\n");
|
|
4063
|
+
}
|
|
3901
4064
|
function formatSummaryPretty(summaries, sectionHeader) {
|
|
3902
4065
|
if (summaries.length === 0) {
|
|
3903
4066
|
return "No epics found.";
|
|
@@ -4012,6 +4175,27 @@ function summaryCommand(program2) {
|
|
|
4012
4175
|
const limitedClosed = limit > 0 ? closedEpics.slice(0, limit) : closedEpics;
|
|
4013
4176
|
const openSummaries = limitedOpen.map(buildSummary);
|
|
4014
4177
|
const closedSummaries = limitedClosed.map(buildSummary);
|
|
4178
|
+
let closedIssues = getIssues({ status: "closed" });
|
|
4179
|
+
closedIssues.sort(
|
|
4180
|
+
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
4181
|
+
);
|
|
4182
|
+
closedIssues = closedIssues.slice(0, 20);
|
|
4183
|
+
const recentlyClosedSummaries = closedIssues.map((issue) => {
|
|
4184
|
+
const summary = {
|
|
4185
|
+
id: issue.id,
|
|
4186
|
+
title: issue.title,
|
|
4187
|
+
type: issue.type,
|
|
4188
|
+
closedAt: issue.updatedAt,
|
|
4189
|
+
lastSource: issue.lastSource
|
|
4190
|
+
};
|
|
4191
|
+
if (issue.parent) {
|
|
4192
|
+
const parentIssue = getIssue(issue.parent, true);
|
|
4193
|
+
if (parentIssue) {
|
|
4194
|
+
summary.parent = { id: parentIssue.id, title: parentIssue.title };
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
return summary;
|
|
4198
|
+
});
|
|
4015
4199
|
if (pretty) {
|
|
4016
4200
|
const output = [];
|
|
4017
4201
|
const inProgressOutput = formatInProgressPretty(inProgressSummaries);
|
|
@@ -4026,12 +4210,16 @@ function summaryCommand(program2) {
|
|
|
4026
4210
|
if (output.length > 0) output.push("");
|
|
4027
4211
|
output.push(formatSummaryPretty(closedSummaries, "Recently Closed Epics (last 72h)"));
|
|
4028
4212
|
}
|
|
4213
|
+
if (recentlyClosedSummaries.length > 0) {
|
|
4214
|
+
if (output.length > 0) output.push("");
|
|
4215
|
+
output.push(formatClosedIssuesPretty(recentlyClosedSummaries));
|
|
4216
|
+
}
|
|
4029
4217
|
if (output.length === 0) {
|
|
4030
4218
|
output.push("No issues in progress and no epics found.");
|
|
4031
4219
|
}
|
|
4032
4220
|
console.log(output.join("\n"));
|
|
4033
4221
|
} else {
|
|
4034
|
-
console.log(formatJson({ inProgress: inProgressSummaries, open: openSummaries, closed: closedSummaries }));
|
|
4222
|
+
console.log(formatJson({ inProgress: inProgressSummaries, open: openSummaries, closed: closedSummaries, recentlyClosed: recentlyClosedSummaries }));
|
|
4035
4223
|
}
|
|
4036
4224
|
} catch (error) {
|
|
4037
4225
|
outputError(error, pretty);
|