@markmdev/pebble 0.1.1 → 0.1.3

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
@@ -13,23 +13,25 @@ A lightweight, JSONL-based issue tracker with CLI and React UI.
13
13
  ## Installation
14
14
 
15
15
  ```bash
16
- npm install -g pebble
16
+ npm install -g @markmdev/pebble
17
17
  ```
18
18
 
19
+ After installation, the `pb` command is available globally.
20
+
19
21
  ## Quick Start
20
22
 
21
23
  ```bash
22
24
  # Create your first issue (auto-initializes .pebble/ directory)
23
- pebble create "Fix login bug" -t bug -p 1
25
+ pb create "Fix login bug" -t bug -p 1
24
26
 
25
27
  # List all issues
26
- pebble list
28
+ pb list
27
29
 
28
30
  # Show ready issues (no open blockers)
29
- pebble ready
31
+ pb ready
30
32
 
31
33
  # View in the browser
32
- pebble ui
34
+ pb ui
33
35
  ```
34
36
 
35
37
  ## Commands
@@ -38,37 +40,37 @@ pebble ui
38
40
 
39
41
  | Command | Description |
40
42
  |---------|-------------|
41
- | `pebble ready` | Issues with no open blockers |
42
- | `pebble blocked` | Issues with open blockers |
43
- | `pebble list [options]` | List issues with filters |
44
- | `pebble show <id>` | Full issue details |
43
+ | `pb ready` | Issues with no open blockers |
44
+ | `pb blocked` | Issues with open blockers |
45
+ | `pb list [options]` | List issues with filters |
46
+ | `pb show <id>` | Full issue details |
45
47
 
46
48
  ### Mutations
47
49
 
48
50
  | Command | Description |
49
51
  |---------|-------------|
50
- | `pebble create <title> [options]` | Create an issue |
51
- | `pebble update <ids...> [options]` | Update issues (supports batch) |
52
- | `pebble claim <ids...>` | Set status to in_progress (shorthand) |
53
- | `pebble close <ids...> [--reason] [--comment]` | Close issues (supports batch) |
54
- | `pebble reopen <id> [--reason]` | Reopen an issue |
52
+ | `pb create <title> [options]` | Create an issue |
53
+ | `pb update <ids...> [options]` | Update issues (supports batch) |
54
+ | `pb claim <ids...>` | Set status to in_progress (shorthand) |
55
+ | `pb close <ids...> [--reason] [--comment]` | Close issues (supports batch) |
56
+ | `pb reopen <id> [--reason]` | Reopen an issue |
55
57
 
56
58
  ### Dependencies
57
59
 
58
60
  | Command | Description |
59
61
  |---------|-------------|
60
- | `pebble dep add <id> <blocker>` | Add blocking dependency |
61
- | `pebble dep remove <id> <blocker>` | Remove dependency |
62
- | `pebble dep list <id>` | Show dependencies |
63
- | `pebble dep tree <id>` | Show dependency tree |
62
+ | `pb dep add <id> <blocker>` | Add blocking dependency |
63
+ | `pb dep remove <id> <blocker>` | Remove dependency |
64
+ | `pb dep list <id>` | Show dependencies |
65
+ | `pb dep tree <id>` | Show dependency tree |
64
66
 
65
67
  ### Comments & Visualization
66
68
 
67
69
  | Command | Description |
68
70
  |---------|-------------|
69
- | `pebble comments add <id> <text>` | Add a comment |
70
- | `pebble graph [--root id]` | Show dependency graph |
71
- | `pebble ui [--port 3333]` | Serve React UI |
71
+ | `pb comments add <id> <text>` | Add a comment |
72
+ | `pb graph [--root id]` | Show dependency graph |
73
+ | `pb ui [--port 3333]` | Serve React UI |
72
74
 
73
75
  ## Options
74
76
 
@@ -129,7 +131,7 @@ All data is stored in `.pebble/issues.jsonl` as append-only events:
129
131
 
130
132
  ## UI Features
131
133
 
132
- The React UI (`pebble ui`) provides full CRUD capabilities with real-time updates:
134
+ The React UI (`pb ui`) provides full CRUD capabilities with real-time updates:
133
135
 
134
136
  - **Issue List**: Hierarchical view (epics with children), sorting, filtering, search
135
137
  - **Create Issues**: "New Issue" button opens creation dialog
