@markmdev/pebble 0.1.17 → 0.1.20
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/README.md
CHANGED
|
@@ -84,7 +84,7 @@ pb ui
|
|
|
84
84
|
- `-t, --type <type>` — Issue type: task, bug, epic (default: task)
|
|
85
85
|
- `-p, --priority <n>` — Priority: 0=critical, 4=backlog (default: 2)
|
|
86
86
|
- `-d, --description <text>` — Description
|
|
87
|
-
- `--parent <id>` — Parent
|
|
87
|
+
- `--parent <id>` — Parent issue ID
|
|
88
88
|
|
|
89
89
|
### List
|
|
90
90
|
|
|
@@ -112,7 +112,7 @@ pb ui
|
|
|
112
112
|
priority: 0-4; // 0=critical, 4=backlog
|
|
113
113
|
status: 'open' | 'in_progress' | 'blocked' | 'closed';
|
|
114
114
|
description?: string;
|
|
115
|
-
parent?: string; // Parent
|
|
115
|
+
parent?: string; // Parent issue ID
|
|
116
116
|
blockedBy: string[]; // IDs of blocking issues
|
|
117
117
|
comments: Comment[];
|
|
118
118
|
createdAt: string;
|
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
|
}
|
|
@@ -707,7 +714,8 @@ function formatIssueDetailPretty(issue, ctx) {
|
|
|
707
714
|
lines.push("\u2500".repeat(60));
|
|
708
715
|
lines.push(`Type: ${formatType(issue.type)}`);
|
|
709
716
|
lines.push(`Priority: ${formatPriority(issue.priority)}`);
|
|
710
|
-
|
|
717
|
+
const statusTime = issue.statusChangedAt ? ` (${formatRelativeTime(issue.statusChangedAt)})` : "";
|
|
718
|
+
lines.push(`Status: ${formatStatus(issue.status)}${statusTime}`);
|
|
711
719
|
if (ctx.ancestry && ctx.ancestry.length > 0) {
|
|
712
720
|
const chain = [...ctx.ancestry].reverse().map((a) => a.id).join(" > ");
|
|
713
721
|
lines.push(`Ancestry: ${chain}`);
|
|
@@ -764,8 +772,8 @@ function formatIssueDetailPretty(issue, ctx) {
|
|
|
764
772
|
}
|
|
765
773
|
}
|
|
766
774
|
lines.push("");
|
|
767
|
-
lines.push(`Created: ${
|
|
768
|
-
lines.push(`Updated: ${
|
|
775
|
+
lines.push(`Created: ${formatRelativeTime(issue.createdAt)}`);
|
|
776
|
+
lines.push(`Updated: ${formatRelativeTime(issue.updatedAt)}`);
|
|
769
777
|
return lines.join("\n");
|
|
770
778
|
}
|
|
771
779
|
function outputIssueDetail(issue, ctx, pretty) {
|
|
@@ -859,11 +867,30 @@ function formatErrorPretty(error) {
|
|
|
859
867
|
const message = error instanceof Error ? error.message : error;
|
|
860
868
|
return `Error: ${message}`;
|
|
861
869
|
}
|
|
862
|
-
function outputMutationSuccess(id, pretty) {
|
|
870
|
+
function outputMutationSuccess(id, pretty, extra) {
|
|
863
871
|
if (pretty) {
|
|
864
|
-
|
|
872
|
+
const notes = [];
|
|
873
|
+
if (extra?.parentReopened) {
|
|
874
|
+
notes.push(`parent ${extra.parentReopened.id} reopened`);
|
|
875
|
+
}
|
|
876
|
+
if (extra?.blockersReopened?.length) {
|
|
877
|
+
const ids = extra.blockersReopened.map((b) => b.id).join(", ");
|
|
878
|
+
notes.push(`blocker${extra.blockersReopened.length > 1 ? "s" : ""} ${ids} reopened`);
|
|
879
|
+
}
|
|
880
|
+
if (notes.length > 0) {
|
|
881
|
+
console.log(`\u2713 ${id} (${notes.join(", ")})`);
|
|
882
|
+
} else {
|
|
883
|
+
console.log(`\u2713 ${id}`);
|
|
884
|
+
}
|
|
865
885
|
} else {
|
|
866
|
-
|
|
886
|
+
const result = { id, success: true };
|
|
887
|
+
if (extra?.parentReopened) {
|
|
888
|
+
result._parentReopened = extra.parentReopened;
|
|
889
|
+
}
|
|
890
|
+
if (extra?.blockersReopened?.length) {
|
|
891
|
+
result._blockersReopened = extra.blockersReopened;
|
|
892
|
+
}
|
|
893
|
+
console.log(JSON.stringify(result));
|
|
867
894
|
}
|
|
868
895
|
}
|
|
869
896
|
function outputIssueList(issues, pretty, limitInfo) {
|
|
@@ -900,6 +927,7 @@ function buildIssueTree(issues) {
|
|
|
900
927
|
priority: issue.priority,
|
|
901
928
|
status: issue.status,
|
|
902
929
|
createdAt: issue.createdAt,
|
|
930
|
+
statusChangedAt: issue.statusChangedAt,
|
|
903
931
|
childrenCount: children.length,
|
|
904
932
|
...children.length > 0 && { children }
|
|
905
933
|
};
|
|
@@ -925,7 +953,7 @@ function formatIssueTreePretty(nodes, sectionHeader) {
|
|
|
925
953
|
const connector = isRoot ? "" : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
|
|
926
954
|
const statusIcon = node.status === "closed" ? "\u2713" : node.status === "in_progress" ? "\u25B6" : node.status === "pending_verification" ? "\u23F3" : "\u25CB";
|
|
927
955
|
const statusText = STATUS_LABELS[node.status].toLowerCase();
|
|
928
|
-
const relativeTime = formatRelativeTime(node.createdAt);
|
|
956
|
+
const relativeTime = node.statusChangedAt ? formatRelativeTime(node.statusChangedAt) : formatRelativeTime(node.createdAt);
|
|
929
957
|
lines.push(`${prefix}${connector}${statusIcon} ${node.id}: ${node.title} [${node.type}] P${node.priority} ${statusText} ${relativeTime}`);
|
|
930
958
|
const children = node.children ?? [];
|
|
931
959
|
const childPrefix = isRoot ? "" : prefix + (isLast ? " " : "\u2502 ");
|
|
@@ -975,8 +1003,9 @@ function formatIssueListVerbose(issues, sectionHeader) {
|
|
|
975
1003
|
}
|
|
976
1004
|
for (const info of issues) {
|
|
977
1005
|
const { issue, blocking, children, verifications, blockers, ancestry } = info;
|
|
1006
|
+
const statusTime = issue.statusChangedAt ? formatRelativeTime(issue.statusChangedAt) : formatRelativeTime(issue.createdAt);
|
|
978
1007
|
lines.push(`${issue.id}: ${issue.title}`);
|
|
979
|
-
lines.push(` Type: ${formatType(issue.type)} | Priority: P${issue.priority} |
|
|
1008
|
+
lines.push(` Type: ${formatType(issue.type)} | Priority: P${issue.priority} | Status: ${formatStatus(issue.status)} (${statusTime})`);
|
|
980
1009
|
if (ancestry.length > 0) {
|
|
981
1010
|
const chain = [...ancestry].reverse().map((a) => a.title).join(" \u2192 ");
|
|
982
1011
|
lines.push(` Ancestry: ${chain}`);
|
|
@@ -1019,7 +1048,7 @@ function outputIssueListVerbose(issues, pretty, sectionHeader, limitInfo) {
|
|
|
1019
1048
|
|
|
1020
1049
|
// src/cli/commands/create.ts
|
|
1021
1050
|
function createCommand(program2) {
|
|
1022
|
-
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
|
|
1051
|
+
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 issue 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) => {
|
|
1023
1052
|
const pretty = program2.opts().pretty ?? false;
|
|
1024
1053
|
try {
|
|
1025
1054
|
let type = options.type;
|
|
@@ -1039,17 +1068,25 @@ function createCommand(program2) {
|
|
|
1039
1068
|
const pebbleDir = getOrCreatePebbleDir();
|
|
1040
1069
|
const config = getConfig(pebbleDir);
|
|
1041
1070
|
let parentId;
|
|
1071
|
+
let parentReopened;
|
|
1042
1072
|
if (options.parent) {
|
|
1043
1073
|
parentId = resolveId(options.parent);
|
|
1044
1074
|
const parent = getIssue(parentId);
|
|
1045
1075
|
if (!parent) {
|
|
1046
1076
|
throw new Error(`Parent issue not found: ${options.parent}`);
|
|
1047
1077
|
}
|
|
1048
|
-
if (parent.type
|
|
1049
|
-
throw new Error(`
|
|
1078
|
+
if (parent.type === "verification") {
|
|
1079
|
+
throw new Error(`Verification issues cannot be parents`);
|
|
1050
1080
|
}
|
|
1051
1081
|
if (parent.status === "closed") {
|
|
1052
|
-
|
|
1082
|
+
const reopenEvent = {
|
|
1083
|
+
type: "reopen",
|
|
1084
|
+
issueId: parentId,
|
|
1085
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1086
|
+
data: { reason: "Reopened to add child" }
|
|
1087
|
+
};
|
|
1088
|
+
appendEvent(reopenEvent, pebbleDir);
|
|
1089
|
+
parentReopened = { id: parentId, title: parent.title };
|
|
1053
1090
|
}
|
|
1054
1091
|
}
|
|
1055
1092
|
let verifiesId;
|
|
@@ -1061,6 +1098,7 @@ function createCommand(program2) {
|
|
|
1061
1098
|
}
|
|
1062
1099
|
}
|
|
1063
1100
|
const blockedByIds = [];
|
|
1101
|
+
const blockersReopened = [];
|
|
1064
1102
|
if (options.blockedBy) {
|
|
1065
1103
|
const ids = options.blockedBy.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1066
1104
|
for (const rawId of ids) {
|
|
@@ -1070,7 +1108,14 @@ function createCommand(program2) {
|
|
|
1070
1108
|
throw new Error(`Blocker issue not found: ${rawId}`);
|
|
1071
1109
|
}
|
|
1072
1110
|
if (blocker.status === "closed") {
|
|
1073
|
-
|
|
1111
|
+
const reopenEvent = {
|
|
1112
|
+
type: "reopen",
|
|
1113
|
+
issueId: resolvedId,
|
|
1114
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1115
|
+
data: { reason: "Reopened to block new issue" }
|
|
1116
|
+
};
|
|
1117
|
+
appendEvent(reopenEvent, pebbleDir);
|
|
1118
|
+
blockersReopened.push({ id: resolvedId, title: blocker.title });
|
|
1074
1119
|
}
|
|
1075
1120
|
blockedByIds.push(resolvedId);
|
|
1076
1121
|
}
|
|
@@ -1123,7 +1168,8 @@ function createCommand(program2) {
|
|
|
1123
1168
|
};
|
|
1124
1169
|
appendEvent(depEvent, pebbleDir);
|
|
1125
1170
|
}
|
|
1126
|
-
|
|
1171
|
+
const extra = parentReopened || blockersReopened.length > 0 ? { parentReopened, blockersReopened: blockersReopened.length > 0 ? blockersReopened : void 0 } : void 0;
|
|
1172
|
+
outputMutationSuccess(id, pretty, extra);
|
|
1127
1173
|
} catch (error) {
|
|
1128
1174
|
outputError(error, pretty);
|
|
1129
1175
|
}
|
|
@@ -1132,7 +1178,7 @@ function createCommand(program2) {
|
|
|
1132
1178
|
|
|
1133
1179
|
// src/cli/commands/update.ts
|
|
1134
1180
|
function updateCommand(program2) {
|
|
1135
|
-
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
|
|
1181
|
+
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 issue ID (use "null" to remove parent)').action(async (ids, options) => {
|
|
1136
1182
|
const pretty = program2.opts().pretty ?? false;
|
|
1137
1183
|
try {
|
|
1138
1184
|
const pebbleDir = getOrCreatePebbleDir();
|
|
@@ -1166,6 +1212,7 @@ function updateCommand(program2) {
|
|
|
1166
1212
|
data.description = options.description;
|
|
1167
1213
|
hasChanges = true;
|
|
1168
1214
|
}
|
|
1215
|
+
let parentReopened;
|
|
1169
1216
|
if (options.parent !== void 0) {
|
|
1170
1217
|
if (options.parent.toLowerCase() === "null") {
|
|
1171
1218
|
data.parent = "";
|
|
@@ -1175,11 +1222,18 @@ function updateCommand(program2) {
|
|
|
1175
1222
|
if (!parentIssue) {
|
|
1176
1223
|
throw new Error(`Parent issue not found: ${options.parent}`);
|
|
1177
1224
|
}
|
|
1178
|
-
if (parentIssue.type
|
|
1179
|
-
throw new Error(`
|
|
1225
|
+
if (parentIssue.type === "verification") {
|
|
1226
|
+
throw new Error(`Verification issues cannot be parents`);
|
|
1180
1227
|
}
|
|
1181
1228
|
if (parentIssue.status === "closed") {
|
|
1182
|
-
|
|
1229
|
+
const reopenEvent = {
|
|
1230
|
+
type: "reopen",
|
|
1231
|
+
issueId: parentId,
|
|
1232
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1233
|
+
data: { reason: "Reopened to add child" }
|
|
1234
|
+
};
|
|
1235
|
+
appendEvent(reopenEvent, pebbleDir);
|
|
1236
|
+
parentReopened = { id: parentId, title: parentIssue.title };
|
|
1183
1237
|
}
|
|
1184
1238
|
data.parent = parentId;
|
|
1185
1239
|
}
|
|
@@ -1218,7 +1272,7 @@ function updateCommand(program2) {
|
|
|
1218
1272
|
if (allIds.length === 1) {
|
|
1219
1273
|
const result = results[0];
|
|
1220
1274
|
if (result.success) {
|
|
1221
|
-
outputMutationSuccess(result.id, pretty);
|
|
1275
|
+
outputMutationSuccess(result.id, pretty, parentReopened ? { parentReopened } : void 0);
|
|
1222
1276
|
} else {
|
|
1223
1277
|
throw new Error(result.error || "Unknown error");
|
|
1224
1278
|
}
|
|
@@ -2714,19 +2768,26 @@ data: ${message}
|
|
|
2714
2768
|
res.status(400).json({ error: "Priority must be 0-4" });
|
|
2715
2769
|
return;
|
|
2716
2770
|
}
|
|
2771
|
+
let parentReopened;
|
|
2717
2772
|
if (parent) {
|
|
2718
2773
|
const parentIssue = getIssue(parent);
|
|
2719
2774
|
if (!parentIssue) {
|
|
2720
2775
|
res.status(400).json({ error: `Parent issue not found: ${parent}` });
|
|
2721
2776
|
return;
|
|
2722
2777
|
}
|
|
2723
|
-
if (parentIssue.type
|
|
2724
|
-
res.status(400).json({ error: "
|
|
2778
|
+
if (parentIssue.type === "verification") {
|
|
2779
|
+
res.status(400).json({ error: "Verification issues cannot be parents" });
|
|
2725
2780
|
return;
|
|
2726
2781
|
}
|
|
2727
2782
|
if (parentIssue.status === "closed") {
|
|
2728
|
-
|
|
2729
|
-
|
|
2783
|
+
const reopenEvent = {
|
|
2784
|
+
type: "reopen",
|
|
2785
|
+
issueId: parent,
|
|
2786
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2787
|
+
data: { reason: "Reopened to add child" }
|
|
2788
|
+
};
|
|
2789
|
+
appendEvent(reopenEvent, pebbleDir);
|
|
2790
|
+
parentReopened = { id: parent, title: parentIssue.title };
|
|
2730
2791
|
}
|
|
2731
2792
|
}
|
|
2732
2793
|
const issueId = generateId(config.prefix);
|
|
@@ -2745,7 +2806,8 @@ data: ${message}
|
|
|
2745
2806
|
};
|
|
2746
2807
|
appendEvent(event, pebbleDir);
|
|
2747
2808
|
const issue = getIssue(issueId);
|
|
2748
|
-
|
|
2809
|
+
const result = parentReopened ? { ...issue, _parentReopened: parentReopened } : issue;
|
|
2810
|
+
res.status(201).json(result);
|
|
2749
2811
|
} catch (error) {
|
|
2750
2812
|
res.status(500).json({ error: error.message });
|
|
2751
2813
|
}
|
|
@@ -2931,8 +2993,8 @@ data: ${message}
|
|
|
2931
2993
|
res.status(400).json({ error: `Parent issue not found: ${parent}` });
|
|
2932
2994
|
return;
|
|
2933
2995
|
}
|
|
2934
|
-
if (parentFound.issue.type
|
|
2935
|
-
res.status(400).json({ error: "
|
|
2996
|
+
if (parentFound.issue.type === "verification") {
|
|
2997
|
+
res.status(400).json({ error: "Verification issues cannot be parents" });
|
|
2936
2998
|
return;
|
|
2937
2999
|
}
|
|
2938
3000
|
} else {
|
|
@@ -2941,8 +3003,8 @@ data: ${message}
|
|
|
2941
3003
|
res.status(400).json({ error: `Parent issue not found: ${parent}` });
|
|
2942
3004
|
return;
|
|
2943
3005
|
}
|
|
2944
|
-
if (parentIssue.type
|
|
2945
|
-
res.status(400).json({ error: "
|
|
3006
|
+
if (parentIssue.type === "verification") {
|
|
3007
|
+
res.status(400).json({ error: "Verification issues cannot be parents" });
|
|
2946
3008
|
return;
|
|
2947
3009
|
}
|
|
2948
3010
|
}
|