@markmdev/pebble 0.1.1 → 0.1.2

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
@@ -7,6 +7,7 @@ import { Command } from "commander";
7
7
  var ISSUE_TYPES = ["task", "bug", "epic"];
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",
@@ -2315,7 +2316,7 @@ function mergeIssues(filePaths) {
2315
2316
  }));
2316
2317
  }
2317
2318
  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) => {
2319
+ 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
2320
  const pretty = program2.opts().pretty ?? false;
2320
2321
  const filePaths = [];
2321
2322
  for (const file of files) {
@@ -2332,13 +2333,13 @@ function mergeCommand(program2) {
2332
2333
  }
2333
2334
  try {
2334
2335
  let output;
2335
- if (options.events) {
2336
- const events = mergeEvents(filePaths);
2337
- output = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
2338
- } else {
2336
+ if (options.state) {
2339
2337
  const issues = mergeIssues(filePaths);
2340
2338
  const outputIssues = options.showSources ? issues : issues.map(({ _sources, ...issue }) => issue);
2341
2339
  output = pretty ? JSON.stringify(outputIssues, null, 2) : JSON.stringify(outputIssues);
2340
+ } else {
2341
+ const events = mergeEvents(filePaths);
2342
+ output = events.map((e) => JSON.stringify(e)).join("\n");
2342
2343
  }
2343
2344
  if (options.output) {
2344
2345
  fs4.writeFileSync(options.output, output + "\n", "utf-8");
@@ -2353,6 +2354,246 @@ function mergeCommand(program2) {
2353
2354
  });
2354
2355
  }
2355
2356
 
2357
+ // src/cli/commands/summary.ts
2358
+ function countChildren(epicId) {
2359
+ const children = getChildren(epicId);
2360
+ return {
2361
+ total: children.length,
2362
+ done: children.filter((c) => c.status === "closed").length,
2363
+ in_progress: children.filter((c) => c.status === "in_progress").length,
2364
+ open: children.filter((c) => c.status === "open").length,
2365
+ blocked: children.filter((c) => c.status === "blocked").length
2366
+ };
2367
+ }
2368
+ function formatSummaryPretty(summaries) {
2369
+ if (summaries.length === 0) {
2370
+ return "No epics found.";
2371
+ }
2372
+ const lines = [];
2373
+ for (const summary of summaries) {
2374
+ const { children } = summary;
2375
+ const progress = children.total > 0 ? `(${children.done}/${children.total} done)` : "(no children)";
2376
+ lines.push(`${summary.id} ${summary.title} ${progress}`);
2377
+ if (summary.description) {
2378
+ const desc = summary.description.split("\n")[0];
2379
+ const truncated = desc.length > 60 ? desc.slice(0, 57) + "..." : desc;
2380
+ lines.push(` ${truncated}`);
2381
+ }
2382
+ }
2383
+ return lines.join("\n");
2384
+ }
2385
+ function summaryCommand(program2) {
2386
+ 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) => {
2387
+ const pretty = program2.opts().pretty ?? false;
2388
+ try {
2389
+ getOrCreatePebbleDir();
2390
+ let epics = getIssues({ type: "epic" });
2391
+ if (options.includeClosed) {
2392
+ } else if (options.status !== void 0) {
2393
+ const status = options.status;
2394
+ if (!STATUSES.includes(status)) {
2395
+ throw new Error(`Invalid status: ${status}. Must be one of: ${STATUSES.join(", ")}`);
2396
+ }
2397
+ epics = epics.filter((e) => e.status === status);
2398
+ } else {
2399
+ epics = epics.filter((e) => e.status !== "closed");
2400
+ }
2401
+ epics.sort(
2402
+ (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
2403
+ );
2404
+ const limit = parseInt(options.limit, 10);
2405
+ if (limit > 0) {
2406
+ epics = epics.slice(0, limit);
2407
+ }
2408
+ const summaries = epics.map((epic) => ({
2409
+ id: epic.id,
2410
+ title: epic.title,
2411
+ description: epic.description,
2412
+ status: epic.status,
2413
+ children: countChildren(epic.id)
2414
+ }));
2415
+ if (pretty) {
2416
+ console.log(formatSummaryPretty(summaries));
2417
+ } else {
2418
+ console.log(formatJson(summaries));
2419
+ }
2420
+ } catch (error) {
2421
+ outputError(error, pretty);
2422
+ }
2423
+ });
2424
+ }
2425
+
2426
+ // src/cli/commands/history.ts
2427
+ function parseDuration(duration) {
2428
+ const match = duration.match(/^(\d+)([dhms])$/);
2429
+ if (!match) {
2430
+ throw new Error(`Invalid duration: ${duration}. Use format like "7d", "24h", "30m", "60s"`);
2431
+ }
2432
+ const value = parseInt(match[1], 10);
2433
+ const unit = match[2];
2434
+ const multipliers = {
2435
+ s: 1e3,
2436
+ m: 60 * 1e3,
2437
+ h: 60 * 60 * 1e3,
2438
+ d: 24 * 60 * 60 * 1e3
2439
+ };
2440
+ return value * multipliers[unit];
2441
+ }
2442
+ function formatRelativeTime(timestamp) {
2443
+ const now = Date.now();
2444
+ const then = new Date(timestamp).getTime();
2445
+ const diff = now - then;
2446
+ const seconds = Math.floor(diff / 1e3);
2447
+ const minutes = Math.floor(seconds / 60);
2448
+ const hours = Math.floor(minutes / 60);
2449
+ const days = Math.floor(hours / 24);
2450
+ if (days > 0) return `${days}d ago`;
2451
+ if (hours > 0) return `${hours}h ago`;
2452
+ if (minutes > 0) return `${minutes}m ago`;
2453
+ return `${seconds}s ago`;
2454
+ }
2455
+ function formatHistoryPretty(entries) {
2456
+ if (entries.length === 0) {
2457
+ return "No events found.";
2458
+ }
2459
+ const lines = [];
2460
+ for (const entry of entries) {
2461
+ const time = formatRelativeTime(entry.timestamp);
2462
+ const eventLabel = entry.event.charAt(0).toUpperCase() + entry.event.slice(1);
2463
+ let line = `[${time}] ${eventLabel} ${entry.issue.id} "${entry.issue.title}" (${entry.issue.type})`;
2464
+ if (entry.parent) {
2465
+ line += ` under ${entry.parent.id}`;
2466
+ }
2467
+ lines.push(line);
2468
+ }
2469
+ return lines.join("\n");
2470
+ }
2471
+ function historyCommand(program2) {
2472
+ program2.command("history").description("Show recent activity log").option("--limit <n>", "Max events to return", "20").option("--type <type>", "Filter by event type (create/close/reopen/update/comment)").option("--since <duration>", 'Only show events since (e.g., "7d", "24h")').action(async (options) => {
2473
+ const pretty = program2.opts().pretty ?? false;
2474
+ try {
2475
+ getOrCreatePebbleDir();
2476
+ const events = readEvents();
2477
+ const state = computeState(events);
2478
+ let filteredEvents = events;
2479
+ if (options.type !== void 0) {
2480
+ const eventType = options.type;
2481
+ if (!EVENT_TYPES.includes(eventType)) {
2482
+ throw new Error(`Invalid event type: ${eventType}. Must be one of: ${EVENT_TYPES.join(", ")}`);
2483
+ }
2484
+ filteredEvents = filteredEvents.filter((e) => e.type === eventType);
2485
+ }
2486
+ if (options.since !== void 0) {
2487
+ const sinceMs = parseDuration(options.since);
2488
+ const cutoff = Date.now() - sinceMs;
2489
+ filteredEvents = filteredEvents.filter(
2490
+ (e) => new Date(e.timestamp).getTime() >= cutoff
2491
+ );
2492
+ }
2493
+ filteredEvents.sort(
2494
+ (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
2495
+ );
2496
+ const limit = parseInt(options.limit, 10);
2497
+ if (limit > 0) {
2498
+ filteredEvents = filteredEvents.slice(0, limit);
2499
+ }
2500
+ const entries = filteredEvents.map((event) => {
2501
+ const issue = state.get(event.issueId);
2502
+ const entry = {
2503
+ timestamp: event.timestamp,
2504
+ event: event.type,
2505
+ issue: {
2506
+ id: event.issueId,
2507
+ title: issue?.title ?? "(unknown)",
2508
+ type: issue?.type ?? "task"
2509
+ }
2510
+ };
2511
+ if (issue?.parent) {
2512
+ const parent = state.get(issue.parent);
2513
+ if (parent) {
2514
+ entry.parent = {
2515
+ id: parent.id,
2516
+ title: parent.title
2517
+ };
2518
+ }
2519
+ }
2520
+ if (event.type === "close" && event.data.reason) {
2521
+ entry.details = { reason: event.data.reason };
2522
+ } else if (event.type === "comment") {
2523
+ entry.details = { text: event.data.text };
2524
+ }
2525
+ return entry;
2526
+ });
2527
+ if (pretty) {
2528
+ console.log(formatHistoryPretty(entries));
2529
+ } else {
2530
+ console.log(formatJson(entries));
2531
+ }
2532
+ } catch (error) {
2533
+ outputError(error, pretty);
2534
+ }
2535
+ });
2536
+ }
2537
+
2538
+ // src/cli/commands/search.ts
2539
+ function searchIssues(issues, query) {
2540
+ const lowerQuery = query.toLowerCase();
2541
+ return issues.filter((issue) => {
2542
+ if (issue.id.toLowerCase().includes(lowerQuery)) {
2543
+ return true;
2544
+ }
2545
+ if (issue.title.toLowerCase().includes(lowerQuery)) {
2546
+ return true;
2547
+ }
2548
+ if (issue.description?.toLowerCase().includes(lowerQuery)) {
2549
+ return true;
2550
+ }
2551
+ if (issue.comments.some((c) => c.text.toLowerCase().includes(lowerQuery))) {
2552
+ return true;
2553
+ }
2554
+ return false;
2555
+ });
2556
+ }
2557
+ function searchCommand(program2) {
2558
+ 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) => {
2559
+ const pretty = program2.opts().pretty ?? false;
2560
+ try {
2561
+ getOrCreatePebbleDir();
2562
+ let issues = getIssues();
2563
+ if (options.status !== void 0) {
2564
+ const status = options.status;
2565
+ if (!STATUSES.includes(status)) {
2566
+ throw new Error(`Invalid status: ${status}. Must be one of: ${STATUSES.join(", ")}`);
2567
+ }
2568
+ issues = issues.filter((i) => i.status === status);
2569
+ }
2570
+ if (options.type !== void 0) {
2571
+ const type = options.type;
2572
+ if (!ISSUE_TYPES.includes(type)) {
2573
+ throw new Error(`Invalid type: ${type}. Must be one of: ${ISSUE_TYPES.join(", ")}`);
2574
+ }
2575
+ issues = issues.filter((i) => i.type === type);
2576
+ }
2577
+ let results = searchIssues(issues, query);
2578
+ const lowerQuery = query.toLowerCase();
2579
+ results.sort((a, b) => {
2580
+ const aInTitle = a.title.toLowerCase().includes(lowerQuery);
2581
+ const bInTitle = b.title.toLowerCase().includes(lowerQuery);
2582
+ if (aInTitle && !bInTitle) return -1;
2583
+ if (!aInTitle && bInTitle) return 1;
2584
+ return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
2585
+ });
2586
+ const limit = parseInt(options.limit, 10);
2587
+ if (limit > 0) {
2588
+ results = results.slice(0, limit);
2589
+ }
2590
+ outputIssueList(results, pretty);
2591
+ } catch (error) {
2592
+ outputError(error, pretty);
2593
+ }
2594
+ });
2595
+ }
2596
+
2356
2597
  // src/cli/index.ts
2357
2598
  var program = new Command();
2358
2599
  program.name("pebble").description("A lightweight JSONL-based issue tracker").version("0.1.0");
@@ -2372,5 +2613,8 @@ graphCommand(program);
2372
2613
  uiCommand(program);
2373
2614
  importCommand(program);
2374
2615
  mergeCommand(program);
2616
+ summaryCommand(program);
2617
+ historyCommand(program);
2618
+ searchCommand(program);
2375
2619
  program.parse();
2376
2620
  //# sourceMappingURL=index.js.map