pipe-kan 0.29.2 → 0.30.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/README.md +41 -5
- package/dist/pipe-kan.js +780 -17
- package/dist/ui/assets/index-CyO_1G_D.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;
|
|
@@ -1207,9 +1231,12 @@ function createApp(opts) {
|
|
|
1207
1231
|
let childrenRaw = [];
|
|
1208
1232
|
let hasChildrenCache = false;
|
|
1209
1233
|
let childrenError;
|
|
1234
|
+
let refreshError;
|
|
1235
|
+
let live = false;
|
|
1210
1236
|
let targetEndFieldId = readTargetEndFieldMap(fieldMapPath).targetEnd ?? targetEndFieldIdFromJiraConfig(safeRead(jiraConfigPath) ?? "");
|
|
1237
|
+
let workflowStates;
|
|
1211
1238
|
function boardOpts() {
|
|
1212
|
-
return { targetEndFieldId };
|
|
1239
|
+
return { targetEndFieldId, ...workflowStates?.length ? { statuses: workflowStates } : {} };
|
|
1213
1240
|
}
|
|
1214
1241
|
function toBoard(raw) {
|
|
1215
1242
|
return issuesToBoard(raw, boardOpts());
|
|
@@ -1305,11 +1332,13 @@ function createApp(opts) {
|
|
|
1305
1332
|
childrenRaw = mergeSelectedChildren(childrenRaw, hydrated, keys);
|
|
1306
1333
|
hasChildrenCache = true;
|
|
1307
1334
|
childrenError = undefined;
|
|
1335
|
+
refreshError = undefined;
|
|
1308
1336
|
return app.board();
|
|
1309
1337
|
} catch (err) {
|
|
1310
1338
|
const message = err instanceof Error ? err.message : "Refresh failed";
|
|
1339
|
+
refreshError = message;
|
|
1311
1340
|
console.error("Refresh failed", message);
|
|
1312
|
-
return
|
|
1341
|
+
return app.board();
|
|
1313
1342
|
}
|
|
1314
1343
|
}
|
|
1315
1344
|
async function tryMove(key, status) {
|
|
@@ -1325,12 +1354,21 @@ function createApp(opts) {
|
|
|
1325
1354
|
return flags;
|
|
1326
1355
|
},
|
|
1327
1356
|
board() {
|
|
1357
|
+
const error = refreshError ?? childrenError;
|
|
1358
|
+
if (!live && (refreshError || opts.liveBackend)) {
|
|
1359
|
+
const empty = toBoard([]);
|
|
1360
|
+
return {
|
|
1361
|
+
columns: empty.columns,
|
|
1362
|
+
epics: [],
|
|
1363
|
+
...error ? { error } : {}
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1328
1366
|
const board = toBoard(payload);
|
|
1329
1367
|
return {
|
|
1330
1368
|
columns: board.columns,
|
|
1331
1369
|
epics: mergeEpics(toBoard(epicsPayload).epics, board.epics),
|
|
1332
1370
|
...hasChildrenCache ? { children: columnsOf(childrenRaw, targetEndFieldId) } : {},
|
|
1333
|
-
...
|
|
1371
|
+
...error ? { error } : {}
|
|
1334
1372
|
};
|
|
1335
1373
|
},
|
|
1336
1374
|
hydrate(raw, hydrateOpts) {
|
|
@@ -1339,6 +1377,8 @@ function createApp(opts) {
|
|
|
1339
1377
|
childrenRaw = [];
|
|
1340
1378
|
hasChildrenCache = false;
|
|
1341
1379
|
childrenError = undefined;
|
|
1380
|
+
refreshError = undefined;
|
|
1381
|
+
live = false;
|
|
1342
1382
|
if (hydrateOpts?.fromStore)
|
|
1343
1383
|
cacheFromStore();
|
|
1344
1384
|
rememberTargetEndId([...payload, ...epicsPayload, ...childrenRaw]);
|
|
@@ -1352,6 +1392,14 @@ function createApp(opts) {
|
|
|
1352
1392
|
flags = next;
|
|
1353
1393
|
console.log("Refresh");
|
|
1354
1394
|
try {
|
|
1395
|
+
if (cli.states) {
|
|
1396
|
+
try {
|
|
1397
|
+
workflowStates = await cli.states(flags);
|
|
1398
|
+
} catch (err) {
|
|
1399
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1400
|
+
console.log(`Refresh states failed; ${message}`);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1355
1403
|
const issues = await cli.list(flags);
|
|
1356
1404
|
const epics = await cli.listEpics(flags);
|
|
1357
1405
|
const listedPayload = JSON.parse(issues);
|
|
@@ -1384,11 +1432,14 @@ function createApp(opts) {
|
|
|
1384
1432
|
childrenRaw = nextHasCache ? await hydrateRaw(nextChildren) : nextChildren;
|
|
1385
1433
|
hasChildrenCache = nextHasCache;
|
|
1386
1434
|
childrenError = nextError;
|
|
1435
|
+
refreshError = undefined;
|
|
1436
|
+
live = true;
|
|
1387
1437
|
return app.board();
|
|
1388
1438
|
} catch (err) {
|
|
1389
1439
|
const message = err instanceof Error ? err.message : "Refresh failed";
|
|
1440
|
+
refreshError = message;
|
|
1390
1441
|
console.error("Refresh failed", message);
|
|
1391
|
-
return
|
|
1442
|
+
return app.board();
|
|
1392
1443
|
}
|
|
1393
1444
|
},
|
|
1394
1445
|
async children(epic) {
|
|
@@ -1458,13 +1509,703 @@ function createApp(opts) {
|
|
|
1458
1509
|
return app;
|
|
1459
1510
|
}
|
|
1460
1511
|
|
|
1512
|
+
// src/plane.ts
|
|
1513
|
+
var DEFAULT_PLANE_HOST = "https://plane.tail48fe8.ts.net";
|
|
1514
|
+
var DEFAULT_PLANE_WORKSPACE = "personal";
|
|
1515
|
+
var STATE_GROUPS = ["backlog", "unstarted", "started", "completed", "cancelled"];
|
|
1516
|
+
function planeHost(env = process.env) {
|
|
1517
|
+
return (env.PLANE_HOST ?? DEFAULT_PLANE_HOST).replace(/\/$/, "");
|
|
1518
|
+
}
|
|
1519
|
+
function planeWorkspace(flags = "", env = process.env) {
|
|
1520
|
+
return parseFlags(flags).workspace || env.PLANE_WORKSPACE || DEFAULT_PLANE_WORKSPACE;
|
|
1521
|
+
}
|
|
1522
|
+
function planeApiBase(host) {
|
|
1523
|
+
const trimmed = host.replace(/\/$/, "");
|
|
1524
|
+
if (trimmed.endsWith("/api/v1"))
|
|
1525
|
+
return trimmed;
|
|
1526
|
+
if (trimmed.endsWith("/api"))
|
|
1527
|
+
return `${trimmed}/v1`;
|
|
1528
|
+
return `${trimmed}/api/v1`;
|
|
1529
|
+
}
|
|
1530
|
+
function planeModuleKey(identifier, moduleId) {
|
|
1531
|
+
const hex = moduleId.replace(/-/g, "").slice(-8);
|
|
1532
|
+
const n = Number.parseInt(hex, 16);
|
|
1533
|
+
return `${identifier.toUpperCase()}M-${Number.isFinite(n) ? n : 0}`;
|
|
1534
|
+
}
|
|
1535
|
+
function planeWorkItemKey(identifier, sequenceId) {
|
|
1536
|
+
return `${identifier.toUpperCase()}-${sequenceId}`;
|
|
1537
|
+
}
|
|
1538
|
+
function titleCasePriority(value) {
|
|
1539
|
+
if (!value || value.toLowerCase() === "none")
|
|
1540
|
+
return;
|
|
1541
|
+
return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
|
|
1542
|
+
}
|
|
1543
|
+
function moduleStatusName(status) {
|
|
1544
|
+
switch ((status ?? "").toLowerCase()) {
|
|
1545
|
+
case "backlog":
|
|
1546
|
+
return "Backlog";
|
|
1547
|
+
case "unstarted":
|
|
1548
|
+
case "planned":
|
|
1549
|
+
return "Todo";
|
|
1550
|
+
case "started":
|
|
1551
|
+
case "in_progress":
|
|
1552
|
+
return "In Progress";
|
|
1553
|
+
case "paused":
|
|
1554
|
+
return "Paused";
|
|
1555
|
+
case "completed":
|
|
1556
|
+
return "Done";
|
|
1557
|
+
case "cancelled":
|
|
1558
|
+
case "canceled":
|
|
1559
|
+
return "Cancelled";
|
|
1560
|
+
default:
|
|
1561
|
+
return status?.trim() || "Todo";
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
function orderStateNames(states) {
|
|
1565
|
+
const seen = new Set;
|
|
1566
|
+
const names = [];
|
|
1567
|
+
const sorted = [...states].sort((a, b) => {
|
|
1568
|
+
const groupA = STATE_GROUPS.indexOf((a.group ?? "").toLowerCase());
|
|
1569
|
+
const groupB = STATE_GROUPS.indexOf((b.group ?? "").toLowerCase());
|
|
1570
|
+
const group = (groupA === -1 ? 99 : groupA) - (groupB === -1 ? 99 : groupB);
|
|
1571
|
+
if (group)
|
|
1572
|
+
return group;
|
|
1573
|
+
return (a.sequence ?? 0) - (b.sequence ?? 0);
|
|
1574
|
+
});
|
|
1575
|
+
for (const state of sorted) {
|
|
1576
|
+
const name = state.name.trim();
|
|
1577
|
+
if (!name || seen.has(name))
|
|
1578
|
+
continue;
|
|
1579
|
+
seen.add(name);
|
|
1580
|
+
names.push(name);
|
|
1581
|
+
}
|
|
1582
|
+
return names;
|
|
1583
|
+
}
|
|
1584
|
+
function asRecord(value) {
|
|
1585
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
1586
|
+
}
|
|
1587
|
+
function asString(value) {
|
|
1588
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
1589
|
+
}
|
|
1590
|
+
function asId(value) {
|
|
1591
|
+
if (typeof value === "string" && value.trim())
|
|
1592
|
+
return value.trim();
|
|
1593
|
+
const row = asRecord(value);
|
|
1594
|
+
return asString(row?.id);
|
|
1595
|
+
}
|
|
1596
|
+
function named(value) {
|
|
1597
|
+
const row = asRecord(value);
|
|
1598
|
+
return asString(row?.name) ?? asString(row?.display_name) ?? asString(row?.displayName);
|
|
1599
|
+
}
|
|
1600
|
+
function stateNameOf(item, states) {
|
|
1601
|
+
const expanded = named(item.state);
|
|
1602
|
+
if (expanded)
|
|
1603
|
+
return expanded;
|
|
1604
|
+
const id = asId(item.state);
|
|
1605
|
+
if (id && states.get(id)?.name)
|
|
1606
|
+
return states.get(id).name;
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
function labelNamesOf(item, labels) {
|
|
1610
|
+
const raw = [
|
|
1611
|
+
...Array.isArray(item.label_details) ? item.label_details : [],
|
|
1612
|
+
...Array.isArray(item.labels) ? item.labels : []
|
|
1613
|
+
];
|
|
1614
|
+
const names = new Set;
|
|
1615
|
+
for (const entry of raw) {
|
|
1616
|
+
const fromName = named(entry) ?? asString(entry);
|
|
1617
|
+
if (fromName && !labels.has(fromName)) {
|
|
1618
|
+
names.add(fromName);
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
const id = asId(entry);
|
|
1622
|
+
const mapped = id ? labels.get(id)?.name : undefined;
|
|
1623
|
+
if (mapped)
|
|
1624
|
+
names.add(mapped);
|
|
1625
|
+
}
|
|
1626
|
+
return [...names];
|
|
1627
|
+
}
|
|
1628
|
+
function assigneeOf(item) {
|
|
1629
|
+
const raw = Array.isArray(item.assignee_details) ? item.assignee_details : Array.isArray(item.assignees) ? item.assignees : [];
|
|
1630
|
+
for (const entry of raw) {
|
|
1631
|
+
const name = named(entry) ?? asString(asRecord(entry)?.email) ?? asString(asRecord(entry)?.first_name);
|
|
1632
|
+
if (name)
|
|
1633
|
+
return name;
|
|
1634
|
+
}
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
function moduleIdsOf(item) {
|
|
1638
|
+
const ids = new Set;
|
|
1639
|
+
for (const entry of [
|
|
1640
|
+
...Array.isArray(item.module_ids) ? item.module_ids : [],
|
|
1641
|
+
...Array.isArray(item.modules) ? item.modules : [],
|
|
1642
|
+
item.module
|
|
1643
|
+
]) {
|
|
1644
|
+
const id = asId(entry);
|
|
1645
|
+
if (id)
|
|
1646
|
+
ids.add(id);
|
|
1647
|
+
}
|
|
1648
|
+
return [...ids];
|
|
1649
|
+
}
|
|
1650
|
+
function descriptionOf(item) {
|
|
1651
|
+
return asString(item.description_stripped) ?? asString(item.description) ?? (asString(item.description_html) ? asString(item.description_html).replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim() : undefined);
|
|
1652
|
+
}
|
|
1653
|
+
function workItemToIssue(item, project, opts) {
|
|
1654
|
+
if (item.archived_at || item.is_draft)
|
|
1655
|
+
return;
|
|
1656
|
+
const sequence = Number(item.sequence_id);
|
|
1657
|
+
if (!Number.isFinite(sequence))
|
|
1658
|
+
return;
|
|
1659
|
+
const status = stateNameOf(item, opts.states);
|
|
1660
|
+
if (!status)
|
|
1661
|
+
return;
|
|
1662
|
+
const key = planeWorkItemKey(project.identifier, sequence);
|
|
1663
|
+
const priority = titleCasePriority(item.priority);
|
|
1664
|
+
const assignee = assigneeOf(item);
|
|
1665
|
+
const labels = labelNamesOf(item, opts.labels);
|
|
1666
|
+
const description = descriptionOf(item);
|
|
1667
|
+
const target = asString(item.target_date);
|
|
1668
|
+
return {
|
|
1669
|
+
key,
|
|
1670
|
+
fields: {
|
|
1671
|
+
summary: asString(item.name) ?? "",
|
|
1672
|
+
status: { name: status },
|
|
1673
|
+
...priority ? { priority: { name: priority } } : {},
|
|
1674
|
+
...assignee ? { assignee: { displayName: assignee } } : {},
|
|
1675
|
+
...target ? { duedate: target, "Target End Date": target } : {},
|
|
1676
|
+
issuetype: { name: "Story" },
|
|
1677
|
+
issueType: { name: "Story" },
|
|
1678
|
+
...opts.moduleKey ? { parent: { key: opts.moduleKey } } : {},
|
|
1679
|
+
...labels.length ? { labels } : {},
|
|
1680
|
+
...asString(item.created_at) ? { created: item.created_at } : {},
|
|
1681
|
+
...description ? { description } : {}
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
function moduleToIssue(module, project) {
|
|
1686
|
+
const target = asString(module.target_date);
|
|
1687
|
+
return {
|
|
1688
|
+
key: planeModuleKey(project.identifier, module.id),
|
|
1689
|
+
fields: {
|
|
1690
|
+
summary: asString(module.name) ?? module.id,
|
|
1691
|
+
status: { name: moduleStatusName(module.status) },
|
|
1692
|
+
issuetype: { name: "Epic" },
|
|
1693
|
+
issueType: { name: "Epic" },
|
|
1694
|
+
...target ? { duedate: target, "Target End Date": target } : {},
|
|
1695
|
+
...asString(module.created_at) ? { created: module.created_at } : {}
|
|
1696
|
+
}
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
function joinUrl(base, path) {
|
|
1700
|
+
if (path.startsWith("http://") || path.startsWith("https://"))
|
|
1701
|
+
return path;
|
|
1702
|
+
const suffix = path.startsWith("/") ? path : `/${path}`;
|
|
1703
|
+
return `${base}${suffix}`;
|
|
1704
|
+
}
|
|
1705
|
+
function queryPath(path, query) {
|
|
1706
|
+
const params = new URLSearchParams;
|
|
1707
|
+
for (const [key, value] of Object.entries(query)) {
|
|
1708
|
+
if (value)
|
|
1709
|
+
params.set(key, value);
|
|
1710
|
+
}
|
|
1711
|
+
const encoded = params.toString();
|
|
1712
|
+
if (!encoded)
|
|
1713
|
+
return path;
|
|
1714
|
+
return `${path}${path.includes("?") ? "&" : "?"}${encoded}`;
|
|
1715
|
+
}
|
|
1716
|
+
function rateLimitWaitMs(headers, nowMs, on429) {
|
|
1717
|
+
const retryAfter = headers.get("Retry-After");
|
|
1718
|
+
if (retryAfter) {
|
|
1719
|
+
const seconds = Number(retryAfter.trim());
|
|
1720
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
1721
|
+
return seconds * 1000;
|
|
1722
|
+
const parsed = Date.parse(retryAfter);
|
|
1723
|
+
if (!Number.isNaN(parsed))
|
|
1724
|
+
return Math.max(0, parsed - nowMs);
|
|
1725
|
+
}
|
|
1726
|
+
const reset = headers.get("X-RateLimit-Reset");
|
|
1727
|
+
if (reset) {
|
|
1728
|
+
const n = Number(reset.trim());
|
|
1729
|
+
if (Number.isFinite(n)) {
|
|
1730
|
+
const resetMs = n < 1000000000000 ? n * 1000 : n;
|
|
1731
|
+
return Math.max(0, resetMs - nowMs);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
if (on429)
|
|
1735
|
+
return 60000;
|
|
1736
|
+
if (headers.get("X-RateLimit-Remaining")?.trim() === "0")
|
|
1737
|
+
return 60000;
|
|
1738
|
+
return 0;
|
|
1739
|
+
}
|
|
1740
|
+
function errorMessage(status, body) {
|
|
1741
|
+
try {
|
|
1742
|
+
const parsed = JSON.parse(body);
|
|
1743
|
+
const detail = asString(parsed.detail) ?? asString(parsed.error) ?? asString(parsed.message) ?? (Array.isArray(parsed.non_field_errors) ? asString(parsed.non_field_errors[0]) : undefined);
|
|
1744
|
+
if (detail)
|
|
1745
|
+
return `Plane ${status}: ${detail}`;
|
|
1746
|
+
} catch {}
|
|
1747
|
+
const snippet = body.replace(/\s+/g, " ").trim().slice(0, 240);
|
|
1748
|
+
return snippet ? `Plane ${status}: ${snippet}` : `Plane ${status}`;
|
|
1749
|
+
}
|
|
1750
|
+
function htmlParagraph(text) {
|
|
1751
|
+
const escaped = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1752
|
+
return `<p>${escaped}</p>`;
|
|
1753
|
+
}
|
|
1754
|
+
function createPlaneCli(opts) {
|
|
1755
|
+
if (!opts.apiKey.trim()) {
|
|
1756
|
+
throw new Error("Plane mode needs PLANE_API_KEY");
|
|
1757
|
+
}
|
|
1758
|
+
const host = (opts.host ?? DEFAULT_PLANE_HOST).replace(/\/$/, "");
|
|
1759
|
+
const apiBase = planeApiBase(host);
|
|
1760
|
+
const defaultFlags = opts.flags ?? DEFAULT_FLAGS;
|
|
1761
|
+
const fetchFn = opts.fetch ?? fetch;
|
|
1762
|
+
const nowFn = opts.now ?? Date.now;
|
|
1763
|
+
const sleepFn = opts.sleep ?? ((ms) => ms <= 0 ? Promise.resolve() : new Promise((resolve) => setTimeout(resolve, ms)));
|
|
1764
|
+
let lastFlags = defaultFlags;
|
|
1765
|
+
let workItemResource = "work-items";
|
|
1766
|
+
let catalog;
|
|
1767
|
+
function workspaceOf(flags) {
|
|
1768
|
+
return planeWorkspace(flags || defaultFlags);
|
|
1769
|
+
}
|
|
1770
|
+
let notBefore = 0;
|
|
1771
|
+
let gate = Promise.resolve();
|
|
1772
|
+
function locked(fn) {
|
|
1773
|
+
const run = gate.then(fn, fn);
|
|
1774
|
+
gate = run.then(() => {
|
|
1775
|
+
return;
|
|
1776
|
+
}, () => {
|
|
1777
|
+
return;
|
|
1778
|
+
});
|
|
1779
|
+
return run;
|
|
1780
|
+
}
|
|
1781
|
+
function fail(status, body) {
|
|
1782
|
+
const error = new Error(errorMessage(status, body));
|
|
1783
|
+
error.status = status;
|
|
1784
|
+
throw error;
|
|
1785
|
+
}
|
|
1786
|
+
function noteHeaders(headers, status) {
|
|
1787
|
+
const remaining = headers.get("X-RateLimit-Remaining")?.trim();
|
|
1788
|
+
if (status !== 429 && remaining !== "0")
|
|
1789
|
+
return;
|
|
1790
|
+
const wait = rateLimitWaitMs(headers, nowFn(), status === 429);
|
|
1791
|
+
notBefore = Math.max(notBefore, nowFn() + wait);
|
|
1792
|
+
}
|
|
1793
|
+
async function request(path, init = {}) {
|
|
1794
|
+
return locked(async () => {
|
|
1795
|
+
let retried = false;
|
|
1796
|
+
while (true) {
|
|
1797
|
+
const headers = new Headers(init.headers);
|
|
1798
|
+
headers.set("X-API-Key", opts.apiKey);
|
|
1799
|
+
if (init.body && !headers.has("content-type")) {
|
|
1800
|
+
headers.set("content-type", "application/json");
|
|
1801
|
+
}
|
|
1802
|
+
const delay = notBefore - nowFn();
|
|
1803
|
+
if (delay > 0)
|
|
1804
|
+
await sleepFn(delay);
|
|
1805
|
+
const response = await fetchFn(joinUrl(apiBase, path), { ...init, headers });
|
|
1806
|
+
const body = await response.text();
|
|
1807
|
+
noteHeaders(response.headers, response.status);
|
|
1808
|
+
if (response.status === 429) {
|
|
1809
|
+
if (retried)
|
|
1810
|
+
fail(response.status, body);
|
|
1811
|
+
retried = true;
|
|
1812
|
+
continue;
|
|
1813
|
+
}
|
|
1814
|
+
if (!response.ok)
|
|
1815
|
+
fail(response.status, body);
|
|
1816
|
+
return { status: response.status, body };
|
|
1817
|
+
}
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
async function requestJson(path, init) {
|
|
1821
|
+
const { body } = await request(path, init);
|
|
1822
|
+
if (!body.trim())
|
|
1823
|
+
return null;
|
|
1824
|
+
return JSON.parse(body);
|
|
1825
|
+
}
|
|
1826
|
+
function asPages(raw) {
|
|
1827
|
+
if (Array.isArray(raw))
|
|
1828
|
+
return raw;
|
|
1829
|
+
const row = asRecord(raw);
|
|
1830
|
+
return Array.isArray(row?.results) ? row.results : [];
|
|
1831
|
+
}
|
|
1832
|
+
async function paginate(path) {
|
|
1833
|
+
const items = [];
|
|
1834
|
+
let cursor;
|
|
1835
|
+
for (let page = 0;page < 50; page++) {
|
|
1836
|
+
const url = queryPath(path, { per_page: "100", ...cursor ? { cursor } : {} });
|
|
1837
|
+
const raw = await requestJson(url);
|
|
1838
|
+
if (Array.isArray(raw) && page === 0 && !asRecord(raw)) {
|
|
1839
|
+
return raw;
|
|
1840
|
+
}
|
|
1841
|
+
const row = asRecord(raw);
|
|
1842
|
+
const chunk = asPages(raw);
|
|
1843
|
+
items.push(...chunk);
|
|
1844
|
+
if (!row?.next_page_results)
|
|
1845
|
+
break;
|
|
1846
|
+
const next = asString(row.next_cursor);
|
|
1847
|
+
if (!next || next === cursor)
|
|
1848
|
+
break;
|
|
1849
|
+
cursor = next;
|
|
1850
|
+
}
|
|
1851
|
+
return items;
|
|
1852
|
+
}
|
|
1853
|
+
async function withResource(run) {
|
|
1854
|
+
try {
|
|
1855
|
+
return await run(workItemResource);
|
|
1856
|
+
} catch (err) {
|
|
1857
|
+
const status = err.status;
|
|
1858
|
+
if (workItemResource === "work-items" && status === 404) {
|
|
1859
|
+
workItemResource = "issues";
|
|
1860
|
+
return run(workItemResource);
|
|
1861
|
+
}
|
|
1862
|
+
throw err;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
function scopedProjects(all, flags) {
|
|
1866
|
+
const { projects } = parseFlags(flags || defaultFlags);
|
|
1867
|
+
if (!projects.length)
|
|
1868
|
+
return all;
|
|
1869
|
+
const wanted = new Set(projects);
|
|
1870
|
+
return all.filter((project) => wanted.has(project.identifier.toUpperCase()));
|
|
1871
|
+
}
|
|
1872
|
+
function parseProject(raw) {
|
|
1873
|
+
const row = asRecord(raw);
|
|
1874
|
+
const id = asString(row?.id);
|
|
1875
|
+
const identifier = asString(row?.identifier)?.toUpperCase();
|
|
1876
|
+
if (!id || !identifier)
|
|
1877
|
+
return;
|
|
1878
|
+
return { id, identifier, name: asString(row?.name) ?? identifier };
|
|
1879
|
+
}
|
|
1880
|
+
function parseState(raw) {
|
|
1881
|
+
const row = asRecord(raw);
|
|
1882
|
+
const id = asString(row?.id);
|
|
1883
|
+
const name = asString(row?.name);
|
|
1884
|
+
if (!id || !name)
|
|
1885
|
+
return;
|
|
1886
|
+
return {
|
|
1887
|
+
id,
|
|
1888
|
+
name,
|
|
1889
|
+
group: asString(row?.group),
|
|
1890
|
+
sequence: typeof row?.sequence === "number" ? row.sequence : undefined
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
function parseLabel(raw) {
|
|
1894
|
+
const row = asRecord(raw);
|
|
1895
|
+
const id = asString(row?.id);
|
|
1896
|
+
const name = asString(row?.name);
|
|
1897
|
+
if (!id || !name)
|
|
1898
|
+
return;
|
|
1899
|
+
return { id, name };
|
|
1900
|
+
}
|
|
1901
|
+
function parseModule(raw) {
|
|
1902
|
+
const row = asRecord(raw);
|
|
1903
|
+
const id = asString(row?.id);
|
|
1904
|
+
if (!id)
|
|
1905
|
+
return;
|
|
1906
|
+
return {
|
|
1907
|
+
id,
|
|
1908
|
+
name: asString(row?.name) ?? id,
|
|
1909
|
+
status: asString(row?.status),
|
|
1910
|
+
created_at: asString(row?.created_at),
|
|
1911
|
+
target_date: asString(row?.target_date),
|
|
1912
|
+
start_date: asString(row?.start_date)
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
async function loadCatalog(flags) {
|
|
1916
|
+
lastFlags = flags || defaultFlags;
|
|
1917
|
+
const workspace = workspaceOf(lastFlags);
|
|
1918
|
+
if (catalog && catalog.flags === lastFlags && catalog.workspace === workspace)
|
|
1919
|
+
return catalog;
|
|
1920
|
+
const projects = scopedProjects((await paginate(`/workspaces/${workspace}/projects/`)).map(parseProject).filter((row) => !!row), lastFlags);
|
|
1921
|
+
const states = new Map;
|
|
1922
|
+
const labels = new Map;
|
|
1923
|
+
const works = new Map;
|
|
1924
|
+
const modules = new Map;
|
|
1925
|
+
const columnStates = [];
|
|
1926
|
+
for (const project of projects) {
|
|
1927
|
+
const stateRows = await paginate(`/workspaces/${workspace}/projects/${project.id}/states/`);
|
|
1928
|
+
const labelRows = await paginate(`/workspaces/${workspace}/projects/${project.id}/labels/`);
|
|
1929
|
+
const moduleRows = await paginate(`/workspaces/${workspace}/projects/${project.id}/modules/`);
|
|
1930
|
+
const workRows = await withResource((resource) => paginate(queryPath(`/workspaces/${workspace}/projects/${project.id}/${resource}/`, {
|
|
1931
|
+
expand: "assignees,labels,state,module"
|
|
1932
|
+
})));
|
|
1933
|
+
const stateById = new Map;
|
|
1934
|
+
for (const row of stateRows) {
|
|
1935
|
+
const state = parseState(row);
|
|
1936
|
+
if (!state)
|
|
1937
|
+
continue;
|
|
1938
|
+
stateById.set(state.id, state);
|
|
1939
|
+
columnStates.push(state);
|
|
1940
|
+
}
|
|
1941
|
+
const labelById = new Map;
|
|
1942
|
+
for (const row of labelRows) {
|
|
1943
|
+
const label = parseLabel(row);
|
|
1944
|
+
if (label)
|
|
1945
|
+
labelById.set(label.id, label);
|
|
1946
|
+
}
|
|
1947
|
+
const moduleById = new Map;
|
|
1948
|
+
for (const row of moduleRows) {
|
|
1949
|
+
const module = parseModule(row);
|
|
1950
|
+
if (!module)
|
|
1951
|
+
continue;
|
|
1952
|
+
const issue = moduleToIssue(module, project);
|
|
1953
|
+
const cached = { issue, module, project };
|
|
1954
|
+
moduleById.set(module.id, cached);
|
|
1955
|
+
modules.set(String(issue.key), cached);
|
|
1956
|
+
}
|
|
1957
|
+
states.set(project.id, stateById);
|
|
1958
|
+
labels.set(project.id, labelById);
|
|
1959
|
+
const workItems = workRows;
|
|
1960
|
+
const missingModules = moduleById.size > 0 && workItems.some((item) => item.module_ids == null && item.modules == null && item.module == null);
|
|
1961
|
+
const issueByModule = new Map;
|
|
1962
|
+
if (missingModules && moduleById.size) {
|
|
1963
|
+
for (const moduleId of moduleById.keys()) {
|
|
1964
|
+
const rows = await paginate(`/workspaces/${workspace}/projects/${project.id}/modules/${moduleId}/module-issues/`);
|
|
1965
|
+
const ids = rows.map((row) => asId(asRecord(row)?.issue) ?? asId(row)).filter((id) => !!id);
|
|
1966
|
+
issueByModule.set(moduleId, ids);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
for (const raw of workItems) {
|
|
1970
|
+
const item = raw;
|
|
1971
|
+
const fromItem = moduleIdsOf(item)[0];
|
|
1972
|
+
const fromJoin = [...issueByModule.entries()].find(([, ids]) => ids.includes(item.id))?.[0];
|
|
1973
|
+
const moduleId = fromItem ?? fromJoin;
|
|
1974
|
+
const moduleKey = moduleId ? moduleById.get(moduleId)?.issue.key : undefined;
|
|
1975
|
+
const issue = workItemToIssue(item, project, {
|
|
1976
|
+
states: stateById,
|
|
1977
|
+
labels: labelById,
|
|
1978
|
+
moduleKey: typeof moduleKey === "string" ? moduleKey : undefined
|
|
1979
|
+
});
|
|
1980
|
+
const issueKey = typeof issue?.key === "string" ? issue.key : undefined;
|
|
1981
|
+
if (!issue || !issueKey)
|
|
1982
|
+
continue;
|
|
1983
|
+
works.set(issueKey, {
|
|
1984
|
+
issue,
|
|
1985
|
+
item,
|
|
1986
|
+
project,
|
|
1987
|
+
...typeof moduleKey === "string" ? { moduleKey } : {}
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
catalog = {
|
|
1992
|
+
flags: lastFlags,
|
|
1993
|
+
workspace,
|
|
1994
|
+
projects,
|
|
1995
|
+
states,
|
|
1996
|
+
labels,
|
|
1997
|
+
works,
|
|
1998
|
+
modules,
|
|
1999
|
+
columns: orderStateNames(columnStates)
|
|
2000
|
+
};
|
|
2001
|
+
return catalog;
|
|
2002
|
+
}
|
|
2003
|
+
function filterWorks(flags, works) {
|
|
2004
|
+
const parsed = parseFlags(flags || defaultFlags);
|
|
2005
|
+
return works.filter(({ issue }) => {
|
|
2006
|
+
const status = typeof issue.fields?.status?.name === "string" ? issue.fields.status.name : "";
|
|
2007
|
+
const type = typeof issue.fields?.issuetype?.name === "string" ? issue.fields.issuetype.name : "";
|
|
2008
|
+
const parent = asString(asRecord(issue.fields?.parent)?.key);
|
|
2009
|
+
const assignee = asString(asRecord(issue.fields?.assignee)?.displayName);
|
|
2010
|
+
if (parsed.jql.includes("type=") && !parsed.jql.includes('type="Epic"')) {
|
|
2011
|
+
const wanted = /type="([^"]+)"/.exec(parsed.jql)?.[1];
|
|
2012
|
+
if (wanted && wanted.toLowerCase() !== type.toLowerCase())
|
|
2013
|
+
return false;
|
|
2014
|
+
}
|
|
2015
|
+
const statusEq = [...parsed.jql.matchAll(/status="([^"]+)"/g)].map((match) => match[1]);
|
|
2016
|
+
const statusNeq = [...parsed.jql.matchAll(/status!="([^"]+)"/g)].map((match) => match[1]);
|
|
2017
|
+
if (statusEq.length && !statusEq.some((name) => name.toLowerCase() === status.toLowerCase())) {
|
|
2018
|
+
return false;
|
|
2019
|
+
}
|
|
2020
|
+
if (statusNeq.some((name) => name.toLowerCase() === status.toLowerCase()))
|
|
2021
|
+
return false;
|
|
2022
|
+
const assigneeWanted = /assignee="([^"]+)"/.exec(parsed.jql)?.[1];
|
|
2023
|
+
if (assigneeWanted && assigneeWanted.toLowerCase() !== (assignee ?? "").toLowerCase()) {
|
|
2024
|
+
return false;
|
|
2025
|
+
}
|
|
2026
|
+
const parentWanted = /parent="([^"]+)"/.exec(parsed.jql)?.[1];
|
|
2027
|
+
if (parentWanted && parentWanted !== parent)
|
|
2028
|
+
return false;
|
|
2029
|
+
return true;
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
async function lookupByKey(key, flags) {
|
|
2033
|
+
const loaded = await loadCatalog(flags);
|
|
2034
|
+
const cached = loaded.works.get(key);
|
|
2035
|
+
if (cached)
|
|
2036
|
+
return cached;
|
|
2037
|
+
if (loaded.modules.has(key))
|
|
2038
|
+
return;
|
|
2039
|
+
const raw = await withResource((resource) => requestJson(`/workspaces/${loaded.workspace}/${resource}/${key}/`));
|
|
2040
|
+
const row = asRecord(raw);
|
|
2041
|
+
if (!row)
|
|
2042
|
+
return;
|
|
2043
|
+
const project = loaded.projects.find((item) => item.id === asString(row.project)) ?? loaded.projects.find((item) => key.toUpperCase().startsWith(`${item.identifier}-`));
|
|
2044
|
+
if (!project)
|
|
2045
|
+
return;
|
|
2046
|
+
const stateById = loaded.states.get(project.id) ?? new Map;
|
|
2047
|
+
const labelById = loaded.labels.get(project.id) ?? new Map;
|
|
2048
|
+
const item = raw;
|
|
2049
|
+
const issue = workItemToIssue(item, project, { states: stateById, labels: labelById });
|
|
2050
|
+
const issueKey = typeof issue?.key === "string" ? issue.key : undefined;
|
|
2051
|
+
if (!issue || !issueKey)
|
|
2052
|
+
return;
|
|
2053
|
+
const next = { issue, item, project };
|
|
2054
|
+
loaded.works.set(issueKey, next);
|
|
2055
|
+
return next;
|
|
2056
|
+
}
|
|
2057
|
+
async function resolveLabelIds(project, names, flags) {
|
|
2058
|
+
if (!names?.length)
|
|
2059
|
+
return [];
|
|
2060
|
+
const loaded = await loadCatalog(flags);
|
|
2061
|
+
const byName = new Map([...loaded.labels.get(project.id)?.values() ?? []].map((label) => [label.name.toLowerCase(), label.id]));
|
|
2062
|
+
return names.map((name) => byName.get(name.toLowerCase())).filter((id) => !!id);
|
|
2063
|
+
}
|
|
2064
|
+
async function resolveStateId(project, status, flags) {
|
|
2065
|
+
const loaded = await loadCatalog(flags);
|
|
2066
|
+
const states = [...loaded.states.get(project.id)?.values() ?? []];
|
|
2067
|
+
return states.find((state) => state.name.toLowerCase() === status.toLowerCase())?.id;
|
|
2068
|
+
}
|
|
2069
|
+
return {
|
|
2070
|
+
async list(flags) {
|
|
2071
|
+
const query = flags || defaultFlags;
|
|
2072
|
+
const loaded = await loadCatalog(query);
|
|
2073
|
+
const issues = filterWorks(query, [...loaded.works.values()]).map((row) => row.issue);
|
|
2074
|
+
return JSON.stringify(issues);
|
|
2075
|
+
},
|
|
2076
|
+
async listEpics(queryFlags = defaultFlags) {
|
|
2077
|
+
const loaded = await loadCatalog(queryFlags || defaultFlags);
|
|
2078
|
+
return JSON.stringify([...loaded.modules.values()].map((row) => row.issue));
|
|
2079
|
+
},
|
|
2080
|
+
async listEpic(key, queryFlags = defaultFlags) {
|
|
2081
|
+
const loaded = await loadCatalog(queryFlags || defaultFlags);
|
|
2082
|
+
const issues = [...loaded.works.values()].filter((row) => row.moduleKey === key).map((row) => row.issue);
|
|
2083
|
+
return JSON.stringify(issues);
|
|
2084
|
+
},
|
|
2085
|
+
async listChildren(keys) {
|
|
2086
|
+
const loaded = await loadCatalog(lastFlags);
|
|
2087
|
+
const wanted = new Set(keys);
|
|
2088
|
+
const issues = [...loaded.works.values()].filter((row) => row.moduleKey && wanted.has(row.moduleKey)).map((row) => row.issue);
|
|
2089
|
+
return JSON.stringify(issues);
|
|
2090
|
+
},
|
|
2091
|
+
async states(flags) {
|
|
2092
|
+
const loaded = await loadCatalog(flags || defaultFlags);
|
|
2093
|
+
return loaded.columns;
|
|
2094
|
+
},
|
|
2095
|
+
async move(key, status) {
|
|
2096
|
+
try {
|
|
2097
|
+
const module = (await loadCatalog(lastFlags)).modules.get(key);
|
|
2098
|
+
if (module) {
|
|
2099
|
+
const mapped = Object.entries({
|
|
2100
|
+
backlog: "backlog",
|
|
2101
|
+
todo: "planned",
|
|
2102
|
+
"to do": "planned",
|
|
2103
|
+
"in progress": "in_progress",
|
|
2104
|
+
paused: "paused",
|
|
2105
|
+
done: "completed",
|
|
2106
|
+
completed: "completed",
|
|
2107
|
+
cancelled: "cancelled",
|
|
2108
|
+
canceled: "cancelled"
|
|
2109
|
+
}).find(([name]) => name === status.toLowerCase())?.[1];
|
|
2110
|
+
if (!mapped)
|
|
2111
|
+
return { ok: false, error: `Unknown Plane module status "${status}"` };
|
|
2112
|
+
await request(`/workspaces/${workspaceOf(lastFlags)}/projects/${module.project.id}/modules/${module.module.id}/`, { method: "PATCH", body: JSON.stringify({ status: mapped }) });
|
|
2113
|
+
catalog = undefined;
|
|
2114
|
+
return { ok: true };
|
|
2115
|
+
}
|
|
2116
|
+
const work = await lookupByKey(key, lastFlags);
|
|
2117
|
+
if (!work)
|
|
2118
|
+
return { ok: false, error: `Work item ${key} not found` };
|
|
2119
|
+
const state = await resolveStateId(work.project, status, lastFlags);
|
|
2120
|
+
if (!state)
|
|
2121
|
+
return { ok: false, error: `Unknown Plane state "${status}" for ${key}` };
|
|
2122
|
+
await withResource((resource) => request(`/workspaces/${workspaceOf(lastFlags)}/projects/${work.project.id}/${resource}/${work.item.id}/`, { method: "PATCH", body: JSON.stringify({ state }) }));
|
|
2123
|
+
catalog = undefined;
|
|
2124
|
+
return { ok: true };
|
|
2125
|
+
} catch (err) {
|
|
2126
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
2127
|
+
}
|
|
2128
|
+
},
|
|
2129
|
+
async create(input) {
|
|
2130
|
+
try {
|
|
2131
|
+
const loaded = await loadCatalog(lastFlags);
|
|
2132
|
+
const parent = input.parent ? loaded.modules.get(input.parent) : undefined;
|
|
2133
|
+
const project = parent?.project ?? loaded.projects[0];
|
|
2134
|
+
if (!project)
|
|
2135
|
+
return { ok: false, error: "No Plane project in Scope" };
|
|
2136
|
+
const state = input.status?.trim() ? await resolveStateId(project, input.status.trim(), lastFlags) : undefined;
|
|
2137
|
+
const labels = await resolveLabelIds(project, input.labels, lastFlags);
|
|
2138
|
+
const body = { name: input.summary };
|
|
2139
|
+
if (input.description)
|
|
2140
|
+
body.description_html = htmlParagraph(input.description);
|
|
2141
|
+
if (state)
|
|
2142
|
+
body.state = state;
|
|
2143
|
+
if (labels.length)
|
|
2144
|
+
body.labels = labels;
|
|
2145
|
+
const created = asRecord(await withResource((resource) => requestJson(`/workspaces/${workspaceOf(lastFlags)}/projects/${project.id}/${resource}/`, {
|
|
2146
|
+
method: "POST",
|
|
2147
|
+
body: JSON.stringify(body)
|
|
2148
|
+
})));
|
|
2149
|
+
const sequence = Number(created?.sequence_id);
|
|
2150
|
+
const key = Number.isFinite(sequence) && sequence > 0 ? planeWorkItemKey(project.identifier, sequence) : undefined;
|
|
2151
|
+
if (parent && created?.id) {
|
|
2152
|
+
await request(`/workspaces/${workspaceOf(lastFlags)}/projects/${project.id}/modules/${parent.module.id}/module-issues/`, { method: "POST", body: JSON.stringify({ issues: [created.id] }) });
|
|
2153
|
+
}
|
|
2154
|
+
catalog = undefined;
|
|
2155
|
+
return key ? { ok: true, key } : { ok: true };
|
|
2156
|
+
} catch (err) {
|
|
2157
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
2158
|
+
}
|
|
2159
|
+
},
|
|
2160
|
+
async edit(key, input) {
|
|
2161
|
+
try {
|
|
2162
|
+
const work = await lookupByKey(key, lastFlags);
|
|
2163
|
+
if (!work)
|
|
2164
|
+
return { ok: false, error: `Work item ${key} not found` };
|
|
2165
|
+
const body = {};
|
|
2166
|
+
if (input.summary !== undefined)
|
|
2167
|
+
body.name = input.summary;
|
|
2168
|
+
if (input.description !== undefined)
|
|
2169
|
+
body.description_html = htmlParagraph(input.description);
|
|
2170
|
+
if (input.labels)
|
|
2171
|
+
body.labels = await resolveLabelIds(work.project, input.labels, lastFlags);
|
|
2172
|
+
await withResource((resource) => request(`/workspaces/${workspaceOf(lastFlags)}/projects/${work.project.id}/${resource}/${work.item.id}/`, { method: "PATCH", body: JSON.stringify(body) }));
|
|
2173
|
+
catalog = undefined;
|
|
2174
|
+
return { ok: true };
|
|
2175
|
+
} catch (err) {
|
|
2176
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
2177
|
+
}
|
|
2178
|
+
},
|
|
2179
|
+
async open(key) {
|
|
2180
|
+
return `${host}/${workspaceOf(lastFlags)}/browse/${key}`;
|
|
2181
|
+
},
|
|
2182
|
+
async view(key) {
|
|
2183
|
+
const loaded = await loadCatalog(lastFlags);
|
|
2184
|
+
const module = loaded.modules.get(key);
|
|
2185
|
+
if (module)
|
|
2186
|
+
return JSON.stringify(module.issue);
|
|
2187
|
+
const work = await lookupByKey(key, lastFlags);
|
|
2188
|
+
if (work)
|
|
2189
|
+
return JSON.stringify(work.issue);
|
|
2190
|
+
throw new Error(`Work item ${key} not found`);
|
|
2191
|
+
}
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
|
|
1461
2195
|
// src/boot.ts
|
|
1462
2196
|
async function createBoardApp(opts) {
|
|
1463
2197
|
const env = opts.env ?? process.env;
|
|
1464
2198
|
const flags = opts.flags ?? "";
|
|
1465
2199
|
const store = IssueStore.fromRaw(opts.raw);
|
|
2200
|
+
const plane = wantsPlane(flags);
|
|
1466
2201
|
const bin = resolveJiraBin(env.JIRA_BIN ?? "jira", env.PATH ?? "");
|
|
1467
|
-
const cli =
|
|
2202
|
+
const cli = plane ? createPlaneCli({
|
|
2203
|
+
host: planeHost(env),
|
|
2204
|
+
apiKey: env.PLANE_API_KEY ?? "",
|
|
2205
|
+
workspace: planeWorkspace(flags, env),
|
|
2206
|
+
flags,
|
|
2207
|
+
fetch: opts.fetch
|
|
2208
|
+
}) : bin ? createJiraCli({
|
|
1468
2209
|
bin,
|
|
1469
2210
|
configPath: env.JIRA_CONFIG_FILE,
|
|
1470
2211
|
token: env.JIRA_API_TOKEN,
|
|
@@ -1474,7 +2215,8 @@ async function createBoardApp(opts) {
|
|
|
1474
2215
|
store,
|
|
1475
2216
|
cli,
|
|
1476
2217
|
flags,
|
|
1477
|
-
jiraConfigPath: jiraCliConfigPath(env)
|
|
2218
|
+
jiraConfigPath: jiraCliConfigPath(env),
|
|
2219
|
+
liveBackend: plane
|
|
1478
2220
|
});
|
|
1479
2221
|
if (opts.piped) {
|
|
1480
2222
|
app.hydrate(opts.raw);
|
|
@@ -1482,10 +2224,12 @@ async function createBoardApp(opts) {
|
|
|
1482
2224
|
const local = createStoreCli(store, flags);
|
|
1483
2225
|
app.hydrate(JSON.parse(await local.list(app.flags)), { fromStore: true });
|
|
1484
2226
|
}
|
|
1485
|
-
return { app, store, kind: bin ? "jira" : "store" };
|
|
2227
|
+
return { app, store, kind: plane ? "plane" : bin ? "jira" : "store" };
|
|
1486
2228
|
}
|
|
1487
2229
|
async function refreshFromJira(app, kind, opts = {}) {
|
|
1488
|
-
if (
|
|
2230
|
+
if (opts.piped)
|
|
2231
|
+
return;
|
|
2232
|
+
if (kind !== "jira" && kind !== "plane")
|
|
1489
2233
|
return;
|
|
1490
2234
|
await app.refresh();
|
|
1491
2235
|
}
|
|
@@ -12563,7 +13307,11 @@ function handleFakeJira(req, res, store) {
|
|
|
12563
13307
|
|
|
12564
13308
|
// src/http.ts
|
|
12565
13309
|
function handleRequest(req, res, ctx) {
|
|
12566
|
-
|
|
13310
|
+
if (handleAppApi(req, res, ctx.app) || handleAgentApi(req, res, ctx.app))
|
|
13311
|
+
return true;
|
|
13312
|
+
if (ctx.kind === "plane")
|
|
13313
|
+
return false;
|
|
13314
|
+
return handleFakeJira(req, res, ctx.store);
|
|
12567
13315
|
}
|
|
12568
13316
|
|
|
12569
13317
|
// src/jira-config.ts
|
|
@@ -12682,7 +13430,7 @@ async function runServer(opts) {
|
|
|
12682
13430
|
flags
|
|
12683
13431
|
});
|
|
12684
13432
|
const server = createServer((req, res) => {
|
|
12685
|
-
if (handleRequest(req, res, { app, store }))
|
|
13433
|
+
if (handleRequest(req, res, { app, store, kind }))
|
|
12686
13434
|
return;
|
|
12687
13435
|
if (opts.fallback) {
|
|
12688
13436
|
opts.fallback(req, res);
|
|
@@ -12693,19 +13441,34 @@ async function runServer(opts) {
|
|
|
12693
13441
|
res.end("pipe-kan UI is missing. Run bun run build.");
|
|
12694
13442
|
}
|
|
12695
13443
|
});
|
|
13444
|
+
if (kind === "plane") {
|
|
13445
|
+
try {
|
|
13446
|
+
await refreshFromJira(app, kind, { piped: Boolean(piped) });
|
|
13447
|
+
} catch (err) {
|
|
13448
|
+
console.error(`${kind} Refresh failed`, err);
|
|
13449
|
+
}
|
|
13450
|
+
const error = app.board().error;
|
|
13451
|
+
if (error)
|
|
13452
|
+
console.error(`${kind} Refresh failed; keeping last live Board`, error);
|
|
13453
|
+
}
|
|
12696
13454
|
const { host, port } = resolveListen();
|
|
12697
13455
|
await bindListen(server, host, port);
|
|
12698
13456
|
const origin = `http://127.0.0.1:${port}`;
|
|
12699
|
-
const fakeConfig = writeJiraConfig(join10(tmpdir3(), "pipe-kan"), origin);
|
|
12700
13457
|
announce(`pipe-kan http://${host}:${port}`);
|
|
12701
|
-
|
|
12702
|
-
|
|
12703
|
-
|
|
13458
|
+
if (kind === "plane") {
|
|
13459
|
+
announce(`cli plane ${planeHost()} ${planeWorkspace(flags)}`);
|
|
13460
|
+
announce(`Plane host default ${DEFAULT_PLANE_HOST}`);
|
|
13461
|
+
} else {
|
|
13462
|
+
const fakeConfig = writeJiraConfig(join10(tmpdir3(), "pipe-kan"), origin);
|
|
13463
|
+
announce(`cli ${kind === "jira" ? resolveJiraBin() : "store"}`);
|
|
13464
|
+
announce(`Fake Jira ${origin}/rest/api/2/search`);
|
|
13465
|
+
announce(`Fake Jira config ${fakeConfig}`);
|
|
13466
|
+
}
|
|
12704
13467
|
if (kind === "jira") {
|
|
12705
13468
|
try {
|
|
12706
13469
|
await refreshFromJira(app, kind, { piped: Boolean(piped) });
|
|
12707
13470
|
} catch (err) {
|
|
12708
|
-
console.error(
|
|
13471
|
+
console.error(`${kind} Refresh failed; keeping Fixture Board`);
|
|
12709
13472
|
console.error(err);
|
|
12710
13473
|
}
|
|
12711
13474
|
}
|