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 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
- Until npm has a release, from a clone: `bun run build && bun dist/pipe-kan.js`.
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`, and opens a GitHub Release. npm publish is wired (`bin`, `files`, `bun run build` on CI) and waits on repo secret `NPM_TOKEN`. After that secret exists, set `npmPublish` to `true` in `.releaserc.json`.
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 = typeof issue.fields?.assignee?.displayName === "string" ? issue.fields.assignee.displayName : undefined;
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
- return cli.open(key);
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((resolve2, reject) => {
728
+ return new Promise((resolve, reject) => {
554
729
  const chunks = [];
555
730
  req.on("data", (chunk) => chunks.push(chunk));
556
- req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
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
- const url2 = await app.open(String(body.key ?? ""));
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((resolve2, reject) => {
785
+ return new Promise((resolve, reject) => {
612
786
  const chunks = [];
613
787
  req.on("data", (chunk) => chunks.push(chunk));
614
- req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
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((resolve3) => server.listen(port, host, resolve3));
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") {