pipe-kan 0.25.4 → 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/README.md +4 -2
- package/dist/pipe-kan.js +615 -105
- package/dist/ui/assets/index-sJ2vPLG9.css +2 -0
- package/dist/ui/assets/index-xaefvhYI.js +56 -0
- package/dist/ui/index.html +2 -2
- package/fixtures/issues.json +16 -8
- package/package.json +4 -1
- package/dist/ui/assets/index-DE44F4tt.js +0 -56
- package/dist/ui/assets/index-DdZGfEDG.css +0 -2
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
|
}
|
|
@@ -448,9 +549,60 @@ Available states for issue ${key}: ${available}`
|
|
|
448
549
|
}
|
|
449
550
|
return { ok: true };
|
|
450
551
|
}
|
|
552
|
+
create(input) {
|
|
553
|
+
if (!input.summary.trim())
|
|
554
|
+
return { ok: false, error: "summary is required" };
|
|
555
|
+
const prefix = (input.project ?? this.issues[0]?.key.split("-")[0] ?? "DEMO").toUpperCase();
|
|
556
|
+
let max = 0;
|
|
557
|
+
for (const issue of this.issues) {
|
|
558
|
+
const [project, n] = issue.key.split("-");
|
|
559
|
+
if (project.toUpperCase() === prefix)
|
|
560
|
+
max = Math.max(max, Number(n) || 0);
|
|
561
|
+
}
|
|
562
|
+
const key = `${prefix}-${max + 1}`;
|
|
563
|
+
const typeName = input.type?.trim() || "Story";
|
|
564
|
+
const issue = {
|
|
565
|
+
key,
|
|
566
|
+
fields: {
|
|
567
|
+
summary: input.summary.trim(),
|
|
568
|
+
description: input.description ?? "",
|
|
569
|
+
labels: input.labels ?? [],
|
|
570
|
+
status: { name: input.status?.trim() || "To Do" },
|
|
571
|
+
issuetype: { name: typeName },
|
|
572
|
+
issueType: { name: typeName },
|
|
573
|
+
created: new Date().toISOString()
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
if (input.parent && validIssueKey(input.parent)) {
|
|
577
|
+
issue.fields.parent = { key: input.parent };
|
|
578
|
+
}
|
|
579
|
+
this.issues.push(issue);
|
|
580
|
+
return { ok: true, key };
|
|
581
|
+
}
|
|
582
|
+
edit(key, input) {
|
|
583
|
+
const issue = this.get(key);
|
|
584
|
+
if (!issue)
|
|
585
|
+
return { ok: false, error: `Issue ${key} not found` };
|
|
586
|
+
if (input.summary !== undefined)
|
|
587
|
+
issue.fields.summary = input.summary;
|
|
588
|
+
if (input.description !== undefined)
|
|
589
|
+
issue.fields.description = input.description;
|
|
590
|
+
if (input.labels !== undefined)
|
|
591
|
+
issue.fields.labels = input.labels;
|
|
592
|
+
return { ok: true };
|
|
593
|
+
}
|
|
451
594
|
}
|
|
452
595
|
|
|
453
596
|
// src/cli.ts
|
|
597
|
+
function createdKeyFromOutput(text) {
|
|
598
|
+
const matches = text.match(/[A-Z][A-Z0-9]*-\d+/gi) ?? [];
|
|
599
|
+
for (let i = matches.length - 1;i >= 0; i--) {
|
|
600
|
+
const key = matches[i];
|
|
601
|
+
if (key && validIssueKey(key))
|
|
602
|
+
return key;
|
|
603
|
+
}
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
454
606
|
function projectClause(flags) {
|
|
455
607
|
const { projects } = parseFlags(flags || DEFAULT_FLAGS);
|
|
456
608
|
if (!projects.length)
|
|
@@ -497,6 +649,12 @@ function createStoreCli(store, defaultFlags = DEFAULT_FLAGS) {
|
|
|
497
649
|
async move(key, status) {
|
|
498
650
|
return store.move(key, status);
|
|
499
651
|
},
|
|
652
|
+
async create(input) {
|
|
653
|
+
return store.create(input);
|
|
654
|
+
},
|
|
655
|
+
async edit(key, input) {
|
|
656
|
+
return store.edit(key, input);
|
|
657
|
+
},
|
|
500
658
|
async open(key) {
|
|
501
659
|
return `/browse/${key}`;
|
|
502
660
|
},
|
|
@@ -612,6 +770,13 @@ function createJiraCli(opts = {}) {
|
|
|
612
770
|
console.log(`jira ${fmt(args)} ${issues.length} total`);
|
|
613
771
|
return JSON.stringify(issues);
|
|
614
772
|
}
|
|
773
|
+
async function moveIssue(key, status) {
|
|
774
|
+
const result = await runRetry(["issue", "move", key, status]);
|
|
775
|
+
if (result.code !== 0) {
|
|
776
|
+
return { ok: false, error: (result.stderr || result.stdout).trim() };
|
|
777
|
+
}
|
|
778
|
+
return { ok: true };
|
|
779
|
+
}
|
|
615
780
|
return {
|
|
616
781
|
async list(flags) {
|
|
617
782
|
const parsed = parseFlags(flags || defaultFlags);
|
|
@@ -730,12 +895,54 @@ function createJiraCli(opts = {}) {
|
|
|
730
895
|
return JSON.stringify(issues);
|
|
731
896
|
},
|
|
732
897
|
async move(key, status) {
|
|
733
|
-
|
|
898
|
+
return moveIssue(key, status);
|
|
899
|
+
},
|
|
900
|
+
async create(input) {
|
|
901
|
+
const args = [
|
|
902
|
+
"issue",
|
|
903
|
+
"create",
|
|
904
|
+
"--no-input",
|
|
905
|
+
"-y",
|
|
906
|
+
"-t",
|
|
907
|
+
input.type?.trim() || "Story",
|
|
908
|
+
"-s",
|
|
909
|
+
input.summary
|
|
910
|
+
];
|
|
911
|
+
if (input.description) {
|
|
912
|
+
args.push("-b", input.description);
|
|
913
|
+
}
|
|
914
|
+
for (const label of input.labels ?? []) {
|
|
915
|
+
args.push("-l", label);
|
|
916
|
+
}
|
|
917
|
+
if (input.parent) {
|
|
918
|
+
args.push("-P", input.parent);
|
|
919
|
+
}
|
|
920
|
+
const result = await runRetry(args);
|
|
734
921
|
if (result.code !== 0) {
|
|
735
|
-
return {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
922
|
+
return { ok: false, error: (result.stderr || result.stdout).trim() };
|
|
923
|
+
}
|
|
924
|
+
const key = createdKeyFromOutput(`${result.stdout}
|
|
925
|
+
${result.stderr}`);
|
|
926
|
+
if (input.status?.trim() && key) {
|
|
927
|
+
const moved = await moveIssue(key, input.status.trim());
|
|
928
|
+
if (!moved.ok)
|
|
929
|
+
return { ok: false, key, error: moved.error };
|
|
930
|
+
}
|
|
931
|
+
return key ? { ok: true, key } : { ok: true };
|
|
932
|
+
},
|
|
933
|
+
async edit(key, input) {
|
|
934
|
+
const args = ["issue", "edit", key, "--no-input", "-y"];
|
|
935
|
+
if (input.summary !== undefined)
|
|
936
|
+
args.push("-s", input.summary);
|
|
937
|
+
if (input.description !== undefined)
|
|
938
|
+
args.push("-b", input.description);
|
|
939
|
+
if (input.labels?.length) {
|
|
940
|
+
for (const label of input.labels)
|
|
941
|
+
args.push("-l", label);
|
|
942
|
+
}
|
|
943
|
+
const result = await runRetry(args);
|
|
944
|
+
if (result.code !== 0) {
|
|
945
|
+
return { ok: false, error: (result.stderr || result.stdout).trim() };
|
|
739
946
|
}
|
|
740
947
|
return { ok: true };
|
|
741
948
|
},
|
|
@@ -895,9 +1102,40 @@ function flattenIssue(raw, url) {
|
|
|
895
1102
|
return [...rows, ...rest];
|
|
896
1103
|
}
|
|
897
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
|
+
|
|
898
1136
|
// src/app.ts
|
|
899
|
-
function columnsOf(raw) {
|
|
900
|
-
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]));
|
|
901
1139
|
}
|
|
902
1140
|
function stampMissingEpic(board, epic) {
|
|
903
1141
|
for (const column of board.columns) {
|
|
@@ -908,21 +1146,76 @@ function stampMissingEpic(board, epic) {
|
|
|
908
1146
|
}
|
|
909
1147
|
return board;
|
|
910
1148
|
}
|
|
911
|
-
function cardsOf(raw) {
|
|
1149
|
+
function cardsOf(raw, fieldId) {
|
|
912
1150
|
if (!Array.isArray(raw))
|
|
913
1151
|
return [];
|
|
914
|
-
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;
|
|
915
1199
|
}
|
|
916
1200
|
function createApp(opts) {
|
|
917
1201
|
const cli = opts.cli ?? createStoreCli(opts.store);
|
|
1202
|
+
const fieldMapPath = opts.fieldMapPath ?? defaultFieldMapPath();
|
|
1203
|
+
const jiraConfigPath = opts.jiraConfigPath ?? defaultJiraConfigPath();
|
|
918
1204
|
let flags = opts.flags ?? DEFAULT_FLAGS;
|
|
919
1205
|
let payload = [];
|
|
920
1206
|
let epicsPayload = [];
|
|
921
1207
|
let childrenRaw = [];
|
|
922
1208
|
let hasChildrenCache = false;
|
|
923
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
|
+
}
|
|
924
1217
|
function listedEpicKeys() {
|
|
925
|
-
return
|
|
1218
|
+
return toBoard(epicsPayload).epics.map((epic) => epic.key);
|
|
926
1219
|
}
|
|
927
1220
|
function cacheFromStore() {
|
|
928
1221
|
epicsPayload = opts.store.list(flagsToJql("-tEpic"));
|
|
@@ -932,7 +1225,56 @@ function createApp(opts) {
|
|
|
932
1225
|
}
|
|
933
1226
|
function currentStatus(key) {
|
|
934
1227
|
const fromPayload = payload.find((issue) => issue.key === key)?.fields?.status?.name;
|
|
935
|
-
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;
|
|
936
1278
|
}
|
|
937
1279
|
async function tryMove(key, status) {
|
|
938
1280
|
const current = currentStatus(key);
|
|
@@ -947,11 +1289,11 @@ function createApp(opts) {
|
|
|
947
1289
|
return flags;
|
|
948
1290
|
},
|
|
949
1291
|
board() {
|
|
950
|
-
const board =
|
|
1292
|
+
const board = toBoard(payload);
|
|
951
1293
|
return {
|
|
952
1294
|
columns: board.columns,
|
|
953
|
-
epics: mergeEpics(
|
|
954
|
-
...hasChildrenCache ? { children: columnsOf(childrenRaw) } : {},
|
|
1295
|
+
epics: mergeEpics(toBoard(epicsPayload).epics, board.epics),
|
|
1296
|
+
...hasChildrenCache ? { children: columnsOf(childrenRaw, targetEndFieldId) } : {},
|
|
955
1297
|
...childrenError ? { error: childrenError } : {}
|
|
956
1298
|
};
|
|
957
1299
|
},
|
|
@@ -963,6 +1305,7 @@ function createApp(opts) {
|
|
|
963
1305
|
childrenError = undefined;
|
|
964
1306
|
if (hydrateOpts?.fromStore)
|
|
965
1307
|
cacheFromStore();
|
|
1308
|
+
rememberTargetEndId([...payload, ...epicsPayload, ...childrenRaw]);
|
|
966
1309
|
return app.board();
|
|
967
1310
|
},
|
|
968
1311
|
async refresh(next) {
|
|
@@ -972,17 +1315,17 @@ function createApp(opts) {
|
|
|
972
1315
|
try {
|
|
973
1316
|
const issues = await cli.list(flags);
|
|
974
1317
|
const epics = await cli.listEpics(flags);
|
|
975
|
-
const
|
|
976
|
-
const
|
|
977
|
-
const keys =
|
|
978
|
-
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;
|
|
979
1322
|
console.log(`Refresh listed ${issueCount} issues, ${keys.length} epics`);
|
|
980
1323
|
let nextChildren = [];
|
|
981
1324
|
let nextHasCache = false;
|
|
982
1325
|
let nextError;
|
|
983
1326
|
try {
|
|
984
1327
|
nextChildren = JSON.parse(await cli.listChildren(keys));
|
|
985
|
-
const cards = cardsOf(nextChildren);
|
|
1328
|
+
const cards = cardsOf(nextChildren, targetEndFieldId);
|
|
986
1329
|
if (cards.length > 0 && !cards.some((card) => card.epic)) {
|
|
987
1330
|
console.log(`Refresh children missing Epic keys; skip ${keys.length} per-Epic lists`);
|
|
988
1331
|
nextChildren = [];
|
|
@@ -997,9 +1340,9 @@ function createApp(opts) {
|
|
|
997
1340
|
nextError = err instanceof Error ? err.message : "Epic children list failed";
|
|
998
1341
|
console.log("Refresh children failed; keeping existing children", nextError);
|
|
999
1342
|
}
|
|
1000
|
-
payload =
|
|
1001
|
-
epicsPayload =
|
|
1002
|
-
childrenRaw = nextChildren;
|
|
1343
|
+
payload = await hydrateRaw(listedPayload);
|
|
1344
|
+
epicsPayload = await hydrateRaw(listedEpics);
|
|
1345
|
+
childrenRaw = nextHasCache ? await hydrateRaw(nextChildren) : nextChildren;
|
|
1003
1346
|
hasChildrenCache = nextHasCache;
|
|
1004
1347
|
childrenError = nextError;
|
|
1005
1348
|
return app.board();
|
|
@@ -1010,12 +1353,12 @@ function createApp(opts) {
|
|
|
1010
1353
|
}
|
|
1011
1354
|
},
|
|
1012
1355
|
async children(epic) {
|
|
1013
|
-
const listed =
|
|
1356
|
+
const listed = toBoard(epicsPayload).epics;
|
|
1014
1357
|
if (!listed.some((e) => e.key === epic)) {
|
|
1015
1358
|
return { columns: [], epics: [] };
|
|
1016
1359
|
}
|
|
1017
1360
|
if (hasChildrenCache) {
|
|
1018
|
-
const cards = Object.fromEntries(Object.entries(columnsOf(childrenRaw)).map(([title, list]) => [
|
|
1361
|
+
const cards = Object.fromEntries(Object.entries(columnsOf(childrenRaw, targetEndFieldId)).map(([title, list]) => [
|
|
1019
1362
|
title,
|
|
1020
1363
|
list.filter((card) => card.epic === epic).map((card) => ({
|
|
1021
1364
|
...card,
|
|
@@ -1026,7 +1369,7 @@ function createApp(opts) {
|
|
|
1026
1369
|
if (columns.length)
|
|
1027
1370
|
return { columns, epics: [] };
|
|
1028
1371
|
}
|
|
1029
|
-
return stampMissingEpic(
|
|
1372
|
+
return stampMissingEpic(toBoard(JSON.parse(await cli.listEpic(epic, flags))), epic);
|
|
1030
1373
|
},
|
|
1031
1374
|
async move(key, status) {
|
|
1032
1375
|
const result = await tryMove(key, status);
|
|
@@ -1043,6 +1386,22 @@ function createApp(opts) {
|
|
|
1043
1386
|
const result = await tryMove(key, status);
|
|
1044
1387
|
return result.ok ? { ok: true, noop: result.noop } : { ok: false, error: result.error };
|
|
1045
1388
|
},
|
|
1389
|
+
async create(input) {
|
|
1390
|
+
const result = await cli.create(input);
|
|
1391
|
+
if (!result.ok) {
|
|
1392
|
+
return { ok: false, error: result.error, key: result.key, board: app.board() };
|
|
1393
|
+
}
|
|
1394
|
+
await app.refresh();
|
|
1395
|
+
return { ok: true, key: result.key, board: app.board() };
|
|
1396
|
+
},
|
|
1397
|
+
async edit(key, input) {
|
|
1398
|
+
const result = await cli.edit(key, input);
|
|
1399
|
+
if (!result.ok) {
|
|
1400
|
+
return { ok: false, error: result.error, board: app.board() };
|
|
1401
|
+
}
|
|
1402
|
+
await app.refresh();
|
|
1403
|
+
return { ok: true, board: app.board() };
|
|
1404
|
+
},
|
|
1046
1405
|
async open(key) {
|
|
1047
1406
|
const urlP = cli.open(key);
|
|
1048
1407
|
try {
|
|
@@ -1072,7 +1431,12 @@ async function createBoardApp(opts) {
|
|
|
1072
1431
|
token: env.JIRA_API_TOKEN,
|
|
1073
1432
|
flags
|
|
1074
1433
|
}) : createStoreCli(store, flags);
|
|
1075
|
-
const app = createApp({
|
|
1434
|
+
const app = createApp({
|
|
1435
|
+
store,
|
|
1436
|
+
cli,
|
|
1437
|
+
flags,
|
|
1438
|
+
jiraConfigPath: jiraCliConfigPath(env)
|
|
1439
|
+
});
|
|
1076
1440
|
if (opts.piped) {
|
|
1077
1441
|
app.hydrate(opts.raw);
|
|
1078
1442
|
} else {
|
|
@@ -1155,6 +1519,38 @@ function handleAppApi(req, res, app) {
|
|
|
1155
1519
|
});
|
|
1156
1520
|
return true;
|
|
1157
1521
|
}
|
|
1522
|
+
if (url.pathname === "/api/issue/create" && method === "POST") {
|
|
1523
|
+
reply(req, res, async (text) => {
|
|
1524
|
+
const body = text ? JSON.parse(text) : {};
|
|
1525
|
+
const labels = Array.isArray(body.labels) ? body.labels.filter((item) => typeof item === "string") : [];
|
|
1526
|
+
const result = await app.create({
|
|
1527
|
+
summary: String(body.summary ?? ""),
|
|
1528
|
+
...body.description !== undefined ? { description: String(body.description) } : {},
|
|
1529
|
+
labels,
|
|
1530
|
+
...body.status !== undefined ? { status: String(body.status) } : {},
|
|
1531
|
+
...body.parent !== undefined ? { parent: String(body.parent) } : {},
|
|
1532
|
+
...body.type !== undefined ? { type: String(body.type) } : {}
|
|
1533
|
+
});
|
|
1534
|
+
json(res, result.ok ? 200 : 409, result);
|
|
1535
|
+
});
|
|
1536
|
+
return true;
|
|
1537
|
+
}
|
|
1538
|
+
if (url.pathname === "/api/issue/edit" && method === "POST") {
|
|
1539
|
+
reply(req, res, async (text) => {
|
|
1540
|
+
const body = text ? JSON.parse(text) : {};
|
|
1541
|
+
const input = {};
|
|
1542
|
+
if (body.summary !== undefined)
|
|
1543
|
+
input.summary = String(body.summary);
|
|
1544
|
+
if (body.description !== undefined)
|
|
1545
|
+
input.description = String(body.description);
|
|
1546
|
+
if (Array.isArray(body.labels)) {
|
|
1547
|
+
input.labels = body.labels.filter((item) => typeof item === "string");
|
|
1548
|
+
}
|
|
1549
|
+
const result = await app.edit(String(body.key ?? ""), input);
|
|
1550
|
+
json(res, result.ok ? 200 : 409, result);
|
|
1551
|
+
});
|
|
1552
|
+
return true;
|
|
1553
|
+
}
|
|
1158
1554
|
return false;
|
|
1159
1555
|
}
|
|
1160
1556
|
|
|
@@ -1244,10 +1640,10 @@ function formatCard(card) {
|
|
|
1244
1640
|
}
|
|
1245
1641
|
|
|
1246
1642
|
// src/server/agent/config.ts
|
|
1247
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1248
|
-
import { homedir } from "node:os";
|
|
1249
|
-
import { delimiter as delimiter2, dirname, join as
|
|
1250
|
-
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");
|
|
1251
1647
|
var FALLBACK_MODELS = {
|
|
1252
1648
|
cursor: [
|
|
1253
1649
|
{ id: "composer-2", name: "Composer 2" },
|
|
@@ -1294,7 +1690,7 @@ function commandOnPath(command, env = process.env) {
|
|
|
1294
1690
|
for (const dir of pathVar.split(delimiter2)) {
|
|
1295
1691
|
if (!dir)
|
|
1296
1692
|
continue;
|
|
1297
|
-
if (existsSync2(
|
|
1693
|
+
if (existsSync2(join4(dir, command)))
|
|
1298
1694
|
return true;
|
|
1299
1695
|
}
|
|
1300
1696
|
return false;
|
|
@@ -1303,7 +1699,7 @@ function loadAgentConfig(path = agentConfigPath()) {
|
|
|
1303
1699
|
if (!existsSync2(path))
|
|
1304
1700
|
return structuredClone(DEFAULT_CONFIG);
|
|
1305
1701
|
try {
|
|
1306
|
-
const raw = JSON.parse(
|
|
1702
|
+
const raw = JSON.parse(readFileSync3(path, "utf8"));
|
|
1307
1703
|
return mergeConfig(raw);
|
|
1308
1704
|
} catch {
|
|
1309
1705
|
return structuredClone(DEFAULT_CONFIG);
|
|
@@ -1322,7 +1718,7 @@ function saveAgentConfig(patch, path = agentConfigPath()) {
|
|
|
1322
1718
|
}
|
|
1323
1719
|
}
|
|
1324
1720
|
ensureAgentConfigDir(path);
|
|
1325
|
-
|
|
1721
|
+
writeFileSync2(path, `${JSON.stringify(next, null, 2)}
|
|
1326
1722
|
`);
|
|
1327
1723
|
return next;
|
|
1328
1724
|
}
|
|
@@ -1338,16 +1734,16 @@ function mergeConfig(raw) {
|
|
|
1338
1734
|
};
|
|
1339
1735
|
}
|
|
1340
1736
|
function ensureAgentConfigDir(path = agentConfigPath()) {
|
|
1341
|
-
const dir =
|
|
1737
|
+
const dir = dirname2(path);
|
|
1342
1738
|
if (!existsSync2(dir))
|
|
1343
|
-
|
|
1739
|
+
mkdirSync2(dir, { recursive: true });
|
|
1344
1740
|
}
|
|
1345
1741
|
|
|
1346
1742
|
// src/server/agent/session.ts
|
|
1347
1743
|
import { spawn as spawn2 } from "node:child_process";
|
|
1348
1744
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
1349
|
-
import { tmpdir } from "node:os";
|
|
1350
|
-
import { join as
|
|
1745
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
1746
|
+
import { join as join5 } from "node:path";
|
|
1351
1747
|
import { Readable } from "node:stream";
|
|
1352
1748
|
|
|
1353
1749
|
// node_modules/@agentclientprotocol/sdk/dist/schema/index.js
|
|
@@ -11073,7 +11469,7 @@ class AcpSession {
|
|
|
11073
11469
|
executeTool = null;
|
|
11074
11470
|
stderr = "";
|
|
11075
11471
|
selectedModelId = null;
|
|
11076
|
-
workspace = mkdtempSync(
|
|
11472
|
+
workspace = mkdtempSync(join5(tmpdir2(), "pipe-kan-agent-"));
|
|
11077
11473
|
constructor(config) {
|
|
11078
11474
|
this.config = config;
|
|
11079
11475
|
this.id = crypto.randomUUID();
|
|
@@ -11385,11 +11781,11 @@ function permissionOptionForDecision(options, decision) {
|
|
|
11385
11781
|
}
|
|
11386
11782
|
|
|
11387
11783
|
// src/server/agent/skills.ts
|
|
11388
|
-
import { existsSync as existsSync3, readFileSync as
|
|
11389
|
-
import { homedir as
|
|
11390
|
-
import { join as
|
|
11391
|
-
function createSkillRegistry(bundledDir =
|
|
11392
|
-
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");
|
|
11393
11789
|
return {
|
|
11394
11790
|
list() {
|
|
11395
11791
|
const bundled = listSkills(bundledDir);
|
|
@@ -11428,11 +11824,11 @@ function listSkills(dir) {
|
|
|
11428
11824
|
return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => readSkill(skillPath(dir, entry.name), entry.name)).filter((skill) => skill !== undefined);
|
|
11429
11825
|
}
|
|
11430
11826
|
function skillPath(dir, id) {
|
|
11431
|
-
return
|
|
11827
|
+
return join6(dir, id, "SKILL.md");
|
|
11432
11828
|
}
|
|
11433
11829
|
function readSkill(path, id) {
|
|
11434
11830
|
try {
|
|
11435
|
-
const text =
|
|
11831
|
+
const text = readFileSync4(path, "utf8");
|
|
11436
11832
|
const front = parseFrontMatter(text);
|
|
11437
11833
|
return {
|
|
11438
11834
|
id,
|
|
@@ -11459,8 +11855,8 @@ function parseFrontMatter(text) {
|
|
|
11459
11855
|
}
|
|
11460
11856
|
|
|
11461
11857
|
// src/server/agent/tools.ts
|
|
11462
|
-
import { readFileSync as
|
|
11463
|
-
import { join as
|
|
11858
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
11859
|
+
import { join as join7 } from "node:path";
|
|
11464
11860
|
var TOOLS = [
|
|
11465
11861
|
{
|
|
11466
11862
|
name: "board_state",
|
|
@@ -11512,6 +11908,30 @@ var TOOLS = [
|
|
|
11512
11908
|
description: "Update the current board filter. Requires user approval.",
|
|
11513
11909
|
parameters: { filter: { type: "object", description: "BoardFilter object" } },
|
|
11514
11910
|
mutates: true
|
|
11911
|
+
},
|
|
11912
|
+
{
|
|
11913
|
+
name: "create_issue",
|
|
11914
|
+
description: "Create a Jira issue via jira-cli. Requires user approval because it changes Jira.",
|
|
11915
|
+
parameters: {
|
|
11916
|
+
summary: { type: "string", description: "Issue summary / title" },
|
|
11917
|
+
description: { type: "string", description: "Issue description" },
|
|
11918
|
+
labels: { type: "string", description: "Comma-separated labels" },
|
|
11919
|
+
status: { type: "string", description: "Column status name" },
|
|
11920
|
+
parent: { type: "string", description: "Parent Epic key" },
|
|
11921
|
+
type: { type: "string", description: "Issue type, default Story" }
|
|
11922
|
+
},
|
|
11923
|
+
mutates: true
|
|
11924
|
+
},
|
|
11925
|
+
{
|
|
11926
|
+
name: "edit_issue",
|
|
11927
|
+
description: "Edit an issue summary, description, and labels via jira-cli. Requires user approval.",
|
|
11928
|
+
parameters: {
|
|
11929
|
+
key: { type: "string", description: "Issue key" },
|
|
11930
|
+
summary: { type: "string", description: "New summary" },
|
|
11931
|
+
description: { type: "string", description: "New description" },
|
|
11932
|
+
labels: { type: "string", description: "Comma-separated replacement labels" }
|
|
11933
|
+
},
|
|
11934
|
+
mutates: true
|
|
11515
11935
|
}
|
|
11516
11936
|
];
|
|
11517
11937
|
var skills = createSkillRegistry();
|
|
@@ -11553,7 +11973,7 @@ var EXECUTORS = {
|
|
|
11553
11973
|
return { ok: false, error: "Path traversal not allowed" };
|
|
11554
11974
|
const repoRoot = process.cwd();
|
|
11555
11975
|
try {
|
|
11556
|
-
const text =
|
|
11976
|
+
const text = readFileSync5(join7(repoRoot, relPath), "utf8");
|
|
11557
11977
|
return { ok: true, value: { path: relPath, text } };
|
|
11558
11978
|
} catch (err) {
|
|
11559
11979
|
return { ok: false, error: String(err) };
|
|
@@ -11585,6 +12005,40 @@ var EXECUTORS = {
|
|
|
11585
12005
|
if (!filter || typeof filter !== "object")
|
|
11586
12006
|
return { ok: false, error: "Missing filter object" };
|
|
11587
12007
|
return { ok: true, value: { __ui_action: "set_filter", filter } };
|
|
12008
|
+
},
|
|
12009
|
+
async create_issue(args, app) {
|
|
12010
|
+
const summary = String(args.summary ?? "").trim();
|
|
12011
|
+
if (!summary)
|
|
12012
|
+
return { ok: false, error: "Missing summary" };
|
|
12013
|
+
const labels = String(args.labels ?? "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
12014
|
+
const result = await app.create({
|
|
12015
|
+
summary,
|
|
12016
|
+
...args.description !== undefined ? { description: String(args.description) } : {},
|
|
12017
|
+
labels,
|
|
12018
|
+
...args.status ? { status: String(args.status) } : {},
|
|
12019
|
+
...args.parent ? { parent: String(args.parent) } : {},
|
|
12020
|
+
...args.type ? { type: String(args.type) } : {}
|
|
12021
|
+
});
|
|
12022
|
+
if (result.error)
|
|
12023
|
+
return { ok: false, error: result.error };
|
|
12024
|
+
return { ok: true, value: { key: result.key, __ui_action: "refresh_board" } };
|
|
12025
|
+
},
|
|
12026
|
+
async edit_issue(args, app) {
|
|
12027
|
+
const key = String(args.key ?? "");
|
|
12028
|
+
if (!key)
|
|
12029
|
+
return { ok: false, error: "Missing key" };
|
|
12030
|
+
const input = {};
|
|
12031
|
+
if (args.summary !== undefined)
|
|
12032
|
+
input.summary = String(args.summary);
|
|
12033
|
+
if (args.description !== undefined)
|
|
12034
|
+
input.description = String(args.description);
|
|
12035
|
+
if (args.labels !== undefined) {
|
|
12036
|
+
input.labels = String(args.labels).split(",").map((item) => item.trim()).filter(Boolean);
|
|
12037
|
+
}
|
|
12038
|
+
const result = await app.edit(key, input);
|
|
12039
|
+
if (result.error)
|
|
12040
|
+
return { ok: false, error: result.error };
|
|
12041
|
+
return { ok: true, value: { key, __ui_action: "refresh_board" } };
|
|
11588
12042
|
}
|
|
11589
12043
|
};
|
|
11590
12044
|
var SYSTEM_TEXT = `Your working context is the attached Jira issues from the pipe. Answer from that payload. Do not search GitHub, git history, or local source files unless the user explicitly asks about the pipe-kan app itself.
|
|
@@ -11949,7 +12403,63 @@ function handleFakeJira(req, res, store) {
|
|
|
11949
12403
|
return true;
|
|
11950
12404
|
}
|
|
11951
12405
|
}
|
|
12406
|
+
if (/^\/rest\/api\/[23]\/issue$/.test(path) && method === "POST") {
|
|
12407
|
+
readBody3(req).then((text) => {
|
|
12408
|
+
const body = text ? JSON.parse(text) : {};
|
|
12409
|
+
const fields = body.fields ?? {};
|
|
12410
|
+
const issuetype = fields.issuetype ?? fields.issueType;
|
|
12411
|
+
const typeName = issuetype && typeof issuetype === "object" && "name" in issuetype ? String(issuetype.name ?? "") : undefined;
|
|
12412
|
+
const parent = fields.parent && typeof fields.parent === "object" && "key" in fields.parent ? String(fields.parent.key ?? "") : undefined;
|
|
12413
|
+
const project = fields.project && typeof fields.project === "object" && "key" in fields.project ? String(fields.project.key ?? "") : undefined;
|
|
12414
|
+
const status = fields.status && typeof fields.status === "object" && "name" in fields.status ? String(fields.status.name ?? "") : undefined;
|
|
12415
|
+
const labels = Array.isArray(fields.labels) ? fields.labels.filter((item) => typeof item === "string") : [];
|
|
12416
|
+
const result = store.create({
|
|
12417
|
+
summary: String(fields.summary ?? ""),
|
|
12418
|
+
description: typeof fields.description === "string" ? fields.description : undefined,
|
|
12419
|
+
labels,
|
|
12420
|
+
...status ? { status } : {},
|
|
12421
|
+
...parent ? { parent } : {},
|
|
12422
|
+
...typeName ? { type: typeName } : {},
|
|
12423
|
+
...project ? { project } : {}
|
|
12424
|
+
});
|
|
12425
|
+
if (!result.ok) {
|
|
12426
|
+
json3(res, 400, { errorMessages: [result.error] });
|
|
12427
|
+
return;
|
|
12428
|
+
}
|
|
12429
|
+
json3(res, 201, {
|
|
12430
|
+
id: "10000",
|
|
12431
|
+
key: result.key,
|
|
12432
|
+
self: `/rest/api/2/issue/${result.key}`
|
|
12433
|
+
});
|
|
12434
|
+
});
|
|
12435
|
+
return true;
|
|
12436
|
+
}
|
|
11952
12437
|
const issueGet = path.match(/^\/rest\/api\/[23]\/issue\/([^/]+)$/);
|
|
12438
|
+
if (issueGet && method === "PUT") {
|
|
12439
|
+
const key = decodeURIComponent(issueGet[1]);
|
|
12440
|
+
readBody3(req).then((text) => {
|
|
12441
|
+
const body = text ? JSON.parse(text) : {};
|
|
12442
|
+
const fields = body.fields ?? {};
|
|
12443
|
+
const input = {};
|
|
12444
|
+
if (typeof fields.summary === "string")
|
|
12445
|
+
input.summary = fields.summary;
|
|
12446
|
+
if (typeof fields.description === "string")
|
|
12447
|
+
input.description = fields.description;
|
|
12448
|
+
if (Array.isArray(fields.labels)) {
|
|
12449
|
+
input.labels = fields.labels.filter((item) => typeof item === "string");
|
|
12450
|
+
}
|
|
12451
|
+
const result = store.edit(key, input);
|
|
12452
|
+
if (!result.ok) {
|
|
12453
|
+
json3(res, result.error.includes("not found") ? 404 : 400, {
|
|
12454
|
+
errorMessages: [result.error]
|
|
12455
|
+
});
|
|
12456
|
+
return;
|
|
12457
|
+
}
|
|
12458
|
+
res.statusCode = 204;
|
|
12459
|
+
res.end();
|
|
12460
|
+
});
|
|
12461
|
+
return true;
|
|
12462
|
+
}
|
|
11953
12463
|
if (issueGet && method === "GET") {
|
|
11954
12464
|
const key = decodeURIComponent(issueGet[1]);
|
|
11955
12465
|
const issue = store.get(key);
|
|
@@ -11996,12 +12506,12 @@ function handleRequest(req, res, ctx) {
|
|
|
11996
12506
|
}
|
|
11997
12507
|
|
|
11998
12508
|
// src/jira-config.ts
|
|
11999
|
-
import { mkdirSync as
|
|
12000
|
-
import { join as
|
|
12509
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
12510
|
+
import { join as join8 } from "node:path";
|
|
12001
12511
|
function writeJiraConfig(dir, server) {
|
|
12002
|
-
|
|
12003
|
-
const path =
|
|
12004
|
-
|
|
12512
|
+
mkdirSync3(dir, { recursive: true });
|
|
12513
|
+
const path = join8(dir, "jira.config.yml");
|
|
12514
|
+
writeFileSync3(path, [
|
|
12005
12515
|
"installation: Cloud",
|
|
12006
12516
|
`server: ${server}`,
|
|
12007
12517
|
`login: ${ME.emailAddress}`,
|
|
@@ -12056,7 +12566,7 @@ function stdinStat() {
|
|
|
12056
12566
|
|
|
12057
12567
|
// src/ui.ts
|
|
12058
12568
|
import { createReadStream, existsSync as existsSync4, statSync } from "node:fs";
|
|
12059
|
-
import { dirname as
|
|
12569
|
+
import { dirname as dirname3, extname, join as join9, resolve as resolve2, sep } from "node:path";
|
|
12060
12570
|
import { fileURLToPath } from "node:url";
|
|
12061
12571
|
var types = {
|
|
12062
12572
|
".css": "text/css; charset=utf-8",
|
|
@@ -12070,10 +12580,10 @@ var types = {
|
|
|
12070
12580
|
".woff2": "font/woff2"
|
|
12071
12581
|
};
|
|
12072
12582
|
function packageRoot(from = import.meta.url) {
|
|
12073
|
-
return resolve2(
|
|
12583
|
+
return resolve2(dirname3(fileURLToPath(from)), "..");
|
|
12074
12584
|
}
|
|
12075
12585
|
function uiDir(root) {
|
|
12076
|
-
return
|
|
12586
|
+
return join9(root, "dist", "ui");
|
|
12077
12587
|
}
|
|
12078
12588
|
function inside(root, file) {
|
|
12079
12589
|
const base = resolve2(root);
|
|
@@ -12082,7 +12592,7 @@ function inside(root, file) {
|
|
|
12082
12592
|
}
|
|
12083
12593
|
function sendUi(root, req, res) {
|
|
12084
12594
|
const ui = uiDir(root);
|
|
12085
|
-
const index =
|
|
12595
|
+
const index = join9(ui, "index.html");
|
|
12086
12596
|
if (!existsSync4(index))
|
|
12087
12597
|
return false;
|
|
12088
12598
|
const path = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
|
|
@@ -12103,7 +12613,7 @@ function announce(line) {
|
|
|
12103
12613
|
}
|
|
12104
12614
|
async function runServer(opts) {
|
|
12105
12615
|
const piped = await readPipe();
|
|
12106
|
-
const raw = piped ?? JSON.parse(
|
|
12616
|
+
const raw = piped ?? JSON.parse(readFileSync6(join10(opts.root, "fixtures/issues.json"), "utf8"));
|
|
12107
12617
|
const flags = argvToFlags(process.argv);
|
|
12108
12618
|
const { app, store, kind } = await createBoardApp({
|
|
12109
12619
|
raw,
|
|
@@ -12125,7 +12635,7 @@ async function runServer(opts) {
|
|
|
12125
12635
|
const { host, port } = resolveListen();
|
|
12126
12636
|
await bindListen(server, host, port);
|
|
12127
12637
|
const origin = `http://127.0.0.1:${port}`;
|
|
12128
|
-
const fakeConfig = writeJiraConfig(
|
|
12638
|
+
const fakeConfig = writeJiraConfig(join10(tmpdir3(), "pipe-kan"), origin);
|
|
12129
12639
|
announce(`pipe-kan http://${host}:${port}`);
|
|
12130
12640
|
announce(`cli ${kind === "jira" ? resolveJiraBin() : "store"}`);
|
|
12131
12641
|
announce(`Fake Jira ${origin}/rest/api/2/search`);
|