pipe-kan 0.33.1 → 0.34.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 +13 -4
- package/dist/pipe-kan.js +631 -67
- package/dist/ui/assets/index-B4MA2yJG.js +56 -0
- package/dist/ui/assets/index-DD60NIO0.css +2 -0
- package/dist/ui/index.html +2 -2
- package/package.json +3 -2
- package/dist/ui/assets/index-BtuKBXwg.css +0 -2
- package/dist/ui/assets/index-DlebjkVt.js +0 -56
package/dist/pipe-kan.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/server.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync8, writeSync } from "node:fs";
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
6
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
7
7
|
import { join as join11 } from "node:path";
|
|
@@ -156,6 +156,9 @@ function formatDueDate(value) {
|
|
|
156
156
|
function createdAt(fields) {
|
|
157
157
|
return typeof fields?.created === "string" && fields.created ? fields.created : undefined;
|
|
158
158
|
}
|
|
159
|
+
function updatedAt(fields) {
|
|
160
|
+
return typeof fields?.updated === "string" && fields.updated ? fields.updated : undefined;
|
|
161
|
+
}
|
|
159
162
|
function issueType(fields) {
|
|
160
163
|
const name = fields?.issuetype?.name ?? fields?.issueType?.name;
|
|
161
164
|
return typeof name === "string" ? name : undefined;
|
|
@@ -247,6 +250,7 @@ function toCard(issue, key, fieldId) {
|
|
|
247
250
|
const epic = epicKey(issue.fields);
|
|
248
251
|
const labels = issueLabels(issue.fields);
|
|
249
252
|
const created = createdAt(issue.fields);
|
|
253
|
+
const updated = updatedAt(issue.fields);
|
|
250
254
|
return {
|
|
251
255
|
key,
|
|
252
256
|
summary,
|
|
@@ -257,7 +261,8 @@ function toCard(issue, key, fieldId) {
|
|
|
257
261
|
...type ? { type } : {},
|
|
258
262
|
...epic ? { epic } : {},
|
|
259
263
|
...labels ? { labels } : {},
|
|
260
|
-
...created ? { created } : {}
|
|
264
|
+
...created ? { created } : {},
|
|
265
|
+
...updated ? { updated } : {}
|
|
261
266
|
};
|
|
262
267
|
}
|
|
263
268
|
function issuesToBoard(raw, opts = {}) {
|
|
@@ -360,6 +365,8 @@ function parseFlags(flags) {
|
|
|
360
365
|
let type = "";
|
|
361
366
|
let raw = "";
|
|
362
367
|
let projectsValue = "";
|
|
368
|
+
let workspacesValue = "";
|
|
369
|
+
let scopesValue = "";
|
|
363
370
|
let plane = false;
|
|
364
371
|
let workspace = "";
|
|
365
372
|
const statusEq = [];
|
|
@@ -372,6 +379,28 @@ function parseFlags(flags) {
|
|
|
372
379
|
plane = true;
|
|
373
380
|
continue;
|
|
374
381
|
}
|
|
382
|
+
if (token === "--workspaces" || token.startsWith("--workspaces=")) {
|
|
383
|
+
const parts = [];
|
|
384
|
+
if (token.startsWith("--workspaces="))
|
|
385
|
+
parts.push(token.slice("--workspaces=".length));
|
|
386
|
+
while (i + 1 < list.length && !list[i + 1].startsWith("-")) {
|
|
387
|
+
i++;
|
|
388
|
+
parts.push(list[i]);
|
|
389
|
+
}
|
|
390
|
+
workspacesValue = parts.join(",");
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (token === "--scopes" || token.startsWith("--scopes=")) {
|
|
394
|
+
const parts = [];
|
|
395
|
+
if (token.startsWith("--scopes="))
|
|
396
|
+
parts.push(token.slice("--scopes=".length));
|
|
397
|
+
while (i + 1 < list.length && !list[i + 1].startsWith("-")) {
|
|
398
|
+
i++;
|
|
399
|
+
parts.push(list[i]);
|
|
400
|
+
}
|
|
401
|
+
scopesValue = parts.join(",");
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
375
404
|
if (token === "--workspace" || token.startsWith("--workspace=")) {
|
|
376
405
|
const [value, next] = takeValue(list, i, "--workspace");
|
|
377
406
|
workspace = value.startsWith("=") ? value.slice(1) : value;
|
|
@@ -420,9 +449,11 @@ function parseFlags(flags) {
|
|
|
420
449
|
}
|
|
421
450
|
}
|
|
422
451
|
const projects = projectsValue.split(",").map((p) => p.trim().toUpperCase()).filter(Boolean);
|
|
452
|
+
const workspaces = uniqueSlugs(workspacesValue);
|
|
453
|
+
const scopes = parseScopes(scopesValue);
|
|
423
454
|
const workspaceSlug = workspace.trim() || undefined;
|
|
424
455
|
if (raw) {
|
|
425
|
-
return { jql: raw, projects, plane, workspace: workspaceSlug };
|
|
456
|
+
return { jql: raw, projects, plane, workspace: workspaceSlug, workspaces, scopes };
|
|
426
457
|
}
|
|
427
458
|
const projectClause = projects.length ? `project in (${projects.map((p) => `"${p}"`).join(", ")})` : "";
|
|
428
459
|
const clauses = [];
|
|
@@ -438,7 +469,47 @@ function parseFlags(flags) {
|
|
|
438
469
|
clauses.push(`status="${status}"`);
|
|
439
470
|
for (const status of statusNeq)
|
|
440
471
|
clauses.push(`status!="${status}"`);
|
|
441
|
-
return { jql: clauses.join(" AND "), projects, plane, workspace: workspaceSlug };
|
|
472
|
+
return { jql: clauses.join(" AND "), projects, plane, workspace: workspaceSlug, workspaces, scopes };
|
|
473
|
+
}
|
|
474
|
+
function uniqueSlugs(value) {
|
|
475
|
+
const seen = new Set;
|
|
476
|
+
const slugs = [];
|
|
477
|
+
for (const part of value.split(",")) {
|
|
478
|
+
const slug = part.trim().toLowerCase();
|
|
479
|
+
if (!slug || seen.has(slug))
|
|
480
|
+
continue;
|
|
481
|
+
seen.add(slug);
|
|
482
|
+
slugs.push(slug);
|
|
483
|
+
}
|
|
484
|
+
return slugs;
|
|
485
|
+
}
|
|
486
|
+
function parseScopes(value) {
|
|
487
|
+
const seen = new Set;
|
|
488
|
+
const scopes = [];
|
|
489
|
+
for (const part of value.split(",")) {
|
|
490
|
+
const trimmed = part.trim();
|
|
491
|
+
if (!trimmed)
|
|
492
|
+
continue;
|
|
493
|
+
const slash = trimmed.indexOf("/");
|
|
494
|
+
if (slash <= 0)
|
|
495
|
+
continue;
|
|
496
|
+
const workspace = trimmed.slice(0, slash).trim().toLowerCase();
|
|
497
|
+
const project = trimmed.slice(slash + 1).trim().toUpperCase();
|
|
498
|
+
if (!workspace || !project)
|
|
499
|
+
continue;
|
|
500
|
+
const key = `${workspace}/${project}`;
|
|
501
|
+
if (seen.has(key))
|
|
502
|
+
continue;
|
|
503
|
+
seen.add(key);
|
|
504
|
+
scopes.push({ workspace, project });
|
|
505
|
+
}
|
|
506
|
+
return scopes;
|
|
507
|
+
}
|
|
508
|
+
function setPlaneFlag(flags, on) {
|
|
509
|
+
const list = tokens(flags.trim()).filter((token) => token !== "--plane");
|
|
510
|
+
if (on)
|
|
511
|
+
list.unshift("--plane");
|
|
512
|
+
return list.join(" ");
|
|
442
513
|
}
|
|
443
514
|
function wantsPlane(flags) {
|
|
444
515
|
return parseFlags(flags).plane;
|
|
@@ -621,6 +692,11 @@ Available states for issue ${key}: ${available}`
|
|
|
621
692
|
issue.fields.description = input.description;
|
|
622
693
|
if (input.labels !== undefined)
|
|
623
694
|
issue.fields.labels = input.labels;
|
|
695
|
+
if (input.priority !== undefined)
|
|
696
|
+
issue.fields.priority = { name: input.priority };
|
|
697
|
+
if (input.assignee !== undefined) {
|
|
698
|
+
issue.fields.assignee = input.assignee ? { displayName: input.assignee } : undefined;
|
|
699
|
+
}
|
|
624
700
|
return { ok: true };
|
|
625
701
|
}
|
|
626
702
|
}
|
|
@@ -972,6 +1048,10 @@ ${result.stderr}`);
|
|
|
972
1048
|
for (const label of input.labels)
|
|
973
1049
|
args.push("-l", label);
|
|
974
1050
|
}
|
|
1051
|
+
if (input.assignee !== undefined)
|
|
1052
|
+
args.push("-a", input.assignee);
|
|
1053
|
+
if (input.priority !== undefined)
|
|
1054
|
+
args.push("--priority", input.priority);
|
|
975
1055
|
const result = await runRetry(args);
|
|
976
1056
|
if (result.code !== 0) {
|
|
977
1057
|
return { ok: false, error: (result.stderr || result.stdout).trim() };
|
|
@@ -1518,11 +1598,246 @@ function createApp(opts) {
|
|
|
1518
1598
|
},
|
|
1519
1599
|
async listProjectIdentifiers(workspace) {
|
|
1520
1600
|
return cli.listProjectIdentifiers?.(workspace) ?? [];
|
|
1601
|
+
},
|
|
1602
|
+
async listProjects(workspace) {
|
|
1603
|
+
return cli.listProjects?.(workspace) ?? [];
|
|
1604
|
+
},
|
|
1605
|
+
async parity() {
|
|
1606
|
+
return cli.parity?.() ?? { initiatives: [], projects: [], workspaces: [] };
|
|
1521
1607
|
}
|
|
1522
1608
|
};
|
|
1523
1609
|
return app;
|
|
1524
1610
|
}
|
|
1525
1611
|
|
|
1612
|
+
// src/plane-fixture.ts
|
|
1613
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
1614
|
+
var STATES = [
|
|
1615
|
+
{ id: "st-backlog", name: "Backlog", group: "backlog", sequence: 1 },
|
|
1616
|
+
{ id: "st-todo", name: "Todo", group: "unstarted", sequence: 2 },
|
|
1617
|
+
{ id: "st-progress", name: "In Progress", group: "started", sequence: 3 },
|
|
1618
|
+
{ id: "st-done", name: "Done", group: "completed", sequence: 4 },
|
|
1619
|
+
{ id: "st-cancelled", name: "Cancelled", group: "cancelled", sequence: 5 }
|
|
1620
|
+
];
|
|
1621
|
+
function mod(id, name, status, extra = {}) {
|
|
1622
|
+
return { id, name, status, created_at: "2026-01-04T00:00:00Z", updated_at: "2026-02-02T00:00:00Z", ...extra };
|
|
1623
|
+
}
|
|
1624
|
+
function issue(id, name, sequence, state, extra = {}) {
|
|
1625
|
+
return { id, name, sequence_id: sequence, state, created_at: "2026-01-06T00:00:00Z", updated_at: "2026-03-03T00:00:00Z", ...extra };
|
|
1626
|
+
}
|
|
1627
|
+
var SYNTHETIC_PLANE = {
|
|
1628
|
+
workspaces: [
|
|
1629
|
+
{ id: "ws-work", name: "Work", slug: "work" },
|
|
1630
|
+
{ id: "ws-personal", name: "Personal", slug: "personal" }
|
|
1631
|
+
],
|
|
1632
|
+
projects: {
|
|
1633
|
+
work: [
|
|
1634
|
+
{ id: "work-pulse", identifier: "PULSE", name: "Pulse" },
|
|
1635
|
+
{ id: "work-aph", identifier: "APH", name: "Aphrodite" }
|
|
1636
|
+
],
|
|
1637
|
+
personal: [
|
|
1638
|
+
{ id: "personal-pulse", identifier: "PULSE", name: "Pulse" },
|
|
1639
|
+
{ id: "personal-pkan", identifier: "PKAN", name: "Pipe kan" }
|
|
1640
|
+
]
|
|
1641
|
+
},
|
|
1642
|
+
states: STATES,
|
|
1643
|
+
labels: [
|
|
1644
|
+
{ id: "lbl-needs", name: "needs-input" },
|
|
1645
|
+
{ id: "lbl-roadmap", name: "roadmap" }
|
|
1646
|
+
],
|
|
1647
|
+
members: [
|
|
1648
|
+
{ id: "user-ada", display_name: "Ada" },
|
|
1649
|
+
{ id: "user-grace", display_name: "Grace" }
|
|
1650
|
+
],
|
|
1651
|
+
modules: {
|
|
1652
|
+
"work-pulse": [
|
|
1653
|
+
mod("mod-00000000000000000000000000000001", "Backlog objective", "backlog", { priority: "low" }),
|
|
1654
|
+
mod("mod-00000000000000000000000000000002", "Planned objective", "todo", { priority: "medium", start_date: "2026-04-01", target_date: "2026-12-01" }),
|
|
1655
|
+
mod("mod-00000000000000000000000000000003", "Slipped objective", "in_progress", { priority: "high", start_date: "2025-01-01", target_date: "2025-06-01" }),
|
|
1656
|
+
mod("mod-00000000000000000000000000000004", "On track objective", "in_progress", { priority: "urgent", start_date: "2026-03-01", target_date: "2026-11-15" }),
|
|
1657
|
+
mod("mod-00000000000000000000000000000005", "Shipped objective", "done", { priority: "medium", start_date: "2025-11-01", target_date: "2026-01-20" }),
|
|
1658
|
+
mod("mod-00000000000000000000000000000006", "Dropped objective", "cancelled", { priority: "low", target_date: "2025-03-01" }),
|
|
1659
|
+
mod("mod-00000000000000000000000000000007", "Undated objective", "in_progress", { priority: "low", start_date: "2026-05-01" })
|
|
1660
|
+
],
|
|
1661
|
+
"personal-pulse": [
|
|
1662
|
+
mod("mod-00000000000000000000000000000011", "Home objective", "in_progress", { priority: "medium", start_date: "2026-02-01", target_date: "2026-08-01" })
|
|
1663
|
+
],
|
|
1664
|
+
"work-aph": [
|
|
1665
|
+
mod("mod-00000000000000000000000000000021", "Aphrodite objective", "todo", { priority: "high", target_date: "2026-07-01" })
|
|
1666
|
+
],
|
|
1667
|
+
"personal-pkan": []
|
|
1668
|
+
},
|
|
1669
|
+
issues: {
|
|
1670
|
+
"work-pulse": [
|
|
1671
|
+
issue("wi-work-1", "Work pulse story", 1, "st-progress", { priority: "high", module_ids: ["mod-00000000000000000000000000000004"], assignee: "Ada", labels: ["lbl-roadmap"], target_date: "2026-11-01" }),
|
|
1672
|
+
issue("wi-work-2", "Work pulse done", 2, "st-done", { priority: "low", module_ids: ["mod-00000000000000000000000000000005"], assignee: "Grace" })
|
|
1673
|
+
],
|
|
1674
|
+
"personal-pulse": [
|
|
1675
|
+
issue("wi-home-1", "Personal pulse story", 1, "st-todo", { priority: "medium", module_ids: ["mod-00000000000000000000000000000011"], assignee: "Ada", labels: ["lbl-needs"], target_date: "2026-08-01" })
|
|
1676
|
+
],
|
|
1677
|
+
"work-aph": [issue("wi-aph-1", "Aphrodite story", 4, "st-backlog", { priority: "medium" })],
|
|
1678
|
+
"personal-pkan": [issue("wi-pkan-1", "Pipe kan story", 3, "st-todo", { priority: "low", assignee: "Grace" })]
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
function page(results) {
|
|
1682
|
+
return { results, next_page_results: false, next_cursor: "" };
|
|
1683
|
+
}
|
|
1684
|
+
function planeFixtureFetch(fixture = SYNTHETIC_PLANE) {
|
|
1685
|
+
const data = JSON.parse(JSON.stringify(fixture));
|
|
1686
|
+
const stateById = new Map(data.states.map((state) => [state.id, state]));
|
|
1687
|
+
const labelById = new Map(data.labels.map((label) => [label.id, label]));
|
|
1688
|
+
return async (input, init) => {
|
|
1689
|
+
const url = new URL(String(input), "https://plane.test");
|
|
1690
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
1691
|
+
const path = url.pathname.replace(/\/+$/, "") || "/";
|
|
1692
|
+
const parts = path.split("/").filter(Boolean);
|
|
1693
|
+
let body;
|
|
1694
|
+
if (init?.body)
|
|
1695
|
+
body = JSON.parse(String(init.body));
|
|
1696
|
+
if (method === "GET" && parts[2] === "users" && parts[4] === "workspaces") {
|
|
1697
|
+
return Response.json(data.workspaces);
|
|
1698
|
+
}
|
|
1699
|
+
if (method === "GET" && parts[2] === "workspaces" && parts.length === 3) {
|
|
1700
|
+
return Response.json(page(data.workspaces));
|
|
1701
|
+
}
|
|
1702
|
+
if (method === "GET" && /\/workspaces\/[^/]+\/projects$/.test(path)) {
|
|
1703
|
+
return Response.json(page(data.projects[parts[3] ?? ""] ?? []));
|
|
1704
|
+
}
|
|
1705
|
+
if (method === "GET" && parts[4] === "members" && parts.length === 5)
|
|
1706
|
+
return Response.json(page(data.members));
|
|
1707
|
+
const projectId = parts[5] ?? "";
|
|
1708
|
+
const collection = parts[6];
|
|
1709
|
+
const itemId = parts[7];
|
|
1710
|
+
const projectIssues = data.issues[projectId] ?? [];
|
|
1711
|
+
const projectModules = data.modules[projectId] ?? [];
|
|
1712
|
+
if (method === "GET" && collection === "states")
|
|
1713
|
+
return Response.json(page(data.states));
|
|
1714
|
+
if (method === "GET" && collection === "labels")
|
|
1715
|
+
return Response.json(page(data.labels));
|
|
1716
|
+
if (method === "GET" && collection === "modules" && !itemId)
|
|
1717
|
+
return Response.json(page(projectModules));
|
|
1718
|
+
if (method === "GET" && collection === "modules" && parts[8] === "module-issues") {
|
|
1719
|
+
const ids = projectIssues.filter((row) => row.module_ids?.includes(itemId ?? "")).map((row) => row.id);
|
|
1720
|
+
return Response.json(page(ids.map((id) => ({ issue: id, module: itemId }))));
|
|
1721
|
+
}
|
|
1722
|
+
if (method === "GET" && (collection === "work-items" || collection === "issues") && !itemId) {
|
|
1723
|
+
return Response.json(page(projectIssues.map((row) => toWorkItem(row, stateById, labelById))));
|
|
1724
|
+
}
|
|
1725
|
+
if (method === "PATCH" && collection === "modules" && itemId) {
|
|
1726
|
+
const module = projectModules.find((row) => row.id === itemId);
|
|
1727
|
+
if (module && typeof body?.status === "string")
|
|
1728
|
+
module.status = body.status;
|
|
1729
|
+
return Response.json({ id: itemId, ...body });
|
|
1730
|
+
}
|
|
1731
|
+
if (method === "PATCH" && (collection === "work-items" || collection === "issues") && itemId) {
|
|
1732
|
+
const issueRow = projectIssues.find((row) => row.id === itemId);
|
|
1733
|
+
if (issueRow && typeof body?.state === "string")
|
|
1734
|
+
issueRow.state = body.state;
|
|
1735
|
+
if (issueRow && typeof body?.priority === "string")
|
|
1736
|
+
issueRow.priority = body.priority;
|
|
1737
|
+
if (issueRow && typeof body?.name === "string")
|
|
1738
|
+
issueRow.name = body.name;
|
|
1739
|
+
return Response.json({ id: itemId, ...body });
|
|
1740
|
+
}
|
|
1741
|
+
if (method === "POST" && (collection === "work-items" || collection === "issues") && !itemId) {
|
|
1742
|
+
const sequence = 100 + projectIssues.length;
|
|
1743
|
+
projectIssues.push(issue(`wi-new-${sequence}`, String(body?.name ?? "New"), sequence, "st-todo"));
|
|
1744
|
+
return Response.json({ id: `wi-new-${sequence}`, sequence_id: sequence, name: body?.name }, { status: 201 });
|
|
1745
|
+
}
|
|
1746
|
+
return new Response(`unhandled ${method} ${path}`, { status: 404 });
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
function toWorkItem(row, states, labels) {
|
|
1750
|
+
const state = states.get(row.state);
|
|
1751
|
+
return {
|
|
1752
|
+
id: row.id,
|
|
1753
|
+
name: row.name,
|
|
1754
|
+
sequence_id: row.sequence_id,
|
|
1755
|
+
priority: row.priority,
|
|
1756
|
+
state: state ? { id: state.id, name: state.name } : row.state,
|
|
1757
|
+
labels: row.labels ?? [],
|
|
1758
|
+
label_details: (row.labels ?? []).map((id) => labels.get(id)).filter(Boolean),
|
|
1759
|
+
module_ids: row.module_ids ?? [],
|
|
1760
|
+
assignees: row.assignee ? [{ display_name: row.assignee }] : [],
|
|
1761
|
+
created_at: row.created_at,
|
|
1762
|
+
updated_at: row.updated_at,
|
|
1763
|
+
target_date: row.target_date,
|
|
1764
|
+
start_date: row.start_date
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
function resolvePlaneFetch(env = process.env) {
|
|
1768
|
+
const fixture = env.PLANE_FIXTURE?.trim();
|
|
1769
|
+
if (!fixture)
|
|
1770
|
+
return;
|
|
1771
|
+
if (fixture === "synthetic")
|
|
1772
|
+
return planeFixtureFetch(SYNTHETIC_PLANE);
|
|
1773
|
+
const parsed = JSON.parse(readFileSync3(fixture, "utf8"));
|
|
1774
|
+
return planeFixtureFetch(parsed);
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// src/parity.ts
|
|
1778
|
+
function normalizeStatus(status) {
|
|
1779
|
+
return status.trim().toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ");
|
|
1780
|
+
}
|
|
1781
|
+
function parseDay(value) {
|
|
1782
|
+
if (!value)
|
|
1783
|
+
return;
|
|
1784
|
+
const day = /^(\d{4})-(\d{2})-(\d{2})/.exec(value.trim());
|
|
1785
|
+
if (day)
|
|
1786
|
+
return new Date(Number(day[1]), Number(day[2]) - 1, Number(day[3]));
|
|
1787
|
+
const parsed = new Date(value);
|
|
1788
|
+
if (Number.isNaN(parsed.getTime()))
|
|
1789
|
+
return;
|
|
1790
|
+
return new Date(parsed.getFullYear(), parsed.getMonth(), parsed.getDate());
|
|
1791
|
+
}
|
|
1792
|
+
function startOfToday(now) {
|
|
1793
|
+
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
1794
|
+
}
|
|
1795
|
+
function computeHealth(status, targetDate, now = new Date) {
|
|
1796
|
+
const key = normalizeStatus(status);
|
|
1797
|
+
if (key === "done" || key === "completed" || key === "complete")
|
|
1798
|
+
return "green";
|
|
1799
|
+
if (key === "cancelled" || key === "canceled")
|
|
1800
|
+
return "red";
|
|
1801
|
+
if (key === "in progress" || key === "started" || key === "active" || key === "paused") {
|
|
1802
|
+
const target = parseDay(targetDate);
|
|
1803
|
+
if (key === "paused")
|
|
1804
|
+
return "yellow";
|
|
1805
|
+
if (target && target.getTime() < startOfToday(now).getTime())
|
|
1806
|
+
return "yellow";
|
|
1807
|
+
return "green";
|
|
1808
|
+
}
|
|
1809
|
+
if (key === "todo" || key === "to do" || key === "backlog" || key === "planned" || key === "unstarted" || key === "") {
|
|
1810
|
+
return "gray";
|
|
1811
|
+
}
|
|
1812
|
+
return "gray";
|
|
1813
|
+
}
|
|
1814
|
+
function isCompletedStatus(status) {
|
|
1815
|
+
const key = normalizeStatus(status);
|
|
1816
|
+
return key === "done" || key === "completed" || key === "complete" || key === "cancelled" || key === "canceled";
|
|
1817
|
+
}
|
|
1818
|
+
function sourceOptions(input) {
|
|
1819
|
+
return [
|
|
1820
|
+
{
|
|
1821
|
+
id: "plane",
|
|
1822
|
+
label: "Plane",
|
|
1823
|
+
enabled: input.planeKey,
|
|
1824
|
+
...input.planeKey ? {} : { reason: "Set PLANE_API_KEY to use Plane" }
|
|
1825
|
+
},
|
|
1826
|
+
{
|
|
1827
|
+
id: "jira",
|
|
1828
|
+
label: "Jira",
|
|
1829
|
+
enabled: true,
|
|
1830
|
+
...input.jiraBin ? {} : { reason: "jira-cli is not on PATH; Fake Jira is used" }
|
|
1831
|
+
},
|
|
1832
|
+
{
|
|
1833
|
+
id: "taskwarrior",
|
|
1834
|
+
label: "Taskwarrior",
|
|
1835
|
+
enabled: false,
|
|
1836
|
+
reason: "Taskwarrior is not connected"
|
|
1837
|
+
}
|
|
1838
|
+
];
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1526
1841
|
// src/plane.ts
|
|
1527
1842
|
var DEFAULT_PLANE_HOST = "https://plane.tail48fe8.ts.net";
|
|
1528
1843
|
var DEFAULT_PLANE_WORKSPACE = "personal";
|
|
@@ -1692,12 +2007,14 @@ function workItemToIssue(item, project, opts) {
|
|
|
1692
2007
|
...opts.moduleKey ? { parent: { key: opts.moduleKey } } : {},
|
|
1693
2008
|
...labels.length ? { labels } : {},
|
|
1694
2009
|
...asString(item.created_at) ? { created: item.created_at } : {},
|
|
2010
|
+
...asString(item.updated_at) ? { updated: item.updated_at } : {},
|
|
1695
2011
|
...description ? { description } : {}
|
|
1696
2012
|
}
|
|
1697
2013
|
};
|
|
1698
2014
|
}
|
|
1699
2015
|
function moduleToIssue(module, project) {
|
|
1700
2016
|
const target = asString(module.target_date);
|
|
2017
|
+
const priority = titleCasePriority(module.priority);
|
|
1701
2018
|
return {
|
|
1702
2019
|
key: planeModuleKey(project.identifier, module.id),
|
|
1703
2020
|
fields: {
|
|
@@ -1705,8 +2022,10 @@ function moduleToIssue(module, project) {
|
|
|
1705
2022
|
status: { name: moduleStatusName(module.status) },
|
|
1706
2023
|
issuetype: { name: "Epic" },
|
|
1707
2024
|
issueType: { name: "Epic" },
|
|
2025
|
+
...priority ? { priority: { name: priority } } : {},
|
|
1708
2026
|
...target ? { duedate: target, "Target End Date": target } : {},
|
|
1709
|
-
...asString(module.created_at) ? { created: module.created_at } : {}
|
|
2027
|
+
...asString(module.created_at) ? { created: module.created_at } : {},
|
|
2028
|
+
...asString(module.updated_at) ? { updated: module.updated_at } : {}
|
|
1710
2029
|
}
|
|
1711
2030
|
};
|
|
1712
2031
|
}
|
|
@@ -1936,25 +2255,76 @@ function createPlaneCli(opts) {
|
|
|
1936
2255
|
status: asString(row?.status),
|
|
1937
2256
|
created_at: asString(row?.created_at),
|
|
1938
2257
|
target_date: asString(row?.target_date),
|
|
1939
|
-
start_date: asString(row?.start_date)
|
|
2258
|
+
start_date: asString(row?.start_date),
|
|
2259
|
+
priority: asString(row?.priority),
|
|
2260
|
+
updated_at: asString(row?.updated_at)
|
|
1940
2261
|
};
|
|
1941
2262
|
}
|
|
2263
|
+
function catalogPlan(flags) {
|
|
2264
|
+
const parsed = parseFlags(flags || defaultFlags);
|
|
2265
|
+
if (parsed.scopes.length) {
|
|
2266
|
+
return {
|
|
2267
|
+
slugs: [...new Set(parsed.scopes.map((scope) => scope.workspace))],
|
|
2268
|
+
pairs: parsed.scopes
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
if (parsed.workspaces.length)
|
|
2272
|
+
return { slugs: parsed.workspaces, pairs: null };
|
|
2273
|
+
return { slugs: [workspaceOf(flags)], pairs: null };
|
|
2274
|
+
}
|
|
2275
|
+
function exposeKey(slug, key, multi) {
|
|
2276
|
+
return multi ? `${slug}/${key}` : key;
|
|
2277
|
+
}
|
|
2278
|
+
function prefixIssue(issue, slug, multi) {
|
|
2279
|
+
if (!multi || typeof issue.key !== "string")
|
|
2280
|
+
return issue;
|
|
2281
|
+
const parent = asRecord(issue.fields?.parent);
|
|
2282
|
+
const parentKey = asString(parent?.key);
|
|
2283
|
+
return {
|
|
2284
|
+
...issue,
|
|
2285
|
+
key: exposeKey(slug, issue.key, true),
|
|
2286
|
+
fields: {
|
|
2287
|
+
...issue.fields,
|
|
2288
|
+
...parentKey ? { parent: { ...parent, key: exposeKey(slug, parentKey, true) } } : {}
|
|
2289
|
+
}
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
const projectCache = new Map;
|
|
2293
|
+
const cacheTtlMs = opts.cacheTtlMs ?? 30000;
|
|
2294
|
+
async function projectsFor(slug) {
|
|
2295
|
+
const hit = projectCache.get(slug);
|
|
2296
|
+
const now = nowFn();
|
|
2297
|
+
if (cacheTtlMs > 0 && hit && now - hit.at < cacheTtlMs)
|
|
2298
|
+
return hit.rows;
|
|
2299
|
+
const rows = (await paginate(`/workspaces/${slug}/projects/`)).map(parseProject).filter((row) => !!row).map((project) => ({ ...project, workspace: slug }));
|
|
2300
|
+
projectCache.set(slug, { at: now, rows });
|
|
2301
|
+
return rows;
|
|
2302
|
+
}
|
|
1942
2303
|
async function loadCatalog(flags) {
|
|
1943
2304
|
lastFlags = flags || defaultFlags;
|
|
1944
|
-
const
|
|
1945
|
-
|
|
2305
|
+
const plan = catalogPlan(lastFlags);
|
|
2306
|
+
const cacheKey = plan.slugs.join(",");
|
|
2307
|
+
if (catalog && catalog.flags === lastFlags && catalog.cacheKey === cacheKey)
|
|
1946
2308
|
return catalog;
|
|
1947
|
-
const
|
|
2309
|
+
const multi = plan.slugs.length > 1;
|
|
2310
|
+
const projects = [];
|
|
1948
2311
|
const states = new Map;
|
|
1949
2312
|
const labels = new Map;
|
|
1950
2313
|
const works = new Map;
|
|
1951
2314
|
const modules = new Map;
|
|
1952
2315
|
const columnStates = [];
|
|
2316
|
+
for (const slug of plan.slugs) {
|
|
2317
|
+
const listed = await projectsFor(slug);
|
|
2318
|
+
const chosen = plan.pairs ? listed.filter((project) => plan.pairs.some((pair) => pair.workspace === slug && pair.project === project.identifier.toUpperCase())) : scopedProjects(listed, lastFlags);
|
|
2319
|
+
for (const project of chosen)
|
|
2320
|
+
projects.push(project);
|
|
2321
|
+
}
|
|
1953
2322
|
for (const project of projects) {
|
|
1954
|
-
const
|
|
1955
|
-
const
|
|
1956
|
-
const
|
|
1957
|
-
const
|
|
2323
|
+
const slug = project.workspace || plan.slugs[0] || workspaceOf(lastFlags);
|
|
2324
|
+
const stateRows = await paginate(`/workspaces/${slug}/projects/${project.id}/states/`);
|
|
2325
|
+
const labelRows = await paginate(`/workspaces/${slug}/projects/${project.id}/labels/`);
|
|
2326
|
+
const moduleRows = await paginate(`/workspaces/${slug}/projects/${project.id}/modules/`);
|
|
2327
|
+
const workRows = await withResource((resource) => paginate(queryPath(`/workspaces/${slug}/projects/${project.id}/${resource}/`, {
|
|
1958
2328
|
expand: "assignees,labels,state,module"
|
|
1959
2329
|
})));
|
|
1960
2330
|
const stateById = new Map;
|
|
@@ -1976,7 +2346,7 @@ function createPlaneCli(opts) {
|
|
|
1976
2346
|
const module = parseModule(row);
|
|
1977
2347
|
if (!module)
|
|
1978
2348
|
continue;
|
|
1979
|
-
const issue = moduleToIssue(module, project);
|
|
2349
|
+
const issue = prefixIssue(moduleToIssue(module, project), slug, multi);
|
|
1980
2350
|
const cached = { issue, module, project };
|
|
1981
2351
|
moduleById.set(module.id, cached);
|
|
1982
2352
|
modules.set(String(issue.key), cached);
|
|
@@ -1988,7 +2358,7 @@ function createPlaneCli(opts) {
|
|
|
1988
2358
|
const issueByModule = new Map;
|
|
1989
2359
|
if (missingModules && moduleById.size) {
|
|
1990
2360
|
for (const moduleId of moduleById.keys()) {
|
|
1991
|
-
const rows = await paginate(`/workspaces/${
|
|
2361
|
+
const rows = await paginate(`/workspaces/${slug}/projects/${project.id}/modules/${moduleId}/module-issues/`);
|
|
1992
2362
|
const ids = rows.map((row) => asId(asRecord(row)?.issue) ?? asId(row)).filter((id) => !!id);
|
|
1993
2363
|
issueByModule.set(moduleId, ids);
|
|
1994
2364
|
}
|
|
@@ -1998,26 +2368,29 @@ function createPlaneCli(opts) {
|
|
|
1998
2368
|
const fromItem = moduleIdsOf(item)[0];
|
|
1999
2369
|
const fromJoin = [...issueByModule.entries()].find(([, ids]) => ids.includes(item.id))?.[0];
|
|
2000
2370
|
const moduleId = fromItem ?? fromJoin;
|
|
2001
|
-
const
|
|
2371
|
+
const bareModuleKey = moduleId ? planeModuleKey(project.identifier, moduleId) : undefined;
|
|
2002
2372
|
const issue = workItemToIssue(item, project, {
|
|
2003
2373
|
states: stateById,
|
|
2004
2374
|
labels: labelById,
|
|
2005
|
-
|
|
2375
|
+
...bareModuleKey ? { moduleKey: bareModuleKey } : {}
|
|
2006
2376
|
});
|
|
2007
|
-
const
|
|
2008
|
-
|
|
2377
|
+
const stamped = issue ? prefixIssue(issue, slug, multi) : undefined;
|
|
2378
|
+
const issueKey = typeof stamped?.key === "string" ? stamped.key : undefined;
|
|
2379
|
+
if (!stamped || !issueKey)
|
|
2009
2380
|
continue;
|
|
2381
|
+
const linked = bareModuleKey ? exposeKey(slug, bareModuleKey, multi) : undefined;
|
|
2010
2382
|
works.set(issueKey, {
|
|
2011
|
-
issue,
|
|
2383
|
+
issue: stamped,
|
|
2012
2384
|
item,
|
|
2013
2385
|
project,
|
|
2014
|
-
...
|
|
2386
|
+
...linked ? { moduleKey: linked } : {}
|
|
2015
2387
|
});
|
|
2016
2388
|
}
|
|
2017
2389
|
}
|
|
2018
2390
|
catalog = {
|
|
2019
2391
|
flags: lastFlags,
|
|
2020
|
-
workspace,
|
|
2392
|
+
workspace: plan.slugs[0] || workspaceOf(lastFlags),
|
|
2393
|
+
cacheKey,
|
|
2021
2394
|
projects,
|
|
2022
2395
|
states,
|
|
2023
2396
|
labels,
|
|
@@ -2027,6 +2400,73 @@ function createPlaneCli(opts) {
|
|
|
2027
2400
|
};
|
|
2028
2401
|
return catalog;
|
|
2029
2402
|
}
|
|
2403
|
+
function projectSlug(project) {
|
|
2404
|
+
return project.workspace || workspaceOf(lastFlags);
|
|
2405
|
+
}
|
|
2406
|
+
function buildParity(loaded) {
|
|
2407
|
+
const projects = loaded.projects.map((project) => {
|
|
2408
|
+
const issues = [...loaded.works.values()].filter((row) => row.project.id === project.id && row.project.workspace === project.workspace);
|
|
2409
|
+
const modules = [...loaded.modules.values()].filter((row) => row.project.id === project.id && row.project.workspace === project.workspace);
|
|
2410
|
+
const total = issues.length;
|
|
2411
|
+
const completed = issues.filter((row) => {
|
|
2412
|
+
const name = typeof row.issue.fields?.status?.name === "string" ? row.issue.fields.status.name : "";
|
|
2413
|
+
const key = name.toLowerCase();
|
|
2414
|
+
return key === "done" || key === "completed";
|
|
2415
|
+
}).length;
|
|
2416
|
+
const started = issues.some((row) => {
|
|
2417
|
+
const name = typeof row.issue.fields?.status?.name === "string" ? row.issue.fields.status.name : "";
|
|
2418
|
+
return name.toLowerCase() === "in progress";
|
|
2419
|
+
});
|
|
2420
|
+
const status = total > 0 && completed === total ? "done" : started ? "in_progress" : "backlog";
|
|
2421
|
+
const dates = [
|
|
2422
|
+
...modules.map((row) => row.module.target_date),
|
|
2423
|
+
...issues.map((row) => row.item.target_date)
|
|
2424
|
+
].filter((value) => Boolean(value));
|
|
2425
|
+
const starts = [
|
|
2426
|
+
...modules.map((row) => row.module.start_date),
|
|
2427
|
+
...issues.map((row) => row.item.start_date)
|
|
2428
|
+
].filter((value) => Boolean(value));
|
|
2429
|
+
const targetDate = dates.sort().at(-1);
|
|
2430
|
+
const startDate = starts.sort()[0];
|
|
2431
|
+
return {
|
|
2432
|
+
workspace: project.workspace || loaded.workspace,
|
|
2433
|
+
id: project.id,
|
|
2434
|
+
identifier: project.identifier,
|
|
2435
|
+
name: project.name,
|
|
2436
|
+
total,
|
|
2437
|
+
completed,
|
|
2438
|
+
status,
|
|
2439
|
+
...startDate ? { startDate } : {},
|
|
2440
|
+
...targetDate ? { targetDate } : {},
|
|
2441
|
+
health: computeHealth(status, targetDate)
|
|
2442
|
+
};
|
|
2443
|
+
});
|
|
2444
|
+
const active = new Map(projects.map((project) => [`${project.workspace}/${project.id}`, project.completed < project.total || project.total === 0]));
|
|
2445
|
+
const initiatives = [...loaded.modules.values()].map((row) => {
|
|
2446
|
+
const raw = row.module.status ?? "backlog";
|
|
2447
|
+
const priority = titleCasePriority(row.module.priority);
|
|
2448
|
+
const open = active.get(`${row.project.workspace}/${row.project.id}`) !== false;
|
|
2449
|
+
return {
|
|
2450
|
+
key: String(row.issue.key),
|
|
2451
|
+
name: row.module.name,
|
|
2452
|
+
workspace: row.project.workspace || loaded.workspace,
|
|
2453
|
+
project: row.project.identifier,
|
|
2454
|
+
status: moduleStatusName(raw),
|
|
2455
|
+
rawStatus: raw,
|
|
2456
|
+
...priority ? { priority } : {},
|
|
2457
|
+
...row.module.start_date ? { startDate: row.module.start_date } : {},
|
|
2458
|
+
...row.module.target_date ? { targetDate: row.module.target_date } : {},
|
|
2459
|
+
...row.module.updated_at ? { updated: row.module.updated_at } : {},
|
|
2460
|
+
health: computeHealth(raw, row.module.target_date),
|
|
2461
|
+
activeProjects: isCompletedStatus(raw) || !open ? 0 : 1
|
|
2462
|
+
};
|
|
2463
|
+
});
|
|
2464
|
+
return {
|
|
2465
|
+
initiatives,
|
|
2466
|
+
projects,
|
|
2467
|
+
workspaces: [...new Set(loaded.projects.map((project) => project.workspace || loaded.workspace))]
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2030
2470
|
function filterWorks(flags, works) {
|
|
2031
2471
|
const parsed = parseFlags(flags || defaultFlags);
|
|
2032
2472
|
return works.filter(({ issue }) => {
|
|
@@ -2136,7 +2576,20 @@ function createPlaneCli(opts) {
|
|
|
2136
2576
|
const slug = workspace.trim();
|
|
2137
2577
|
if (!slug)
|
|
2138
2578
|
return [];
|
|
2139
|
-
return (await
|
|
2579
|
+
return (await projectsFor(slug)).map((row) => row.identifier);
|
|
2580
|
+
},
|
|
2581
|
+
async listProjects(workspace) {
|
|
2582
|
+
const slug = workspace.trim();
|
|
2583
|
+
if (!slug)
|
|
2584
|
+
return [];
|
|
2585
|
+
return (await projectsFor(slug)).map((row) => ({
|
|
2586
|
+
id: row.id,
|
|
2587
|
+
identifier: row.identifier,
|
|
2588
|
+
name: row.name
|
|
2589
|
+
}));
|
|
2590
|
+
},
|
|
2591
|
+
async parity(flags) {
|
|
2592
|
+
return buildParity(await loadCatalog(flags || lastFlags));
|
|
2140
2593
|
},
|
|
2141
2594
|
async move(key, status) {
|
|
2142
2595
|
try {
|
|
@@ -2155,7 +2608,7 @@ function createPlaneCli(opts) {
|
|
|
2155
2608
|
}).find(([name]) => name === status.toLowerCase())?.[1];
|
|
2156
2609
|
if (!mapped)
|
|
2157
2610
|
return { ok: false, error: `Unknown Plane module status "${status}"` };
|
|
2158
|
-
await request(`/workspaces/${
|
|
2611
|
+
await request(`/workspaces/${projectSlug(module.project)}/projects/${module.project.id}/modules/${module.module.id}/`, { method: "PATCH", body: JSON.stringify({ status: mapped }) });
|
|
2159
2612
|
catalog = undefined;
|
|
2160
2613
|
return { ok: true };
|
|
2161
2614
|
}
|
|
@@ -2165,7 +2618,7 @@ function createPlaneCli(opts) {
|
|
|
2165
2618
|
const state = await resolveStateId(work.project, status, lastFlags);
|
|
2166
2619
|
if (!state)
|
|
2167
2620
|
return { ok: false, error: `Unknown Plane state "${status}" for ${key}` };
|
|
2168
|
-
await withResource((resource) => request(`/workspaces/${
|
|
2621
|
+
await withResource((resource) => request(`/workspaces/${projectSlug(work.project)}/projects/${work.project.id}/${resource}/${work.item.id}/`, { method: "PATCH", body: JSON.stringify({ state }) }));
|
|
2169
2622
|
catalog = undefined;
|
|
2170
2623
|
return { ok: true };
|
|
2171
2624
|
} catch (err) {
|
|
@@ -2188,14 +2641,14 @@ function createPlaneCli(opts) {
|
|
|
2188
2641
|
body.state = state;
|
|
2189
2642
|
if (labels.length)
|
|
2190
2643
|
body.labels = labels;
|
|
2191
|
-
const created = asRecord(await withResource((resource) => requestJson(`/workspaces/${
|
|
2644
|
+
const created = asRecord(await withResource((resource) => requestJson(`/workspaces/${projectSlug(project)}/projects/${project.id}/${resource}/`, {
|
|
2192
2645
|
method: "POST",
|
|
2193
2646
|
body: JSON.stringify(body)
|
|
2194
2647
|
})));
|
|
2195
2648
|
const sequence = Number(created?.sequence_id);
|
|
2196
2649
|
const key = Number.isFinite(sequence) && sequence > 0 ? planeWorkItemKey(project.identifier, sequence) : undefined;
|
|
2197
2650
|
if (parent && created?.id) {
|
|
2198
|
-
await request(`/workspaces/${
|
|
2651
|
+
await request(`/workspaces/${projectSlug(project)}/projects/${project.id}/modules/${parent.module.id}/module-issues/`, { method: "POST", body: JSON.stringify({ issues: [created.id] }) });
|
|
2199
2652
|
}
|
|
2200
2653
|
catalog = undefined;
|
|
2201
2654
|
return key ? { ok: true, key } : { ok: true };
|
|
@@ -2205,6 +2658,24 @@ function createPlaneCli(opts) {
|
|
|
2205
2658
|
},
|
|
2206
2659
|
async edit(key, input) {
|
|
2207
2660
|
try {
|
|
2661
|
+
const loaded = await loadCatalog(lastFlags);
|
|
2662
|
+
const module = loaded.modules.get(key);
|
|
2663
|
+
if (module) {
|
|
2664
|
+
if (input.assignee !== undefined)
|
|
2665
|
+
return { ok: false, error: "Plane modules have no assignee field" };
|
|
2666
|
+
if (input.labels)
|
|
2667
|
+
return { ok: false, error: "Plane modules have no labels field" };
|
|
2668
|
+
const body = {};
|
|
2669
|
+
if (input.summary !== undefined)
|
|
2670
|
+
body.name = input.summary;
|
|
2671
|
+
if (input.priority !== undefined)
|
|
2672
|
+
body.priority = input.priority.toLowerCase();
|
|
2673
|
+
if (Object.keys(body).length) {
|
|
2674
|
+
await request(`/workspaces/${projectSlug(module.project)}/projects/${module.project.id}/modules/${module.module.id}/`, { method: "PATCH", body: JSON.stringify(body) });
|
|
2675
|
+
}
|
|
2676
|
+
catalog = undefined;
|
|
2677
|
+
return { ok: true };
|
|
2678
|
+
}
|
|
2208
2679
|
const work = await lookupByKey(key, lastFlags);
|
|
2209
2680
|
if (!work)
|
|
2210
2681
|
return { ok: false, error: `Work item ${key} not found` };
|
|
@@ -2215,7 +2686,19 @@ function createPlaneCli(opts) {
|
|
|
2215
2686
|
body.description_html = htmlParagraph(input.description);
|
|
2216
2687
|
if (input.labels)
|
|
2217
2688
|
body.labels = await resolveLabelIds(work.project, input.labels, lastFlags);
|
|
2218
|
-
|
|
2689
|
+
if (input.priority !== undefined)
|
|
2690
|
+
body.priority = input.priority.toLowerCase();
|
|
2691
|
+
if (input.assignee !== undefined) {
|
|
2692
|
+
const slug = projectSlug(work.project);
|
|
2693
|
+
const members = (await paginate(`/workspaces/${slug}/members/`)).map((row) => asRecord(row));
|
|
2694
|
+
const wanted = input.assignee.trim().toLowerCase();
|
|
2695
|
+
const member = members.find((row) => asString(row?.display_name)?.toLowerCase() === wanted);
|
|
2696
|
+
const memberId = asString(member?.id);
|
|
2697
|
+
if (wanted && !memberId)
|
|
2698
|
+
return { ok: false, error: `Plane member "${input.assignee}" was not found` };
|
|
2699
|
+
body.assignees = memberId ? [memberId] : [];
|
|
2700
|
+
}
|
|
2701
|
+
await withResource((resource) => request(`/workspaces/${projectSlug(work.project)}/projects/${work.project.id}/${resource}/${work.item.id}/`, { method: "PATCH", body: JSON.stringify(body) }));
|
|
2219
2702
|
catalog = undefined;
|
|
2220
2703
|
return { ok: true };
|
|
2221
2704
|
} catch (err) {
|
|
@@ -2223,7 +2706,12 @@ function createPlaneCli(opts) {
|
|
|
2223
2706
|
}
|
|
2224
2707
|
},
|
|
2225
2708
|
async open(key) {
|
|
2226
|
-
|
|
2709
|
+
const loaded = await loadCatalog(lastFlags);
|
|
2710
|
+
const module = loaded.modules.get(key);
|
|
2711
|
+
const work = loaded.works.get(key);
|
|
2712
|
+
const slug = module?.project.workspace || work?.project.workspace || workspaceOf(lastFlags);
|
|
2713
|
+
const bare = key.includes("/") ? key.slice(key.indexOf("/") + 1) : key;
|
|
2714
|
+
return `${host}/${slug}/browse/${bare}`;
|
|
2227
2715
|
},
|
|
2228
2716
|
async view(key) {
|
|
2229
2717
|
const loaded = await loadCatalog(lastFlags);
|
|
@@ -2245,13 +2733,15 @@ async function createBoardApp(opts) {
|
|
|
2245
2733
|
const store = IssueStore.fromRaw(opts.raw);
|
|
2246
2734
|
const plane = wantsPlane(flags);
|
|
2247
2735
|
const bin = resolveJiraBin(env.JIRA_BIN ?? "jira", env.PATH ?? "");
|
|
2736
|
+
const fixtureMode = Boolean(env.PLANE_FIXTURE?.trim());
|
|
2737
|
+
const useJira = Boolean(bin) && !fixtureMode;
|
|
2248
2738
|
const cli = plane ? createPlaneCli({
|
|
2249
2739
|
host: planeHost(env),
|
|
2250
2740
|
apiKey: env.PLANE_API_KEY ?? "",
|
|
2251
2741
|
workspace: planeWorkspace(flags, env),
|
|
2252
2742
|
flags,
|
|
2253
|
-
fetch: opts.fetch
|
|
2254
|
-
}) :
|
|
2743
|
+
fetch: opts.fetch ?? resolvePlaneFetch(env)
|
|
2744
|
+
}) : useJira ? createJiraCli({
|
|
2255
2745
|
bin,
|
|
2256
2746
|
configPath: env.JIRA_CONFIG_FILE,
|
|
2257
2747
|
token: env.JIRA_API_TOKEN,
|
|
@@ -2270,7 +2760,7 @@ async function createBoardApp(opts) {
|
|
|
2270
2760
|
const local = createStoreCli(store, flags);
|
|
2271
2761
|
app.hydrate(JSON.parse(await local.list(app.flags)), { fromStore: true });
|
|
2272
2762
|
}
|
|
2273
|
-
return { app, store, kind: plane ? "plane" :
|
|
2763
|
+
return { app, store, kind: plane ? "plane" : useJira ? "jira" : "store" };
|
|
2274
2764
|
}
|
|
2275
2765
|
async function refreshFromJira(app, kind, opts = {}) {
|
|
2276
2766
|
if (opts.piped)
|
|
@@ -2281,7 +2771,7 @@ async function refreshFromJira(app, kind, opts = {}) {
|
|
|
2281
2771
|
}
|
|
2282
2772
|
|
|
2283
2773
|
// src/factory-ledger.ts
|
|
2284
|
-
import { existsSync as existsSync2, readFileSync as
|
|
2774
|
+
import { existsSync as existsSync2, readFileSync as readFileSync4, statSync } from "node:fs";
|
|
2285
2775
|
import { homedir as homedir3 } from "node:os";
|
|
2286
2776
|
import { join as join4 } from "node:path";
|
|
2287
2777
|
var DEFAULT_LEDGER_PATH = "~/code/software-factory/asf/runtime/factory-coordinator/jobs.jsonl";
|
|
@@ -2388,7 +2878,7 @@ function readLedger(env = process.env) {
|
|
|
2388
2878
|
if (_cache && _cache.path === path && _cache.mtime === mtime) {
|
|
2389
2879
|
return { jobs: _cache.jobs };
|
|
2390
2880
|
}
|
|
2391
|
-
const text =
|
|
2881
|
+
const text = readFileSync4(path, "utf8");
|
|
2392
2882
|
const jobs = classifyJobs(parseJobsJsonl(text));
|
|
2393
2883
|
_cache = { path, mtime, jobs };
|
|
2394
2884
|
return { jobs };
|
|
@@ -2456,7 +2946,9 @@ function handleAppApi(req, res, app, opts = {}) {
|
|
|
2456
2946
|
return;
|
|
2457
2947
|
}
|
|
2458
2948
|
try {
|
|
2459
|
-
|
|
2949
|
+
const projects = await app.listProjectIdentifiers?.(workspace) ?? [];
|
|
2950
|
+
const details = await app.listProjects?.(workspace) ?? [];
|
|
2951
|
+
json(res, 200, { projects, ...details.length ? { details } : {} });
|
|
2460
2952
|
} catch (err) {
|
|
2461
2953
|
const message = err instanceof Error ? err.message : "catalog failed";
|
|
2462
2954
|
json(res, 409, { error: message });
|
|
@@ -2464,6 +2956,38 @@ function handleAppApi(req, res, app, opts = {}) {
|
|
|
2464
2956
|
});
|
|
2465
2957
|
return true;
|
|
2466
2958
|
}
|
|
2959
|
+
if (url.pathname === "/api/parity" && method === "GET") {
|
|
2960
|
+
reply(req, res, async () => {
|
|
2961
|
+
json(res, 200, await app.parity?.() ?? { initiatives: [], projects: [], workspaces: [] });
|
|
2962
|
+
});
|
|
2963
|
+
return true;
|
|
2964
|
+
}
|
|
2965
|
+
if (url.pathname === "/api/sources" && method === "GET") {
|
|
2966
|
+
reply(req, res, async () => {
|
|
2967
|
+
const current = kind === "plane" ? "plane" : "jira";
|
|
2968
|
+
json(res, 200, {
|
|
2969
|
+
source: current,
|
|
2970
|
+
kind,
|
|
2971
|
+
sources: sourceOptions({
|
|
2972
|
+
planeKey: Boolean(env.PLANE_API_KEY?.trim() || env.PLANE_FIXTURE?.trim()),
|
|
2973
|
+
jiraBin: Boolean(resolveJiraBin(env.JIRA_BIN ?? "jira", env.PATH ?? ""))
|
|
2974
|
+
})
|
|
2975
|
+
});
|
|
2976
|
+
});
|
|
2977
|
+
return true;
|
|
2978
|
+
}
|
|
2979
|
+
if (url.pathname === "/api/source" && method === "POST") {
|
|
2980
|
+
reply(req, res, async (text) => {
|
|
2981
|
+
if (!opts.onSource) {
|
|
2982
|
+
json(res, 404, { error: "source switch unavailable" });
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
const body = text ? JSON.parse(text) : {};
|
|
2986
|
+
const result = await opts.onSource(String(body.source ?? ""));
|
|
2987
|
+
json(res, result.ok ? 200 : 409, result);
|
|
2988
|
+
});
|
|
2989
|
+
return true;
|
|
2990
|
+
}
|
|
2467
2991
|
if (url.pathname === "/api/refresh" && method === "POST") {
|
|
2468
2992
|
reply(req, res, async (text) => {
|
|
2469
2993
|
const body = text ? JSON.parse(text) : {};
|
|
@@ -2533,6 +3057,10 @@ function handleAppApi(req, res, app, opts = {}) {
|
|
|
2533
3057
|
input.summary = String(body.summary);
|
|
2534
3058
|
if (body.description !== undefined)
|
|
2535
3059
|
input.description = String(body.description);
|
|
3060
|
+
if (body.assignee !== undefined)
|
|
3061
|
+
input.assignee = String(body.assignee);
|
|
3062
|
+
if (body.priority !== undefined)
|
|
3063
|
+
input.priority = String(body.priority);
|
|
2536
3064
|
if (Array.isArray(body.labels)) {
|
|
2537
3065
|
input.labels = body.labels.filter((item) => typeof item === "string");
|
|
2538
3066
|
}
|
|
@@ -2650,7 +3178,7 @@ function formatCard(card) {
|
|
|
2650
3178
|
}
|
|
2651
3179
|
|
|
2652
3180
|
// src/server/agent/config.ts
|
|
2653
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as
|
|
3181
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2654
3182
|
import { homedir as homedir4 } from "node:os";
|
|
2655
3183
|
import { delimiter as delimiter2, dirname as dirname2, join as join5 } from "node:path";
|
|
2656
3184
|
var DEFAULT_CONFIG_PATH = join5(homedir4(), ".config", "pipe-kan", "agent.json");
|
|
@@ -2709,7 +3237,7 @@ function loadAgentConfig(path = agentConfigPath()) {
|
|
|
2709
3237
|
if (!existsSync3(path))
|
|
2710
3238
|
return structuredClone(DEFAULT_CONFIG);
|
|
2711
3239
|
try {
|
|
2712
|
-
const raw = JSON.parse(
|
|
3240
|
+
const raw = JSON.parse(readFileSync5(path, "utf8"));
|
|
2713
3241
|
return mergeConfig(raw);
|
|
2714
3242
|
} catch {
|
|
2715
3243
|
return structuredClone(DEFAULT_CONFIG);
|
|
@@ -3236,7 +3764,7 @@ function parsedType(data) {
|
|
|
3236
3764
|
}
|
|
3237
3765
|
return t;
|
|
3238
3766
|
}
|
|
3239
|
-
function
|
|
3767
|
+
function issue2(...args) {
|
|
3240
3768
|
const [iss, input, inst] = args;
|
|
3241
3769
|
if (typeof iss === "string") {
|
|
3242
3770
|
return {
|
|
@@ -5663,7 +6191,7 @@ function handleRefineResult(result, payload, input, inst) {
|
|
|
5663
6191
|
};
|
|
5664
6192
|
if (inst._zod.def.params)
|
|
5665
6193
|
_iss.params = inst._zod.def.params;
|
|
5666
|
-
payload.issues.push(
|
|
6194
|
+
payload.issues.push(issue2(_iss));
|
|
5667
6195
|
}
|
|
5668
6196
|
}
|
|
5669
6197
|
// node_modules/zod/v4/core/memoizer.js
|
|
@@ -6489,11 +7017,11 @@ function _refine(Class, fn, _params) {
|
|
|
6489
7017
|
}
|
|
6490
7018
|
function _superRefine(fn, params) {
|
|
6491
7019
|
const ch = _check((payload) => {
|
|
6492
|
-
payload.addIssue = (
|
|
6493
|
-
if (typeof
|
|
6494
|
-
payload.issues.push(issue
|
|
7020
|
+
payload.addIssue = (issue) => {
|
|
7021
|
+
if (typeof issue === "string") {
|
|
7022
|
+
payload.issues.push(issue2(issue, payload.value, ch._zod.def));
|
|
6495
7023
|
} else {
|
|
6496
|
-
const _issue =
|
|
7024
|
+
const _issue = issue;
|
|
6497
7025
|
if (_issue.fatal)
|
|
6498
7026
|
_issue.continue = false;
|
|
6499
7027
|
_issue.code ?? (_issue.code = "custom");
|
|
@@ -6501,7 +7029,7 @@ function _superRefine(fn, params) {
|
|
|
6501
7029
|
_issue.input = payload.value;
|
|
6502
7030
|
_issue.inst ?? (_issue.inst = ch);
|
|
6503
7031
|
_issue.continue ?? (_issue.continue = !ch._zod.def.abort);
|
|
6504
|
-
payload.issues.push(
|
|
7032
|
+
payload.issues.push(issue2(_issue));
|
|
6505
7033
|
}
|
|
6506
7034
|
};
|
|
6507
7035
|
return fn(payload.value, payload);
|
|
@@ -8243,18 +8771,18 @@ var ZodTransform = /* @__PURE__ */ $constructor("ZodTransform", (inst, def) => {
|
|
|
8243
8771
|
if (_ctx.direction === "backward") {
|
|
8244
8772
|
throw new $ZodEncodeError(inst.constructor.name);
|
|
8245
8773
|
}
|
|
8246
|
-
payload.addIssue = (
|
|
8247
|
-
if (typeof
|
|
8248
|
-
payload.issues.push(issue
|
|
8774
|
+
payload.addIssue = (issue) => {
|
|
8775
|
+
if (typeof issue === "string") {
|
|
8776
|
+
payload.issues.push(issue2(issue, payload.value, def));
|
|
8249
8777
|
} else {
|
|
8250
|
-
const _issue =
|
|
8778
|
+
const _issue = issue;
|
|
8251
8779
|
if (_issue.fatal)
|
|
8252
8780
|
_issue.continue = false;
|
|
8253
8781
|
_issue.code ?? (_issue.code = "custom");
|
|
8254
8782
|
if (!("input" in _issue))
|
|
8255
8783
|
_issue.input = payload.value;
|
|
8256
8784
|
_issue.inst ?? (_issue.inst = inst);
|
|
8257
|
-
payload.issues.push(
|
|
8785
|
+
payload.issues.push(issue2(_issue));
|
|
8258
8786
|
}
|
|
8259
8787
|
};
|
|
8260
8788
|
const output = def.transform(payload.value, payload);
|
|
@@ -12791,7 +13319,7 @@ function permissionOptionForDecision(options, decision) {
|
|
|
12791
13319
|
}
|
|
12792
13320
|
|
|
12793
13321
|
// src/server/agent/skills.ts
|
|
12794
|
-
import { existsSync as existsSync4, readFileSync as
|
|
13322
|
+
import { existsSync as existsSync4, readFileSync as readFileSync6, readdirSync } from "node:fs";
|
|
12795
13323
|
import { homedir as homedir5 } from "node:os";
|
|
12796
13324
|
import { join as join7 } from "node:path";
|
|
12797
13325
|
import { fileURLToPath } from "node:url";
|
|
@@ -12840,7 +13368,7 @@ function skillPath(dir, id) {
|
|
|
12840
13368
|
}
|
|
12841
13369
|
function readSkill(path, id) {
|
|
12842
13370
|
try {
|
|
12843
|
-
const text =
|
|
13371
|
+
const text = readFileSync6(path, "utf8");
|
|
12844
13372
|
const front = parseFrontMatter(text);
|
|
12845
13373
|
return {
|
|
12846
13374
|
id,
|
|
@@ -12867,7 +13395,7 @@ function parseFrontMatter(text) {
|
|
|
12867
13395
|
}
|
|
12868
13396
|
|
|
12869
13397
|
// src/server/agent/tools.ts
|
|
12870
|
-
import { mkdirSync as mkdirSync3, readFileSync as
|
|
13398
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
|
|
12871
13399
|
import { join as join8 } from "node:path";
|
|
12872
13400
|
var TOOLS = [
|
|
12873
13401
|
{
|
|
@@ -13017,7 +13545,7 @@ var EXECUTORS = {
|
|
|
13017
13545
|
return { ok: false, error: "Path traversal not allowed" };
|
|
13018
13546
|
const repoRoot = process.cwd();
|
|
13019
13547
|
try {
|
|
13020
|
-
const text =
|
|
13548
|
+
const text = readFileSync7(join8(repoRoot, relPath), "utf8");
|
|
13021
13549
|
return { ok: true, value: { path: relPath, text } };
|
|
13022
13550
|
} catch (err) {
|
|
13023
13551
|
return { ok: false, error: String(err) };
|
|
@@ -13641,7 +14169,7 @@ function handleFakeJira(req, res, store) {
|
|
|
13641
14169
|
|
|
13642
14170
|
// src/http.ts
|
|
13643
14171
|
function handleRequest(req, res, ctx) {
|
|
13644
|
-
if (handleAppApi(req, res, ctx.app, { kind: ctx.kind }) || handleAgentApi(req, res, ctx.app)) {
|
|
14172
|
+
if (handleAppApi(req, res, ctx.app, { kind: ctx.kind, env: ctx.env, onSource: ctx.switchSource }) || handleAgentApi(req, res, ctx.app)) {
|
|
13645
14173
|
return true;
|
|
13646
14174
|
}
|
|
13647
14175
|
if (ctx.kind === "plane")
|
|
@@ -13757,15 +14285,51 @@ function announce(line) {
|
|
|
13757
14285
|
}
|
|
13758
14286
|
async function runServer(opts) {
|
|
13759
14287
|
const piped = await readPipe();
|
|
13760
|
-
const raw = piped ?? JSON.parse(
|
|
14288
|
+
const raw = piped ?? JSON.parse(readFileSync8(join11(opts.root, "fixtures/issues.json"), "utf8"));
|
|
13761
14289
|
const flags = argvToFlags(process.argv);
|
|
13762
|
-
const
|
|
14290
|
+
const booted = await createBoardApp({
|
|
13763
14291
|
raw,
|
|
13764
14292
|
piped: Boolean(piped),
|
|
13765
14293
|
flags
|
|
13766
14294
|
});
|
|
14295
|
+
const live = {
|
|
14296
|
+
app: booted.app,
|
|
14297
|
+
store: booted.store,
|
|
14298
|
+
kind: booted.kind,
|
|
14299
|
+
raw
|
|
14300
|
+
};
|
|
14301
|
+
async function switchSource(source) {
|
|
14302
|
+
if (source === "taskwarrior") {
|
|
14303
|
+
return { ok: false, kind: live.kind, source, reason: "Taskwarrior is not connected" };
|
|
14304
|
+
}
|
|
14305
|
+
if (source !== "plane" && source !== "jira") {
|
|
14306
|
+
return { ok: false, kind: live.kind, source, error: `Unknown source ${source}` };
|
|
14307
|
+
}
|
|
14308
|
+
if (source === "plane" && !process.env.PLANE_API_KEY?.trim()) {
|
|
14309
|
+
return { ok: false, kind: live.kind, source, reason: "Set PLANE_API_KEY to use Plane" };
|
|
14310
|
+
}
|
|
14311
|
+
const nextFlags = setPlaneFlag(live.app.flags, source === "plane");
|
|
14312
|
+
try {
|
|
14313
|
+
const next = await createBoardApp({ raw: live.raw, piped: false, flags: nextFlags });
|
|
14314
|
+
live.app = next.app;
|
|
14315
|
+
live.store = next.store;
|
|
14316
|
+
live.kind = next.kind;
|
|
14317
|
+
if (next.kind === "plane" || next.kind === "jira") {
|
|
14318
|
+
try {
|
|
14319
|
+
await refreshFromJira(live.app, next.kind);
|
|
14320
|
+
} catch (err) {
|
|
14321
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
14322
|
+
return { ok: true, kind: live.kind, source, error: message };
|
|
14323
|
+
}
|
|
14324
|
+
}
|
|
14325
|
+
return { ok: true, kind: live.kind, source };
|
|
14326
|
+
} catch (err) {
|
|
14327
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
14328
|
+
return { ok: false, kind: live.kind, source, error: message };
|
|
14329
|
+
}
|
|
14330
|
+
}
|
|
13767
14331
|
const server = createServer((req, res) => {
|
|
13768
|
-
if (handleRequest(req, res, { app, store, kind }))
|
|
14332
|
+
if (handleRequest(req, res, { app: live.app, store: live.store, kind: live.kind, switchSource }))
|
|
13769
14333
|
return;
|
|
13770
14334
|
if (opts.fallback) {
|
|
13771
14335
|
opts.fallback(req, res);
|
|
@@ -13776,34 +14340,34 @@ async function runServer(opts) {
|
|
|
13776
14340
|
res.end("pipe-kan UI is missing. Run bun run build.");
|
|
13777
14341
|
}
|
|
13778
14342
|
});
|
|
13779
|
-
if (kind === "plane") {
|
|
14343
|
+
if (live.kind === "plane") {
|
|
13780
14344
|
try {
|
|
13781
|
-
await refreshFromJira(app, kind, { piped: Boolean(piped) });
|
|
14345
|
+
await refreshFromJira(live.app, live.kind, { piped: Boolean(piped) });
|
|
13782
14346
|
} catch (err) {
|
|
13783
|
-
console.error(`${kind} Refresh failed`, err);
|
|
14347
|
+
console.error(`${live.kind} Refresh failed`, err);
|
|
13784
14348
|
}
|
|
13785
|
-
const error = app.board().error;
|
|
14349
|
+
const error = live.app.board().error;
|
|
13786
14350
|
if (error)
|
|
13787
|
-
console.error(`${kind} Refresh failed; keeping last live Board`, error);
|
|
14351
|
+
console.error(`${live.kind} Refresh failed; keeping last live Board`, error);
|
|
13788
14352
|
}
|
|
13789
14353
|
const { host, port } = resolveListen();
|
|
13790
14354
|
await bindListen(server, host, port);
|
|
13791
14355
|
const origin = `http://127.0.0.1:${port}`;
|
|
13792
14356
|
announce(`pipe-kan http://${host}:${port}`);
|
|
13793
|
-
if (kind === "plane") {
|
|
14357
|
+
if (live.kind === "plane") {
|
|
13794
14358
|
announce(`cli plane ${planeHost()} ${planeWorkspace(flags)}`);
|
|
13795
14359
|
announce(`Plane host default ${DEFAULT_PLANE_HOST}`);
|
|
13796
14360
|
} else {
|
|
13797
14361
|
const fakeConfig = writeJiraConfig(join11(tmpdir3(), "pipe-kan"), origin);
|
|
13798
|
-
announce(`cli ${kind === "jira" ? resolveJiraBin() : "store"}`);
|
|
14362
|
+
announce(`cli ${live.kind === "jira" ? resolveJiraBin() : "store"}`);
|
|
13799
14363
|
announce(`Fake Jira ${origin}/rest/api/2/search`);
|
|
13800
14364
|
announce(`Fake Jira config ${fakeConfig}`);
|
|
13801
14365
|
}
|
|
13802
|
-
if (kind === "jira") {
|
|
14366
|
+
if (live.kind === "jira") {
|
|
13803
14367
|
try {
|
|
13804
|
-
await refreshFromJira(app, kind, { piped: Boolean(piped) });
|
|
14368
|
+
await refreshFromJira(live.app, live.kind, { piped: Boolean(piped) });
|
|
13805
14369
|
} catch (err) {
|
|
13806
|
-
console.error(`${kind} Refresh failed; keeping Fixture Board`);
|
|
14370
|
+
console.error(`${live.kind} Refresh failed; keeping Fixture Board`);
|
|
13807
14371
|
console.error(err);
|
|
13808
14372
|
}
|
|
13809
14373
|
}
|