pipe-kan 0.23.1 → 0.23.3

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.
Files changed (2) hide show
  1. package/dist/pipe-kan.js +205 -138
  2. package/package.json +1 -1
package/dist/pipe-kan.js CHANGED
@@ -300,6 +300,134 @@ function flagsToJql(flags) {
300
300
  return clauses.join(" AND ");
301
301
  }
302
302
 
303
+ // src/store.ts
304
+ var ME = {
305
+ displayName: "Person A",
306
+ emailAddress: "user@test.com",
307
+ name: "user@test.com"
308
+ };
309
+ var TRANSITIONS = [
310
+ { id: "11", name: "To Do" },
311
+ { id: "21", name: "In Progress" },
312
+ { id: "31", name: "Done" }
313
+ ];
314
+ var ALLOWED = {
315
+ "To Do": ["In Progress"],
316
+ "In Progress": ["To Do", "Done"],
317
+ Done: ["In Progress"]
318
+ };
319
+ function validIssueKey(key) {
320
+ return /^[A-Z][A-Z0-9]*-\d+$/i.test(key.trim());
321
+ }
322
+ function fieldValue(issue, field) {
323
+ switch (field) {
324
+ case "project":
325
+ return issue.key.split("-")[0] ?? "";
326
+ case "assignee":
327
+ return [
328
+ issue.fields.assignee?.emailAddress,
329
+ issue.fields.assignee?.displayName
330
+ ].filter(Boolean).join(`
331
+ `);
332
+ case "status":
333
+ return issue.fields.status.name;
334
+ case "parent":
335
+ return issue.fields.parent?.key ?? "";
336
+ case "type": {
337
+ const named = issue.fields.issuetype ?? issue.fields.issueType;
338
+ if (named && typeof named === "object" && "name" in named && typeof named.name === "string") {
339
+ return named.name;
340
+ }
341
+ return "";
342
+ }
343
+ case "key":
344
+ return issue.key;
345
+ default:
346
+ return "";
347
+ }
348
+ }
349
+ function matchesClause(issue, clause) {
350
+ const match = clause.trim().match(/^(\w+)\s*(!=|=)\s*"?([^"]+)"?$/);
351
+ if (!match)
352
+ return true;
353
+ const [, field, op, raw] = match;
354
+ const expected = raw.trim();
355
+ const actual = fieldValue(issue, field);
356
+ const hit = field === "assignee" ? actual.split(`
357
+ `).some((value) => value.toLowerCase() === expected.toLowerCase()) : actual.toLowerCase() === expected.toLowerCase();
358
+ return op === "!=" ? !hit : hit;
359
+ }
360
+ function matchJql(issue, jql) {
361
+ const stripped = jql.replace(/\s+ORDER BY\s+.+$/i, "").trim();
362
+ if (!stripped)
363
+ return true;
364
+ return stripped.split(/\s+AND\s+/i).every((clause) => matchesClause(issue, clause));
365
+ }
366
+
367
+ class IssueStore {
368
+ issues;
369
+ constructor(issues) {
370
+ this.issues = issues;
371
+ }
372
+ static fromRaw(raw) {
373
+ if (!Array.isArray(raw)) {
374
+ throw new Error("Fixture must be a JSON array");
375
+ }
376
+ return new IssueStore(structuredClone(raw));
377
+ }
378
+ all() {
379
+ return this.issues;
380
+ }
381
+ rawJson() {
382
+ return JSON.stringify(this.issues, null, 2);
383
+ }
384
+ get(key) {
385
+ return this.issues.find((issue) => issue.key.toUpperCase() === key.toUpperCase());
386
+ }
387
+ list(jql = "") {
388
+ return this.issues.filter((issue) => matchJql(issue, jql));
389
+ }
390
+ childrenOf(keys) {
391
+ const listed = new Set(keys.filter(validIssueKey));
392
+ return this.issues.filter((issue) => {
393
+ if (fieldValue(issue, "type").toLowerCase() === "epic")
394
+ return false;
395
+ return listed.has(fieldValue(issue, "parent"));
396
+ });
397
+ }
398
+ transitions(key) {
399
+ const issue = this.get(key);
400
+ if (!issue)
401
+ return [];
402
+ const allowed = ALLOWED[issue.fields.status.name] ?? [];
403
+ return TRANSITIONS.map((transition) => ({
404
+ ...transition,
405
+ isAvailable: allowed.includes(transition.name)
406
+ }));
407
+ }
408
+ move(key, status) {
409
+ const issue = this.get(key);
410
+ if (!issue)
411
+ return { ok: false, error: `Issue ${key} not found` };
412
+ const transition = this.transitions(key).find((item) => item.name.toLowerCase() === status.toLowerCase() || item.id === status);
413
+ if (!transition || !transition.isAvailable) {
414
+ const available = this.transitions(key).filter((item) => item.isAvailable).map((item) => item.name).join(", ");
415
+ return {
416
+ ok: false,
417
+ error: `invalid transition state "${status}"
418
+ Available states for issue ${key}: ${available}`
419
+ };
420
+ }
421
+ issue.fields.status.name = transition.name;
422
+ if (transition.name === "Done") {
423
+ issue.fields.resolution = { name: "Done" };
424
+ } else {
425
+ delete issue.fields.resolution;
426
+ }
427
+ return { ok: true };
428
+ }
429
+ }
430
+
303
431
  // src/cli.ts
