pipe-kan 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pipe-kan.js
CHANGED
|
@@ -1,10 +1,143 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/server.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync6, writeSync } from "node:fs";
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
|
-
import { tmpdir as
|
|
7
|
-
import { join as
|
|
6
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
7
|
+
import { join as join10 } from "node:path";
|
|
8
|
+
|
|
9
|
+
// src/app.ts
|
|
10
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
11
|
+
import { availableParallelism as availableParallelism2, homedir as homedir2, tmpdir } from "node:os";
|
|
12
|
+
import { join as join3 } from "node:path";
|
|
13
|
+
|
|
14
|
+
// src/target-end.ts
|
|
15
|
+
var EXACT_NAME = "Target End Date";
|
|
16
|
+
var ALIAS_NAMES = new Set(["Target End", "Target end"]);
|
|
17
|
+
var NAMED_FIELD_KEYS = [
|
|
18
|
+
"Target End Date",
|
|
19
|
+
"Target End",
|
|
20
|
+
"Target end",
|
|
21
|
+
"targetEndDate",
|
|
22
|
+
"targetenddate",
|
|
23
|
+
"targetEnd",
|
|
24
|
+
"targetend"
|
|
25
|
+
];
|
|
26
|
+
function isTargetStart(name) {
|
|
27
|
+
const folded = name.trim().toLowerCase();
|
|
28
|
+
return folded === "target start" || folded === "target start date";
|
|
29
|
+
}
|
|
30
|
+
function asNames(raw) {
|
|
31
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
32
|
+
return;
|
|
33
|
+
const names = {};
|
|
34
|
+
for (const [id, value] of Object.entries(raw)) {
|
|
35
|
+
if (typeof value === "string" && value.trim())
|
|
36
|
+
names[id] = value.trim();
|
|
37
|
+
}
|
|
38
|
+
return Object.keys(names).length ? names : undefined;
|
|
39
|
+
}
|
|
40
|
+
function resolveTargetEndFieldId(names) {
|
|
41
|
+
if (!names)
|
|
42
|
+
return;
|
|
43
|
+
let alias;
|
|
44
|
+
for (const [id, name] of Object.entries(names)) {
|
|
45
|
+
if (!id.startsWith("customfield_"))
|
|
46
|
+
continue;
|
|
47
|
+
if (isTargetStart(name))
|
|
48
|
+
continue;
|
|
49
|
+
if (name === EXACT_NAME)
|
|
50
|
+
return id;
|
|
51
|
+
if (ALIAS_NAMES.has(name) && !alias)
|
|
52
|
+
alias = id;
|
|
53
|
+
}
|
|
54
|
+
return alias;
|
|
55
|
+
}
|
|
56
|
+
function isoDay(value) {
|
|
57
|
+
if (typeof value === "string" && value.trim()) {
|
|
58
|
+
const text = value.trim();
|
|
59
|
+
const day = /^(\d{4})-(\d{2})-(\d{2})/.exec(text);
|
|
60
|
+
if (day)
|
|
61
|
+
return `${day[1]}-${day[2]}-${day[3]}`;
|
|
62
|
+
const parsed = Date.parse(text);
|
|
63
|
+
if (Number.isNaN(parsed))
|
|
64
|
+
return;
|
|
65
|
+
const date = new Date(parsed);
|
|
66
|
+
const y = date.getFullYear();
|
|
67
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
68
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
69
|
+
return `${y}-${m}-${d}`;
|
|
70
|
+
}
|
|
71
|
+
if (value && typeof value === "object") {
|
|
72
|
+
const item = value;
|
|
73
|
+
return isoDay(item.value) ?? isoDay(item.date);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
function issueTargetEnd(fields, opts = {}) {
|
|
78
|
+
if (!fields)
|
|
79
|
+
return;
|
|
80
|
+
for (const key of NAMED_FIELD_KEYS) {
|
|
81
|
+
const day = isoDay(fields[key]);
|
|
82
|
+
if (day)
|
|
83
|
+
return day;
|
|
84
|
+
}
|
|
85
|
+
const mapped = resolveTargetEndFieldId(opts.names) ?? opts.fieldId;
|
|
86
|
+
if (mapped && mapped.startsWith("customfield_")) {
|
|
87
|
+
const day = isoDay(fields[mapped]);
|
|
88
|
+
if (day)
|
|
89
|
+
return day;
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
function mergeViewIntoIssue(listIssue, viewIssue) {
|
|
94
|
+
if (!viewIssue || typeof viewIssue !== "object")
|
|
95
|
+
return listIssue;
|
|
96
|
+
if (!listIssue || typeof listIssue !== "object")
|
|
97
|
+
return viewIssue;
|
|
98
|
+
const list = listIssue;
|
|
99
|
+
const view = viewIssue;
|
|
100
|
+
const names = { ...asNames(list.names), ...asNames(view.names) };
|
|
101
|
+
return {
|
|
102
|
+
...list,
|
|
103
|
+
...view,
|
|
104
|
+
...Object.keys(names).length ? { names } : {},
|
|
105
|
+
fields: { ...list.fields, ...view.fields }
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function unquoteYaml(value) {
|
|
109
|
+
const trimmed = value.trim();
|
|
110
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
111
|
+
return trimmed.slice(1, -1).trim();
|
|
112
|
+
}
|
|
113
|
+
return trimmed;
|
|
114
|
+
}
|
|
115
|
+
function targetEndFieldIdFromJiraConfig(yaml) {
|
|
116
|
+
if (!yaml.trim())
|
|
117
|
+
return;
|
|
118
|
+
const names = {};
|
|
119
|
+
let name;
|
|
120
|
+
let key;
|
|
121
|
+
const flush = () => {
|
|
122
|
+
if (name && key)
|
|
123
|
+
names[key] = name;
|
|
124
|
+
name = undefined;
|
|
125
|
+
key = undefined;
|
|
126
|
+
};
|
|
127
|
+
for (const rawLine of yaml.split(/\r?\n/)) {
|
|
128
|
+
const line = rawLine.replace(/\t/g, " ");
|
|
129
|
+
if (/^\s*-\s*$/.test(line) || /^\s*-\s+\S/.test(line))
|
|
130
|
+
flush();
|
|
131
|
+
const nameMatch = line.match(/^\s*-?\s*name:\s*(.+?)\s*$/);
|
|
132
|
+
if (nameMatch)
|
|
133
|
+
name = unquoteYaml(nameMatch[1] ?? "");
|
|
134
|
+
const keyMatch = line.match(/^\s*-?\s*key:\s*['"]?(customfield_\d+)['"]?/);
|
|
135
|
+
if (keyMatch)
|
|
136
|
+
key = keyMatch[1];
|
|
137
|
+
}
|
|
138
|
+
flush();
|
|
139
|
+
return resolveTargetEndFieldId(names);
|
|
140
|
+
}
|
|
8
141
|
|
|
9
142
|
// src/board.ts
|
|
10
143
|
function formatDueDate(value) {
|
|
@@ -20,19 +153,6 @@ function formatDueDate(value) {
|
|
|
20
153
|
year: "numeric"
|
|
21
154
|
});
|
|
22
155
|
}
|
|
23
|
-
function formatTargetEnd(value) {
|
|
24
|
-
if (typeof value !== "string" || !value)
|
|
25
|
-
return;
|
|
26
|
-
const day = /^(\d{4})-(\d{2})-(\d{2})/.exec(value);
|
|
27
|
-
const date = day ? new Date(Number(day[1]), Number(day[2]) - 1, Number(day[3])) : new Date(value);
|
|
28
|
-
if (Number.isNaN(date.getTime()))
|
|
29
|
-
return;
|
|
30
|
-
return date.toLocaleDateString("en-US", {
|
|
31
|
-
month: "short",
|
|
32
|
-
day: "numeric",
|
|
33
|
-
year: "numeric"
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
156
|
function createdAt(fields) {
|
|
37
157
|
return typeof fields?.created === "string" && fields.created ? fields.created : undefined;
|
|
38
158
|
}
|
|
@@ -101,31 +221,6 @@ function issueAssignee(fields) {
|
|
|
101
221
|
}
|
|
102
222
|
return;
|
|
103
223
|
}
|
|
104
|
-
function issueTargetEnd(fields) {
|
|
105
|
-
if (!fields)
|
|
106
|
-
return;
|
|
107
|
-
const candidates = [
|
|
108
|
-
fields.targetEnd,
|
|
109
|
-
fields.targetend,
|
|
110
|
-
fields.targetEndDate,
|
|
111
|
-
fields.targetenddate,
|
|
112
|
-
fields["Target End"],
|
|
113
|
-
fields["Target End Date"]
|
|
114
|
-
];
|
|
115
|
-
const direct = candidates.find((value) => typeof value === "string" && value.trim());
|
|
116
|
-
if (direct)
|
|
117
|
-
return formatTargetEnd(direct);
|
|
118
|
-
for (const [key, value] of Object.entries(fields)) {
|
|
119
|
-
if (!key.startsWith("customfield_"))
|
|
120
|
-
continue;
|
|
121
|
-
if (typeof value === "string" && value.trim()) {
|
|
122
|
-
const formatted = formatTargetEnd(value.trim());
|
|
123
|
-
if (formatted)
|
|
124
|
-
return formatted;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
224
|
function toEpic(face) {
|
|
130
225
|
return {
|
|
131
226
|
key: face.key,
|
|
@@ -135,15 +230,19 @@ function toEpic(face) {
|
|
|
135
230
|
...face.assignee ? { assignee: face.assignee } : {},
|
|
136
231
|
...face.dueDate ? { dueDate: face.dueDate } : {},
|
|
137
232
|
...face.targetEnd ? { targetEnd: face.targetEnd } : {},
|
|
138
|
-
...face.labels ? { labels: face.labels } : {}
|
|
233
|
+
...face.labels ? { labels: face.labels } : {},
|
|
234
|
+
...face.created ? { created: face.created } : {}
|
|
139
235
|
};
|
|
140
236
|
}
|
|
141
|
-
function toCard(issue, key) {
|
|
237
|
+
function toCard(issue, key, fieldId) {
|
|
142
238
|
const summary = typeof issue.fields?.summary === "string" ? issue.fields.summary : "";
|
|
143
239
|
const priority = typeof issue.fields?.priority?.name === "string" ? issue.fields.priority.name : undefined;
|
|
144
240
|
const assignee = issueAssignee(issue.fields);
|
|
145
241
|
const dueDate = formatDueDate(issue.fields?.duedate);
|
|
146
|
-
const targetEnd = issueTargetEnd(issue.fields
|
|
242
|
+
const targetEnd = issueTargetEnd(issue.fields, {
|
|
243
|
+
names: asNames(issue.names),
|
|
244
|
+
fieldId
|
|
245
|
+
});
|
|
147
246
|
const type = issueType(issue.fields);
|
|
148
247
|
const epic = epicKey(issue.fields);
|
|
149
248
|
const labels = issueLabels(issue.fields);
|
|
@@ -161,7 +260,7 @@ function toCard(issue, key) {
|
|
|
161
260
|
...created ? { created } : {}
|
|
162
261
|
};
|
|
163
262
|
}
|
|
164
|
-
function issuesToBoard(raw) {
|
|
263
|
+
function issuesToBoard(raw, opts = {}) {
|
|
165
264
|
if (!Array.isArray(raw)) {
|
|
166
265
|
throw new Error("jira-cli --raw payload must be a JSON array");
|
|
167
266
|
}
|
|
@@ -185,7 +284,7 @@ function issuesToBoard(raw) {
|
|
|
185
284
|
const status = typeof issue?.fields?.status?.name === "string" ? issue.fields.status.name : "";
|
|
186
285
|
if (!key || !status)
|
|
187
286
|
continue;
|
|
188
|
-
const card = toCard(issue, key);
|
|
287
|
+
const card = toCard(issue, key, opts.targetEndFieldId);
|
|
189
288
|
if ((card.type ?? "").toLowerCase() === "epic") {
|
|
190
289
|
rememberEpic(toEpic({ ...card, status: issueStatus(issue.fields) }));
|
|
191
290
|
continue;
|
|
@@ -217,6 +316,8 @@ function mergeEpics(listed, fromBoard) {
|
|
|
217
316
|
Object.assign(existing, epic);
|
|
218
317
|
if (!existing.targetEnd && epic.targetEnd)
|
|
219
318
|
existing.targetEnd = epic.targetEnd;
|
|
319
|
+
if (!existing.created && epic.created)
|
|
320
|
+
existing.created = epic.created;
|
|
220
321
|
}
|
|
221
322
|
return [...byKey.values()];
|
|
222
323
|
}
|
|
@@ -1001,9 +1102,40 @@ function flattenIssue(raw, url) {
|
|
|
1001
1102
|
return [...rows, ...rest];
|
|
1002
1103
|
}
|
|
1003
1104
|
|
|
1105
|
+
// src/field-map.ts
|
|
1106
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1107
|
+
import { homedir } from "node:os";
|
|
1108
|
+
import { dirname, join as join2 } from "node:path";
|
|
1109
|
+
function jiraCliConfigPath(env = process.env) {
|
|
1110
|
+
const explicit = env.JIRA_CONFIG_FILE?.trim();
|
|
1111
|
+
if (explicit)
|
|
1112
|
+
return explicit;
|
|
1113
|
+
const xdg = env.XDG_CONFIG_HOME?.trim();
|
|
1114
|
+
if (xdg)
|
|
1115
|
+
return join2(xdg, ".jira", ".config.yml");
|
|
1116
|
+
return join2(homedir(), ".config", ".jira", ".config.yml");
|
|
1117
|
+
}
|
|
1118
|
+
function readTargetEndFieldMap(path) {
|
|
1119
|
+
try {
|
|
1120
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
1121
|
+
const targetEnd = typeof raw.targetEnd === "string" && raw.targetEnd.startsWith("customfield_") ? raw.targetEnd : undefined;
|
|
1122
|
+
return targetEnd ? { targetEnd } : {};
|
|
1123
|
+
} catch {
|
|
1124
|
+
return {};
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
function writeTargetEndFieldMap(path, map) {
|
|
1128
|
+
const targetEnd = typeof map.targetEnd === "string" && map.targetEnd.startsWith("customfield_") ? map.targetEnd : undefined;
|
|
1129
|
+
if (!targetEnd)
|
|
1130
|
+
return;
|
|
1131
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1132
|
+
writeFileSync(path, `${JSON.stringify({ targetEnd }, null, 2)}
|
|
1133
|
+
`);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1004
1136
|
// src/app.ts
|
|
1005
|
-
function columnsOf(raw) {
|
|
1006
|
-
return Object.fromEntries(issuesToBoard(raw).columns.map((column) => [column.title, column.cards]));
|
|
1137
|
+
function columnsOf(raw, fieldId) {
|
|
1138
|
+
return Object.fromEntries(issuesToBoard(raw, { targetEndFieldId: fieldId }).columns.map((column) => [column.title, column.cards]));
|
|
1007
1139
|
}
|
|
1008
1140
|
function stampMissingEpic(board, epic) {
|
|
1009
1141
|
for (const column of board.columns) {
|
|
@@ -1014,21 +1146,76 @@ function stampMissingEpic(board, epic) {
|
|
|
1014
1146
|
}
|
|
1015
1147
|
return board;
|
|
1016
1148
|
}
|
|
1017
|
-
function cardsOf(raw) {
|
|
1149
|
+
function cardsOf(raw, fieldId) {
|
|
1018
1150
|
if (!Array.isArray(raw))
|
|
1019
1151
|
return [];
|
|
1020
|
-
return issuesToBoard(raw).columns.flatMap((column) => column.cards);
|
|
1152
|
+
return issuesToBoard(raw, { targetEndFieldId: fieldId }).columns.flatMap((column) => column.cards);
|
|
1153
|
+
}
|
|
1154
|
+
function issueKeyOf(issue) {
|
|
1155
|
+
if (!issue || typeof issue !== "object" || !("key" in issue))
|
|
1156
|
+
return;
|
|
1157
|
+
return typeof issue.key === "string" ? issue.key : undefined;
|
|
1158
|
+
}
|
|
1159
|
+
function viewConcurrency() {
|
|
1160
|
+
const env = Number(process.env.PIPE_KAN_CHILDREN_CONCURRENCY);
|
|
1161
|
+
if (!Number.isNaN(env) && env > 0)
|
|
1162
|
+
return env;
|
|
1163
|
+
return Math.max(3, Math.min(10, availableParallelism2()));
|
|
1164
|
+
}
|
|
1165
|
+
function defaultFieldMapPath() {
|
|
1166
|
+
if (process.env.PIPE_KAN_FIELD_MAP)
|
|
1167
|
+
return process.env.PIPE_KAN_FIELD_MAP;
|
|
1168
|
+
if (process.env.VITEST) {
|
|
1169
|
+
return join3(tmpdir(), `pipe-kan-field-map-${process.pid}-${Math.random().toString(16).slice(2)}.json`);
|
|
1170
|
+
}
|
|
1171
|
+
return join3(homedir2(), ".pipe-kan", "field-map.json");
|
|
1172
|
+
}
|
|
1173
|
+
function defaultJiraConfigPath() {
|
|
1174
|
+
if (process.env.JIRA_CONFIG_FILE)
|
|
1175
|
+
return process.env.JIRA_CONFIG_FILE;
|
|
1176
|
+
return join3(process.env.XDG_CONFIG_HOME || join3(homedir2(), ".config"), ".jira", ".config.yml");
|
|
1177
|
+
}
|
|
1178
|
+
function safeRead(path) {
|
|
1179
|
+
if (!path)
|
|
1180
|
+
return;
|
|
1181
|
+
try {
|
|
1182
|
+
return readFileSync2(path, "utf8");
|
|
1183
|
+
} catch {
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
async function poolMap(items, concurrency, worker) {
|
|
1188
|
+
const results = new Array(items.length);
|
|
1189
|
+
let next = 0;
|
|
1190
|
+
async function run() {
|
|
1191
|
+
while (next < items.length) {
|
|
1192
|
+
const index = next++;
|
|
1193
|
+
results[index] = await worker(items[index]);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
const workers = Math.min(Math.max(1, concurrency), Math.max(1, items.length));
|
|
1197
|
+
await Promise.all(Array.from({ length: workers }, run));
|
|
1198
|
+
return results;
|
|
1021
1199
|
}
|
|
1022
1200
|
function createApp(opts) {
|
|
1023
1201
|
const cli = opts.cli ?? createStoreCli(opts.store);
|
|
1202
|
+
const fieldMapPath = opts.fieldMapPath ?? defaultFieldMapPath();
|
|
1203
|
+
const jiraConfigPath = opts.jiraConfigPath ?? defaultJiraConfigPath();
|
|
1024
1204
|
let flags = opts.flags ?? DEFAULT_FLAGS;
|
|
1025
1205
|
let payload = [];
|
|
1026
1206
|
let epicsPayload = [];
|
|
1027
1207
|
let childrenRaw = [];
|
|
1028
1208
|
let hasChildrenCache = false;
|
|
1029
1209
|
let childrenError;
|
|
1210
|
+
let targetEndFieldId = readTargetEndFieldMap(fieldMapPath).targetEnd ?? targetEndFieldIdFromJiraConfig(safeRead(jiraConfigPath) ?? "");
|
|
1211
|
+
function boardOpts() {
|
|
1212
|
+
return { targetEndFieldId };
|
|
1213
|
+
}
|
|
1214
|
+
function toBoard(raw) {
|
|
1215
|
+
return issuesToBoard(raw, boardOpts());
|
|
1216
|
+
}
|
|
1030
1217
|
function listedEpicKeys() {
|
|
1031
|
-
return
|
|
1218
|
+
return toBoard(epicsPayload).epics.map((epic) => epic.key);
|
|
1032
1219
|
}
|
|
1033
1220
|
function cacheFromStore() {
|
|
1034
1221
|
epicsPayload = opts.store.list(flagsToJql("-tEpic"));
|
|
@@ -1038,7 +1225,56 @@ function createApp(opts) {
|
|
|
1038
1225
|
}
|
|
1039
1226
|
function currentStatus(key) {
|
|
1040
1227
|
const fromPayload = payload.find((issue) => issue.key === key)?.fields?.status?.name;
|
|
1041
|
-
return fromPayload ??
|
|
1228
|
+
return fromPayload ?? toBoard(epicsPayload).epics.find((epic) => epic.key === key)?.status ?? app.board().epics.find((epic) => epic.key === key)?.status;
|
|
1229
|
+
}
|
|
1230
|
+
function rememberTargetEndId(issues) {
|
|
1231
|
+
for (const issue of issues) {
|
|
1232
|
+
if (!issue || typeof issue !== "object")
|
|
1233
|
+
continue;
|
|
1234
|
+
const row = issue;
|
|
1235
|
+
const id = resolveTargetEndFieldId(asNames(row.names));
|
|
1236
|
+
if (id) {
|
|
1237
|
+
targetEndFieldId = id;
|
|
1238
|
+
writeTargetEndFieldMap(fieldMapPath, { targetEnd: id });
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
const fromConfig = targetEndFieldIdFromJiraConfig(safeRead(jiraConfigPath) ?? "");
|
|
1243
|
+
if (!fromConfig)
|
|
1244
|
+
return;
|
|
1245
|
+
targetEndFieldId = fromConfig;
|
|
1246
|
+
writeTargetEndFieldMap(fieldMapPath, { targetEnd: fromConfig });
|
|
1247
|
+
}
|
|
1248
|
+
async function hydrateRaw(raw) {
|
|
1249
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
1250
|
+
return Array.isArray(raw) ? raw : [];
|
|
1251
|
+
const keys = [...new Set(raw.map(issueKeyOf).filter((key) => Boolean(key)))];
|
|
1252
|
+
if (!keys.length)
|
|
1253
|
+
return raw;
|
|
1254
|
+
const concurrency = viewConcurrency();
|
|
1255
|
+
console.log(`Refresh view-raw ${keys.length} issues; concurrency ${concurrency}`);
|
|
1256
|
+
const views = await poolMap(keys, concurrency, async (key) => {
|
|
1257
|
+
try {
|
|
1258
|
+
return JSON.parse(await cli.view(key));
|
|
1259
|
+
} catch (err) {
|
|
1260
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1261
|
+
console.log(`Refresh view ${key} failed; ${message}`);
|
|
1262
|
+
return null;
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
const byKey = new Map;
|
|
1266
|
+
for (const view of views) {
|
|
1267
|
+
const key = issueKeyOf(view);
|
|
1268
|
+
if (key)
|
|
1269
|
+
byKey.set(key, view);
|
|
1270
|
+
}
|
|
1271
|
+
const merged = raw.map((issue) => {
|
|
1272
|
+
const key = issueKeyOf(issue);
|
|
1273
|
+
const view = key ? byKey.get(key) : undefined;
|
|
1274
|
+
return view ? mergeViewIntoIssue(issue, view) : issue;
|
|
1275
|
+
});
|
|
1276
|
+
rememberTargetEndId(merged);
|
|
1277
|
+
return merged;
|
|
1042
1278
|
}
|
|
1043
1279
|
async function tryMove(key, status) {
|
|
1044
1280
|
const current = currentStatus(key);
|
|
@@ -1053,11 +1289,11 @@ function createApp(opts) {
|
|
|
1053
1289
|
return flags;
|
|
1054
1290
|
},
|
|
1055
1291
|
board() {
|
|
1056
|
-
const board =
|
|
1292
|
+
const board = toBoard(payload);
|
|
1057
1293
|
return {
|
|
1058
1294
|
columns: board.columns,
|
|
1059
|
-
epics: mergeEpics(
|
|
1060
|
-
...hasChildrenCache ? { children: columnsOf(childrenRaw) } : {},
|
|
1295
|
+
epics: mergeEpics(toBoard(epicsPayload).epics, board.epics),
|
|
1296
|
+
...hasChildrenCache ? { children: columnsOf(childrenRaw, targetEndFieldId) } : {},
|
|
1061
1297
|
...childrenError ? { error: childrenError } : {}
|
|
1062
1298
|
};
|
|
1063
1299
|
},
|
|
@@ -1069,6 +1305,7 @@ function createApp(opts) {
|
|
|
1069
1305
|
childrenError = undefined;
|
|
1070
1306
|
if (hydrateOpts?.fromStore)
|
|
1071
1307
|
cacheFromStore();
|
|
1308
|
+
rememberTargetEndId([...payload, ...epicsPayload, ...childrenRaw]);
|
|
1072
1309
|
return app.board();
|
|
1073
1310
|
},
|
|
1074
1311
|
async refresh(next) {
|
|
@@ -1078,17 +1315,17 @@ function createApp(opts) {
|
|
|
1078
1315
|
try {
|
|
1079
1316
|
const issues = await cli.list(flags);
|
|
1080
1317
|
const epics = await cli.listEpics(flags);
|
|
1081
|
-
const
|
|
1082
|
-
const
|
|
1083
|
-
const keys =
|
|
1084
|
-
const issueCount = Array.isArray(
|
|
1318
|
+
const listedPayload = JSON.parse(issues);
|
|
1319
|
+
const listedEpics = JSON.parse(epics);
|
|
1320
|
+
const keys = toBoard(listedEpics).epics.map((epic) => epic.key);
|
|
1321
|
+
const issueCount = Array.isArray(listedPayload) ? listedPayload.length : 0;
|
|
1085
1322
|
console.log(`Refresh listed ${issueCount} issues, ${keys.length} epics`);
|
|
1086
1323
|
let nextChildren = [];
|
|
1087
1324
|
let nextHasCache = false;
|
|
1088
1325
|
let nextError;
|
|
1089
1326
|
try {
|
|
1090
1327
|
nextChildren = JSON.parse(await cli.listChildren(keys));
|
|
1091
|
-
const cards = cardsOf(nextChildren);
|
|
1328
|
+
const cards = cardsOf(nextChildren, targetEndFieldId);
|
|
1092
1329
|
if (cards.length > 0 && !cards.some((card) => card.epic)) {
|
|
1093
1330
|
console.log(`Refresh children missing Epic keys; skip ${keys.length} per-Epic lists`);
|
|
1094
1331
|
nextChildren = [];
|
|
@@ -1103,9 +1340,9 @@ function createApp(opts) {
|
|
|
1103
1340
|
nextError = err instanceof Error ? err.message : "Epic children list failed";
|
|
1104
1341
|
console.log("Refresh children failed; keeping existing children", nextError);
|
|
1105
1342
|
}
|
|
1106
|
-
payload =
|
|
1107
|
-
epicsPayload =
|
|
1108
|
-
childrenRaw = nextChildren;
|
|
1343
|
+
payload = await hydrateRaw(listedPayload);
|
|
1344
|
+
epicsPayload = await hydrateRaw(listedEpics);
|
|
1345
|
+
childrenRaw = nextHasCache ? await hydrateRaw(nextChildren) : nextChildren;
|
|
1109
1346
|
hasChildrenCache = nextHasCache;
|
|
1110
1347
|
childrenError = nextError;
|
|
1111
1348
|
return app.board();
|
|
@@ -1116,12 +1353,12 @@ function createApp(opts) {
|
|
|
1116
1353
|
}
|
|
1117
1354
|
},
|
|
1118
1355
|
async children(epic) {
|
|
1119
|
-
const listed =
|
|
1356
|
+
const listed = toBoard(epicsPayload).epics;
|
|
1120
1357
|
if (!listed.some((e) => e.key === epic)) {
|
|
1121
1358
|
return { columns: [], epics: [] };
|
|
1122
1359
|
}
|
|
1123
1360
|
if (hasChildrenCache) {
|
|
1124
|
-
const cards = Object.fromEntries(Object.entries(columnsOf(childrenRaw)).map(([title, list]) => [
|
|
1361
|
+
const cards = Object.fromEntries(Object.entries(columnsOf(childrenRaw, targetEndFieldId)).map(([title, list]) => [
|
|
1125
1362
|
title,
|
|
1126
1363
|
list.filter((card) => card.epic === epic).map((card) => ({
|
|
1127
1364
|
...card,
|
|
@@ -1132,7 +1369,7 @@ function createApp(opts) {
|
|
|
1132
1369
|
if (columns.length)
|
|
1133
1370
|
return { columns, epics: [] };
|
|
1134
1371
|
}
|
|
1135
|
-
return stampMissingEpic(
|
|
1372
|
+
return stampMissingEpic(toBoard(JSON.parse(await cli.listEpic(epic, flags))), epic);
|
|
1136
1373
|
},
|
|
1137
1374
|
async move(key, status) {
|
|
1138
1375
|
const result = await tryMove(key, status);
|
|
@@ -1194,7 +1431,12 @@ async function createBoardApp(opts) {
|
|
|
1194
1431
|
token: env.JIRA_API_TOKEN,
|
|
1195
1432
|
flags
|
|
1196
1433
|
}) : createStoreCli(store, flags);
|
|
1197
|
-
const app = createApp({
|
|
1434
|
+
const app = createApp({
|
|
1435
|
+
store,
|
|
1436
|
+
cli,
|
|
1437
|
+
flags,
|
|
1438
|
+
jiraConfigPath: jiraCliConfigPath(env)
|
|
1439
|
+
});
|
|
1198
1440
|
if (opts.piped) {
|
|
1199
1441
|
app.hydrate(opts.raw);
|
|
1200
1442
|
} else {
|
|
@@ -1398,10 +1640,10 @@ function formatCard(card) {
|
|
|
1398
1640
|
}
|
|
1399
1641
|
|
|
1400
1642
|
// src/server/agent/config.ts
|
|
1401
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1402
|
-
import { homedir } from "node:os";
|
|
1403
|
-
import { delimiter as delimiter2, dirname, join as
|
|
1404
|
-
var DEFAULT_CONFIG_PATH =
|
|
1643
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1644
|
+
import { homedir as homedir3 } from "node:os";
|
|
1645
|
+
import { delimiter as delimiter2, dirname as dirname2, join as join4 } from "node:path";
|
|
1646
|
+
var DEFAULT_CONFIG_PATH = join4(homedir3(), ".config", "pipe-kan", "agent.json");
|
|
1405
1647
|
var FALLBACK_MODELS = {
|
|
1406
1648
|
cursor: [
|
|
1407
1649
|
{ id: "composer-2", name: "Composer 2" },
|
|
@@ -1448,7 +1690,7 @@ function commandOnPath(command, env = process.env) {
|
|
|
1448
1690
|
for (const dir of pathVar.split(delimiter2)) {
|
|
1449
1691
|
if (!dir)
|
|
1450
1692
|
continue;
|
|
1451
|
-
if (existsSync2(
|
|
1693
|
+
if (existsSync2(join4(dir, command)))
|
|
1452
1694
|
return true;
|
|
1453
1695
|
}
|
|
1454
1696
|
return false;
|
|
@@ -1457,7 +1699,7 @@ function loadAgentConfig(path = agentConfigPath()) {
|
|
|
1457
1699
|
if (!existsSync2(path))
|
|
1458
1700
|
return structuredClone(DEFAULT_CONFIG);
|
|
1459
1701
|
try {
|
|
1460
|
-
const raw = JSON.parse(
|
|
1702
|
+
const raw = JSON.parse(readFileSync3(path, "utf8"));
|
|
1461
1703
|
return mergeConfig(raw);
|
|
1462
1704
|
} catch {
|
|
1463
1705
|
return structuredClone(DEFAULT_CONFIG);
|
|
@@ -1476,7 +1718,7 @@ function saveAgentConfig(patch, path = agentConfigPath()) {
|
|
|
1476
1718
|
}
|
|
1477
1719
|
}
|
|
1478
1720
|
ensureAgentConfigDir(path);
|
|
1479
|
-
|
|
1721
|
+
writeFileSync2(path, `${JSON.stringify(next, null, 2)}
|
|
1480
1722
|
`);
|
|
1481
1723
|
return next;
|
|
1482
1724
|
}
|
|
@@ -1492,16 +1734,16 @@ function mergeConfig(raw) {
|
|
|
1492
1734
|
};
|
|
1493
1735
|
}
|
|
1494
1736
|
function ensureAgentConfigDir(path = agentConfigPath()) {
|
|
1495
|
-
const dir =
|
|
1737
|
+
const dir = dirname2(path);
|
|
1496
1738
|
if (!existsSync2(dir))
|
|
1497
|
-
|
|
1739
|
+
mkdirSync2(dir, { recursive: true });
|
|
1498
1740
|
}
|
|
1499
1741
|
|
|
1500
1742
|
// src/server/agent/session.ts
|
|
1501
1743
|
import { spawn as spawn2 } from "node:child_process";
|
|
1502
1744
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
1503
|
-
import { tmpdir } from "node:os";
|
|
1504
|
-
import { join as
|
|
1745
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
1746
|
+
import { join as join5 } from "node:path";
|
|
1505
1747
|
import { Readable } from "node:stream";
|
|
1506
1748
|
|
|
1507
1749
|
// node_modules/@agentclientprotocol/sdk/dist/schema/index.js
|
|
@@ -11227,7 +11469,7 @@ class AcpSession {
|
|
|
11227
11469
|
executeTool = null;
|
|
11228
11470
|
stderr = "";
|
|
11229
11471
|
selectedModelId = null;
|
|
11230
|
-
workspace = mkdtempSync(
|
|
11472
|
+
workspace = mkdtempSync(join5(tmpdir2(), "pipe-kan-agent-"));
|
|
11231
11473
|
constructor(config) {
|
|
11232
11474
|
this.config = config;
|
|
11233
11475
|
this.id = crypto.randomUUID();
|
|
@@ -11539,11 +11781,11 @@ function permissionOptionForDecision(options, decision) {
|
|
|
11539
11781
|
}
|
|
11540
11782
|
|
|
11541
11783
|
// src/server/agent/skills.ts
|
|
11542
|
-
import { existsSync as existsSync3, readFileSync as
|
|
11543
|
-
import { homedir as
|
|
11544
|
-
import { join as
|
|
11545
|
-
function createSkillRegistry(bundledDir =
|
|
11546
|
-
const userDir =
|
|
11784
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync } from "node:fs";
|
|
11785
|
+
import { homedir as homedir4 } from "node:os";
|
|
11786
|
+
import { join as join6 } from "node:path";
|
|
11787
|
+
function createSkillRegistry(bundledDir = join6(import.meta.dirname, "..", "..", "..", ".agents", "skills")) {
|
|
11788
|
+
const userDir = join6(homedir4(), ".pi", "agent", "skills");
|
|
11547
11789
|
return {
|
|
11548
11790
|
list() {
|
|
11549
11791
|
const bundled = listSkills(bundledDir);
|
|
@@ -11582,11 +11824,11 @@ function listSkills(dir) {
|
|
|
11582
11824
|
return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => readSkill(skillPath(dir, entry.name), entry.name)).filter((skill) => skill !== undefined);
|
|
11583
11825
|
}
|
|
11584
11826
|
function skillPath(dir, id) {
|
|
11585
|
-
return
|
|
11827
|
+
return join6(dir, id, "SKILL.md");
|
|
11586
11828
|
}
|
|
11587
11829
|
function readSkill(path, id) {
|
|
11588
11830
|
try {
|
|
11589
|
-
const text =
|
|
11831
|
+
const text = readFileSync4(path, "utf8");
|
|
11590
11832
|
const front = parseFrontMatter(text);
|
|
11591
11833
|
return {
|
|
11592
11834
|
id,
|
|
@@ -11613,8 +11855,8 @@ function parseFrontMatter(text) {
|
|
|
11613
11855
|
}
|
|
11614
11856
|
|
|
11615
11857
|
// src/server/agent/tools.ts
|
|
11616
|
-
import { readFileSync as
|
|
11617
|
-
import { join as
|
|
11858
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
11859
|
+
import { join as join7 } from "node:path";
|
|
11618
11860
|
var TOOLS = [
|
|
11619
11861
|
{
|
|
11620
11862
|
name: "board_state",
|
|
@@ -11731,7 +11973,7 @@ var EXECUTORS = {
|
|
|
11731
11973
|
return { ok: false, error: "Path traversal not allowed" };
|
|
11732
11974
|
const repoRoot = process.cwd();
|
|
11733
11975
|
try {
|
|
11734
|
-
const text =
|
|
11976
|
+
const text = readFileSync5(join7(repoRoot, relPath), "utf8");
|
|
11735
11977
|
return { ok: true, value: { path: relPath, text } };
|
|
11736
11978
|
} catch (err) {
|
|
11737
11979
|
return { ok: false, error: String(err) };
|
|
@@ -12264,12 +12506,12 @@ function handleRequest(req, res, ctx) {
|
|
|
12264
12506
|
}
|
|
12265
12507
|
|
|
12266
12508
|
// src/jira-config.ts
|
|
12267
|
-
import { mkdirSync as
|
|
12268
|
-
import { join as
|
|
12509
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
12510
|
+
import { join as join8 } from "node:path";
|
|
12269
12511
|
function writeJiraConfig(dir, server) {
|
|
12270
|
-
|
|
12271
|
-
const path =
|
|
12272
|
-
|
|
12512
|
+
mkdirSync3(dir, { recursive: true });
|
|
12513
|
+
const path = join8(dir, "jira.config.yml");
|
|
12514
|
+
writeFileSync3(path, [
|
|
12273
12515
|
"installation: Cloud",
|
|
12274
12516
|
`server: ${server}`,
|
|
12275
12517
|
`login: ${ME.emailAddress}`,
|
|
@@ -12324,7 +12566,7 @@ function stdinStat() {
|
|
|
12324
12566
|
|
|
12325
12567
|
// src/ui.ts
|
|
12326
12568
|
import { createReadStream, existsSync as existsSync4, statSync } from "node:fs";
|
|
12327
|
-
import { dirname as
|
|
12569
|
+
import { dirname as dirname3, extname, join as join9, resolve as resolve2, sep } from "node:path";
|
|
12328
12570
|
import { fileURLToPath } from "node:url";
|
|
12329
12571
|
var types = {
|
|
12330
12572
|
".css": "text/css; charset=utf-8",
|
|
@@ -12338,10 +12580,10 @@ var types = {
|
|
|
12338
12580
|
".woff2": "font/woff2"
|
|
12339
12581
|
};
|
|
12340
12582
|
function packageRoot(from = import.meta.url) {
|
|
12341
|
-
return resolve2(
|
|
12583
|
+
return resolve2(dirname3(fileURLToPath(from)), "..");
|
|
12342
12584
|
}
|
|
12343
12585
|
function uiDir(root) {
|
|
12344
|
-
return
|
|
12586
|
+
return join9(root, "dist", "ui");
|
|
12345
12587
|
}
|
|
12346
12588
|
function inside(root, file) {
|
|
12347
12589
|
const base = resolve2(root);
|
|
@@ -12350,7 +12592,7 @@ function inside(root, file) {
|
|
|
12350
12592
|
}
|
|
12351
12593
|
function sendUi(root, req, res) {
|
|
12352
12594
|
const ui = uiDir(root);
|
|
12353
|
-
const index =
|
|
12595
|
+
const index = join9(ui, "index.html");
|
|
12354
12596
|
if (!existsSync4(index))
|
|
12355
12597
|
return false;
|
|
12356
12598
|
const path = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
|
|
@@ -12371,7 +12613,7 @@ function announce(line) {
|
|
|
12371
12613
|
}
|
|
12372
12614
|
async function runServer(opts) {
|
|
12373
12615
|
const piped = await readPipe();
|
|
12374
|
-
const raw = piped ?? JSON.parse(
|
|
12616
|
+
const raw = piped ?? JSON.parse(readFileSync6(join10(opts.root, "fixtures/issues.json"), "utf8"));
|
|
12375
12617
|
const flags = argvToFlags(process.argv);
|
|
12376
12618
|
const { app, store, kind } = await createBoardApp({
|
|
12377
12619
|
raw,
|
|
@@ -12393,7 +12635,7 @@ async function runServer(opts) {
|
|
|
12393
12635
|
const { host, port } = resolveListen();
|
|
12394
12636
|
await bindListen(server, host, port);
|
|
12395
12637
|
const origin = `http://127.0.0.1:${port}`;
|
|
12396
|
-
const fakeConfig = writeJiraConfig(
|
|
12638
|
+
const fakeConfig = writeJiraConfig(join10(tmpdir3(), "pipe-kan"), origin);
|
|
12397
12639
|
announce(`pipe-kan http://${host}:${port}`);
|
|
12398
12640
|
announce(`cli ${kind === "jira" ? resolveJiraBin() : "store"}`);
|
|
12399
12641
|
announce(`Fake Jira ${origin}/rest/api/2/search`);
|