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/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 readFileSync4, writeSync } from "node:fs";
4
+ import { readFileSync as readFileSync6, writeSync } from "node:fs";
5
5
  import { createServer } from "node:http";
6
- import { tmpdir as tmpdir2 } from "node:os";
7
- import { join as join8 } from "node:path";
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
- const result = await runRetry(["issue", "move", key, status]);
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
- ok: false,
737
- error: (result.stderr || result.stdout).trim()
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 issuesToBoard(epicsPayload).epics.map((epic) => epic.key);
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 ?? issuesToBoard(epicsPayload).epics.find((epic) => epic.key === key)?.status ?? app.board().epics.find((epic) => epic.key === key)?.status;
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 = issuesToBoard(payload);
1292
+ const board = toBoard(payload);
951
1293
  return {
952
1294
  columns: board.columns,
953
- epics: mergeEpics(issuesToBoard(epicsPayload).epics, board.epics),
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 nextPayload = JSON.parse(issues);
976
- const nextEpics = JSON.parse(epics);
977
- const keys = issuesToBoard(nextEpics).epics.map((epic) => epic.key);
978
- const issueCount = Array.isArray(nextPayload) ? nextPayload.length : 0;
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 = nextPayload;
1001
- epicsPayload = nextEpics;
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 = issuesToBoard(epicsPayload).epics;
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(issuesToBoard(JSON.parse(await cli.listEpic(epic, flags))), epic);
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({ store, cli, flags });
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 join2 } from "node:path";
1250
- var DEFAULT_CONFIG_PATH = join2(homedir(), ".config", "pipe-kan", "agent.json");
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(join2(dir, command)))
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(readFileSync(path, "utf8"));
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
- writeFileSync(path, `${JSON.stringify(next, null, 2)}
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 = dirname(path);
1737
+ const dir = dirname2(path);
1342
1738
  if (!existsSync2(dir))
1343
- mkdirSync(dir, { recursive: true });
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 join3 } from "node:path";
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(join3(tmpdir(), "pipe-kan-agent-"));
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 readFileSync2, readdirSync } from "node:fs";
11389
- import { homedir as homedir2 } from "node:os";
11390
- import { join as join4 } from "node:path";
11391
- function createSkillRegistry(bundledDir = join4(import.meta.dirname, "..", "..", "..", ".agents", "skills")) {
11392
- const userDir = join4(homedir2(), ".pi", "agent", "skills");
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 join4(dir, id, "SKILL.md");
11827
+ return join6(dir, id, "SKILL.md");
11432
11828
  }
11433
11829
  function readSkill(path, id) {
11434
11830
  try {
11435
- const text = readFileSync2(path, "utf8");
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 readFileSync3 } from "node:fs";
11463
- import { join as join5 } from "node:path";
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 = readFileSync3(join5(repoRoot, relPath), "utf8");
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 mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
12000
- import { join as join6 } from "node:path";
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
- mkdirSync2(dir, { recursive: true });
12003
- const path = join6(dir, "jira.config.yml");
12004
- writeFileSync2(path, [
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 dirname2, extname, join as join7, resolve as resolve2, sep } from "node:path";
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(dirname2(fileURLToPath(from)), "..");
12583
+ return resolve2(dirname3(fileURLToPath(from)), "..");
12074
12584
  }
12075
12585
  function uiDir(root) {
12076
- return join7(root, "dist", "ui");
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 = join7(ui, "index.html");
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(readFileSync4(join8(opts.root, "fixtures/issues.json"), "utf8"));
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(join8(tmpdir2(), "pipe-kan"), origin);
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`);