@markmdev/pebble 0.1.5 → 0.1.6

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
@@ -149,7 +149,7 @@ function getConfigPath(pebbleDir) {
149
149
  function getConfig(pebbleDir) {
150
150
  const configPath = getConfigPath(pebbleDir);
151
151
  if (!fs.existsSync(configPath)) {
152
- throw new Error("Config file not found. Initialize .pebble first.");
152
+ throw new Error("No .pebble directory found. Run 'pb init' to initialize.");
153
153
  }
154
154
  const content = fs.readFileSync(configPath, "utf-8");
155
155
  return JSON.parse(content);
@@ -211,7 +211,7 @@ function computeState(events) {
211
211
  issue.description = updateEvent.data.description;
212
212
  }
213
213
  if (updateEvent.data.parent !== void 0) {
214
- issue.parent = updateEvent.data.parent;
214
+ issue.parent = updateEvent.data.parent || void 0;
215
215
  }
216
216
  if (updateEvent.data.blockedBy !== void 0) {
217
217
  issue.blockedBy = updateEvent.data.blockedBy;
@@ -613,10 +613,48 @@ function outputError(error, pretty) {
613
613
  }
614
614
  process.exit(1);
615
615
  }
616
+ function formatIssueListVerbose(issues) {
617
+ if (issues.length === 0) {
618
+ return "No issues found.";
619
+ }
620
+ const lines = [];
621
+ for (const info of issues) {
622
+ const { issue, blocking, children, verifications, blockers } = info;
623
+ lines.push(`${issue.id} - ${issue.title}`);
624
+ lines.push("\u2500".repeat(60));
625
+ lines.push(` Type: ${formatType(issue.type)}`);
626
+ lines.push(` Priority: P${issue.priority}`);
627
+ lines.push(` Status: ${issue.status}`);
628
+ lines.push(` Parent: ${issue.parent || "-"}`);
629
+ lines.push(` Children: ${issue.type === "epic" ? children : "-"}`);
630
+ lines.push(` Blocking: ${blocking.length > 0 ? blocking.join(", ") : "[]"}`);
631
+ lines.push(` Verifications: ${verifications}`);
632
+ if (blockers && blockers.length > 0) {
633
+ lines.push(` Blocked by: ${blockers.join(", ")}`);
634
+ }
635
+ lines.push("");
636
+ }
637
+ lines.push(`Total: ${issues.length} issue(s)`);
638
+ return lines.join("\n");
639
+ }
640
+ function outputIssueListVerbose(issues, pretty) {
641
+ if (pretty) {
642
+ console.log(formatIssueListVerbose(issues));
643
+ } else {
644
+ const output = issues.map(({ issue, blocking, children, verifications, blockers }) => ({
645
+ ...issue,
646
+ blocking,
647
+ childrenCount: issue.type === "epic" ? children : void 0,
648
+ verificationsCount: verifications,
649
+ ...blockers && { openBlockers: blockers }
650
+ }));
651
+ console.log(formatJson(output));
652
+ }
653
+ }
616
654
 
617
655
  // src/cli/commands/create.ts
618
656
  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) => {
657
+ 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
658
  const pretty = program2.opts().pretty ?? false;
621
659
  try {
622
660
  let type = options.type;
@@ -657,6 +695,33 @@ function createCommand(program2) {
657
695
  throw new Error(`Target issue not found: ${options.verifies}`);
658
696
  }
659
697
  }
698
+ const blockedByIds = [];
699
+ if (options.blockedBy) {
700
+ const ids = options.blockedBy.split(",").map((s) => s.trim()).filter(Boolean);
701
+ for (const rawId of ids) {
702
+ const resolvedId = resolveId(rawId);
703
+ const blocker = getIssue(resolvedId);
704
+ if (!blocker) {
705
+ throw new Error(`Blocker issue not found: ${rawId}`);
706
+ }
707
+ if (blocker.status === "closed") {
708
+ throw new Error(`Cannot be blocked by closed issue: ${resolvedId}`);
709
+ }
710
+ blockedByIds.push(resolvedId);
711
+ }
712
+ }
713
+ const blocksIds = [];
714
+ if (options.blocks) {
715
+ const ids = options.blocks.split(",").map((s) => s.trim()).filter(Boolean);
716
+ for (const rawId of ids) {
717
+ const resolvedId = resolveId(rawId);
718
+ const blocked = getIssue(resolvedId);
719
+ if (!blocked) {
720
+ throw new Error(`Issue to block not found: ${rawId}`);
721
+ }
722
+ blocksIds.push(resolvedId);
723
+ }
724
+ }
660
725
  const id = generateId(config.prefix);
661
726
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
662
727
  const event = {
@@ -673,6 +738,26 @@ function createCommand(program2) {
673
738
  }
674
739
  };
675
740
  appendEvent(event, pebbleDir);
741
+ if (blockedByIds.length > 0) {
742
+ const depEvent = {
743
+ type: "update",
744
+ issueId: id,
745
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
746
+ data: { blockedBy: blockedByIds }
747
+ };
748
+ appendEvent(depEvent, pebbleDir);
749
+ }
750
+ for (const targetId of blocksIds) {
751
+ const target = getIssue(targetId);
752
+ const existingBlockers = target?.blockedBy || [];
753
+ const depEvent = {
754
+ type: "update",
755
+ issueId: targetId,
756
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
757
+ data: { blockedBy: [...existingBlockers, id] }
758
+ };
759
+ appendEvent(depEvent, pebbleDir);
760
+ }
676
761
  outputMutationSuccess(id, pretty);
677
762
  } catch (error) {
678
763
  outputError(error, pretty);
@@ -682,7 +767,7 @@ function createCommand(program2) {
682
767
 
683
768
  // src/cli/commands/update.ts
684
769
  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) => {
770
+ 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
771
  const pretty = program2.opts().pretty ?? false;
687
772
  try {
688
773
  const pebbleDir = getOrCreatePebbleDir();
@@ -716,8 +801,27 @@ function updateCommand(program2) {
716
801
  data.description = options.description;
717
802
  hasChanges = true;
718
803
  }
804
+ if (options.parent !== void 0) {
805
+ if (options.parent.toLowerCase() === "null") {
806
+ data.parent = "";
807
+ } else {
808
+ const parentId = resolveId(options.parent);
809
+ const parentIssue = getIssue(parentId);
810
+ if (!parentIssue) {
811
+ throw new Error(`Parent issue not found: ${options.parent}`);
812
+ }
813
+ if (parentIssue.type !== "epic") {
814
+ throw new Error(`Parent must be an epic. ${parentId} is a ${parentIssue.type}`);
815
+ }
816
+ if (parentIssue.status === "closed") {
817
+ throw new Error(`Cannot set parent to closed epic: ${parentId}`);
818
+ }
819
+ data.parent = parentId;
820
+ }
821
+ hasChanges = true;
822
+ }
719
823
  if (!hasChanges) {
720
- throw new Error("No changes specified. Use --status, --priority, --title, or --description");
824
+ throw new Error("No changes specified. Use --status, --priority, --title, --description, or --parent");
721
825
  }
722
826
  const results = [];
723
827
  for (const id of allIds) {
@@ -1041,12 +1145,22 @@ function showCommand(program2) {
1041
1145
 
1042
1146
  // src/cli/commands/ready.ts
1043
1147
  function readyCommand(program2) {
1044
- program2.command("ready").description("Show issues ready for work (no open blockers)").action(async () => {
1148
+ 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
1149
  const pretty = program2.opts().pretty ?? false;
1046
1150
  try {
1047
1151
  getOrCreatePebbleDir();
1048
1152
  const issues = getReady();
1049
- outputIssueList(issues, pretty);
1153
+ if (options.verbose) {
1154
+ const verboseIssues = issues.map((issue) => ({
1155
+ issue,
1156
+ blocking: getBlocking(issue.id).map((i) => i.id),
1157
+ children: getChildren(issue.id).length,
1158
+ verifications: getVerifications(issue.id).length
1159
+ }));
1160
+ outputIssueListVerbose(verboseIssues, pretty);
1161
+ } else {
1162
+ outputIssueList(issues, pretty);
1163
+ }
1050
1164
  } catch (error) {
1051
1165
  outputError(error, pretty);
1052
1166
  }
@@ -1055,12 +1169,27 @@ function readyCommand(program2) {
1055
1169
 
1056
1170
  // src/cli/commands/blocked.ts
1057
1171
  function blockedCommand(program2) {
1058
- program2.command("blocked").description("Show blocked issues (have open blockers)").action(async () => {
1172
+ 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
1173
  const pretty = program2.opts().pretty ?? false;
1060
1174
  try {
1061
1175
  getOrCreatePebbleDir();
1062
1176
  const issues = getBlocked();
1063
- outputIssueList(issues, pretty);
1177
+ if (options.verbose) {
1178
+ const verboseIssues = issues.map((issue) => {
1179
+ const allBlockers = getBlockers(issue.id);
1180
+ const openBlockers = allBlockers.filter((b) => b.status !== "closed").map((b) => b.id);
1181
+ return {
1182
+ issue,
1183
+ blocking: getBlocking(issue.id).map((i) => i.id),
1184
+ children: getChildren(issue.id).length,
1185
+ verifications: getVerifications(issue.id).length,
1186
+ blockers: openBlockers
1187
+ };
1188
+ });
1189
+ outputIssueListVerbose(verboseIssues, pretty);
1190
+ } else {
1191
+ outputIssueList(issues, pretty);
1192
+ }
1064
1193
  } catch (error) {
1065
1194
  outputError(error, pretty);
1066
1195
  }
@@ -2439,15 +2568,19 @@ function generateSuffix() {
2439
2568
  import * as fs4 from "fs";
2440
2569
  import * as path4 from "path";
2441
2570
  function mergeEvents(filePaths) {
2442
- const allEvents = [];
2571
+ const seen = /* @__PURE__ */ new Map();
2443
2572
  for (const filePath of filePaths) {
2444
2573
  const events = readEventsFromFile(filePath);
2445
2574
  for (const event of events) {
2446
- allEvents.push({ ...event, _source: filePath });
2575
+ const key = `${event.issueId}-${event.timestamp}-${event.type}`;
2576
+ if (!seen.has(key)) {
2577
+ seen.set(key, event);
2578
+ }
2447
2579
  }
2448
2580
  }
2581
+ const allEvents = Array.from(seen.values());
2449
2582
  allEvents.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
2450
- return allEvents.map(({ _source, ...event }) => event);
2583
+ return allEvents;
2451
2584
  }
2452
2585
  function mergeIssues(filePaths) {
2453
2586
  const merged = /* @__PURE__ */ new Map();
@@ -2808,6 +2941,29 @@ function verificationsCommand(program2) {
2808
2941
  });
2809
2942
  }
2810
2943
 
2944
+ // src/cli/commands/init.ts
2945
+ import * as path5 from "path";
2946
+ function initCommand(program2) {
2947
+ program2.command("init").description("Initialize a new .pebble directory in the current directory").option("--force", "Re-initialize even if .pebble already exists").action((options) => {
2948
+ const existing = discoverPebbleDir();
2949
+ if (existing && !options.force) {
2950
+ console.error(JSON.stringify({
2951
+ error: "Already initialized",
2952
+ path: existing,
2953
+ hint: "Use --force to re-initialize"
2954
+ }));
2955
+ process.exit(1);
2956
+ }
2957
+ const pebbleDir = ensurePebbleDir(process.cwd());
2958
+ console.log(JSON.stringify({
2959
+ initialized: true,
2960
+ path: pebbleDir,
2961
+ configPath: path5.join(pebbleDir, "config.json"),
2962
+ issuesPath: path5.join(pebbleDir, "issues.jsonl")
2963
+ }));
2964
+ });
2965
+ }
2966
+
2811
2967
  // src/cli/index.ts
2812
2968
  var program = new Command();
2813
2969
  program.name("pebble").description("A lightweight JSONL-based issue tracker").version("0.1.0");
@@ -2831,5 +2987,6 @@ summaryCommand(program);
2831
2987
  historyCommand(program);
2832
2988
  searchCommand(program);
2833
2989
  verificationsCommand(program);
2990
+ initCommand(program);
2834
2991
  program.parse();
2835
2992
  //# sourceMappingURL=index.js.map