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/README.md +38 -4
- package/dist/pipe-kan.js +690 -11
- package/dist/ui/assets/index-BRqmsBb_.js +56 -0
- package/dist/ui/index.html +1 -1
- package/package.json +2 -2
- package/dist/ui/assets/index-ChnNJHrZ.js +0 -56
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
12702
|
-
|
|
12703
|
-
|
|
12704
|
-
|
|
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(
|
|
13387
|
+
console.error(`${kind} Refresh failed; keeping Fixture Board`);
|
|
12709
13388
|
console.error(err);
|
|
12710
13389
|
}
|
|
12711
13390
|
}
|