@markmdev/pebble 0.1.0 → 0.1.1

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