@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 epic ID
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 epic ID
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
- lines.push(`Status: ${formatStatus(issue.status)}`);
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: ${new Date(issue.createdAt).toLocaleString()}`);
768
- lines.push(`Updated: ${new Date(issue.updatedAt).toLocaleString()}`);
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
- console.log(`\u2713 ${id}`);
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
- console.log(JSON.stringify({ id, success: true }));
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} | Created: ${formatRelativeTime(issue.createdAt)}`);
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 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) => {
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 !== "epic") {
1049
- throw new Error(`Parent must be an epic, got: ${parent.type}`);
1078
+ if (parent.type === "verification") {
1079
+ throw new Error(`Verification issues cannot be parents`);
1050
1080
  }
1051
1081
  if (parent.status === "closed") {
1052
- throw new Error(`Cannot add children to closed epic: ${parentId}`);
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
- throw new Error(`Cannot be blocked by closed issue: ${resolvedId}`);
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
- outputMutationSuccess(id, pretty);
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 epic ID (use "null" to remove parent)').action(async (ids, options) => {
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 !== "epic") {
1179
- throw new Error(`Parent must be an epic. ${parentId} is a ${parentIssue.type}`);
1225
+ if (parentIssue.type === "verification") {
1226
+ throw new Error(`Verification issues cannot be parents`);
1180
1227
  }
1181
1228
  if (parentIssue.status === "closed") {
1182
- throw new Error(`Cannot set parent to closed epic: ${parentId}`);
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 !== "epic") {
2724
- res.status(400).json({ error: "Parent must be an epic" });
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
- res.status(400).json({ error: "Cannot add children to a closed epic" });
2729
- return;
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
- res.status(201).json(issue);
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 !== "epic") {
2935
- res.status(400).json({ error: "Parent must be an epic" });
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 !== "epic") {
2945
- res.status(400).json({ error: "Parent must be an epic" });
3006
+ if (parentIssue.type === "verification") {
3007
+ res.status(400).json({ error: "Verification issues cannot be parents" });
2946
3008
  return;
2947
3009
  }
2948
3010
  }