@markmdev/pebble 0.1.18 → 0.1.21
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
|
@@ -294,6 +294,8 @@ function computeState(events) {
|
|
|
294
294
|
comments: [],
|
|
295
295
|
createdAt: event.timestamp,
|
|
296
296
|
updatedAt: event.timestamp,
|
|
297
|
+
statusChangedAt: event.timestamp,
|
|
298
|
+
// Initial status is 'open'
|
|
297
299
|
lastSource: event.source
|
|
298
300
|
};
|
|
299
301
|
issues.set(event.issueId, issue);
|
|
@@ -313,6 +315,9 @@ function computeState(events) {
|
|
|
313
315
|
issue.priority = updateEvent.data.priority;
|
|
314
316
|
}
|
|
315
317
|
if (updateEvent.data.status !== void 0) {
|
|
318
|
+
if (issue.status !== updateEvent.data.status) {
|
|
319
|
+
issue.statusChangedAt = event.timestamp;
|
|
320
|
+
}
|
|
316
321
|
issue.status = updateEvent.data.status;
|
|
317
322
|
}
|
|
318
323
|
if (updateEvent.data.description !== void 0) {
|
|
@@ -336,6 +341,7 @@ function computeState(events) {
|
|
|
336
341
|
const issue = issues.get(event.issueId);
|
|
337
342
|
if (issue) {
|
|
338
343
|
issue.status = "closed";
|
|
344
|
+
issue.statusChangedAt = event.timestamp;
|
|
339
345
|
issue.updatedAt = event.timestamp;
|
|
340
346
|
if (event.source) issue.lastSource = event.source;
|
|
341
347
|
}
|
|
@@ -345,6 +351,7 @@ function computeState(events) {
|
|
|
345
351
|
const issue = issues.get(event.issueId);
|
|
346
352
|
if (issue) {
|
|
347
353
|
issue.status = "open";
|
|
354
|
+
issue.statusChangedAt = event.timestamp;
|
|
348
355
|
issue.updatedAt = event.timestamp;
|
|
349
356
|
if (event.source) issue.lastSource = event.source;
|
|
350
357
|
}
|
|
@@ -559,10 +566,12 @@ function getBlocking(issueId) {
|
|
|
559
566
|
(issue) => issue.blockedBy.includes(issueId)
|
|
560
567
|
);
|
|
561
568
|
}
|
|
562
|
-
function getChildren(epicId) {
|
|
569
|
+
function getChildren(epicId, includeDeleted = false) {
|
|
563
570
|
const events = readEvents();
|
|
564
571
|
const state = computeState(events);
|
|
565
|
-
return Array.from(state.values()).filter(
|
|
572
|
+
return Array.from(state.values()).filter(
|
|
573
|
+
(issue) => issue.parent === epicId && (includeDeleted || !issue.deleted)
|
|
574
|
+
);
|
|
566
575
|
}
|
|
567
576
|
function getVerifications(issueId) {
|
|
568
577
|
const events = readEvents();
|
|
@@ -707,7 +716,8 @@ function formatIssueDetailPretty(issue, ctx) {
|
|
|
707
716
|
lines.push("\u2500".repeat(60));
|
|
708
717
|
lines.push(`Type: ${formatType(issue.type)}`);
|
|
709
718
|
lines.push(`Priority: ${formatPriority(issue.priority)}`);
|
|
710
|
-
|
|
719
|
+
const statusTime = issue.statusChangedAt ? ` (${formatRelativeTime(issue.statusChangedAt)})` : "";
|
|
720
|
+
lines.push(`Status: ${formatStatus(issue.status)}${statusTime}`);
|
|
711
721
|
if (ctx.ancestry && ctx.ancestry.length > 0) {
|
|
712
722
|
const chain = [...ctx.ancestry].reverse().map((a) => a.id).join(" > ");
|
|
713
723
|
lines.push(`Ancestry: ${chain}`);
|
|
@@ -764,8 +774,8 @@ function formatIssueDetailPretty(issue, ctx) {
|
|
|
764
774
|
}
|
|
765
775
|
}
|
|
766
776
|
lines.push("");
|
|
767
|
-
lines.push(`Created: ${
|
|
768
|
-
lines.push(`Updated: ${
|
|
777
|
+
lines.push(`Created: ${formatRelativeTime(issue.createdAt)}`);
|
|
778
|
+
lines.push(`Updated: ${formatRelativeTime(issue.updatedAt)}`);
|
|
769
779
|
return lines.join("\n");
|
|
770
780
|
}
|
|
771
781
|
function outputIssueDetail(issue, ctx, pretty) {
|
|
@@ -859,11 +869,30 @@ function formatErrorPretty(error) {
|
|
|
859
869
|
const message = error instanceof Error ? error.message : error;
|
|
860
870
|
return `Error: ${message}`;
|
|
861
871
|
}
|
|
862
|
-
function outputMutationSuccess(id, pretty) {
|
|
872
|
+
function outputMutationSuccess(id, pretty, extra) {
|
|
863
873
|
if (pretty) {
|
|
864
|
-
|
|
874
|
+
const notes = [];
|
|
875
|
+
if (extra?.parentReopened) {
|
|
876
|
+
notes.push(`parent ${extra.parentReopened.id} reopened`);
|
|
877
|
+
}
|
|
878
|
+
if (extra?.blockersReopened?.length) {
|
|
879
|
+
const ids = extra.blockersReopened.map((b) => b.id).join(", ");
|
|
880
|
+
notes.push(`blocker${extra.blockersReopened.length > 1 ? "s" : ""} ${ids} reopened`);
|
|
881
|
+
}
|
|
882
|
+
if (notes.length > 0) {
|
|
883
|
+
console.log(`\u2713 ${id} (${notes.join(", ")})`);
|
|
884
|
+
} else {
|
|
885
|
+
console.log(`\u2713 ${id}`);
|
|
886
|
+
}
|
|
865
887
|
} else {
|
|
866
|
-
|
|
888
|
+
const result = { id, success: true };
|
|
889
|
+
if (extra?.parentReopened) {
|
|
890
|
+
result._parentReopened = extra.parentReopened;
|
|
891
|
+
}
|
|
892
|
+
if (extra?.blockersReopened?.length) {
|
|
893
|
+
result._blockersReopened = extra.blockersReopened;
|
|
894
|
+
}
|
|
895
|
+
console.log(JSON.stringify(result));
|
|
867
896
|
}
|
|
868
897
|
}
|
|
869
898
|
function outputIssueList(issues, pretty, limitInfo) {
|
|
@@ -900,6 +929,7 @@ function buildIssueTree(issues) {
|
|
|
900
929
|
priority: issue.priority,
|
|
901
930
|
status: issue.status,
|
|
902
931
|
createdAt: issue.createdAt,
|
|
932
|
+
statusChangedAt: issue.statusChangedAt,
|
|
903
933
|
childrenCount: children.length,
|
|
904
934
|
...children.length > 0 && { children }
|
|
905
935
|
};
|
|
@@ -925,7 +955,7 @@ function formatIssueTreePretty(nodes, sectionHeader) {
|
|
|
925
955
|
const connector = isRoot ? "" : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
|
|
926
956
|
const statusIcon = node.status === "closed" ? "\u2713" : node.status === "in_progress" ? "\u25B6" : node.status === "pending_verification" ? "\u23F3" : "\u25CB";
|
|
927
957
|
const statusText = STATUS_LABELS[node.status].toLowerCase();
|
|
928
|
-
const relativeTime = formatRelativeTime(node.createdAt);
|
|
958
|
+
const relativeTime = node.statusChangedAt ? formatRelativeTime(node.statusChangedAt) : formatRelativeTime(node.createdAt);
|
|
929
959
|
lines.push(`${prefix}${connector}${statusIcon} ${node.id}: ${node.title} [${node.type}] P${node.priority} ${statusText} ${relativeTime}`);
|
|
930
960
|
const children = node.children ?? [];
|
|
931
961
|
const childPrefix = isRoot ? "" : prefix + (isLast ? " " : "\u2502 ");
|
|
@@ -975,8 +1005,9 @@ function formatIssueListVerbose(issues, sectionHeader) {
|
|
|
975
1005
|
}
|
|
976
1006
|
for (const info of issues) {
|
|
977
1007
|
const { issue, blocking, children, verifications, blockers, ancestry } = info;
|
|
1008
|
+
const statusTime = issue.statusChangedAt ? formatRelativeTime(issue.statusChangedAt) : formatRelativeTime(issue.createdAt);
|
|
978
1009
|
lines.push(`${issue.id}: ${issue.title}`);
|
|
979
|
-
lines.push(` Type: ${formatType(issue.type)} | Priority: P${issue.priority} |
|
|
1010
|
+
lines.push(` Type: ${formatType(issue.type)} | Priority: P${issue.priority} | Status: ${formatStatus(issue.status)} (${statusTime})`);
|
|
980
1011
|
if (ancestry.length > 0) {
|
|
981
1012
|
const chain = [...ancestry].reverse().map((a) => a.title).join(" \u2192 ");
|
|
982
1013
|
lines.push(` Ancestry: ${chain}`);
|
|
@@ -1039,6 +1070,7 @@ function createCommand(program2) {
|
|
|
1039
1070
|
const pebbleDir = getOrCreatePebbleDir();
|
|
1040
1071
|
const config = getConfig(pebbleDir);
|
|
1041
1072
|
let parentId;
|
|
1073
|
+
let parentReopened;
|
|
1042
1074
|
if (options.parent) {
|
|
1043
1075
|
parentId = resolveId(options.parent);
|
|
1044
1076
|
const parent = getIssue(parentId);
|
|
@@ -1049,7 +1081,14 @@ function createCommand(program2) {
|
|
|
1049
1081
|
throw new Error(`Verification issues cannot be parents`);
|
|
1050
1082
|
}
|
|
1051
1083
|
if (parent.status === "closed") {
|
|
1052
|
-
|
|
1084
|
+
const reopenEvent = {
|
|
1085
|
+
type: "reopen",
|
|
1086
|
+
issueId: parentId,
|
|
1087
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1088
|
+
data: { reason: "Reopened to add child" }
|
|
1089
|
+
};
|
|
1090
|
+
appendEvent(reopenEvent, pebbleDir);
|
|
1091
|
+
parentReopened = { id: parentId, title: parent.title };
|
|
1053
1092
|
}
|
|
1054
1093
|
}
|
|
1055
1094
|
let verifiesId;
|
|
@@ -1061,6 +1100,7 @@ function createCommand(program2) {
|
|
|
1061
1100
|
}
|
|
1062
1101
|
}
|
|
1063
1102
|
const blockedByIds = [];
|
|
1103
|
+
const blockersReopened = [];
|
|
1064
1104
|
if (options.blockedBy) {
|
|
1065
1105
|
const ids = options.blockedBy.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1066
1106
|
for (const rawId of ids) {
|
|
@@ -1070,7 +1110,14 @@ function createCommand(program2) {
|
|
|
1070
1110
|
throw new Error(`Blocker issue not found: ${rawId}`);
|
|
1071
1111
|
}
|
|
1072
1112
|
if (blocker.status === "closed") {
|
|
1073
|
-
|
|
1113
|
+
const reopenEvent = {
|
|
1114
|
+
type: "reopen",
|
|
1115
|
+
issueId: resolvedId,
|
|
1116
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1117
|
+
data: { reason: "Reopened to block new issue" }
|
|
1118
|
+
};
|
|
1119
|
+
appendEvent(reopenEvent, pebbleDir);
|
|
1120
|
+
blockersReopened.push({ id: resolvedId, title: blocker.title });
|
|
1074
1121
|
}
|
|
1075
1122
|
blockedByIds.push(resolvedId);
|
|
1076
1123
|
}
|
|
@@ -1103,6 +1150,15 @@ function createCommand(program2) {
|
|
|
1103
1150
|
}
|
|
1104
1151
|
};
|
|
1105
1152
|
appendEvent(event, pebbleDir);
|
|
1153
|
+
if (parentId) {
|
|
1154
|
+
const parentUpdateEvent = {
|
|
1155
|
+
type: "update",
|
|
1156
|
+
issueId: parentId,
|
|
1157
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1158
|
+
data: {}
|
|
1159
|
+
};
|
|
1160
|
+
appendEvent(parentUpdateEvent, pebbleDir);
|
|
1161
|
+
}
|
|
1106
1162
|
if (blockedByIds.length > 0) {
|
|
1107
1163
|
const depEvent = {
|
|
1108
1164
|
type: "update",
|
|
@@ -1123,7 +1179,8 @@ function createCommand(program2) {
|
|
|
1123
1179
|
};
|
|
1124
1180
|
appendEvent(depEvent, pebbleDir);
|
|
1125
1181
|
}
|
|
1126
|
-
|
|
1182
|
+
const extra = parentReopened || blockersReopened.length > 0 ? { parentReopened, blockersReopened: blockersReopened.length > 0 ? blockersReopened : void 0 } : void 0;
|
|
1183
|
+
outputMutationSuccess(id, pretty, extra);
|
|
1127
1184
|
} catch (error) {
|
|
1128
1185
|
outputError(error, pretty);
|
|
1129
1186
|
}
|
|
@@ -1166,6 +1223,7 @@ function updateCommand(program2) {
|
|
|
1166
1223
|
data.description = options.description;
|
|
1167
1224
|
hasChanges = true;
|
|
1168
1225
|
}
|
|
1226
|
+
let parentReopened;
|
|
1169
1227
|
if (options.parent !== void 0) {
|
|
1170
1228
|
if (options.parent.toLowerCase() === "null") {
|
|
1171
1229
|
data.parent = "";
|
|
@@ -1179,7 +1237,14 @@ function updateCommand(program2) {
|
|
|
1179
1237
|
throw new Error(`Verification issues cannot be parents`);
|
|
1180
1238
|
}
|
|
1181
1239
|
if (parentIssue.status === "closed") {
|
|
1182
|
-
|
|
1240
|
+
const reopenEvent = {
|
|
1241
|
+
type: "reopen",
|
|
1242
|
+
issueId: parentId,
|
|
1243
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1244
|
+
data: { reason: "Reopened to add child" }
|
|
1245
|
+
};
|
|
1246
|
+
appendEvent(reopenEvent, pebbleDir);
|
|
1247
|
+
parentReopened = { id: parentId, title: parentIssue.title };
|
|
1183
1248
|
}
|
|
1184
1249
|
data.parent = parentId;
|
|
1185
1250
|
}
|
|
@@ -1218,7 +1283,7 @@ function updateCommand(program2) {
|
|
|
1218
1283
|
if (allIds.length === 1) {
|
|
1219
1284
|
const result = results[0];
|
|
1220
1285
|
if (result.success) {
|
|
1221
|
-
outputMutationSuccess(result.id, pretty);
|
|
1286
|
+
outputMutationSuccess(result.id, pretty, parentReopened ? { parentReopened } : void 0);
|
|
1222
1287
|
} else {
|
|
1223
1288
|
throw new Error(result.error || "Unknown error");
|
|
1224
1289
|
}
|
|
@@ -1268,8 +1333,8 @@ function closeCommand(program2) {
|
|
|
1268
1333
|
results.push({ id: resolvedId, success: false, error: `Issue is already closed: ${resolvedId}` });
|
|
1269
1334
|
continue;
|
|
1270
1335
|
}
|
|
1271
|
-
if (
|
|
1272
|
-
results.push({ id: resolvedId, success: false, error: `Cannot close
|
|
1336
|
+
if (hasOpenChildren(resolvedId)) {
|
|
1337
|
+
results.push({ id: resolvedId, success: false, error: `Cannot close issue with open children: ${resolvedId}` });
|
|
1273
1338
|
continue;
|
|
1274
1339
|
}
|
|
1275
1340
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2714,6 +2779,7 @@ data: ${message}
|
|
|
2714
2779
|
res.status(400).json({ error: "Priority must be 0-4" });
|
|
2715
2780
|
return;
|
|
2716
2781
|
}
|
|
2782
|
+
let parentReopened;
|
|
2717
2783
|
if (parent) {
|
|
2718
2784
|
const parentIssue = getIssue(parent);
|
|
2719
2785
|
if (!parentIssue) {
|
|
@@ -2725,8 +2791,14 @@ data: ${message}
|
|
|
2725
2791
|
return;
|
|
2726
2792
|
}
|
|
2727
2793
|
if (parentIssue.status === "closed") {
|
|
2728
|
-
|
|
2729
|
-
|
|
2794
|
+
const reopenEvent = {
|
|
2795
|
+
type: "reopen",
|
|
2796
|
+
issueId: parent,
|
|
2797
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2798
|
+
data: { reason: "Reopened to add child" }
|
|
2799
|
+
};
|
|
2800
|
+
appendEvent(reopenEvent, pebbleDir);
|
|
2801
|
+
parentReopened = { id: parent, title: parentIssue.title };
|
|
2730
2802
|
}
|
|
2731
2803
|
}
|
|
2732
2804
|
const issueId = generateId(config.prefix);
|
|
@@ -2744,8 +2816,18 @@ data: ${message}
|
|
|
2744
2816
|
}
|
|
2745
2817
|
};
|
|
2746
2818
|
appendEvent(event, pebbleDir);
|
|
2819
|
+
if (parent) {
|
|
2820
|
+
const parentUpdateEvent = {
|
|
2821
|
+
type: "update",
|
|
2822
|
+
issueId: parent,
|
|
2823
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2824
|
+
data: {}
|
|
2825
|
+
};
|
|
2826
|
+
appendEvent(parentUpdateEvent, pebbleDir);
|
|
2827
|
+
}
|
|
2747
2828
|
const issue = getIssue(issueId);
|
|
2748
|
-
|
|
2829
|
+
const result = parentReopened ? { ...issue, _parentReopened: parentReopened } : issue;
|
|
2830
|
+
res.status(201).json(result);
|
|
2749
2831
|
} catch (error) {
|
|
2750
2832
|
res.status(500).json({ error: error.message });
|
|
2751
2833
|
}
|
|
@@ -2771,8 +2853,8 @@ data: ${message}
|
|
|
2771
2853
|
results.push({ id: issueId, success: true });
|
|
2772
2854
|
continue;
|
|
2773
2855
|
}
|
|
2774
|
-
if (
|
|
2775
|
-
results.push({ id: issueId, success: false, error: "Cannot close
|
|
2856
|
+
if (hasOpenChildren(issueId)) {
|
|
2857
|
+
results.push({ id: issueId, success: false, error: "Cannot close issue with open children" });
|
|
2776
2858
|
continue;
|
|
2777
2859
|
}
|
|
2778
2860
|
const pendingVerifications = getVerifications(issueId).filter((v) => v.status !== "closed");
|
|
@@ -3022,8 +3104,8 @@ data: ${message}
|
|
|
3022
3104
|
res.status(400).json({ error: "Issue is already closed" });
|
|
3023
3105
|
return;
|
|
3024
3106
|
}
|
|
3025
|
-
if (!isMultiWorktree() &&
|
|
3026
|
-
res.status(400).json({ error: "Cannot close
|
|
3107
|
+
if (!isMultiWorktree() && hasOpenChildren(issueId)) {
|
|
3108
|
+
res.status(400).json({ error: "Cannot close issue with open children" });
|
|
3027
3109
|
return;
|
|
3028
3110
|
}
|
|
3029
3111
|
const { reason } = req.body;
|
|
@@ -3836,6 +3918,21 @@ function formatInProgressPretty(issues) {
|
|
|
3836
3918
|
}
|
|
3837
3919
|
return lines.join("\n");
|
|
3838
3920
|
}
|
|
3921
|
+
function formatClosedIssuesPretty(issues) {
|
|
3922
|
+
if (issues.length === 0) return "";
|
|
3923
|
+
const lines = [];
|
|
3924
|
+
lines.push(`## Recently Closed (${issues.length})`);
|
|
3925
|
+
lines.push("");
|
|
3926
|
+
for (const issue of issues) {
|
|
3927
|
+
lines.push(`\u2713 ${issue.id}: ${issue.title} [${issue.type}]`);
|
|
3928
|
+
lines.push(` Closed: ${formatRelativeTime(issue.closedAt)}${issue.lastSource ? ` | Source: ${issue.lastSource}` : ""}`);
|
|
3929
|
+
if (issue.parent) {
|
|
3930
|
+
lines.push(` Parent: ${issue.parent.title}`);
|
|
3931
|
+
}
|
|
3932
|
+
lines.push("");
|
|
3933
|
+
}
|
|
3934
|
+
return lines.join("\n");
|
|
3935
|
+
}
|
|
3839
3936
|
function formatSummaryPretty(summaries, sectionHeader) {
|
|
3840
3937
|
if (summaries.length === 0) {
|
|
3841
3938
|
return "No epics found.";
|
|
@@ -3950,6 +4047,27 @@ function summaryCommand(program2) {
|
|
|
3950
4047
|
const limitedClosed = limit > 0 ? closedEpics.slice(0, limit) : closedEpics;
|
|
3951
4048
|
const openSummaries = limitedOpen.map(buildSummary);
|
|
3952
4049
|
const closedSummaries = limitedClosed.map(buildSummary);
|
|
4050
|
+
let closedIssues = getIssues({ status: "closed" });
|
|
4051
|
+
closedIssues.sort(
|
|
4052
|
+
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
4053
|
+
);
|
|
4054
|
+
closedIssues = closedIssues.slice(0, 20);
|
|
4055
|
+
const recentlyClosedSummaries = closedIssues.map((issue) => {
|
|
4056
|
+
const summary = {
|
|
4057
|
+
id: issue.id,
|
|
4058
|
+
title: issue.title,
|
|
4059
|
+
type: issue.type,
|
|
4060
|
+
closedAt: issue.updatedAt,
|
|
4061
|
+
lastSource: issue.lastSource
|
|
4062
|
+
};
|
|
4063
|
+
if (issue.parent) {
|
|
4064
|
+
const parentIssue = getIssue(issue.parent, true);
|
|
4065
|
+
if (parentIssue) {
|
|
4066
|
+
summary.parent = { id: parentIssue.id, title: parentIssue.title };
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
return summary;
|
|
4070
|
+
});
|
|
3953
4071
|
if (pretty) {
|
|
3954
4072
|
const output = [];
|
|
3955
4073
|
const inProgressOutput = formatInProgressPretty(inProgressSummaries);
|
|
@@ -3964,12 +4082,16 @@ function summaryCommand(program2) {
|
|
|
3964
4082
|
if (output.length > 0) output.push("");
|
|
3965
4083
|
output.push(formatSummaryPretty(closedSummaries, "Recently Closed Epics (last 72h)"));
|
|
3966
4084
|
}
|
|
4085
|
+
if (recentlyClosedSummaries.length > 0) {
|
|
4086
|
+
if (output.length > 0) output.push("");
|
|
4087
|
+
output.push(formatClosedIssuesPretty(recentlyClosedSummaries));
|
|
4088
|
+
}
|
|
3967
4089
|
if (output.length === 0) {
|
|
3968
4090
|
output.push("No issues in progress and no epics found.");
|
|
3969
4091
|
}
|
|
3970
4092
|
console.log(output.join("\n"));
|
|
3971
4093
|
} else {
|
|
3972
|
-
console.log(formatJson({ inProgress: inProgressSummaries, open: openSummaries, closed: closedSummaries }));
|
|
4094
|
+
console.log(formatJson({ inProgress: inProgressSummaries, open: openSummaries, closed: closedSummaries, recentlyClosed: recentlyClosedSummaries }));
|
|
3973
4095
|
}
|
|
3974
4096
|
} catch (error) {
|
|
3975
4097
|
outputError(error, pretty);
|