@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 +24 -22
- package/dist/cli/index.js +249 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/ui/assets/{index-D87QqSD9.js → index-AlD6QW_g.js} +94 -89
- package/dist/ui/assets/index-C5MfnA01.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-ZZBUE9NI.css +0 -1
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
|
-
|
|
25
|
+
pb create "Fix login bug" -t bug -p 1
|
|
24
26
|
|
|
25
27
|
# List all issues
|
|
26
|
-
|
|
28
|
+
pb list
|
|
27
29
|
|
|
28
30
|
# Show ready issues (no open blockers)
|
|
29
|
-
|
|
31
|
+
pb ready
|
|
30
32
|
|
|
31
33
|
# View in the browser
|
|
32
|
-
|
|
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
|
-
| `
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
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
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
54
|
-
| `
|
|
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
|
-
| `
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
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
|
-
| `
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
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 (`
|
|
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("--
|
|
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.
|
|
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
|