304
432
  function emptyList(text) {
305
433
  return /no result found/i.test(text);
@@ -332,7 +460,7 @@ function createStoreCli(store) {
332
460
  return JSON.stringify(issues, null, 2);
333
461
  },
334
462
  async listChildren(keys) {
335
- return JSON.stringify(store.childrenOf(keys), null, 2);
463
+ return JSON.stringify(store.childrenOf(keys.filter(validIssueKey)), null, 2);
336
464
  },
337
465
  async move(key, status) {
338
466
  return store.move(key, status);
@@ -468,15 +596,58 @@ function createJiraCli(opts = {}) {
468
596
  ]);
469
597
  },
470
598
  async listChildren(keys) {
471
- if (!keys.length)
599
+ const validKeys = keys.filter(validIssueKey);
600
+ if (!validKeys.length) {
601
+ console.log("Refresh children skipped; no valid parent keys");
472
602
  return "[]";
473
- const list = keys.map((key) => `"${key}"`).join(", ");
474
- return listAll([
475
- "issue",
476
- "list",
477
- "-q",
478
- `(parent in (${list}) OR "Epic Link" in (${list}))`
479
- ]);
603
+ }
604
+ const CHUNK = 50;
605
+ const issues = [];
606
+ const seen = new Set;
607
+ let anyFailed = false;
608
+ function buildJql(chunkKeys, mode) {
609
+ const list = chunkKeys.map((key) => `"${key}"`).join(", ");
610
+ if (mode === "epic")
611
+ return `"Epic Link" in (${list})`;
612
+ if (mode === "parent")
613
+ return `parent in (${list})`;
614
+ return `(parent in (${list}) OR "Epic Link" in (${list}))`;
615
+ }
616
+ for (let i = 0;i < validKeys.length; i += CHUNK) {
617
+ const chunk = validKeys.slice(i, i + CHUNK);
618
+ const modes = ["or", "epic", "parent"];
619
+ let chunkIssues;
620
+ let lastError;
621
+ for (const mode of modes) {
622
+ try {
623
+ chunkIssues = JSON.parse(await listAll([
624
+ "issue",
625
+ "list",
626
+ "-q",
627
+ buildJql(chunk, mode)
628
+ ]));
629
+ break;
630
+ } catch (err) {
631
+ lastError = err instanceof Error ? err.message : String(err);
632
+ }
633
+ }
634
+ if (chunkIssues) {
635
+ for (const issue of chunkIssues) {
636
+ const key = issueKey(issue);
637
+ if (key && !seen.has(key)) {
638
+ seen.add(key);
639
+ issues.push(issue);
640
+ }
641
+ }
642
+ } else {
643
+ anyFailed = true;
644
+ console.log(`Refresh children chunk ${i / CHUNK + 1} failed; ${lastError ?? "unknown error"}`);
645
+ }
646
+ }
647
+ if (anyFailed && issues.length === 0) {
648
+ throw new Error("Epic children fetch failed for all batches");
649
+ }
650
+ return JSON.stringify(issues);
480
651
  },