package/dist/cli/index.js CHANGED
@@ -4,9 +4,10 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/shared/types.ts
7
- var ISSUE_TYPES = ["task", "bug", "epic"];
7
+ var ISSUE_TYPES = ["task", "bug", "epic", "verification"];
8
8
  var PRIORITIES = [0, 1, 2, 3, 4];
9
9
  var STATUSES = ["open", "in_progress", "blocked", "closed"];
10
+ var EVENT_TYPES = ["create", "update", "close", "reopen", "comment"];
10
11
  var PRIORITY_LABELS = {
11
12
  0: "critical",
12
13
  1: "high",
@@ -23,7 +24,8 @@ var STATUS_LABELS = {
23
24
  var TYPE_LABELS = {
24
25
  task: "Task",
25
26
  bug: "Bug",
26
- epic: "Epic"
27
+ epic: "Epic",
28
+ verification: "Verification"
27
29
  };
28
30
 
29
31
  // src/cli/lib/storage.ts
@@ -175,6 +177,7 @@ function computeState(events) {
175
177
  description: createEvent.data.description,
176
178
  parent: createEvent.data.parent,
177
179
  blockedBy: [],
180
+ verifies: createEvent.data.verifies,
178
181
  comments: [],
179
182
  createdAt: event.timestamp,
180
183
  updatedAt: event.timestamp
@@ -315,6 +318,12 @@ function getReady() {
315
318
  return false;
316
319
  }
317
320
  }
321
+ if (issue.type === "verification" && issue.verifies) {
322
+ const target = state.get(issue.verifies);
323
+ if (!target || target.status !== "closed") {
324
+ return false;
325
+ }
326
+ }
318
327
  return true;
319
328
  });
320
329
  }
@@ -401,10 +410,46 @@ function getChildren(epicId) {
401
410
  const state = computeState(events);
402
411
  return Array.from(state.values()).filter((issue) => issue.parent === epicId);
403
412
  }
413
+ function getVerifications(issueId) {
414
+ const events = readEvents();
415
+ const state = computeState(events);
416
+ return Array.from(state.values()).filter((issue) => issue.verifies === issueId);
417
+ }
404
418
  function hasOpenChildren(epicId) {
405
419
  const children = getChildren(epicId);
406
420
  return children.some((child) => child.status !== "closed");
407
421
  }
422
+ function getNewlyUnblocked(closedIssueId) {
423
+ const events = readEvents();
424
+ const state = computeState(events);
425
+ const result = [];
426
+ for (const issue of state.values()) {
427
+ if (issue.status === "closed") continue;
428
+ let isUnblockedByThis = false;
429
+ if (issue.blockedBy.includes(closedIssueId)) {
430
+ const allBlockersClosed = issue.blockedBy.every((blockerId) => {
431
+ const blocker = state.get(blockerId);
432
+ return blocker?.status === "closed";
433
+ });
434
+ if (allBlockersClosed) {
435
+ isUnblockedByThis = true;
436
+ }
437
+ }
438
+ if (issue.type === "verification" && issue.verifies === closedIssueId) {
439
+ const allBlockersClosed = issue.blockedBy.every((blockerId) => {
440
+ const blocker = state.get(blockerId);
441
+ return blocker?.status === "closed";
442
+ });
443
+ if (allBlockersClosed) {
444
+ isUnblockedByThis = true;
445
+ }
446
+ }
447
+ if (isUnblockedByThis) {
448
+ result.push(issue);
449
+ }
450
+ }
451
+ return result;
452
+ }
408
453
 
409
454
  // src/cli/lib/output.ts
