@markmdev/pebble 0.1.4 → 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
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
2
8
 
3
9
  // src/cli/index.ts
4
10
  import { Command } from "commander";
@@ -143,7 +149,7 @@ function getConfigPath(pebbleDir) {
143
149
  function getConfig(pebbleDir) {
144
150
  const configPath = getConfigPath(pebbleDir);
145
151
  if (!fs.existsSync(configPath)) {
146
- throw new Error("Config file not found. Initialize .pebble first.");
152
+ throw new Error("No .pebble directory found. Run 'pb init' to initialize.");
147
153
  }
148
154
  const content = fs.readFileSync(configPath, "utf-8");
149
155
  return JSON.parse(content);
@@ -205,7 +211,7 @@ function computeState(events) {
205
211
  issue.description = updateEvent.data.description;
206
212
  }
207
213
  if (updateEvent.data.parent !== void 0) {
208
- issue.parent = updateEvent.data.parent;
214
+ issue.parent = updateEvent.data.parent || void 0;
209
215
  }
210
216
  if (updateEvent.data.blockedBy !== void 0) {
211
217
  issue.blockedBy = updateEvent.data.blockedBy;
@@ -607,10 +613,48 @@ function outputError(error, pretty) {
607
613
  }
608
614
  process.exit(1);
609
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
+ }
610
654
 
611
655
  // src/cli/commands/create.ts
612
656
  function createCommand(program2) {
613
- 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) => {
614
658
  const pretty = program2.opts().pretty ?? false;
615
659
  try {
616
660
  let type = options.type;
@@ -651,6 +695,33 @@ function createCommand(program2) {
651
695
  throw new Error(`Target issue not found: ${options.verifies}`);
652
696
  }
653
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
+ }
654
725
  const id = generateId(config.prefix);
655
726
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
656
727
  const event = {
@@ -667,6 +738,26 @@ function createCommand(program2) {
667
738
  }
668
739
  };
669
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
+ }
670
761
  outputMutationSuccess(id, pretty);
671
762
  } catch (error) {
672
763
  outputError(error, pretty);
@@ -676,7 +767,7 @@ function createCommand(program2) {
676
767
 
677
768
  // src/cli/commands/update.ts
678
769
  function updateCommand(program2) {
679
- 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) => {
680
771
  const pretty = program2.opts().pretty ?? false;
681
772
  try {
682
773
  const pebbleDir = getOrCreatePebbleDir();
@@ -710,8 +801,27 @@ function updateCommand(program2) {
710
801
  data.description = options.description;
711
802
  hasChanges = true;
712
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
+ }
713
823
  if (!hasChanges) {
714
- 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");
715
825
  }
716
826
  const results = [];
717
827
  for (const id of allIds) {
@@ -1035,12 +1145,22 @@ function showCommand(program2) {
1035
1145
 
1036
1146
  // src/cli/commands/ready.ts
1037
1147
  function readyCommand(program2) {
1038
- 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) => {
1039
1149
  const pretty = program2.opts().pretty ?? false;
1040
1150
  try {
1041
1151
  getOrCreatePebbleDir();
1042
1152
  const issues = getReady();
1043
- 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
+ }
1044
1164
  } catch (error) {
1045
1165
  outputError(error, pretty);
1046
1166
  }
@@ -1049,12 +1169,27 @@ function readyCommand(program2) {
1049
1169
 
1050
1170
  // src/cli/commands/blocked.ts
1051
1171
  function blockedCommand(program2) {
1052
- 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) => {
1053
1173
  const pretty = program2.opts().pretty ?? false;
1054
1174
  try {
1055
1175
  getOrCreatePebbleDir();
1056
1176
  const issues = getBlocked();
1057
- 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
+ }
1058
1193
  } catch (error) {
1059
1194
  outputError(error, pretty);
1060
1195
  }
@@ -1410,15 +1545,20 @@ async function findAvailablePort(startPort, maxAttempts = 10) {
1410
1545
  }
1411
1546
  throw new Error(`No available port found (tried ${startPort}-${startPort + maxAttempts - 1})`);
1412
1547
  }
1413
- function readEventsFromFiles(filePaths) {
1414
- const allEvents = [];
1548
+ function mergeEventsFromFiles(filePaths) {
1549
+ const merged = /* @__PURE__ */ new Map();
1415
1550
  for (const filePath of filePaths) {
1416
1551
  const events = readEventsFromFile(filePath);
1417
1552
  for (const event of events) {
1418
- allEvents.push({ ...event, _source: filePath });
1553
+ const key = `${event.issueId}-${event.timestamp}-${event.type}`;
1554
+ if (!merged.has(key)) {
1555
+ merged.set(key, event);
1556
+ }
1419
1557
  }
1420
1558
  }
1421
- return allEvents;
1559
+ return Array.from(merged.values()).sort(
1560
+ (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
1561
+ );
1422
1562
  }
1423
1563
  function mergeIssuesFromFiles(filePaths) {
1424
1564
  const merged = /* @__PURE__ */ new Map();
@@ -1541,6 +1681,57 @@ function uiCommand(program2) {
1541
1681
  res.status(500).json({ error: error.message });
1542
1682
  }
1543
1683
  });
1684
+ app.get("/api/worktrees", (_req, res) => {
1685
+ try {
1686
+ const { execSync } = __require("child_process");
1687
+ let worktreeOutput;
1688
+ try {
1689
+ worktreeOutput = execSync("git worktree list --porcelain", {
1690
+ encoding: "utf-8",
1691
+ cwd: process.cwd()
1692
+ });
1693
+ } catch {
1694
+ res.json({ worktrees: [] });
1695
+ return;
1696
+ }
1697
+ const worktrees = [];
1698
+ const blocks = worktreeOutput.trim().split("\n\n");
1699
+ for (const block of blocks) {
1700
+ const lines = block.split("\n");
1701
+ let worktreePath = "";
1702
+ let branch = null;
1703
+ for (const line of lines) {
1704
+ if (line.startsWith("worktree ")) {
1705
+ worktreePath = line.slice("worktree ".length);
1706
+ } else if (line.startsWith("branch ")) {
1707
+ branch = line.slice("branch ".length).replace("refs/heads/", "");
1708
+ }
1709
+ }
1710
+ if (worktreePath) {
1711
+ const issuesFile = path2.join(worktreePath, ".pebble", "issues.jsonl");
1712
+ const hasIssues = fs2.existsSync(issuesFile);
1713
+ const isActive = issueFiles.includes(issuesFile);
1714
+ let issueCount = 0;
1715
+ if (hasIssues) {
1716
+ const events = readEventsFromFile(issuesFile);
1717
+ const state = computeState(events);
1718
+ issueCount = state.size;
1719
+ }
1720
+ worktrees.push({
1721
+ path: worktreePath,
1722
+ branch,
1723
+ issuesFile: hasIssues ? issuesFile : null,
1724
+ hasIssues,
1725
+ isActive,
1726
+ issueCount
1727
+ });
1728
+ }
1729
+ }
1730
+ res.json({ worktrees });
1731
+ } catch (error) {
1732
+ res.status(500).json({ error: error.message });
1733
+ }
1734
+ });
1544
1735
  app.get("/api/issues", (_req, res) => {
1545
1736
  try {
1546
1737
  const issues = mergeIssuesFromFiles(issueFiles);
@@ -1551,7 +1742,7 @@ function uiCommand(program2) {
1551
1742
  });
1552
1743
  app.get("/api/events", (_req, res) => {
1553
1744
  try {
1554
- const events = readEventsFromFiles(issueFiles);
1745
+ const events = mergeEventsFromFiles(issueFiles);
1555
1746
  res.json(events);
1556
1747
  } catch (error) {
1557
1748
  res.status(500).json({ error: error.message });
@@ -2377,15 +2568,19 @@ function generateSuffix() {
2377
2568
  import * as fs4 from "fs";
2378
2569
  import * as path4 from "path";
2379
2570
  function mergeEvents(filePaths) {
2380
- const allEvents = [];
2571
+ const seen = /* @__PURE__ */ new Map();
2381
2572
  for (const filePath of filePaths) {
2382
2573
  const events = readEventsFromFile(filePath);
2383
2574
  for (const event of events) {
2384
- 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
+ }
2385
2579
  }
2386
2580
  }
2581
+ const allEvents = Array.from(seen.values());
2387
2582
  allEvents.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
2388
- return allEvents.map(({ _source, ...event }) => event);
2583
+ return allEvents;
2389
2584
  }
2390
2585
  function mergeIssues(filePaths) {
2391
2586
  const merged = /* @__PURE__ */ new Map();
@@ -2746,6 +2941,29 @@ function verificationsCommand(program2) {
2746
2941
  });
2747
2942
  }
2748
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
+
2749
2967
  // src/cli/index.ts
2750
2968
  var program = new Command();
2751
2969
  program.name("pebble").description("A lightweight JSONL-based issue tracker").version("0.1.0");
@@ -2769,5 +2987,6 @@ summaryCommand(program);
2769
2987
  historyCommand(program);
2770
2988
  searchCommand(program);
2771
2989
  verificationsCommand(program);
2990
+ initCommand(program);
2772
2991
  program.parse();
2773
2992
  //# sourceMappingURL=index.js.map