481
652
  async move(key, status) {
482
653
  const result = await runRetry(["issue", "move", key, status]);
@@ -741,10 +912,10 @@ function createApp(opts) {
741
912
  console.log(`Refresh children ${cards.length}`);
742
913
  }
743
914
  } catch (err) {
744
- nextHasCache = false;
745
- nextChildren = [];
915
+ nextChildren = childrenRaw;
916
+ nextHasCache = hasChildrenCache;
746
917
  nextError = err instanceof Error ? err.message : "Epic children list failed";
747
- console.log("Refresh children failed", nextError);
918
+ console.log("Refresh children failed; keeping existing children", nextError);
748
919
  }
749
920
  payload = nextPayload;
750
921
  epicsPayload = nextEpics;
@@ -805,131 +976,6 @@ function createApp(opts) {
805
976
  return app;
806
977
  }
807
978
 
808
- // src/store.ts
809
- var ME = {
810
- displayName: "Person A",
811
- emailAddress: "user@test.com",
812
- name: "user@test.com"
813
- };
814
- var TRANSITIONS = [
815
- { id: "11", name: "To Do" },
816
- { id: "21", name: "In Progress" },
817
- { id: "31", name: "Done" }
818
- ];
819
- var ALLOWED = {
820
- "To Do": ["In Progress"],
821
- "In Progress": ["To Do", "Done"],
822
- Done: ["In Progress"]
823
- };
824
- function fieldValue(issue, field) {
825
- switch (field) {
826
- case "project":
827
- return issue.key.split("-")[0] ?? "";
828
- case "assignee":
829
- return [
830
- issue.fields.assignee?.emailAddress,
831
- issue.fields.assignee?.displayName
832
- ].filter(Boolean).join(`
833
- `);
834
- case "status":
835
- return issue.fields.status.name;
836
- case "parent":
837
- return issue.fields.parent?.key ?? "";
838
- case "type": {
839
- const named = issue.fields.issuetype ?? issue.fields.issueType;
840
- if (named && typeof named === "object" && "name" in named && typeof named.name === "string") {
841
- return named.name;
842
- }
843
- return "";
844
- }
845
- case "key":
846
- return issue.key;
847
- default:
848
- return "";
849
- }
850
- }
851
- function matchesClause(issue, clause) {
852
- const match = clause.trim().match(/^(\w+)\s*(!=|=)\s*"?([^"]+)"?$/);
853
- if (!match)
854
- return true;
855
- const [, field, op, raw] = match;
856
- const expected = raw.trim();
857
- const actual = fieldValue(issue, field);
858
- const hit = field === "assignee" ? actual.split(`
859
- `).some((value) => value.toLowerCase() === expected.toLowerCase()) : actual.toLowerCase() === expected.toLowerCase();
860
- return op === "!=" ? !hit : hit;
861
- }
862
- function matchJql(issue, jql) {
863
- const stripped = jql.replace(/\s+ORDER BY\s+.+$/i, "").trim();
864
- if (!stripped)
865
- return true;
866
- return stripped.split(/\s+AND\s+/i).every((clause) => matchesClause(issue, clause));
867
- }
868
-
869
- class IssueStore {
870
- issues;
871
- constructor(issues) {
872
- this.issues = issues;
873
- }
874
- static fromRaw(raw) {
875
- if (!Array.isArray(raw)) {
876
- throw new Error("Fixture must be a JSON array");
877
- }
878
- return new IssueStore(structuredClone(raw));
879
- }
880
- all() {
881
- return this.issues;
882
- }
883
- rawJson() {
884
- return JSON.stringify(this.issues, null, 2);
885
- }
886
- get(key) {
887
- return this.issues.find((issue) => issue.key.toUpperCase() === key.toUpperCase());
888
- }
889
- list(jql = "") {
890
- return this.issues.filter((issue) => matchJql(issue, jql));
891
- }
892
- childrenOf(keys) {
893
- const listed = new Set(keys);
894
- return this.issues.filter((issue) => {
895
- if (fieldValue(issue, "type").toLowerCase() === "epic")
896
- return false;
897
- return listed.has(fieldValue(issue, "parent"));
898
- });
899
- }
900
- transitions(key) {
901
- const issue = this.get(key);
902
- if (!issue)
903
- return [];
904
- const allowed = ALLOWED[issue.fields.status.name] ?? [];
905
- return TRANSITIONS.map((transition) => ({
906
- ...transition,
907
- isAvailable: allowed.includes(transition.name)
908
- }));
909
- }
910
- move(key, status) {
911
- const issue = this.get(key);
912
- if (!issue)
913
- return { ok: false, error: `Issue ${key} not found` };
914
- const transition = this.transitions(key).find((item) => item.name.toLowerCase() === status.toLowerCase() || item.id === status);
915
- if (!transition || !transition.isAvailable) {
916
- const available = this.transitions(key).filter((item) => item.isAvailable).map((item) => item.name).join(", ");
917
- return {
918
- ok: false,
919
- error: `invalid transition state "${status}"
920
- Available states for issue ${key}: ${available}`
921
- };
922
- }
923
- issue.fields.status.name = transition.name;
924
- if (transition.name === "Done") {
925
- issue.fields.resolution = { name: "Done" };
926
- } else {
927
- delete issue.fields.resolution;
928
- }
929
- return { ok: true };
930
- }
931
- }
932
-
933
979
  // src/boot.ts
