@markmdev/pebble 0.1.0 → 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 +503 -90
- package/dist/cli/index.js.map +1 -1
- package/dist/ui/assets/index-AlD6QW_g.js +333 -0
- package/dist/ui/assets/index-C5MfnA01.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +2 -2
- package/dist/ui/assets/index-CdQQtrFF.js +0 -328
- package/dist/ui/assets/index-ZZBUE9NI.css +0 -1
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",
|
|
@@ -886,6 +887,7 @@ function listCommand(program2) {
|
|
|
886
887
|
program2.command("list").description("List issues").option("--status <status>", "Filter by status").option("-t, --type <type>", "Filter by type").option("--priority <priority>", "Filter by priority").option("--parent <id>", "Filter by parent epic").action(async (options) => {
|
|
887
888
|
const pretty = program2.opts().pretty ?? false;
|
|
888
889
|
try {
|
|
890
|
+
getOrCreatePebbleDir();
|
|
889
891
|
const filters = {};
|
|
890
892
|
if (options.status !== void 0) {
|
|
891
893
|
const status = options.status;
|
|
@@ -924,6 +926,7 @@ function showCommand(program2) {
|
|
|
924
926
|
program2.command("show <id>").description("Show issue details").action(async (id) => {
|
|
925
927
|
const pretty = program2.opts().pretty ?? false;
|
|
926
928
|
try {
|
|
929
|
+
getOrCreatePebbleDir();
|
|
927
930
|
const resolvedId = resolveId(id);
|
|
928
931
|
const issue = getIssue(resolvedId);
|
|
929
932
|
if (!issue) {
|
|
@@ -941,6 +944,7 @@ function readyCommand(program2) {
|
|
|
941
944
|
program2.command("ready").description("Show issues ready for work (no open blockers)").action(async () => {
|
|
942
945
|
const pretty = program2.opts().pretty ?? false;
|
|
943
946
|
try {
|
|
947
|
+
getOrCreatePebbleDir();
|
|
944
948
|
const issues = getReady();
|
|
945
949
|
outputIssueList(issues, pretty);
|
|
946
950
|
} catch (error) {
|
|
@@ -954,6 +958,7 @@ function blockedCommand(program2) {
|
|
|
954
958
|
program2.command("blocked").description("Show blocked issues (have open blockers)").action(async () => {
|
|
955
959
|
const pretty = program2.opts().pretty ?? false;
|
|
956
960
|
try {
|
|
961
|
+
getOrCreatePebbleDir();
|
|
957
962
|
const issues = getBlocked();
|
|
958
963
|
outputIssueList(issues, pretty);
|
|
959
964
|
} catch (error) {
|
|
@@ -1157,6 +1162,7 @@ function graphCommand(program2) {
|
|
|
1157
1162
|
program2.command("graph").description("Show dependency graph").option("--root <id>", "Filter to subtree rooted at issue").action(async (options) => {
|
|
1158
1163
|
const pretty = program2.opts().pretty ?? false;
|
|
1159
1164
|
try {
|
|
1165
|
+
getOrCreatePebbleDir();
|
|
1160
1166
|
let issues;
|
|
1161
1167
|
if (options.root) {
|
|
1162
1168
|
const rootId = resolveId(options.root);
|
|
@@ -1342,6 +1348,27 @@ function mergeIssuesFromFiles(filePaths) {
|
|
|
1342
1348
|
_sources: Array.from(sources)
|
|
1343
1349
|
}));
|
|
1344
1350
|
}
|
|
1351
|
+
function findIssueInSources(issueId, filePaths) {
|
|
1352
|
+
const allIssues = mergeIssuesFromFiles(filePaths);
|
|
1353
|
+
let found = allIssues.find((i) => i.id === issueId);
|
|
1354
|
+
if (!found) {
|
|
1355
|
+
const matches = allIssues.filter((i) => i.id.startsWith(issueId));
|
|
1356
|
+
if (matches.length === 1) {
|
|
1357
|
+
found = matches[0];
|
|
1358
|
+
} else if (matches.length > 1) {
|
|
1359
|
+
return null;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
if (!found) {
|
|
1363
|
+
return null;
|
|
1364
|
+
}
|
|
1365
|
+
const targetFile = found._sources[0];
|
|
1366
|
+
return { issue: found, targetFile };
|
|
1367
|
+
}
|
|
1368
|
+
function appendEventToFile(event, filePath) {
|
|
1369
|
+
const line = JSON.stringify(event) + "\n";
|
|
1370
|
+
fs2.appendFileSync(filePath, line, "utf-8");
|
|
1371
|
+
}
|
|
1345
1372
|
function uiCommand(program2) {
|
|
1346
1373
|
const defaultPort = process.env.PEBBLE_UI_PORT || "3333";
|
|
1347
1374
|
program2.command("ui").description("Serve the React UI").option("--port <port>", "Port to serve on", defaultPort).option("--no-open", "Do not open browser automatically").option("--files <paths>", "Comma-separated paths to issues.jsonl files for multi-worktree view").action(async (options) => {
|
|
@@ -1422,26 +1449,16 @@ function uiCommand(program2) {
|
|
|
1422
1449
|
});
|
|
1423
1450
|
app.get("/api/issues", (_req, res) => {
|
|
1424
1451
|
try {
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
res.json(issues);
|
|
1428
|
-
} else {
|
|
1429
|
-
const issues = getIssues({});
|
|
1430
|
-
res.json(issues);
|
|
1431
|
-
}
|
|
1452
|
+
const issues = mergeIssuesFromFiles(issueFiles);
|
|
1453
|
+
res.json(issues);
|
|
1432
1454
|
} catch (error) {
|
|
1433
1455
|
res.status(500).json({ error: error.message });
|
|
1434
1456
|
}
|
|
1435
1457
|
});
|
|
1436
1458
|
app.get("/api/events", (_req, res) => {
|
|
1437
1459
|
try {
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
res.json(events);
|
|
1441
|
-
} else {
|
|
1442
|
-
const events = readEvents();
|
|
1443
|
-
res.json(events);
|
|
1444
|
-
}
|
|
1460
|
+
const events = readEventsFromFiles(issueFiles);
|
|
1461
|
+
res.json(events);
|
|
1445
1462
|
} catch (error) {
|
|
1446
1463
|
res.status(500).json({ error: error.message });
|
|
1447
1464
|
}
|
|
@@ -1653,12 +1670,28 @@ data: ${message}
|
|
|
1653
1670
|
});
|
|
1654
1671
|
app.put("/api/issues/:id", (req, res) => {
|
|
1655
1672
|
try {
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
if (
|
|
1660
|
-
|
|
1661
|
-
|
|
1673
|
+
let issue;
|
|
1674
|
+
let issueId;
|
|
1675
|
+
let targetFile;
|
|
1676
|
+
if (isMultiWorktree()) {
|
|
1677
|
+
const found = findIssueInSources(req.params.id, issueFiles);
|
|
1678
|
+
if (!found) {
|
|
1679
|
+
res.status(404).json({ error: `Issue not found: ${req.params.id}` });
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
issue = found.issue;
|
|
1683
|
+
issueId = issue.id;
|
|
1684
|
+
targetFile = found.targetFile;
|
|
1685
|
+
} else {
|
|
1686
|
+
const pebbleDir = getOrCreatePebbleDir();
|
|
1687
|
+
issueId = resolveId(req.params.id);
|
|
1688
|
+
const localIssue = getIssue(issueId);
|
|
1689
|
+
if (!localIssue) {
|
|
1690
|
+
res.status(404).json({ error: `Issue not found: ${req.params.id}` });
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
issue = localIssue;
|
|
1694
|
+
targetFile = path2.join(pebbleDir, "issues.jsonl");
|
|
1662
1695
|
}
|
|
1663
1696
|
const { title, type, priority, status, description, parent } = req.body;
|
|
1664
1697
|
const updates = {};
|
|
@@ -1695,14 +1728,26 @@ data: ${message}
|
|
|
1695
1728
|
}
|
|
1696
1729
|
if (parent !== void 0) {
|
|
1697
1730
|
if (parent !== null) {
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1731
|
+
if (isMultiWorktree()) {
|
|
1732
|
+
const parentFound = findIssueInSources(parent, issueFiles);
|
|
1733
|
+
if (!parentFound) {
|
|
1734
|
+
res.status(400).json({ error: `Parent issue not found: ${parent}` });
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
if (parentFound.issue.type !== "epic") {
|
|
1738
|
+
res.status(400).json({ error: "Parent must be an epic" });
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
} else {
|
|
1742
|
+
const parentIssue = getIssue(parent);
|
|
1743
|
+
if (!parentIssue) {
|
|
1744
|
+
res.status(400).json({ error: `Parent issue not found: ${parent}` });
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
if (parentIssue.type !== "epic") {
|
|
1748
|
+
res.status(400).json({ error: "Parent must be an epic" });
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1706
1751
|
}
|
|
1707
1752
|
}
|
|
1708
1753
|
updates.parent = parent;
|
|
@@ -1718,27 +1763,47 @@ data: ${message}
|
|
|
1718
1763
|
timestamp,
|
|
1719
1764
|
data: updates
|
|
1720
1765
|
};
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1766
|
+
appendEventToFile(event, targetFile);
|
|
1767
|
+
if (isMultiWorktree()) {
|
|
1768
|
+
const updated = findIssueInSources(issueId, issueFiles);
|
|
1769
|
+
res.json(updated?.issue || { ...issue, ...updates, updatedAt: timestamp });
|
|
1770
|
+
} else {
|
|
1771
|
+
res.json(getIssue(issueId));
|
|
1772
|
+
}
|
|
1724
1773
|
} catch (error) {
|
|
1725
1774
|
res.status(500).json({ error: error.message });
|
|
1726
1775
|
}
|
|
1727
1776
|
});
|
|
1728
1777
|
app.post("/api/issues/:id/close", (req, res) => {
|
|
1729
1778
|
try {
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
if (
|
|
1734
|
-
|
|
1735
|
-
|
|
1779
|
+
let issue;
|
|
1780
|
+
let issueId;
|
|
1781
|
+
let targetFile;
|
|
1782
|
+
if (isMultiWorktree()) {
|
|
1783
|
+
const found = findIssueInSources(req.params.id, issueFiles);
|
|
1784
|
+
if (!found) {
|
|
1785
|
+
res.status(404).json({ error: `Issue not found: ${req.params.id}` });
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
issue = found.issue;
|
|
1789
|
+
issueId = issue.id;
|
|
1790
|
+
targetFile = found.targetFile;
|
|
1791
|
+
} else {
|
|
1792
|
+
const pebbleDir = getOrCreatePebbleDir();
|
|
1793
|
+
issueId = resolveId(req.params.id);
|
|
1794
|
+
const localIssue = getIssue(issueId);
|
|
1795
|
+
if (!localIssue) {
|
|
1796
|
+
res.status(404).json({ error: `Issue not found: ${req.params.id}` });
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
issue = localIssue;
|
|
1800
|
+
targetFile = path2.join(pebbleDir, "issues.jsonl");
|
|
1736
1801
|
}
|
|
1737
1802
|
if (issue.status === "closed") {
|
|
1738
1803
|
res.status(400).json({ error: "Issue is already closed" });
|
|
1739
1804
|
return;
|
|
1740
1805
|
}
|
|
1741
|
-
if (issue.type === "epic" && hasOpenChildren(issueId)) {
|
|
1806
|
+
if (!isMultiWorktree() && issue.type === "epic" && hasOpenChildren(issueId)) {
|
|
1742
1807
|
res.status(400).json({ error: "Cannot close epic with open children" });
|
|
1743
1808
|
return;
|
|
1744
1809
|
}
|
|
@@ -1750,21 +1815,41 @@ data: ${message}
|
|
|
1750
1815
|
timestamp,
|
|
1751
1816
|
data: { reason }
|
|
1752
1817
|
};
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1818
|
+
appendEventToFile(event, targetFile);
|
|
1819
|
+
if (isMultiWorktree()) {
|
|
1820
|
+
const updated = findIssueInSources(issueId, issueFiles);
|
|
1821
|
+
res.json(updated?.issue || { ...issue, status: "closed", updatedAt: timestamp });
|
|
1822
|
+
} else {
|
|
1823
|
+
res.json(getIssue(issueId));
|
|
1824
|
+
}
|
|
1756
1825
|
} catch (error) {
|
|
1757
1826
|
res.status(500).json({ error: error.message });
|
|
1758
1827
|
}
|
|
1759
1828
|
});
|
|
1760
1829
|
app.post("/api/issues/:id/reopen", (req, res) => {
|
|
1761
1830
|
try {
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
if (
|
|
1766
|
-
|
|
1767
|
-
|
|
1831
|
+
let issue;
|
|
1832
|
+
let issueId;
|
|
1833
|
+
let targetFile;
|
|
1834
|
+
if (isMultiWorktree()) {
|
|
1835
|
+
const found = findIssueInSources(req.params.id, issueFiles);
|
|
1836
|
+
if (!found) {
|
|
1837
|
+
res.status(404).json({ error: `Issue not found: ${req.params.id}` });
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
issue = found.issue;
|
|
1841
|
+
issueId = issue.id;
|
|
1842
|
+
targetFile = found.targetFile;
|
|
1843
|
+
} else {
|
|
1844
|
+
const pebbleDir = getOrCreatePebbleDir();
|
|
1845
|
+
issueId = resolveId(req.params.id);
|
|
1846
|
+
const localIssue = getIssue(issueId);
|
|
1847
|
+
if (!localIssue) {
|
|
1848
|
+
res.status(404).json({ error: `Issue not found: ${req.params.id}` });
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
issue = localIssue;
|
|
1852
|
+
targetFile = path2.join(pebbleDir, "issues.jsonl");
|
|
1768
1853
|
}
|
|
1769
1854
|
if (issue.status !== "closed") {
|
|
1770
1855
|
res.status(400).json({ error: "Issue is not closed" });
|
|
@@ -1778,21 +1863,41 @@ data: ${message}
|
|
|
1778
1863
|
timestamp,
|
|
1779
1864
|
data: { reason }
|
|
1780
1865
|
};
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1866
|
+
appendEventToFile(event, targetFile);
|
|
1867
|
+
if (isMultiWorktree()) {
|
|
1868
|
+
const updated = findIssueInSources(issueId, issueFiles);
|
|
1869
|
+
res.json(updated?.issue || { ...issue, status: "open", updatedAt: timestamp });
|
|
1870
|
+
} else {
|
|
1871
|
+
res.json(getIssue(issueId));
|
|
1872
|
+
}
|
|
1784
1873
|
} catch (error) {
|
|
1785
1874
|
res.status(500).json({ error: error.message });
|
|
1786
1875
|
}
|
|
1787
1876
|
});
|
|
1788
1877
|
app.post("/api/issues/:id/comments", (req, res) => {
|
|
1789
1878
|
try {
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
if (
|
|
1794
|
-
|
|
1795
|
-
|
|
1879
|
+
let issue;
|
|
1880
|
+
let issueId;
|
|
1881
|
+
let targetFile;
|
|
1882
|
+
if (isMultiWorktree()) {
|
|
1883
|
+
const found = findIssueInSources(req.params.id, issueFiles);
|
|
1884
|
+
if (!found) {
|
|
1885
|
+
res.status(404).json({ error: `Issue not found: ${req.params.id}` });
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
issue = found.issue;
|
|
1889
|
+
issueId = issue.id;
|
|
1890
|
+
targetFile = found.targetFile;
|
|
1891
|
+
} else {
|
|
1892
|
+
const pebbleDir = getOrCreatePebbleDir();
|
|
1893
|
+
issueId = resolveId(req.params.id);
|
|
1894
|
+
const localIssue = getIssue(issueId);
|
|
1895
|
+
if (!localIssue) {
|
|
1896
|
+
res.status(404).json({ error: `Issue not found: ${req.params.id}` });
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
issue = localIssue;
|
|
1900
|
+
targetFile = path2.join(pebbleDir, "issues.jsonl");
|
|
1796
1901
|
}
|
|
1797
1902
|
const { text, author } = req.body;
|
|
1798
1903
|
if (!text || typeof text !== "string" || text.trim() === "") {
|
|
@@ -1810,38 +1915,68 @@ data: ${message}
|
|
|
1810
1915
|
author
|
|
1811
1916
|
}
|
|
1812
1917
|
};
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1918
|
+
appendEventToFile(event, targetFile);
|
|
1919
|
+
if (isMultiWorktree()) {
|
|
1920
|
+
const updated = findIssueInSources(issueId, issueFiles);
|
|
1921
|
+
res.json(updated?.issue || issue);
|
|
1922
|
+
} else {
|
|
1923
|
+
res.json(getIssue(issueId));
|
|
1924
|
+
}
|
|
1816
1925
|
} catch (error) {
|
|
1817
1926
|
res.status(500).json({ error: error.message });
|
|
1818
1927
|
}
|
|
1819
1928
|
});
|
|
1820
1929
|
app.post("/api/issues/:id/deps", (req, res) => {
|
|
1821
1930
|
try {
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
if (
|
|
1826
|
-
|
|
1827
|
-
|
|
1931
|
+
let issue;
|
|
1932
|
+
let issueId;
|
|
1933
|
+
let targetFile;
|
|
1934
|
+
if (isMultiWorktree()) {
|
|
1935
|
+
const found = findIssueInSources(req.params.id, issueFiles);
|
|
1936
|
+
if (!found) {
|
|
1937
|
+
res.status(404).json({ error: `Issue not found: ${req.params.id}` });
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
issue = found.issue;
|
|
1941
|
+
issueId = issue.id;
|
|
1942
|
+
targetFile = found.targetFile;
|
|
1943
|
+
} else {
|
|
1944
|
+
const pebbleDir = getOrCreatePebbleDir();
|
|
1945
|
+
issueId = resolveId(req.params.id);
|
|
1946
|
+
const localIssue = getIssue(issueId);
|
|
1947
|
+
if (!localIssue) {
|
|
1948
|
+
res.status(404).json({ error: `Issue not found: ${req.params.id}` });
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
issue = localIssue;
|
|
1952
|
+
targetFile = path2.join(pebbleDir, "issues.jsonl");
|
|
1828
1953
|
}
|
|
1829
1954
|
const { blockerId } = req.body;
|
|
1830
1955
|
if (!blockerId) {
|
|
1831
1956
|
res.status(400).json({ error: "blockerId is required" });
|
|
1832
1957
|
return;
|
|
1833
1958
|
}
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1959
|
+
let resolvedBlockerId;
|
|
1960
|
+
if (isMultiWorktree()) {
|
|
1961
|
+
const blockerFound = findIssueInSources(blockerId, issueFiles);
|
|
1962
|
+
if (!blockerFound) {
|
|
1963
|
+
res.status(404).json({ error: `Blocker issue not found: ${blockerId}` });
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
resolvedBlockerId = blockerFound.issue.id;
|
|
1967
|
+
} else {
|
|
1968
|
+
resolvedBlockerId = resolveId(blockerId);
|
|
1969
|
+
const blockerIssue = getIssue(resolvedBlockerId);
|
|
1970
|
+
if (!blockerIssue) {
|
|
1971
|
+
res.status(404).json({ error: `Blocker issue not found: ${blockerId}` });
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1839
1974
|
}
|
|
1840
1975
|
if (issue.blockedBy.includes(resolvedBlockerId)) {
|
|
1841
1976
|
res.status(400).json({ error: "Dependency already exists" });
|
|
1842
1977
|
return;
|
|
1843
1978
|
}
|
|
1844
|
-
if (detectCycle(issueId, resolvedBlockerId)) {
|
|
1979
|
+
if (!isMultiWorktree() && detectCycle(issueId, resolvedBlockerId)) {
|
|
1845
1980
|
res.status(400).json({ error: "Adding this dependency would create a cycle" });
|
|
1846
1981
|
return;
|
|
1847
1982
|
}
|
|
@@ -1854,39 +1989,74 @@ data: ${message}
|
|
|
1854
1989
|
blockedBy: [...issue.blockedBy, resolvedBlockerId]
|
|
1855
1990
|
}
|
|
1856
1991
|
};
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1992
|
+
appendEventToFile(event, targetFile);
|
|
1993
|
+
if (isMultiWorktree()) {
|
|
1994
|
+
const updated = findIssueInSources(issueId, issueFiles);
|
|
1995
|
+
res.json(updated?.issue || { ...issue, blockedBy: [...issue.blockedBy, resolvedBlockerId], updatedAt: timestamp });
|
|
1996
|
+
} else {
|
|
1997
|
+
res.json(getIssue(issueId));
|
|
1998
|
+
}
|
|
1860
1999
|
} catch (error) {
|
|
1861
2000
|
res.status(500).json({ error: error.message });
|
|
1862
2001
|
}
|
|
1863
2002
|
});
|
|
1864
2003
|
app.delete("/api/issues/:id/deps/:blockerId", (req, res) => {
|
|
1865
2004
|
try {
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
if (
|
|
1870
|
-
|
|
1871
|
-
|
|
2005
|
+
let issue;
|
|
2006
|
+
let issueId;
|
|
2007
|
+
let targetFile;
|
|
2008
|
+
if (isMultiWorktree()) {
|
|
2009
|
+
const found = findIssueInSources(req.params.id, issueFiles);
|
|
2010
|
+
if (!found) {
|
|
2011
|
+
res.status(404).json({ error: `Issue not found: ${req.params.id}` });
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
issue = found.issue;
|
|
2015
|
+
issueId = issue.id;
|
|
2016
|
+
targetFile = found.targetFile;
|
|
2017
|
+
} else {
|
|
2018
|
+
const pebbleDir = getOrCreatePebbleDir();
|
|
2019
|
+
issueId = resolveId(req.params.id);
|
|
2020
|
+
const localIssue = getIssue(issueId);
|
|
2021
|
+
if (!localIssue) {
|
|
2022
|
+
res.status(404).json({ error: `Issue not found: ${req.params.id}` });
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
issue = localIssue;
|
|
2026
|
+
targetFile = path2.join(pebbleDir, "issues.jsonl");
|
|
2027
|
+
}
|
|
2028
|
+
let resolvedBlockerId;
|
|
2029
|
+
if (isMultiWorktree()) {
|
|
2030
|
+
const blockerFound = findIssueInSources(req.params.blockerId, issueFiles);
|
|
2031
|
+
if (blockerFound) {
|
|
2032
|
+
resolvedBlockerId = blockerFound.issue.id;
|
|
2033
|
+
} else {
|
|
2034
|
+
resolvedBlockerId = req.params.blockerId;
|
|
2035
|
+
}
|
|
2036
|
+
} else {
|
|
2037
|
+
resolvedBlockerId = resolveId(req.params.blockerId);
|
|
1872
2038
|
}
|
|
1873
|
-
const resolvedBlockerId = resolveId(req.params.blockerId);
|
|
1874
2039
|
if (!issue.blockedBy.includes(resolvedBlockerId)) {
|
|
1875
2040
|
res.status(400).json({ error: "Dependency does not exist" });
|
|
1876
2041
|
return;
|
|
1877
2042
|
}
|
|
1878
2043
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2044
|
+
const newBlockedBy = issue.blockedBy.filter((id) => id !== resolvedBlockerId);
|
|
1879
2045
|
const event = {
|
|
1880
2046
|
type: "update",
|
|
1881
2047
|
issueId,
|
|
1882
2048
|
timestamp,
|
|
1883
2049
|
data: {
|
|
1884
|
-
blockedBy:
|
|
2050
|
+
blockedBy: newBlockedBy
|
|
1885
2051
|
}
|
|
1886
2052
|
};
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
2053
|
+
appendEventToFile(event, targetFile);
|
|
2054
|
+
if (isMultiWorktree()) {
|
|
2055
|
+
const updated = findIssueInSources(issueId, issueFiles);
|
|
2056
|
+
res.json(updated?.issue || { ...issue, blockedBy: newBlockedBy, updatedAt: timestamp });
|
|
2057
|
+
} else {
|
|
2058
|
+
res.json(getIssue(issueId));
|
|
2059
|
+
}
|
|
1890
2060
|
} catch (error) {
|
|
1891
2061
|
res.status(500).json({ error: error.message });
|
|
1892
2062
|
}
|
|
@@ -2146,7 +2316,7 @@ function mergeIssues(filePaths) {
|
|
|
2146
2316
|
}));
|
|
2147
2317
|
}
|
|
2148
2318
|
function mergeCommand(program2) {
|
|
2149
|
-
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) => {
|
|
2150
2320
|
const pretty = program2.opts().pretty ?? false;
|
|
2151
2321
|
const filePaths = [];
|
|
2152
2322
|
for (const file of files) {
|
|
@@ -2163,13 +2333,13 @@ function mergeCommand(program2) {
|
|
|
2163
2333
|
}
|
|
2164
2334
|
try {
|
|
2165
2335
|
let output;
|
|
2166
|
-
if (options.
|
|
2167
|
-
const events = mergeEvents(filePaths);
|
|
2168
|
-
output = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
2169
|
-
} else {
|
|
2336
|
+
if (options.state) {
|
|
2170
2337
|
const issues = mergeIssues(filePaths);
|
|
2171
2338
|
const outputIssues = options.showSources ? issues : issues.map(({ _sources, ...issue }) => issue);
|
|
2172
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");
|
|
2173
2343
|
}
|
|
2174
2344
|
if (options.output) {
|
|
2175
2345
|
fs4.writeFileSync(options.output, output + "\n", "utf-8");
|
|
@@ -2184,6 +2354,246 @@ function mergeCommand(program2) {
|
|
|
2184
2354
|
});
|
|
2185
2355
|
}
|
|
2186
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
|
+
|
|
2187
2597
|
// src/cli/index.ts
|
|
2188
2598
|
var program = new Command();
|
|
2189
2599
|
program.name("pebble").description("A lightweight JSONL-based issue tracker").version("0.1.0");
|
|
@@ -2203,5 +2613,8 @@ graphCommand(program);
|
|
|
2203
2613
|
uiCommand(program);
|
|
2204
2614
|
importCommand(program);
|
|
2205
2615
|
mergeCommand(program);
|
|
2616
|
+
summaryCommand(program);
|
|
2617
|
+
historyCommand(program);
|
|
2618
|
+
searchCommand(program);
|
|
2206
2619
|
program.parse();
|
|
2207
2620
|
//# sourceMappingURL=index.js.map
|