pipe-kan 0.23.1 → 0.23.2

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 +159 -132
  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,9 +596,12 @@ 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(", ");
603
+ }
604
+ const list = validKeys.map((key) => `"${key}"`).join(", ");
474
605
  return listAll([
475
606
  "issue",
476
607
  "list",
@@ -741,10 +872,10 @@ function createApp(opts) {
741
872
  console.log(`Refresh children ${cards.length}`);
742
873
  }
743
874
  } catch (err) {
744
- nextHasCache = false;
745
- nextChildren = [];
875
+ nextChildren = childrenRaw;
876
+ nextHasCache = hasChildrenCache;
746
877
  nextError = err instanceof Error ? err.message : "Epic children list failed";
747
- console.log("Refresh children failed", nextError);
878
+ console.log("Refresh children failed; keeping existing children", nextError);
748
879
  }
749
880
  payload = nextPayload;
750
881
  epicsPayload = nextEpics;
@@ -805,131 +936,6 @@ function createApp(opts) {
805
936
  return app;
806
937
  }
807
938
 
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
939
  // src/boot.ts
934
940
  async function createBoardApp(opts) {
935
941
  const env = opts.env ?? process.env;
@@ -11754,6 +11760,21 @@ function readBody3(req) {
11754
11760
  function pathOf3(req) {
11755
11761
  return new URL(req.url ?? "/", "http://127.0.0.1");
11756
11762
  }
11763
+ function childrenJqlError(jql) {
11764
+ const trimmed = jql.trim();
11765
+ if (/\bin\s*\(\s*\)/i.test(trimmed)) {
11766
+ return "The value '' does not exist for the field 'parent'.";
11767
+ }
11768
+ const childrenMatch = trimmed.match(/(?:parent|"Epic Link")\s+in\s+\(([^)]+)\)/i);
11769
+ if (childrenMatch) {
11770
+ const values = childrenMatch[1].split(",").map((value) => value.trim().replace(/^["']|["']$/g, ""));
11771
+ const invalid = values.filter((value) => !validIssueKey(value));
11772
+ if (invalid.length) {
11773
+ return `No issues have a parent epic with key or name '${invalid[0]}'.`;
11774
+ }
11775
+ }
11776
+ return;
11777
+ }
11757
11778
  function handleFakeJira(req, res, store) {
11758
11779
  const url = pathOf3(req);
11759
11780
  const path = url.pathname;
@@ -11763,7 +11784,13 @@ function handleFakeJira(req, res, store) {
11763
11784
  return true;
11764
11785
  }
11765
11786
  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") ?? "");
11787
+ const jql = url.searchParams.get("jql") ?? "";
11788
+ const error = childrenJqlError(jql);
11789
+ if (error) {
11790
+ json3(res, 400, { errorMessages: [error] });
11791
+ return true;
11792
+ }
11793
+ const issues = store.list(jql);
11767
11794
  json3(res, 200, { expand: "schema,names", isLast: true, issues });
11768
11795
  return true;
11769
11796
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pipe-kan",
3
- "version": "0.23.1",
3
+ "version": "0.23.2",
4
4
  "description": "Local Kanban for jira-cli",
5
5
  "license": "MIT",
6
6
  "type": "module",