410
455
  function formatJson(data) {
@@ -426,7 +471,7 @@ function truncate(str, maxLength) {
426
471
  function pad(str, width) {
427
472
  return str.padEnd(width);
428
473
  }
429
- function formatIssuePretty(issue) {
474
+ function formatIssuePrettyWithBlocking(issue, blocking) {
430
475
  const lines = [];
431
476
  lines.push(`${issue.id} - ${issue.title}`);
432
477
  lines.push("\u2500".repeat(60));
@@ -445,6 +490,10 @@ function formatIssuePretty(issue) {
445
490
  lines.push("");
446
491
  lines.push(`Blocked by: ${issue.blockedBy.join(", ")}`);
447
492
  }
493
+ if (blocking.length > 0) {
494
+ lines.push("");
495
+ lines.push(`Blocking: ${blocking.map((i) => i.id).join(", ")}`);
496
+ }
448
497
  if (issue.comments.length > 0) {
449
498
  lines.push("");
450
499
  lines.push("Comments:");
@@ -525,11 +574,15 @@ function formatErrorPretty(error) {
525
574
  const message = error instanceof Error ? error.message : error;
526
575
  return `Error: ${message}`;
527
576
  }
528
- function outputIssue(issue, pretty) {
577
+ function outputIssueWithBlocking(issue, blocking, pretty) {
529
578
  if (pretty) {
530
- console.log(formatIssuePretty(issue));
579
+ console.log(formatIssuePrettyWithBlocking(issue, blocking));
531
580
  } else {
532
- console.log(formatJson(issue));
581
+ const output = {
582
+ ...issue,
583
+ blocking: blocking.map((i) => i.id)
584
+ };
585
+ console.log(formatJson(output));
533
586
  }
534
587
  }
535
588
  function outputMutationSuccess(id, pretty) {
@@ -557,13 +610,19 @@ function outputError(error, pretty) {
557
610
 
558
611
  // src/cli/commands/create.ts
559
612
  function createCommand(program2) {
560
- program2.command("create <title>").description("Create a new issue").option("-t, --type <type>", "Issue type (task, bug, epic)", "task").option("-p, --priority <priority>", "Priority (0-4)", "2").option("-d, --description <desc>", "Description").option("--parent <id>", "Parent epic ID").action(async (title, options) => {
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) => {
561
614
  const pretty = program2.opts().pretty ?? false;
562
615
  try {
563
- const type = options.type;
616
+ let type = options.type;
617
+ if (options.verifies && type !== "verification") {
618
+ type = "verification";
619
+ }
564
620
  if (!ISSUE_TYPES.includes(type)) {
565
621
  throw new Error(`Invalid type: ${type}. Must be one of: ${ISSUE_TYPES.join(", ")}`);
566
622
  }
623
+ if (type === "verification" && !options.verifies) {
624
+ throw new Error("Verification issues require --verifies <id> to specify the issue being verified");
625
+ }
567
626
  const priority = parseInt(options.priority, 10);
568
627
  if (!PRIORITIES.includes(priority)) {
569
628
  throw new Error(`Invalid priority: ${options.priority}. Must be 0-4`);
@@ -584,6 +643,14 @@ function createCommand(program2) {
584
643
  throw new Error(`Cannot add children to closed epic: ${parentId}`);
585
644
  }
586
645
  }
646
+ let verifiesId;
647
+ if (options.verifies) {
648
+ verifiesId = resolveId(options.verifies);
649
+ const target = getIssue(verifiesId);
650
+ if (!target) {
651
+ throw new Error(`Target issue not found: ${options.verifies}`);
652
+ }
653
+ }
587
654
  const id = generateId(config.prefix);
588
655
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
589
656
  const event = {
@@ -595,7 +662,8 @@ function createCommand(program2) {
595
662
  type,
596
663
  priority,
597
664
  description: options.description,
598
- parent: parentId
665
+ parent: parentId,
666
+ verifies: verifiesId
599
667
  }
600
668
  };
601
669
  appendEvent(event, pebbleDir);
@@ -745,7 +813,12 @@ function closeCommand(program2) {
745
813
  }
746
814
  };
747
815
  appendEvent(closeEvent, pebbleDir);
748
- results.push({ id: resolvedId, success: true });
816
+ const unblocked = getNewlyUnblocked(resolvedId);
817
+ results.push({
818
+ id: resolvedId,
819
+ success: true,
820
+ unblocked: unblocked.length > 0 ? unblocked.map((i) => ({ id: i.id, title: i.title })) : void 0
821
+ });
749
822
  } catch (error) {
750
823
  results.push({ id, success: false, error: error.message });
751
824
  }
@@ -753,7 +826,22 @@ function closeCommand(program2) {
753
826
  if (allIds.length === 1) {
754
827
  const result = results[0];
755
828
  if (result.success) {
756
- outputMutationSuccess(result.id, pretty);
829
+ if (pretty) {
830
+ console.log(`\u2713 ${result.id}`);
831
+ if (result.unblocked && result.unblocked.length > 0) {
832
+ console.log(`
833
+ Unblocked:`);
834
+ for (const u of result.unblocked) {
835
+ console.log(` \u2192 ${u.id} - ${u.title}`);
836
+ }
837
+ }
838
+ } else {
839
+ console.log(formatJson({
840
+ id: result.id,
841
+ success: true,
842
+ ...result.unblocked && { unblocked: result.unblocked }
843
+ }));
844
+ }
757
845
  } else {
758
846
  throw new Error(result.error || "Unknown error");
759
847
  }
@@ -762,6 +850,11 @@ function closeCommand(program2) {
762
850
  for (const result of results) {
763
851
  if (result.success) {
764
852
  console.log(`\u2713 ${result.id}`);
853
+ if (result.unblocked && result.unblocked.length > 0) {
854
+ for (const u of result.unblocked) {
855
+ console.log(` \u2192 ${u.id} - ${u.title}`);
856
+ }
857
+ }
765
858
  } else {
766
859
  console.log(`\u2717 ${result.id}: ${result.error}`);
767
860
  }
@@ -770,7 +863,8 @@ function closeCommand(program2) {
770
863
  console.log(formatJson(results.map((r) => ({
771
864
  id: r.id,
772
865
  success: r.success,
773
- ...r.error && { error: r.error }
866
+ ...r.error && { error: r.error },
867
+ ...r.unblocked && { unblocked: r.unblocked }
774
868
  }))));
775
869
  }
776
870
  }
@@ -931,7 +1025,8 @@ function showCommand(program2) {
931
1025
  if (!issue) {
932
1026
  throw new Error(`Issue not found: ${id}`);
933
1027
  }
934
- outputIssue(issue, pretty);
1028
+ const blocking = getBlocking(resolvedId);
1029
+ outputIssueWithBlocking(issue, blocking, pretty);
935
1030
  } catch (error) {
936
1031
  outputError(error, pretty);
937
1032
  }
@@ -2315,7 +2410,7 @@ function mergeIssues(filePaths) {
2315
2410
  }));
2316
2411
  }
2317
2412
  function mergeCommand(program2) {
2318
- program2.command("merge <files...>").description("Merge multiple issues.jsonl files into one").option("-o, --output <file>", "Output file (default: stdout)").option("--events", "Output raw events instead of computed state").option("--show-sources", "Include _sources field showing which files contained each issue").action((files, options) => {
2413
+ program2.command("merge <files...>").description("Merge multiple issues.jsonl files into one").option("-o, --output <file>", "Output file (default: stdout)").option("--state", "Output computed state instead of raw events").option("--show-sources", "Include _sources field (only with --state)").action((files, options) => {
2319
2414
  const pretty = program2.opts().pretty ?? false;
2320
2415
  const filePaths = [];
2321
2416
  for (const file of files) {
@@ -2332,13 +2427,13 @@ function mergeCommand(program2) {
2332
2427
  }
2333
2428
  try {
2334
2429
  let output;
2335
- if (options.events) {
2336
- const events = mergeEvents(filePaths);
2337
- output = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
2338
- } else {
2430
+ if (options.state) {
2339
2431
  const issues = mergeIssues(filePaths);
2340
2432
  const outputIssues = options.showSources ? issues : issues.map(({ _sources, ...issue }) => issue);
2341
2433
  output = pretty ? JSON.stringify(outputIssues, null, 2) : JSON.stringify(outputIssues);
2434
+ } else {
2435
+ const events = mergeEvents(filePaths);
2436
+ output = events.map((e) => JSON.stringify(e)).join("\n");
2342
2437
  }
2343
2438
  if (options.output) {
2344
2439
  fs4.writeFileSync(options.output, output + "\n", "utf-8");
@@ -2353,10 +2448,308 @@ function mergeCommand(program2) {
2353
2448
  });
2354
2449
  }
2355
2450
 
2451
+ // src/cli/commands/summary.ts
2452
+ function countChildren(epicId) {
2453
+ const children = getChildren(epicId);
2454
+ return {
2455
+ total: children.length,
2456
+ done: children.filter((c) => c.status === "closed").length,
2457
+ in_progress: children.filter((c) => c.status === "in_progress").length,
2458
+ open: children.filter((c) => c.status === "open").length,
2459
+ blocked: children.filter((c) => c.status === "blocked").length
2460
+ };
2461
+ }
2462
+ function formatSummaryPretty(summaries) {
2463
+ if (summaries.length === 0) {
2464
+ return "No epics found.";
2465
+ }
2466
+ const lines = [];
2467
+ for (const summary of summaries) {
2468
+ const { children } = summary;
2469
+ const progress = children.total > 0 ? `(${children.done}/${children.total} done)` : "(no children)";
2470
+ lines.push(`${summary.id} ${summary.title} ${progress}`);
2471
+ if (summary.parent) {
2472
+ lines.push(` Parent: ${summary.parent.id} "${summary.parent.title}"`);
2473
+ }
2474
+ if (summary.description) {
2475
+ const desc = summary.description.split("\n")[0];
2476
+ const truncated = desc.length > 60 ? desc.slice(0, 57) + "..." : desc;
2477
+ lines.push(` ${truncated}`);
2478
+ }
2479
+ }
2480
+ return lines.join("\n");
2481
+ }
2482
+ function summaryCommand(program2) {
2483
+ program2.command("summary").description("Show epic summary with child completion status").option("--status <status>", "Filter epics by status (default: open)").option("--limit <n>", "Max epics to return", "10").option("--include-closed", "Include closed epics").action(async (options) => {
2484
+ const pretty = program2.opts().pretty ?? false;
2485
+ try {
2486
+ getOrCreatePebbleDir();
2487
+ let epics = getIssues({ type: "epic" });
2488
+ if (options.includeClosed) {
2489
+ } else if (options.status !== void 0) {
2490
+ const status = options.status;
2491
+ if (!STATUSES.includes(status)) {
2492
+ throw new Error(`Invalid status: ${status}. Must be one of: ${STATUSES.join(", ")}`);
2493
+ }
2494
+ epics = epics.filter((e) => e.status === status);
2495
+ } else {
2496
+ epics = epics.filter((e) => e.status !== "closed");
2497
+ }
2498
+ epics.sort(
2499
+ (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
2500
+ );
2501
+ const limit = parseInt(options.limit, 10);
2502
+ if (limit > 0) {
2503
+ epics = epics.slice(0, limit);
2504
+ }
2505
+ const summaries = epics.map((epic) => {
2506
+ const summary = {
2507
+ id: epic.id,
2508
+ title: epic.title,
2509
+ description: epic.description,
2510
+ status: epic.status,
2511
+ children: countChildren(epic.id)
2512
+ };
2513
+ if (epic.parent) {
2514
+ const parentIssue = getIssue(epic.parent);
2515
+ if (parentIssue) {
2516
+ summary.parent = {
2517
+ id: parentIssue.id,
2518
+ title: parentIssue.title
2519
+ };
2520
+ }
2521
+ }
2522
+ return summary;
2523
+ });
2524
+ if (pretty) {
2525
+ console.log(formatSummaryPretty(summaries));
2526
+ } else {
2527
+ console.log(formatJson(summaries));
2528
+ }
2529
+ } catch (error) {
2530
+ outputError(error, pretty);
2531
+ }
2532
+ });
2533
+ }
2534
+
2535
+ // src/cli/commands/history.ts
2536
+ function parseDuration(duration) {
2537
+ const match = duration.match(/^(\d+)([dhms])$/);
2538
+ if (!match) {
2539
+ throw new Error(`Invalid duration: ${duration}. Use format like "7d", "24h", "30m", "60s"`);
2540
+ }
2541
+ const value = parseInt(match[1], 10);
2542
+ const unit = match[2];
2543
+ const multipliers = {
2544
+ s: 1e3,
2545
+ m: 60 * 1e3,
2546
+ h: 60 * 60 * 1e3,
2547
+ d: 24 * 60 * 60 * 1e3
2548
+ };
2549
+ return value * multipliers[unit];
2550
+ }
2551
+ function formatRelativeTime(timestamp) {
2552
+ const now = Date.now();
2553
+ const then = new Date(timestamp).getTime();
2554
+ const diff = now - then;
2555
+ const seconds = Math.floor(diff / 1e3);
2556
+ const minutes = Math.floor(seconds / 60);
2557
+ const hours = Math.floor(minutes / 60);
2558
+ const days = Math.floor(hours / 24);
2559
+ if (days > 0) return `${days}d ago`;
2560
+ if (hours > 0) return `${hours}h ago`;
2561
+ if (minutes > 0) return `${minutes}m ago`;
2562
+ return `${seconds}s ago`;
2563
+ }
2564
+ function formatHistoryPretty(entries) {
2565
+ if (entries.length === 0) {
2566
+ return "No events found.";
2567
+ }
2568
+ const lines = [];
2569
+ for (const entry of entries) {
2570
+ const time = formatRelativeTime(entry.timestamp);
2571
+ const eventLabel = entry.event.charAt(0).toUpperCase() + entry.event.slice(1);
2572
+ let line = `[${time}] ${eventLabel} ${entry.issue.id} "${entry.issue.title}" (${entry.issue.type})`;
2573
+ if (entry.parent) {
2574
+ line += ` under ${entry.parent.id}`;
2575
+ }
2576
+ lines.push(line);
2577
+ }
2578
+ return lines.join("\n");
2579
+ }
2580
+ function historyCommand(program2) {
2581
+ program2.command("history").description("Show recent activity log").option("--limit <n>", "Max events to return", "20").option("--type <types>", "Filter by event type(s), comma-separated (create,close,reopen,update,comment)").option("--since <duration>", 'Only show events since (e.g., "7d", "24h")').action(async (options) => {
2582
+ const pretty = program2.opts().pretty ?? false;
2583
+ try {
2584
+ getOrCreatePebbleDir();
2585
+ const events = readEvents();
2586
+ const state = computeState(events);
2587
+ let filteredEvents = events;
2588
+ if (options.type !== void 0) {
2589
+ const types = options.type.split(",").map((t) => t.trim());
2590
+ for (const t of types) {
2591
+ if (!EVENT_TYPES.includes(t)) {
2592
+ throw new Error(`Invalid event type: ${t}. Must be one of: ${EVENT_TYPES.join(", ")}`);
2593
+ }
2594
+ }
2595
+ filteredEvents = filteredEvents.filter((e) => types.includes(e.type));
2596
+ }
2597
+ if (options.since !== void 0) {
2598
+ const sinceMs = parseDuration(options.since);
2599
+ const cutoff = Date.now() - sinceMs;
2600
+ filteredEvents = filteredEvents.filter(
2601
+ (e) => new Date(e.timestamp).getTime() >= cutoff
2602
+ );
2603
+ }
2604
+ filteredEvents.sort(
2605
+ (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
2606
+ );
2607
+ const limit = parseInt(options.limit, 10);
2608
+ if (limit > 0) {
2609
+ filteredEvents = filteredEvents.slice(0, limit);
2610
+ }
2611
+ const entries = filteredEvents.map((event) => {
2612
+ const issue = state.get(event.issueId);
2613
+ const entry = {
2614
+ timestamp: event.timestamp,
2615
+ event: event.type,
2616
+ issue: {
2617
+ id: event.issueId,
2618
+ title: issue?.title ?? "(unknown)",
2619
+ type: issue?.type ?? "task"
2620
+ }
2621
+ };
2622
+ if (issue?.parent) {
2623
+ const parent = state.get(issue.parent);
2624
+ if (parent) {
2625
+ entry.parent = {
2626
+ id: parent.id,
2627
+ title: parent.title
2628
+ };
2629
+ }
2630
+ }
2631
+ if (event.type === "close" && event.data.reason) {
2632
+ entry.details = { reason: event.data.reason };
2633
+ } else if (event.type === "comment") {
2634
+ entry.details = { text: event.data.text };
2635
+ }
2636
+ return entry;
2637
+ });
2638
+ if (pretty) {
2639
+ console.log(formatHistoryPretty(entries));
2640
+ } else {
2641
+ console.log(formatJson(entries));
2642
+ }
2643
+ } catch (error) {
2644
+ outputError(error, pretty);
2645
+ }
2646
+ });
2647
+ }
2648
+
2649
+ // src/cli/commands/search.ts
2650
+ function searchIssues(issues, query) {
2651
+ const lowerQuery = query.toLowerCase();
2652
+ return issues.filter((issue) => {
2653
+ if (issue.id.toLowerCase().includes(lowerQuery)) {
2654
+ return true;
2655
+ }
2656
+ if (issue.title.toLowerCase().includes(lowerQuery)) {
2657
+ return true;
2658
+ }
2659
+ if (issue.description?.toLowerCase().includes(lowerQuery)) {
2660
+ return true;
2661
+ }
2662
+ if (issue.comments.some((c) => c.text.toLowerCase().includes(lowerQuery))) {
2663
+ return true;
2664
+ }
2665
+ return false;
2666
+ });
2667
+ }
2668
+ function searchCommand(program2) {
2669
+ program2.command("search <query>").description("Search issues by text in title, description, and comments").option("--status <status>", "Filter by status").option("-t, --type <type>", "Filter by type").option("--limit <n>", "Max results", "20").action(async (query, options) => {
2670
+ const pretty = program2.opts().pretty ?? false;
2671
+ try {
2672
+ getOrCreatePebbleDir();
2673
+ let issues = getIssues();
2674
+ if (options.status !== void 0) {
2675
+ const status = options.status;
2676
+ if (!STATUSES.includes(status)) {
2677
+ throw new Error(`Invalid status: ${status}. Must be one of: ${STATUSES.join(", ")}`);
2678
+ }
2679
+ issues = issues.filter((i) => i.status === status);
2680
+ }
2681
+ if (options.type !== void 0) {
2682
+ const type = options.type;
2683
+ if (!ISSUE_TYPES.includes(type)) {
2684
+ throw new Error(`Invalid type: ${type}. Must be one of: ${ISSUE_TYPES.join(", ")}`);
2685
+ }
2686
+ issues = issues.filter((i) => i.type === type);
2687
+ }
2688
+ let results = searchIssues(issues, query);
2689
+ const lowerQuery = query.toLowerCase();
2690
+ results.sort((a, b) => {
2691
+ const aInTitle = a.title.toLowerCase().includes(lowerQuery);
2692
+ const bInTitle = b.title.toLowerCase().includes(lowerQuery);
2693
+ if (aInTitle && !bInTitle) return -1;
2694
+ if (!aInTitle && bInTitle) return 1;
2695
+ return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
2696
+ });
2697
+ const limit = parseInt(options.limit, 10);
2698
+ if (limit > 0) {
2699
+ results = results.slice(0, limit);
2700
+ }
2701
+ outputIssueList(results, pretty);
2702
+ } catch (error) {
2703
+ outputError(error, pretty);
2704
+ }
2705
+ });
2706
+ }
2707
+
2708
+ // src/cli/commands/verifications.ts
2709
+ function verificationsCommand(program2) {
2710
+ program2.command("verifications <id>").description("List verification issues for a given issue").action(async (id) => {
2711
+ const pretty = program2.opts().pretty ?? false;
2712
+ try {
2713
+ getOrCreatePebbleDir();
2714
+ const resolvedId = resolveId(id);
2715
+ const issue = getIssue(resolvedId);
2716
+ if (!issue) {
2717
+ throw new Error(`Issue not found: ${id}`);
2718
+ }
2719
+ const verifications = getVerifications(resolvedId);
2720
+ if (pretty) {
2721
+ if (verifications.length === 0) {
2722
+ console.log(`No verifications for ${resolvedId}`);
2723
+ } else {
2724
+ console.log(`Verifications for ${resolvedId} "${issue.title}"`);
2725
+ console.log("\u2500".repeat(50));
2726
+ for (const v of verifications) {
2727
+ const status = v.status === "closed" ? "\u2713" : "\u25CB";
2728
+ console.log(` ${status} ${v.id} - ${v.title}`);
2729
+ }
2730
+ console.log("");
2731
+ console.log(`Total: ${verifications.length} verification(s)`);
2732
+ }
2733
+ } else {
2734
+ console.log(formatJson({
2735
+ issueId: resolvedId,
2736
+ verifications: verifications.map((v) => ({
2737
+ id: v.id,
2738
+ title: v.title,
2739
+ status: v.status
2740
+ }))
2741
+ }));
2742
+ }
2743
+ } catch (error) {
2744
+ outputError(error, pretty);
2745
+ }
2746
+ });
2747
+ }
2748
+
2356
2749
  // src/cli/index.ts
2357
2750
  var program = new Command();
2358
2751
  program.name("pebble").description("A lightweight JSONL-based issue tracker").version("0.1.0");
2359
- program.option("--pretty", "Human-readable output (default: JSON)");
2752
+ program.option("-P, --pretty", "Human-readable output (default: JSON)");
2360
2753
  createCommand(program);
2361
2754
  updateCommand(program);
2362
2755
  closeCommand(program);
@@ -2372,5 +2765,9 @@ graphCommand(program);
2372
2765
  uiCommand(program);
2373
2766
  importCommand(program);
2374
2767
  mergeCommand(program);
2768
+ summaryCommand(program);
2769
+ historyCommand(program);
2770
+ searchCommand(program);
2771
+ verificationsCommand(program);
2375
2772
  program.parse();
2376
2773
  //# sourceMappingURL=index.js.map