@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 +236 -17
- package/dist/cli/index.js.map +1 -1
- package/dist/ui/assets/index-B9ZwNZIm.js +317 -0
- package/dist/ui/assets/index-C8VlXm0M.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-BBrKbKcz.css +0 -1
- package/dist/ui/assets/index-Bk8EwO3i.js +0 -343
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("
|
|
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 --
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1414
|
-
const
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|