@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 +24 -22
- package/dist/cli/index.js +416 -19
- package/dist/cli/index.js.map +1 -1
- package/dist/ui/assets/index-B8UDK8Q5.js +343 -0
- package/dist/ui/assets/index-DzR1Z7C2.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-D87QqSD9.js +0 -328
- 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
|
@@ -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
|
|
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
|
|
577
|
+
function outputIssueWithBlocking(issue, blocking, pretty) {
|
|
529
578
|
if (pretty) {
|
|
530
|
-
console.log(
|
|
579
|
+
console.log(formatIssuePrettyWithBlocking(issue, blocking));
|
|
531
580
|
} else {
|
|
532
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("--
|
|
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.
|
|
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
|