pipe-kan 0.29.2 → 0.30.0

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/pipe-kan.js CHANGED
@@ -268,6 +268,14 @@ function issuesToBoard(raw, opts = {}) {
268
268
  const byStatus = new Map;
269
269
  const epics = [];
270
270
  const epicByKey = new Map;
271
+ for (const status of opts.statuses ?? []) {
272
+ const title = status.trim();
273
+ if (!title || byStatus.has(title))
274
+ continue;
275
+ const column = { id: title, title, cards: [] };
276
+ byStatus.set(title, column);
277
+ columns.push(column);
278
+ }
271
279
  function rememberEpic(epic) {
272
280
  const existing = epicByKey.get(epic.key);
273
281
  if (!existing) {
@@ -352,12 +360,24 @@ function parseFlags(flags) {
352
360
  let type = "";
353
361
  let raw = "";
354
362
  let projectsValue = "";
363
+ let plane = false;
364
+ let workspace = "";
355
365
  const statusEq = [];
356
366
  const statusNeq = [];
357
367
  for (let i = 0;i < list.length; i++) {
358
368
  const token = list[i];
359
369
  if (token === "--raw")
360
370
  continue;
371
+ if (token === "--plane") {
372
+ plane = true;
373
+ continue;
374
+ }
375
+ if (token === "--workspace" || token.startsWith("--workspace=")) {
376
+ const [value, next] = takeValue(list, i, "--workspace");
377
+ workspace = value.startsWith("=") ? value.slice(1) : value;
378
+ i = next;
379
+ continue;
380
+ }
361
381
  if (token.startsWith("-a")) {
362
382
  [assignee, i] = takeValue(list, i, "-a");
363
383
  continue;
@@ -392,8 +412,9 @@ function parseFlags(flags) {
392
412
  }
393
413
  }
394
414
  const projects = projectsValue.split(",").map((p) => p.trim().toUpperCase()).filter(Boolean);
415
+ const workspaceSlug = workspace.trim() || undefined;
395
416
  if (raw) {
396
- return { jql: raw, projects };
417
+ return { jql: raw, projects, plane, workspace: workspaceSlug };
397
418
  }
398
419
  const projectClause = projects.length ? `project in (${projects.map((p) => `"${p}"`).join(", ")})` : "";
399
420
  const clauses = [];
@@ -409,7 +430,10 @@ function parseFlags(flags) {
409
430
  clauses.push(`status="${status}"`);
410
431
  for (const status of statusNeq)
411
432
  clauses.push(`status!="${status}"`);
412
- return { jql: clauses.join(" AND "), projects };
433
+ return { jql: clauses.join(" AND "), projects, plane, workspace: workspaceSlug };
434
+ }
435
+ function wantsPlane(flags) {
436
+ return parseFlags(flags).plane;
413
437
  }
414
438
  function flagsToJql(flags) {
415
439
  return parseFlags(flags).jql;
@@ -1208,8 +1232,9 @@ function createApp(opts) {
1208
1232
  let hasChildrenCache = false;
1209
1233
  let childrenError;
1210
1234
  let targetEndFieldId = readTargetEndFieldMap(fieldMapPath).targetEnd ?? targetEndFieldIdFromJiraConfig(safeRead(jiraConfigPath) ?? "");
1235
+ let workflowStates;
1211
1236
  function boardOpts() {
1212
- return { targetEndFieldId };
1237
+ return { targetEndFieldId, ...workflowStates?.length ? { statuses: workflowStates } : {} };
1213
1238
  }
1214
1239
  function toBoard(raw) {
1215
1240
  return issuesToBoard(raw, boardOpts());
@@ -1352,6 +1377,14 @@ function createApp(opts) {
1352
1377
  flags = next;
1353
1378
  console.log("Refresh");
1354
1379
  try {
1380
+ if (cli.states) {
1381
+ try {
1382
+ workflowStates = await cli.states(flags);
1383
+ } catch (err) {
1384
+ const message = err instanceof Error ? err.message : String(err);
1385
+ console.log(`Refresh states failed; ${message}`);
1386
+ }
1387
+ }
1355
1388
  const issues = await cli.list(flags);
1356
1389
  const epics = await cli.listEpics(flags);
1357
1390
  const listedPayload = JSON.parse(issues);
@@ -1458,13 +1491,652 @@ function createApp(opts) {
1458
1491
  return app;
1459
1492
  }
1460
1493
 
1494
+ // src/plane.ts
1495
+ var DEFAULT_PLANE_HOST = "https://plane.tail48fe8.ts.net";
1496
+ var DEFAULT_PLANE_WORKSPACE = "personal";
1497
+ var STATE_GROUPS = ["backlog", "unstarted", "started", "completed", "cancelled"];
1498
+ var RETRY_LIMIT2 = 4;
1499
+ function planeHost(env = process.env) {
1500
+ return (env.PLANE_HOST ?? DEFAULT_PLANE_HOST).replace(/\/$/, "");
1501
+ }
1502
+ function planeWorkspace(flags = "", env = process.env) {
1503
+ return parseFlags(flags).workspace || env.PLANE_WORKSPACE || DEFAULT_PLANE_WORKSPACE;
1504
+ }
1505
+ function planeApiBase(host) {
1506
+ const trimmed = host.replace(/\/$/, "");
1507
+ if (trimmed.endsWith("/api/v1"))
1508
+ return trimmed;
1509
+ if (trimmed.endsWith("/api"))
1510
+ return `${trimmed}/v1`;
1511
+ return `${trimmed}/api/v1`;
1512
+ }
1513
+ function planeModuleKey(identifier, moduleId) {
1514
+ const hex = moduleId.replace(/-/g, "").slice(-8);
1515
+ const n = Number.parseInt(hex, 16);
1516
+ return `${identifier.toUpperCase()}M-${Number.isFinite(n) ? n : 0}`;
1517
+ }
1518
+ function planeWorkItemKey(identifier, sequenceId) {
1519
+ return `${identifier.toUpperCase()}-${sequenceId}`;
1520
+ }
1521
+ function titleCasePriority(value) {
1522
+ if (!value || value.toLowerCase() === "none")
1523
+ return;
1524
+ return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
1525
+ }
1526
+ function moduleStatusName(status) {
1527
+ switch ((status ?? "").toLowerCase()) {
1528
+ case "backlog":
1529
+ return "Backlog";
1530
+ case "unstarted":
1531
+ case "planned":
1532
+ return "Todo";
1533
+ case "started":
1534
+ case "in_progress":
1535
+ return "In Progress";
1536
+ case "paused":
1537
+ return "Paused";
1538
+ case "completed":
1539
+ return "Done";
1540
+ case "cancelled":
1541
+ case "canceled":
1542
+ return "Cancelled";
1543
+ default:
1544
+ return status?.trim() || "Todo";
1545
+ }
1546
+ }
1547
+ function orderStateNames(states) {
1548
+ const seen = new Set;
1549
+ const names = [];
1550
+ const sorted = [...states].sort((a, b) => {
1551
+ const groupA = STATE_GROUPS.indexOf((a.group ?? "").toLowerCase());
1552
+ const groupB = STATE_GROUPS.indexOf((b.group ?? "").toLowerCase());
1553
+ const group = (groupA === -1 ? 99 : groupA) - (groupB === -1 ? 99 : groupB);
1554
+ if (group)
1555
+ return group;
1556
+ return (a.sequence ?? 0) - (b.sequence ?? 0);
1557
+ });
1558
+ for (const state of sorted) {
1559
+ const name = state.name.trim();
1560
+ if (!name || seen.has(name))
1561
+ continue;
1562
+ seen.add(name);
1563
+ names.push(name);
1564
+ }
1565
+ return names;
1566
+ }
1567
+ function asRecord(value) {
1568
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
1569
+ }
1570
+ function asString(value) {
1571
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
1572
+ }
1573
+ function asId(value) {
1574
+ if (typeof value === "string" && value.trim())
1575
+ return value.trim();
1576
+ const row = asRecord(value);
1577
+ return asString(row?.id);
1578
+ }
1579
+ function named(value) {
1580
+ const row = asRecord(value);
1581
+ return asString(row?.name) ?? asString(row?.display_name) ?? asString(row?.displayName);
1582
+ }
1583
+ function stateNameOf(item, states) {
1584
+ const expanded = named(item.state);
1585
+ if (expanded)
1586
+ return expanded;
1587
+ const id = asId(item.state);
1588
+ if (id && states.get(id)?.name)
1589
+ return states.get(id).name;
1590
+ return;
1591
+ }
1592
+ function labelNamesOf(item, labels) {
1593
+ const raw = [
1594
+ ...Array.isArray(item.label_details) ? item.label_details : [],
1595
+ ...Array.isArray(item.labels) ? item.labels : []
1596
+ ];
1597
+ const names = new Set;
1598
+ for (const entry of raw) {
1599
+ const fromName = named(entry) ?? asString(entry);
1600
+ if (fromName && !labels.has(fromName)) {
1601
+ names.add(fromName);
1602
+ continue;
1603
+ }
1604
+ const id = asId(entry);
1605
+ const mapped = id ? labels.get(id)?.name : undefined;
1606
+ if (mapped)
1607
+ names.add(mapped);
1608
+ }
1609
+ return [...names];
1610
+ }
1611
+ function assigneeOf(item) {
1612
+ const raw = Array.isArray(item.assignee_details) ? item.assignee_details : Array.isArray(item.assignees) ? item.assignees : [];
1613
+ for (const entry of raw) {
1614
+ const name = named(entry) ?? asString(asRecord(entry)?.email) ?? asString(asRecord(entry)?.first_name);
1615
+ if (name)
1616
+ return name;
1617
+ }
1618
+ return;
1619
+ }
1620
+ function moduleIdsOf(item) {
1621
+ const ids = new Set;
1622
+ for (const entry of [
1623
+ ...Array.isArray(item.module_ids) ? item.module_ids : [],
1624
+ ...Array.isArray(item.modules) ? item.modules : [],
1625
+ item.module
1626
+ ]) {
1627
+ const id = asId(entry);
1628
+ if (id)
1629
+ ids.add(id);
1630
+ }
1631
+ return [...ids];
1632
+ }
1633
+ function descriptionOf(item) {
1634
+ return asString(item.description_stripped) ?? asString(item.description) ?? (asString(item.description_html) ? asString(item.description_html).replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim() : undefined);
1635
+ }
1636
+ function workItemToIssue(item, project, opts) {
1637
+ if (item.archived_at || item.is_draft)
1638
+ return;
1639
+ const sequence = Number(item.sequence_id);
1640
+ if (!Number.isFinite(sequence))
1641
+ return;
1642
+ const status = stateNameOf(item, opts.states);
1643
+ if (!status)
1644
+ return;
1645
+ const key = planeWorkItemKey(project.identifier, sequence);
1646
+ const priority = titleCasePriority(item.priority);
1647
+ const assignee = assigneeOf(item);
1648
+ const labels = labelNamesOf(item, opts.labels);
1649
+ const description = descriptionOf(item);
1650
+ const target = asString(item.target_date);
1651
+ return {
1652
+ key,
1653
+ fields: {
1654
+ summary: asString(item.name) ?? "",
1655
+ status: { name: status },
1656
+ ...priority ? { priority: { name: priority } } : {},
1657
+ ...assignee ? { assignee: { displayName: assignee } } : {},
1658
+ ...target ? { duedate: target, "Target End Date": target } : {},
1659
+ issuetype: { name: "Story" },
1660
+ issueType: { name: "Story" },
1661
+ ...opts.moduleKey ? { parent: { key: opts.moduleKey } } : {},
1662
+ ...labels.length ? { labels } : {},
1663
+ ...asString(item.created_at) ? { created: item.created_at } : {},
1664
+ ...description ? { description } : {}
1665
+ }
1666
+ };
1667
+ }
1668
+ function moduleToIssue(module, project) {
1669
+ const target = asString(module.target_date);
1670
+ return {
1671
+ key: planeModuleKey(project.identifier, module.id),
1672
+ fields: {
1673
+ summary: asString(module.name) ?? module.id,
1674
+ status: { name: moduleStatusName(module.status) },
1675
+ issuetype: { name: "Epic" },
1676
+ issueType: { name: "Epic" },
1677
+ ...target ? { duedate: target, "Target End Date": target } : {},
1678
+ ...asString(module.created_at) ? { created: module.created_at } : {}
1679
+ }
1680
+ };
1681
+ }
1682
+ function joinUrl(base, path) {
1683
+ if (path.startsWith("http://") || path.startsWith("https://"))
1684
+ return path;
1685
+ const suffix = path.startsWith("/") ? path : `/${path}`;
1686
+ return `${base}${suffix}`;
1687
+ }
1688
+ function queryPath(path, query) {
1689
+ const params = new URLSearchParams;
1690
+ for (const [key, value] of Object.entries(query)) {
1691
+ if (value)
1692
+ params.set(key, value);
1693
+ }
1694
+ const encoded = params.toString();
1695
+ if (!encoded)
1696
+ return path;
1697
+ return `${path}${path.includes("?") ? "&" : "?"}${encoded}`;
1698
+ }
1699
+ function errorMessage(status, body) {
1700
+ try {
1701
+ const parsed = JSON.parse(body);
1702
+ const detail = asString(parsed.detail) ?? asString(parsed.error) ?? asString(parsed.message) ?? (Array.isArray(parsed.non_field_errors) ? asString(parsed.non_field_errors[0]) : undefined);
1703
+ if (detail)
1704
+ return `Plane ${status}: ${detail}`;
1705
+ } catch {}
1706
+ const snippet = body.replace(/\s+/g, " ").trim().slice(0, 240);
1707
+ return snippet ? `Plane ${status}: ${snippet}` : `Plane ${status}`;
1708
+ }
1709
+ function htmlParagraph(text) {
1710
+ const escaped = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1711
+ return `<p>${escaped}</p>`;
1712
+ }
1713
+ function createPlaneCli(opts) {
1714
+ if (!opts.apiKey.trim()) {
1715
+ throw new Error("Plane mode needs PLANE_API_KEY");
1716
+ }
1717
+ const host = (opts.host ?? DEFAULT_PLANE_HOST).replace(/\/$/, "");
1718
+ const apiBase = planeApiBase(host);
1719
+ const defaultFlags = opts.flags ?? DEFAULT_FLAGS;
1720
+ const fetchFn = opts.fetch ?? fetch;
1721
+ const retryDelayMs = opts.retryDelayMs ?? 1000;
1722
+ let lastFlags = defaultFlags;
1723
+ let workItemResource = "work-items";
1724
+ let catalog;
1725
+ function workspaceOf(flags) {
1726
+ return planeWorkspace(flags || defaultFlags);
1727
+ }
1728
+ async function request(path, init = {}, attempt = 0) {
1729
+ const headers = new Headers(init.headers);
1730
+ headers.set("X-API-Key", opts.apiKey);
1731
+ if (init.body && !headers.has("content-type")) {
1732
+ headers.set("content-type", "application/json");
1733
+ }
1734
+ const response = await fetchFn(joinUrl(apiBase, path), { ...init, headers });
1735
+ const body = await response.text();
1736
+ if (response.status === 429 && attempt < RETRY_LIMIT2) {
1737
+ const delay = retryDelayMs * 2 ** attempt;
1738
+ if (retryDelayMs)
1739
+ await new Promise((resolve) => setTimeout(resolve, delay));
1740
+ return request(path, init, attempt + 1);
1741
+ }
1742
+ if (!response.ok) {
1743
+ const error = new Error(errorMessage(response.status, body));
1744
+ error.status = response.status;
1745
+ throw error;
1746
+ }
1747
+ return { status: response.status, body };
1748
+ }
1749
+ async function requestJson(path, init) {
1750
+ const { body } = await request(path, init);
1751
+ if (!body.trim())
1752
+ return null;
1753
+ return JSON.parse(body);
1754
+ }
1755
+ function asPages(raw) {
1756
+ if (Array.isArray(raw))
1757
+ return raw;
1758
+ const row = asRecord(raw);
1759
+ return Array.isArray(row?.results) ? row.results : [];
1760
+ }
1761
+ async function paginate(path) {
1762
+ const items = [];
1763
+ let cursor;
1764
+ for (let page = 0;page < 50; page++) {
1765
+ const url = queryPath(path, { per_page: "100", ...cursor ? { cursor } : {} });
1766
+ const raw = await requestJson(url);
1767
+ if (Array.isArray(raw) && page === 0 && !asRecord(raw)) {
1768
+ return raw;
1769
+ }
1770
+ const row = asRecord(raw);
1771
+ const chunk = asPages(raw);
1772
+ items.push(...chunk);
1773
+ if (!row?.next_page_results)
1774
+ break;
1775
+ const next = asString(row.next_cursor);
1776
+ if (!next || next === cursor)
1777
+ break;
1778
+ cursor = next;
1779
+ }
1780
+ return items;
1781
+ }
1782
+ async function withResource(run) {
1783
+ try {
1784
+ return await run(workItemResource);
1785
+ } catch (err) {
1786
+ const status = err.status;
1787
+ if (workItemResource === "work-items" && status === 404) {
1788
+ workItemResource = "issues";
1789
+ return run(workItemResource);
1790
+ }
1791
+ throw err;
1792
+ }
1793
+ }
1794
+ function scopedProjects(all, flags) {
1795
+ const { projects } = parseFlags(flags || defaultFlags);
1796
+ if (!projects.length)
1797
+ return all;
1798
+ const wanted = new Set(projects);
1799
+ return all.filter((project) => wanted.has(project.identifier.toUpperCase()));
1800
+ }
1801
+ function parseProject(raw) {
1802
+ const row = asRecord(raw);
1803
+ const id = asString(row?.id);
1804
+ const identifier = asString(row?.identifier)?.toUpperCase();
1805
+ if (!id || !identifier)
1806
+ return;
1807
+ return { id, identifier, name: asString(row?.name) ?? identifier };
1808
+ }
1809
+ function parseState(raw) {
1810
+ const row = asRecord(raw);
1811
+ const id = asString(row?.id);
1812
+ const name = asString(row?.name);
1813
+ if (!id || !name)
1814
+ return;
1815
+ return {
1816
+ id,
1817
+ name,
1818
+ group: asString(row?.group),
1819
+ sequence: typeof row?.sequence === "number" ? row.sequence : undefined
1820
+ };
1821
+ }
1822
+ function parseLabel(raw) {
1823
+ const row = asRecord(raw);
1824
+ const id = asString(row?.id);
1825
+ const name = asString(row?.name);
1826
+ if (!id || !name)
1827
+ return;
1828
+ return { id, name };
1829
+ }
1830
+ function parseModule(raw) {
1831
+ const row = asRecord(raw);
1832
+ const id = asString(row?.id);
1833
+ if (!id)
1834
+ return;
1835
+ return {
1836
+ id,
1837
+ name: asString(row?.name) ?? id,
1838
+ status: asString(row?.status),
1839
+ created_at: asString(row?.created_at),
1840
+ target_date: asString(row?.target_date),
1841
+ start_date: asString(row?.start_date)
1842
+ };
1843
+ }
1844
+ async function loadCatalog(flags) {
1845
+ lastFlags = flags || defaultFlags;
1846
+ const workspace = workspaceOf(lastFlags);
1847
+ if (catalog && catalog.flags === lastFlags && catalog.workspace === workspace)
1848
+ return catalog;
1849
+ const projects = scopedProjects((await paginate(`/workspaces/${workspace}/projects/`)).map(parseProject).filter((row) => !!row), lastFlags);
1850
+ const states = new Map;
1851
+ const labels = new Map;
1852
+ const works = new Map;
1853
+ const modules = new Map;
1854
+ const columnStates = [];
1855
+ for (const project of projects) {
1856
+ const [stateRows, labelRows, moduleRows, workRows] = await Promise.all([
1857
+ paginate(`/workspaces/${workspace}/projects/${project.id}/states/`),
1858
+ paginate(`/workspaces/${workspace}/projects/${project.id}/labels/`),
1859
+ paginate(`/workspaces/${workspace}/projects/${project.id}/modules/`),
1860
+ withResource((resource) => paginate(queryPath(`/workspaces/${workspace}/projects/${project.id}/${resource}/`, {
1861
+ expand: "assignees,labels,state,module"
1862
+ })))
1863
+ ]);
1864
+ const stateById = new Map;
1865
+ for (const row of stateRows) {
1866
+ const state = parseState(row);
1867
+ if (!state)
1868
+ continue;
1869
+ stateById.set(state.id, state);
1870
+ columnStates.push(state);
1871
+ }
1872
+ const labelById = new Map;
1873
+ for (const row of labelRows) {
1874
+ const label = parseLabel(row);
1875
+ if (label)
1876
+ labelById.set(label.id, label);
1877
+ }
1878
+ const moduleById = new Map;
1879
+ for (const row of moduleRows) {
1880
+ const module = parseModule(row);
1881
+ if (!module)
1882
+ continue;
1883
+ const issue = moduleToIssue(module, project);
1884
+ const cached = { issue, module, project };
1885
+ moduleById.set(module.id, cached);
1886
+ modules.set(String(issue.key), cached);
1887
+ }
1888
+ states.set(project.id, stateById);
1889
+ labels.set(project.id, labelById);
1890
+ const workItems = workRows;
1891
+ const missingModules = moduleById.size > 0 && workItems.some((item) => item.module_ids == null && item.modules == null && item.module == null);
1892
+ const issueByModule = new Map;
1893
+ if (missingModules && moduleById.size) {
1894
+ await Promise.all([...moduleById.keys()].map(async (moduleId) => {
1895
+ const rows = await paginate(`/workspaces/${workspace}/projects/${project.id}/modules/${moduleId}/module-issues/`);
1896
+ const ids = rows.map((row) => asId(asRecord(row)?.issue) ?? asId(row)).filter((id) => !!id);
1897
+ issueByModule.set(moduleId, ids);
1898
+ }));
1899
+ }
1900
+ for (const raw of workItems) {
1901
+ const item = raw;
1902
+ const fromItem = moduleIdsOf(item)[0];
1903
+ const fromJoin = [...issueByModule.entries()].find(([, ids]) => ids.includes(item.id))?.[0];
1904
+ const moduleId = fromItem ?? fromJoin;
1905
+ const moduleKey = moduleId ? moduleById.get(moduleId)?.issue.key : undefined;
1906
+ const issue = workItemToIssue(item, project, {
1907
+ states: stateById,
1908
+ labels: labelById,
1909
+ moduleKey: typeof moduleKey === "string" ? moduleKey : undefined
1910
+ });
1911
+ const issueKey = typeof issue?.key === "string" ? issue.key : undefined;
1912
+ if (!issue || !issueKey)
1913
+ continue;
1914
+ works.set(issueKey, {
1915
+ issue,
1916
+ item,
1917
+ project,
1918
+ ...typeof moduleKey === "string" ? { moduleKey } : {}
1919
+ });
1920
+ }
1921
+ }
1922
+ catalog = {
1923
+ flags: lastFlags,
1924
+ workspace,
1925
+ projects,
1926
+ states,
1927
+ labels,
1928
+ works,
1929
+ modules,
1930
+ columns: orderStateNames(columnStates)
1931
+ };
1932
+ return catalog;
1933
+ }
1934
+ function filterWorks(flags, works) {
1935
+ const parsed = parseFlags(flags || defaultFlags);
1936
+ return works.filter(({ issue }) => {
1937
+ const status = typeof issue.fields?.status?.name === "string" ? issue.fields.status.name : "";
1938
+ const type = typeof issue.fields?.issuetype?.name === "string" ? issue.fields.issuetype.name : "";
1939
+ const parent = asString(asRecord(issue.fields?.parent)?.key);
1940
+ const assignee = asString(asRecord(issue.fields?.assignee)?.displayName);
1941
+ if (parsed.jql.includes("type=") && !parsed.jql.includes('type="Epic"')) {
1942
+ const wanted = /type="([^"]+)"/.exec(parsed.jql)?.[1];
1943
+ if (wanted && wanted.toLowerCase() !== type.toLowerCase())
1944
+ return false;
1945
+ }
1946
+ const statusEq = [...parsed.jql.matchAll(/status="([^"]+)"/g)].map((match) => match[1]);
1947
+ const statusNeq = [...parsed.jql.matchAll(/status!="([^"]+)"/g)].map((match) => match[1]);
1948
+ if (statusEq.length && !statusEq.some((name) => name.toLowerCase() === status.toLowerCase())) {
1949
+ return false;
1950
+ }
1951
+ if (statusNeq.some((name) => name.toLowerCase() === status.toLowerCase()))
1952
+ return false;
1953
+ const assigneeWanted = /assignee="([^"]+)"/.exec(parsed.jql)?.[1];
1954
+ if (assigneeWanted && assigneeWanted.toLowerCase() !== (assignee ?? "").toLowerCase()) {
1955
+ return false;
1956
+ }
1957
+ const parentWanted = /parent="([^"]+)"/.exec(parsed.jql)?.[1];
1958
+ if (parentWanted && parentWanted !== parent)
1959
+ return false;
1960
+ return true;
1961
+ });
1962
+ }
1963
+ async function lookupByKey(key, flags) {
1964
+ const loaded = await loadCatalog(flags);
1965
+ const cached = loaded.works.get(key);
1966
+ if (cached)
1967
+ return cached;
1968
+ if (loaded.modules.has(key))
1969
+ return;
1970
+ const raw = await withResource((resource) => requestJson(`/workspaces/${loaded.workspace}/${resource}/${key}/`));
1971
+ const row = asRecord(raw);
1972
+ if (!row)
1973
+ return;
1974
+ const project = loaded.projects.find((item) => item.id === asString(row.project)) ?? loaded.projects.find((item) => key.toUpperCase().startsWith(`${item.identifier}-`));
1975
+ if (!project)
1976
+ return;
1977
+ const stateById = loaded.states.get(project.id) ?? new Map;
1978
+ const labelById = loaded.labels.get(project.id) ?? new Map;
1979
+ const item = raw;
1980
+ const issue = workItemToIssue(item, project, { states: stateById, labels: labelById });
1981
+ const issueKey = typeof issue?.key === "string" ? issue.key : undefined;
1982
+ if (!issue || !issueKey)
1983
+ return;
1984
+ const next = { issue, item, project };
1985
+ loaded.works.set(issueKey, next);
1986
+ return next;
1987
+ }
1988
+ async function resolveLabelIds(project, names, flags) {
1989
+ if (!names?.length)
1990
+ return [];
1991
+ const loaded = await loadCatalog(flags);
1992
+ const byName = new Map([...loaded.labels.get(project.id)?.values() ?? []].map((label) => [label.name.toLowerCase(), label.id]));
1993
+ return names.map((name) => byName.get(name.toLowerCase())).filter((id) => !!id);
1994
+ }
1995
+ async function resolveStateId(project, status, flags) {
1996
+ const loaded = await loadCatalog(flags);
1997
+ const states = [...loaded.states.get(project.id)?.values() ?? []];
1998
+ return states.find((state) => state.name.toLowerCase() === status.toLowerCase())?.id;
1999
+ }
2000
+ return {
2001
+ async list(flags) {
2002
+ const query = flags || defaultFlags;
2003
+ const loaded = await loadCatalog(query);
2004
+ const issues = filterWorks(query, [...loaded.works.values()]).map((row) => row.issue);
2005
+ return JSON.stringify(issues);
2006
+ },
2007
+ async listEpics(queryFlags = defaultFlags) {
2008
+ const loaded = await loadCatalog(queryFlags || defaultFlags);
2009
+ return JSON.stringify([...loaded.modules.values()].map((row) => row.issue));
2010
+ },
2011
+ async listEpic(key, queryFlags = defaultFlags) {
2012
+ const loaded = await loadCatalog(queryFlags || defaultFlags);
2013
+ const issues = [...loaded.works.values()].filter((row) => row.moduleKey === key).map((row) => row.issue);
2014
+ return JSON.stringify(issues);
2015
+ },
2016
+ async listChildren(keys) {
2017
+ const loaded = await loadCatalog(lastFlags);
2018
+ const wanted = new Set(keys);
2019
+ const issues = [...loaded.works.values()].filter((row) => row.moduleKey && wanted.has(row.moduleKey)).map((row) => row.issue);
2020
+ return JSON.stringify(issues);
2021
+ },
2022
+ async states(flags) {
2023
+ const loaded = await loadCatalog(flags || defaultFlags);
2024
+ return loaded.columns;
2025
+ },
2026
+ async move(key, status) {
2027
+ try {
2028
+ const module = (await loadCatalog(lastFlags)).modules.get(key);
2029
+ if (module) {
2030
+ const mapped = Object.entries({
2031
+ backlog: "backlog",
2032
+ todo: "planned",
2033
+ "to do": "planned",
2034
+ "in progress": "in_progress",
2035
+ paused: "paused",
2036
+ done: "completed",
2037
+ completed: "completed",
2038
+ cancelled: "cancelled",
2039
+ canceled: "cancelled"
2040
+ }).find(([name]) => name === status.toLowerCase())?.[1];
2041
+ if (!mapped)
2042
+ return { ok: false, error: `Unknown Plane module status "${status}"` };
2043
+ await request(`/workspaces/${workspaceOf(lastFlags)}/projects/${module.project.id}/modules/${module.module.id}/`, { method: "PATCH", body: JSON.stringify({ status: mapped }) });
2044
+ catalog = undefined;
2045
+ return { ok: true };
2046
+ }
2047
+ const work = await lookupByKey(key, lastFlags);
2048
+ if (!work)
2049
+ return { ok: false, error: `Work item ${key} not found` };
2050
+ const state = await resolveStateId(work.project, status, lastFlags);
2051
+ if (!state)
2052
+ return { ok: false, error: `Unknown Plane state "${status}" for ${key}` };
2053
+ await withResource((resource) => request(`/workspaces/${workspaceOf(lastFlags)}/projects/${work.project.id}/${resource}/${work.item.id}/`, { method: "PATCH", body: JSON.stringify({ state }) }));
2054
+ catalog = undefined;
2055
+ return { ok: true };
2056
+ } catch (err) {
2057
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
2058
+ }
2059
+ },
2060
+ async create(input) {
2061
+ try {
2062
+ const loaded = await loadCatalog(lastFlags);
2063
+ const parent = input.parent ? loaded.modules.get(input.parent) : undefined;
2064
+ const project = parent?.project ?? loaded.projects[0];
2065
+ if (!project)
2066
+ return { ok: false, error: "No Plane project in Scope" };
2067
+ const state = input.status?.trim() ? await resolveStateId(project, input.status.trim(), lastFlags) : undefined;
2068
+ const labels = await resolveLabelIds(project, input.labels, lastFlags);
2069
+ const body = { name: input.summary };
2070
+ if (input.description)
2071
+ body.description_html = htmlParagraph(input.description);
2072
+ if (state)
2073
+ body.state = state;
2074
+ if (labels.length)
2075
+ body.labels = labels;
2076
+ const created = asRecord(await withResource((resource) => requestJson(`/workspaces/${workspaceOf(lastFlags)}/projects/${project.id}/${resource}/`, {
2077
+ method: "POST",
2078
+ body: JSON.stringify(body)
2079
+ })));
2080
+ const sequence = Number(created?.sequence_id);
2081
+ const key = Number.isFinite(sequence) && sequence > 0 ? planeWorkItemKey(project.identifier, sequence) : undefined;
2082
+ if (parent && created?.id) {
2083
+ await request(`/workspaces/${workspaceOf(lastFlags)}/projects/${project.id}/modules/${parent.module.id}/module-issues/`, { method: "POST", body: JSON.stringify({ issues: [created.id] }) });
2084
+ }
2085
+ catalog = undefined;
2086
+ return key ? { ok: true, key } : { ok: true };
2087
+ } catch (err) {
2088
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
2089
+ }
2090
+ },
2091
+ async edit(key, input) {
2092
+ try {
2093
+ const work = await lookupByKey(key, lastFlags);
2094
+ if (!work)
2095
+ return { ok: false, error: `Work item ${key} not found` };
2096
+ const body = {};
2097
+ if (input.summary !== undefined)
2098
+ body.name = input.summary;
2099
+ if (input.description !== undefined)
2100
+ body.description_html = htmlParagraph(input.description);
2101
+ if (input.labels)
2102
+ body.labels = await resolveLabelIds(work.project, input.labels, lastFlags);
2103
+ await withResource((resource) => request(`/workspaces/${workspaceOf(lastFlags)}/projects/${work.project.id}/${resource}/${work.item.id}/`, { method: "PATCH", body: JSON.stringify(body) }));
2104
+ catalog = undefined;
2105
+ return { ok: true };
2106
+ } catch (err) {
2107
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
2108
+ }
2109
+ },
2110
+ async open(key) {
2111
+ return `${host}/${workspaceOf(lastFlags)}/browse/${key}`;
2112
+ },
2113
+ async view(key) {
2114
+ const loaded = await loadCatalog(lastFlags);
2115
+ const module = loaded.modules.get(key);
2116
+ if (module)
2117
+ return JSON.stringify(module.issue);
2118
+ const work = await lookupByKey(key, lastFlags);
2119
+ if (work)
2120
+ return JSON.stringify(work.issue);
2121
+ throw new Error(`Work item ${key} not found`);
2122
+ }
2123
+ };
2124
+ }
2125
+
1461
2126
  // src/boot.ts
1462
2127
  async function createBoardApp(opts) {
1463
2128
  const env = opts.env ?? process.env;
1464
2129
  const flags = opts.flags ?? "";
1465
2130
  const store = IssueStore.fromRaw(opts.raw);
2131
+ const plane = wantsPlane(flags);
1466
2132
  const bin = resolveJiraBin(env.JIRA_BIN ?? "jira", env.PATH ?? "");
1467
- const cli = bin ? createJiraCli({
2133
+ const cli = plane ? createPlaneCli({
2134
+ host: planeHost(env),
2135
+ apiKey: env.PLANE_API_KEY ?? "",
2136
+ workspace: planeWorkspace(flags, env),
2137
+ flags,
2138
+ fetch: opts.fetch
2139
+ }) : bin ? createJiraCli({
1468
2140
  bin,
1469
2141
  configPath: env.JIRA_CONFIG_FILE,
1470
2142
  token: env.JIRA_API_TOKEN,
@@ -1482,10 +2154,12 @@ async function createBoardApp(opts) {
1482
2154
  const local = createStoreCli(store, flags);
1483
2155
  app.hydrate(JSON.parse(await local.list(app.flags)), { fromStore: true });
1484
2156
  }
1485
- return { app, store, kind: bin ? "jira" : "store" };
2157
+ return { app, store, kind: plane ? "plane" : bin ? "jira" : "store" };
1486
2158
  }
1487
2159
  async function refreshFromJira(app, kind, opts = {}) {
1488
- if (kind !== "jira" || opts.piped)
2160
+ if (opts.piped)
2161
+ return;
2162
+ if (kind !== "jira" && kind !== "plane")
1489
2163
  return;
1490
2164
  await app.refresh();
1491
2165
  }
@@ -12698,14 +13372,19 @@ async function runServer(opts) {
12698
13372
  const origin = `http://127.0.0.1:${port}`;
12699
13373
  const fakeConfig = writeJiraConfig(join10(tmpdir3(), "pipe-kan"), origin);
12700
13374
  announce(`pipe-kan http://${host}:${port}`);
12701
- announce(`cli ${kind === "jira" ? resolveJiraBin() : "store"}`);
12702
- announce(`Fake Jira ${origin}/rest/api/2/search`);
12703
- announce(`Fake Jira config ${fakeConfig}`);
12704
- if (kind === "jira") {
13375
+ if (kind === "plane") {
13376
+ announce(`cli plane ${planeHost()} ${planeWorkspace(flags)}`);
13377
+ announce(`Plane host default ${DEFAULT_PLANE_HOST}`);
13378
+ } else {
13379
+ announce(`cli ${kind === "jira" ? resolveJiraBin() : "store"}`);
13380
+ announce(`Fake Jira ${origin}/rest/api/2/search`);
13381
+ announce(`Fake Jira config ${fakeConfig}`);
13382
+ }
13383
+ if (kind === "jira" || kind === "plane") {
12705
13384
  try {
12706
13385
  await refreshFromJira(app, kind, { piped: Boolean(piped) });
12707
13386
  } catch (err) {
12708
- console.error("jira Refresh failed; keeping Fixture Board");
13387
+ console.error(`${kind} Refresh failed; keeping Fixture Board`);
12709
13388
  console.error(err);
12710
13389
  }
12711
13390
  }