934
980
  async function createBoardApp(opts) {
935
981
  const env = opts.env ?? process.env;
@@ -11754,6 +11800,21 @@ function readBody3(req) {
11754
11800
  function pathOf3(req) {
11755
11801
  return new URL(req.url ?? "/", "http://127.0.0.1");
11756
11802
  }
11803
+ function childrenJqlError(jql) {
11804
+ const trimmed = jql.trim();
11805
+ if (/\bin\s*\(\s*\)/i.test(trimmed)) {
11806
+ return "The value '' does not exist for the field 'parent'.";
11807
+ }
11808
+ const childrenMatch = trimmed.match(/(?:parent|"Epic Link")\s+in\s+\(([^)]+)\)/i);
11809
+ if (childrenMatch) {
11810
+ const values = childrenMatch[1].split(",").map((value) => value.trim().replace(/^["']|["']$/g, ""));
11811
+ const invalid = values.filter((value) => !validIssueKey(value));
11812
+ if (invalid.length) {
11813
+ return `No issues have a parent epic with key or name '${invalid[0]}'.`;
11814
+ }
11815
+ }
11816
+ return;
11817
+ }
11757
11818
  function handleFakeJira(req, res, store) {
11758
11819
  const url = pathOf3(req);
11759
11820
  const path = url.pathname;
@@ -11763,7 +11824,13 @@ function handleFakeJira(req, res, store) {
11763
11824
  return true;
11764
11825
  }
11765
11826
  if ((path === "/rest/api/3/search/jql" || path === "/rest/api/2/search" || path === "/rest/api/3/search") && method === "GET") {
11766
- const issues = store.list(url.searchParams.get("jql") ?? "");
11827
+ const jql = url.searchParams.get("jql") ?? "";
11828
+ const error = childrenJqlError(jql);
11829
+ if (error) {
11830
+ json3(res, 400, { errorMessages: [error] });
11831
+ return true;
11832
+ }
11833
+ const issues = store.list(jql);
11767
11834
  json3(res, 200, { expand: "schema,names", isLast: true, issues });
11768
11835
  return true;
11769
11836
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pipe-kan",
3
- "version": "0.23.1",
3
+ "version": "0.23.3",
4
4
  "description": "Local Kanban for jira-cli",
5
5
  "license": "MIT",
6
6
  "type": "module",