pipe-kan 0.9.1 → 0.10.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 +2 -2
- package/dist/pipe-kan.js +194 -9
- package/dist/ui/assets/index-3amN8pWS.js +54 -0
- package/dist/ui/assets/index-Cnla0OoQ.css +2 -0
- package/dist/ui/index.html +2 -2
- package/package.json +2 -1
- package/dist/ui/assets/index-DHk68jTP.js +0 -14
- package/dist/ui/assets/index-HGWYfGrG.css +0 -2
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Opens `http://127.0.0.1:5173`. No clone, no `bun install`. `bunx` / `npx` instal
|
|
|
14
14
|
|
|
15
15
|
First paint is the Fixture. If `jira` is on PATH, the process then Refresh-es from your existing `jira init`. If not, Refresh and Move use the in-process store.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
The published CLI is on npm. From a clone: `bun run build && bun dist/pipe-kan.js`.
|
|
18
18
|
|
|
19
19
|
- Left: Epics. Center: Cards (one Column per status in the payload). Right: Open URL; remote Jira is a link, not an iframe.
|
|
20
20
|
- Drop a Card on a Column to Move (`jira issue move`).
|
|
@@ -87,6 +87,6 @@ Pushes to `main` run `bun audit --audit-level=high`, tests, then [semantic-relea
|
|
|
87
87
|
| `feat!:` or `BREAKING CHANGE:` | major |
|
|
88
88
|
| `docs:`, `chore:`, `test:` | none |
|
|
89
89
|
|
|
90
|
-
It writes `package.json`, tags `vX.Y.Z`,
|
|
90
|
+
It writes `package.json`, tags `vX.Y.Z`, opens a GitHub Release, and publishes to npm from this workflow via [trusted publishing](https://docs.npmjs.com/trusted-publishers/). No `NPM_TOKEN`. Provenance is attached automatically.
|
|
91
91
|
|
|
92
92
|
First Board is tagged `v0.1.0`. Later `fix:` / `feat:` commits on `main` bump from there. Without a `v*` tag the first release is `1.0.0`.
|
package/dist/pipe-kan.js
CHANGED
|
@@ -51,6 +51,21 @@ function issueLabels(fields) {
|
|
|
51
51
|
function issueStatus(fields) {
|
|
52
52
|
return typeof fields?.status?.name === "string" && fields.status.name ? fields.status.name : undefined;
|
|
53
53
|
}
|
|
54
|
+
function issueAssignee(fields) {
|
|
55
|
+
const raw = fields?.assignee;
|
|
56
|
+
if (typeof raw === "string" && raw.trim())
|
|
57
|
+
return raw.trim();
|
|
58
|
+
if (!raw || typeof raw !== "object")
|
|
59
|
+
return;
|
|
60
|
+
const assignee = raw;
|
|
61
|
+
if (typeof assignee.displayName === "string" && assignee.displayName.trim()) {
|
|
62
|
+
return assignee.displayName.trim();
|
|
63
|
+
}
|
|
64
|
+
if (typeof assignee.name === "string" && assignee.name.trim()) {
|
|
65
|
+
return assignee.name.trim();
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
54
69
|
function toEpic(face) {
|
|
55
70
|
return {
|
|
56
71
|
key: face.key,
|
|
@@ -64,7 +79,7 @@ function toEpic(face) {
|
|
|
64
79
|
function toCard(issue, key) {
|
|
65
80
|
const summary = typeof issue.fields?.summary === "string" ? issue.fields.summary : "";
|
|
66
81
|
const priority = typeof issue.fields?.priority?.name === "string" ? issue.fields.priority.name : undefined;
|
|
67
|
-
const assignee =
|
|
82
|
+
const assignee = issueAssignee(issue.fields);
|
|
68
83
|
const dueDate = formatDueDate(issue.fields?.duedate);
|
|
69
84
|
const type = issueType(issue.fields);
|
|
70
85
|
const epic = epicKey(issue.fields);
|
|
@@ -242,6 +257,12 @@ function createStoreCli(store) {
|
|
|
242
257
|
},
|
|
243
258
|
async open(key) {
|
|
244
259
|
return `/browse/${key}`;
|
|
260
|
+
},
|
|
261
|
+
async view(key) {
|
|
262
|
+
const issue = store.get(key);
|
|
263
|
+
if (!issue)
|
|
264
|
+
throw new Error(`Issue ${key} not found`);
|
|
265
|
+
return JSON.stringify(issue);
|
|
245
266
|
}
|
|
246
267
|
};
|
|
247
268
|
}
|
|
@@ -335,10 +356,154 @@ function createJiraCli(opts = {}) {
|
|
|
335
356
|
const result = await run(["open", key, "--no-browser"]);
|
|
336
357
|
return result.stdout.trim().split(`
|
|
337
358
|
`).pop() ?? `/browse/${key}`;
|
|
359
|
+
},
|
|
360
|
+
async view(key) {
|
|
361
|
+
const result = await run(["issue", "view", key, "--raw"]);
|
|
362
|
+
if (result.code !== 0) {
|
|
363
|
+
throw new Error((result.stderr || result.stdout || "jira issue view failed").trim());
|
|
364
|
+
}
|
|
365
|
+
return result.stdout;
|
|
338
366
|
}
|
|
339
367
|
};
|
|
340
368
|
}
|
|
341
369
|
|
|
370
|
+
// src/open.ts
|
|
371
|
+
var SKIP_FIELDS = new Set([
|
|
372
|
+
"comment",
|
|
373
|
+
"comments",
|
|
374
|
+
"changelog",
|
|
375
|
+
"worklog",
|
|
376
|
+
"schema",
|
|
377
|
+
"names",
|
|
378
|
+
"renderedFields",
|
|
379
|
+
"editmeta",
|
|
380
|
+
"transitions",
|
|
381
|
+
"operations"
|
|
382
|
+
]);
|
|
383
|
+
var LEADING = [
|
|
384
|
+
"summary",
|
|
385
|
+
"priority",
|
|
386
|
+
"assignee",
|
|
387
|
+
"duedate",
|
|
388
|
+
"labels",
|
|
389
|
+
"description"
|
|
390
|
+
];
|
|
391
|
+
function stripHtml(value) {
|
|
392
|
+
return value.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
393
|
+
}
|
|
394
|
+
function flattenAdf(node) {
|
|
395
|
+
if (!node || typeof node !== "object")
|
|
396
|
+
return "";
|
|
397
|
+
const item = node;
|
|
398
|
+
if (typeof item.text === "string")
|
|
399
|
+
return item.text;
|
|
400
|
+
const inner = (item.content ?? []).map(flattenAdf).join("");
|
|
401
|
+
if (item.type === "paragraph" || item.type === "heading" || item.type === "listItem" || item.type === "blockquote") {
|
|
402
|
+
return `${inner}
|
|
403
|
+
`;
|
|
404
|
+
}
|
|
405
|
+
return inner;
|
|
406
|
+
}
|
|
407
|
+
function isSchemaDump(value) {
|
|
408
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
409
|
+
return false;
|
|
410
|
+
const item = value;
|
|
411
|
+
return typeof item.type === "string" && (item.system !== undefined || item.items !== undefined) && item.content === undefined;
|
|
412
|
+
}
|
|
413
|
+
function flattenValue(value) {
|
|
414
|
+
if (value == null)
|
|
415
|
+
return;
|
|
416
|
+
if (typeof value === "string") {
|
|
417
|
+
const trimmed = value.trim();
|
|
418
|
+
if (!trimmed)
|
|
419
|
+
return;
|
|
420
|
+
if (trimmed.includes("<") && trimmed.includes(">"))
|
|
421
|
+
return stripHtml(trimmed) || undefined;
|
|
422
|
+
return trimmed;
|
|
423
|
+
}
|
|
424
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
425
|
+
return String(value);
|
|
426
|
+
if (Array.isArray(value)) {
|
|
427
|
+
const parts = value.map(flattenValue).filter((part) => Boolean(part));
|
|
428
|
+
return parts.length ? parts.join(", ") : undefined;
|
|
429
|
+
}
|
|
430
|
+
if (typeof value === "object") {
|
|
431
|
+
if (isSchemaDump(value))
|
|
432
|
+
return;
|
|
433
|
+
const item = value;
|
|
434
|
+
if (item.type === "doc") {
|
|
435
|
+
const text = flattenAdf(value).replace(/\n+/g, `
|
|
436
|
+
`).trim();
|
|
437
|
+
return text || undefined;
|
|
438
|
+
}
|
|
439
|
+
if (typeof item.displayName === "string" && item.displayName.trim())
|
|
440
|
+
return item.displayName.trim();
|
|
441
|
+
if (typeof item.value === "string" && item.value.trim())
|
|
442
|
+
return item.value.trim();
|
|
443
|
+
if (typeof item.name === "string" && item.name.trim())
|
|
444
|
+
return item.name.trim();
|
|
445
|
+
}
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
function fieldLabel(key, value) {
|
|
449
|
+
if (key.startsWith("customfield_") && value && typeof value === "object" && "name" in value) {
|
|
450
|
+
const name = value.name;
|
|
451
|
+
if (typeof name === "string" && name.trim())
|
|
452
|
+
return name.trim();
|
|
453
|
+
}
|
|
454
|
+
return key;
|
|
455
|
+
}
|
|
456
|
+
function labelsOf(value) {
|
|
457
|
+
if (!Array.isArray(value))
|
|
458
|
+
return;
|
|
459
|
+
const pills = value.map((item) => {
|
|
460
|
+
if (typeof item === "string" && item)
|
|
461
|
+
return item;
|
|
462
|
+
if (item && typeof item === "object" && "name" in item) {
|
|
463
|
+
const name = item.name;
|
|
464
|
+
if (typeof name === "string" && name)
|
|
465
|
+
return name;
|
|
466
|
+
}
|
|
467
|
+
return;
|
|
468
|
+
}).filter((item) => Boolean(item));
|
|
469
|
+
return pills.length ? pills : undefined;
|
|
470
|
+
}
|
|
471
|
+
function flattenIssue(raw, url) {
|
|
472
|
+
const issue = raw;
|
|
473
|
+
const fields = issue?.fields ?? {};
|
|
474
|
+
const rows = [];
|
|
475
|
+
const key = typeof issue?.key === "string" ? issue.key : "";
|
|
476
|
+
if (key)
|
|
477
|
+
rows.push({ label: "Key", value: key });
|
|
478
|
+
const summary = flattenValue(fields.summary);
|
|
479
|
+
if (summary)
|
|
480
|
+
rows.push({ label: "Summary", value: summary });
|
|
481
|
+
if (url)
|
|
482
|
+
rows.push({ label: "Jira URL", value: url });
|
|
483
|
+
const priority = flattenValue(fields.priority);
|
|
484
|
+
if (priority)
|
|
485
|
+
rows.push({ label: "Priority", value: priority });
|
|
486
|
+
const assignee = flattenValue(fields.assignee);
|
|
487
|
+
if (assignee)
|
|
488
|
+
rows.push({ label: "Assignee", value: assignee });
|
|
489
|
+
const due = flattenValue(fields.duedate);
|
|
490
|
+
if (due)
|
|
491
|
+
rows.push({ label: "Due date", value: due });
|
|
492
|
+
const pills = labelsOf(fields.labels);
|
|
493
|
+
if (pills)
|
|
494
|
+
rows.push({ label: "Labels", value: pills.join(", "), pills });
|
|
495
|
+
const description = flattenValue(fields.description);
|
|
496
|
+
if (description)
|
|
497
|
+
rows.push({ label: "Description", value: description });
|
|
498
|
+
const rest = Object.entries(fields).filter(([key]) => !LEADING.includes(key) && !SKIP_FIELDS.has(key)).map(([key, value]) => {
|
|
499
|
+
const text = flattenValue(value);
|
|
500
|
+
if (!text)
|
|
501
|
+
return;
|
|
502
|
+
return { label: fieldLabel(key, value), value: text };
|
|
503
|
+
}).filter((row) => Boolean(row)).sort((a, b) => a.label.localeCompare(b.label));
|
|
504
|
+
return [...rows, ...rest];
|
|
505
|
+
}
|
|
506
|
+
|
|
342
507
|
// src/app.ts
|
|
343
508
|
function createApp(opts) {
|
|
344
509
|
const cli = opts.cli ?? createStoreCli(opts.store);
|
|
@@ -395,7 +560,17 @@ function createApp(opts) {
|
|
|
395
560
|
return { ok: true, board: app.board() };
|
|
396
561
|
},
|
|
397
562
|
async open(key) {
|
|
398
|
-
|
|
563
|
+
const urlP = cli.open(key);
|
|
564
|
+
try {
|
|
565
|
+
const [url, raw] = await Promise.all([urlP, cli.view(key)]);
|
|
566
|
+
return { url, fields: flattenIssue(JSON.parse(raw), url) };
|
|
567
|
+
} catch (err) {
|
|
568
|
+
return {
|
|
569
|
+
url: await urlP.catch(() => `/browse/${key}`),
|
|
570
|
+
fields: [],
|
|
571
|
+
error: err instanceof Error ? err.message : "jira issue view failed"
|
|
572
|
+
};
|
|
573
|
+
}
|
|
399
574
|
}
|
|
400
575
|
};
|
|
401
576
|
return app;
|
|
@@ -550,10 +725,10 @@ function json(res, status, body) {
|
|
|
550
725
|
res.end(JSON.stringify(body));
|
|
551
726
|
}
|
|
552
727
|
function readBody(req) {
|
|
553
|
-
return new Promise((
|
|
728
|
+
return new Promise((resolve, reject) => {
|
|
554
729
|
const chunks = [];
|
|
555
730
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
556
|
-
req.on("end", () =>
|
|
731
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
557
732
|
req.on("error", reject);
|
|
558
733
|
});
|
|
559
734
|
}
|
|
@@ -593,8 +768,7 @@ function handleAppApi(req, res, app) {
|
|
|
593
768
|
if (url.pathname === "/api/open" && method === "POST") {
|
|
594
769
|
readBody(req).then(async (text) => {
|
|
595
770
|
const body = text ? JSON.parse(text) : {};
|
|
596
|
-
|
|
597
|
-
json(res, 200, { url: url2 });
|
|
771
|
+
json(res, 200, await app.open(String(body.key ?? "")));
|
|
598
772
|
});
|
|
599
773
|
return true;
|
|
600
774
|
}
|
|
@@ -608,10 +782,10 @@ function json2(res, status, body) {
|
|
|
608
782
|
res.end(JSON.stringify(body));
|
|
609
783
|
}
|
|
610
784
|
function readBody2(req) {
|
|
611
|
-
return new Promise((
|
|
785
|
+
return new Promise((resolve, reject) => {
|
|
612
786
|
const chunks = [];
|
|
613
787
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
614
|
-
req.on("end", () =>
|
|
788
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
615
789
|
req.on("error", reject);
|
|
616
790
|
});
|
|
617
791
|
}
|
|
@@ -660,6 +834,17 @@ function handleFakeJira(req, res, store) {
|
|
|
660
834
|
return true;
|
|
661
835
|
}
|
|
662
836
|
}
|
|
837
|
+
const issueGet = path.match(/^\/rest\/api\/[23]\/issue\/([^/]+)$/);
|
|
838
|
+
if (issueGet && method === "GET") {
|
|
839
|
+
const key = decodeURIComponent(issueGet[1]);
|
|
840
|
+
const issue = store.get(key);
|
|
841
|
+
if (!issue) {
|
|
842
|
+
json2(res, 404, { errorMessages: [`Issue ${key} not found`] });
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
json2(res, 200, issue);
|
|
846
|
+
return true;
|
|
847
|
+
}
|
|
663
848
|
const browse = path.match(/^\/browse\/([^/]+)$/);
|
|
664
849
|
if (browse && method === "GET") {
|
|
665
850
|
const key = decodeURIComponent(browse[1]);
|
|
@@ -793,7 +978,7 @@ async function runServer(opts) {
|
|
|
793
978
|
}
|
|
794
979
|
});
|
|
795
980
|
const { host, port } = resolveListen();
|
|
796
|
-
await new Promise((
|
|
981
|
+
await new Promise((resolve) => server.listen(port, host, resolve));
|
|
797
982
|
const origin = `http://127.0.0.1:${port}`;
|
|
798
983
|
const fakeConfig = writeJiraConfig(join4(tmpdir(), "pipe-kan"), origin);
|
|
799
984
|
if (kind === "jira") {
|