@enrichlayer/el-linear 1.9.0 → 1.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.
Files changed (60) hide show
  1. package/README.md +13 -0
  2. package/dist/commands/attachments.js +2 -1
  3. package/dist/commands/comments.js +17 -28
  4. package/dist/commands/cycles.js +2 -1
  5. package/dist/commands/documents.js +2 -1
  6. package/dist/commands/graphql.js +4 -6
  7. package/dist/commands/issue-id.js +1 -3
  8. package/dist/commands/issues/description.js +2 -6
  9. package/dist/commands/issues/link-references.d.ts +21 -0
  10. package/dist/commands/issues/link-references.js +171 -0
  11. package/dist/commands/issues/relations.d.ts +55 -0
  12. package/dist/commands/issues/relations.js +132 -0
  13. package/dist/commands/issues.js +65 -284
  14. package/dist/commands/labels.js +13 -23
  15. package/dist/commands/project-milestones.js +13 -20
  16. package/dist/commands/projects.js +67 -38
  17. package/dist/commands/refs.js +8 -0
  18. package/dist/commands/releases.js +26 -30
  19. package/dist/commands/search.js +21 -30
  20. package/dist/commands/teams.js +2 -1
  21. package/dist/commands/templates.js +9 -14
  22. package/dist/commands/users.js +2 -1
  23. package/dist/config/resolver.js +2 -3
  24. package/dist/queries/attachments-types.d.ts +30 -0
  25. package/dist/queries/attachments-types.js +5 -0
  26. package/dist/queries/comments-types.d.ts +49 -0
  27. package/dist/queries/comments-types.js +5 -0
  28. package/dist/queries/documents-types.d.ts +61 -0
  29. package/dist/queries/documents-types.js +9 -0
  30. package/dist/queries/introspect-types.d.ts +57 -0
  31. package/dist/queries/introspect-types.js +10 -0
  32. package/dist/queries/issues-types.d.ts +416 -0
  33. package/dist/queries/issues-types.js +23 -0
  34. package/dist/queries/issues.d.ts +2 -0
  35. package/dist/queries/issues.js +22 -0
  36. package/dist/queries/labels-types.d.ts +64 -0
  37. package/dist/queries/labels-types.js +5 -0
  38. package/dist/queries/project-milestones-types.d.ts +91 -0
  39. package/dist/queries/project-milestones-types.js +10 -0
  40. package/dist/queries/projects-types.d.ts +75 -0
  41. package/dist/queries/projects-types.js +5 -0
  42. package/dist/queries/projects.d.ts +2 -0
  43. package/dist/queries/projects.js +22 -0
  44. package/dist/queries/releases-types.d.ts +84 -0
  45. package/dist/queries/releases-types.js +5 -0
  46. package/dist/queries/search-types.d.ts +86 -0
  47. package/dist/queries/search-types.js +6 -0
  48. package/dist/queries/templates-types.d.ts +61 -0
  49. package/dist/queries/templates-types.js +9 -0
  50. package/dist/types/linear.d.ts +8 -2
  51. package/dist/utils/auto-link-references.js +29 -24
  52. package/dist/utils/graphql-attachments-service.js +6 -9
  53. package/dist/utils/graphql-documents-service.js +19 -25
  54. package/dist/utils/graphql-issues-service.d.ts +12 -2
  55. package/dist/utils/graphql-issues-service.js +151 -172
  56. package/dist/utils/output.d.ts +2 -0
  57. package/dist/utils/output.js +16 -6
  58. package/dist/utils/workspace-url.d.ts +5 -1
  59. package/dist/utils/workspace-url.js +35 -5
  60. package/package.json +1 -1
@@ -4,22 +4,22 @@ import { enforceValidation, validateIssueCreation, } from "../config/issue-valid
4
4
  import { resolveAssignee, resolveLabels, resolveMember, resolveTeam, } from "../config/resolver.js";
5
5
  import { resolveDefaultStatus } from "../config/status-defaults.js";
6
6
  import { enforceTerms } from "../config/term-enforcer.js";
7
- import { LIST_COMMENTS_QUERY } from "../queries/comments.js";
8
- import { GET_ISSUE_RELATIONS_QUERY, GET_ISSUE_STATE_HISTORY_QUERY, ISSUE_RELATION_CREATE_MUTATION, SCAN_ISSUES_QUERY, } from "../queries/issues.js";
9
- import { autoLinkReferences, } from "../utils/auto-link-references.js";
7
+ import { GET_ISSUE_RELATIONS_QUERY, GET_ISSUE_STATE_HISTORY_QUERY, } from "../queries/issues.js";
10
8
  import { createFileService } from "../utils/file-service.js";
11
9
  import { applyFooter } from "../utils/footer.js";
12
10
  import { createGraphQLAttachmentsService } from "../utils/graphql-attachments-service.js";
13
11
  import { GraphQLIssuesService, } from "../utils/graphql-issues-service.js";
14
- import { createGraphQLService, } from "../utils/graphql-service.js";
15
- import { createLinearService, } from "../utils/linear-service.js";
12
+ import { createGraphQLService } from "../utils/graphql-service.js";
13
+ import { createLinearService } from "../utils/linear-service.js";
16
14
  import { logger } from "../utils/logger.js";
17
15
  import { handleAsyncCommand, outputSuccess, outputWarning, } from "../utils/output.js";
18
16
  import { getRootOpts } from "../utils/root-opts.js";
19
17
  import { formatCsv, formatMarkdown, formatTable, } from "../utils/table-formatter.js";
20
- import { parsePriorityFilter, splitList, validatePriority, } from "../utils/validators.js";
18
+ import { parsePositiveInt, parsePriorityFilter, splitList, validatePriority, } from "../utils/validators.js";
21
19
  import { gitCheckoutBranch, toBranchName } from "./issues/branch.js";
22
- import { maybeAutoLink, prepareAutoLinkedDescription, prepareDescriptionRewrite, pushDescriptionUpdate, readDescriptionFile, resolveDescription, } from "./issues/description.js";
20
+ import { maybeAutoLink, prepareAutoLinkedDescription, readDescriptionFile, resolveDescription, } from "./issues/description.js";
21
+ import { handleLinkReferencesIssue } from "./issues/link-references.js";
22
+ import { buildIncomingRelationEntries, buildOutgoingRelationEntries, createRelations, } from "./issues/relations.js";
23
23
  import { readIssues } from "./read-shortcut.js";
24
24
  const IMAGE_EXTENSIONS = new Set([
25
25
  ".png",
@@ -35,53 +35,6 @@ function isImageFile(filename) {
35
35
  const ext = filename.lastIndexOf(".");
36
36
  return ext !== -1 && IMAGE_EXTENSIONS.has(filename.slice(ext).toLowerCase());
37
37
  }
38
- function transformIssueRelation(rel) {
39
- const issue = rel.issue;
40
- const relatedIssue = rel.relatedIssue;
41
- return {
42
- id: rel.id,
43
- type: rel.type,
44
- issue: {
45
- id: issue.id,
46
- identifier: issue.identifier,
47
- title: issue.title,
48
- },
49
- relatedIssue: {
50
- id: relatedIssue.id,
51
- identifier: relatedIssue.identifier,
52
- title: relatedIssue.title,
53
- },
54
- };
55
- }
56
- async function createRelations(sourceId, options, graphQLService, linearService) {
57
- const relations = [];
58
- const relationSpecs = [
59
- { option: "relatedTo", type: "related" },
60
- { option: "blocks", type: "blocks" },
61
- { option: "blockedBy", type: "blocks", reverse: true },
62
- { option: "duplicateOf", type: "duplicate" },
63
- ];
64
- for (const spec of relationSpecs) {
65
- const value = options[spec.option];
66
- if (!value) {
67
- continue;
68
- }
69
- for (const id of splitList(value)) {
70
- const targetId = await linearService.resolveIssueId(id);
71
- const rel = await graphQLService.rawRequest(ISSUE_RELATION_CREATE_MUTATION, {
72
- input: {
73
- issueId: spec.reverse ? targetId : sourceId,
74
- relatedIssueId: spec.reverse ? sourceId : targetId,
75
- type: spec.type,
76
- },
77
- });
78
- const create = rel.issueRelationCreate;
79
- const issueRelation = create?.issueRelation;
80
- relations.push(transformIssueRelation(issueRelation));
81
- }
82
- }
83
- return relations;
84
- }
85
38
  function validateUpdateOptions(options) {
86
39
  if (options.parentTicket && options.clearParentTicket) {
87
40
  throw new Error("Cannot use --parent-ticket and --clear-parent-ticket together");
@@ -212,7 +165,7 @@ async function handleListIssues(options, command) {
212
165
  ? parsePriorityFilter(options.priority)
213
166
  : undefined,
214
167
  orderBy: options.sort === "created" ? "createdAt" : "updatedAt",
215
- limit: Number.parseInt(options.limit, 10),
168
+ limit: parsePositiveInt(options.limit, "--limit"),
216
169
  };
217
170
  const result = sortIssues(await issuesService.searchIssues(searchArgs), options.sort);
218
171
  outputIssues(result, options.format, options.fields, {
@@ -220,7 +173,7 @@ async function handleListIssues(options, command) {
220
173
  });
221
174
  }
222
175
  else {
223
- const result = sortIssues(await issuesService.getIssues(Number.parseInt(options.limit, 10)), options.sort);
176
+ const result = sortIssues(await issuesService.getIssues(parsePositiveInt(options.limit, "--limit")), options.sort);
224
177
  outputIssues(result, options.format, options.fields, {});
225
178
  }
226
179
  }
@@ -245,7 +198,7 @@ async function handleSearchIssues(query, options, command) {
245
198
  priority: options.priority
246
199
  ? parsePriorityFilter(options.priority)
247
200
  : undefined,
248
- limit: Number.parseInt(options.limit, 10),
201
+ limit: parsePositiveInt(options.limit, "--limit"),
249
202
  };
250
203
  const result = sortIssues(await issuesService.searchIssues(searchArgs), options.sort);
251
204
  outputIssues(result, options.format, options.fields, { query });
@@ -460,22 +413,19 @@ async function handleHistoryIssue(issueId, _options, command) {
460
413
  const graphQLService = await createGraphQLService(rootOpts);
461
414
  const linearService = await createLinearService(rootOpts);
462
415
  const resolvedId = await linearService.resolveIssueId(issueId);
463
- const result = await graphQLService.rawRequest(GET_ISSUE_STATE_HISTORY_QUERY, {
464
- id: resolvedId,
465
- });
416
+ const result = await graphQLService.rawRequest(GET_ISSUE_STATE_HISTORY_QUERY, { id: resolvedId });
466
417
  if (!result.issue) {
467
418
  throw new Error(`Issue "${issueId}" not found`);
468
419
  }
469
420
  const issue = result.issue;
470
- const stateHistory = issue.stateHistory;
471
- const spans = (stateHistory?.nodes ?? []).map((span) => ({
421
+ const spans = issue.stateHistory.nodes.map((span) => ({
472
422
  state: {
473
423
  id: span.state.id,
474
424
  name: span.state.name,
475
425
  type: span.state.type,
476
426
  },
477
427
  startedAt: span.startedAt,
478
- endedAt: span.endedAt || undefined,
428
+ endedAt: span.endedAt ?? undefined,
479
429
  }));
480
430
  outputSuccess({
481
431
  id: issue.id,
@@ -499,82 +449,19 @@ async function handleRelateIssue(issueId, options, command) {
499
449
  meta: { count: relations.length, source: issueId },
500
450
  });
501
451
  }
502
- /**
503
- * Invert relation type for incoming (inverse) relations so the output
504
- * reads naturally from the perspective of the queried issue.
505
- * e.g. if DEV-100 "blocks" DEV-200, and we query DEV-200,
506
- * the inverse relation type is "blocks" but direction is incoming → "blockedBy".
507
- */
508
- function normalizeInverseType(type) {
509
- if (type === "blocks") {
510
- return "blockedBy";
511
- }
512
- return type;
513
- }
514
- function buildRelatedIssueSummary(peer) {
515
- const state = peer.state;
516
- const assignee = peer.assignee;
517
- const team = peer.team;
518
- return {
519
- id: peer.id,
520
- identifier: peer.identifier,
521
- title: peer.title,
522
- ...(state
523
- ? { state: { id: state.id, name: state.name } }
524
- : {}),
525
- ...(peer.priority == null ? {} : { priority: peer.priority }),
526
- ...(assignee
527
- ? {
528
- assignee: {
529
- id: assignee.id,
530
- name: assignee.name,
531
- },
532
- }
533
- : {}),
534
- ...(team
535
- ? {
536
- team: {
537
- id: team.id,
538
- key: team.key,
539
- name: team.name,
540
- },
541
- }
542
- : {}),
543
- };
544
- }
545
- function buildRelationEntries(nodes, peerKey, direction, normalizeType) {
546
- const entries = [];
547
- for (const rel of nodes ?? []) {
548
- const peer = rel[peerKey];
549
- if (!peer) {
550
- continue;
551
- }
552
- entries.push({
553
- id: rel.id,
554
- type: normalizeType(rel.type),
555
- direction,
556
- issue: buildRelatedIssueSummary(peer),
557
- });
558
- }
559
- return entries;
560
- }
561
452
  async function handleRelatedIssues(issueId, _options, command) {
562
453
  const rootOpts = getRootOpts(command);
563
454
  const graphQLService = await createGraphQLService(rootOpts);
564
455
  const linearService = await createLinearService(rootOpts);
565
456
  const resolvedId = await linearService.resolveIssueId(issueId);
566
- const result = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, {
567
- id: resolvedId,
568
- });
457
+ const result = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, { id: resolvedId });
569
458
  if (!result.issue) {
570
459
  throw new Error(`Issue "${issueId}" not found`);
571
460
  }
572
461
  const issue = result.issue;
573
- const relations = issue.relations;
574
- const inverseRelations = issue.inverseRelations;
575
462
  const entries = [
576
- ...buildRelationEntries(relations?.nodes, "relatedIssue", "outgoing", (raw) => raw),
577
- ...buildRelationEntries(inverseRelations?.nodes, "issue", "incoming", normalizeInverseType),
463
+ ...buildOutgoingRelationEntries(issue.relations.nodes),
464
+ ...buildIncomingRelationEntries(issue.inverseRelations.nodes),
578
465
  ];
579
466
  outputSuccess({
580
467
  id: issue.id,
@@ -584,158 +471,6 @@ async function handleRelatedIssues(issueId, _options, command) {
584
471
  meta: { count: entries.length },
585
472
  });
586
473
  }
587
- async function fetchCommentBodies(issueUuid, graphQLService) {
588
- const result = await graphQLService.rawRequest(LIST_COMMENTS_QUERY, {
589
- issueId: issueUuid,
590
- first: 250,
591
- });
592
- const issue = result.issue;
593
- const comments = issue?.comments;
594
- const nodes = comments?.nodes ?? [];
595
- return nodes
596
- .map((c) => c.body)
597
- .filter((b) => typeof b === "string" && b.length > 0);
598
- }
599
- async function linkReferencesForIssue(args) {
600
- const comments = args.includeComments
601
- ? await fetchCommentBodies(args.issueUuid, args.graphQLService)
602
- : undefined;
603
- return autoLinkReferences({
604
- issueId: args.issueUuid,
605
- identifier: args.identifier,
606
- description: args.description,
607
- comments,
608
- dryRun: args.dryRun,
609
- graphQLService: args.graphQLService,
610
- linearService: args.linearService,
611
- preResolved: args.preResolved,
612
- });
613
- }
614
- async function handleLinkReferencesSingle(issueId, options, graphQLService, linearService) {
615
- const resolvedId = await linearService.resolveIssueId(issueId);
616
- // GET_ISSUE_RELATIONS_QUERY also returns description, so this single call gives us everything
617
- // we need for the description-only path. Comments are fetched separately on demand.
618
- const issueResult = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, {
619
- id: resolvedId,
620
- });
621
- if (!issueResult.issue) {
622
- throw new Error(`Issue "${issueId}" not found`);
623
- }
624
- const issue = issueResult.issue;
625
- const identifier = issue.identifier;
626
- const description = issue.description ?? "";
627
- const dryRun = Boolean(options.dryRun);
628
- const includeComments = Boolean(options.includeComments);
629
- const rewriteDescription = Boolean(options.rewriteDescription);
630
- // If rewriting, validate refs once up front so we can both wrap the description and
631
- // pass the resolved map down to autoLink — avoids a second resolve round trip.
632
- const rewrite = rewriteDescription
633
- ? await prepareDescriptionRewrite(description, identifier, linearService, graphQLService)
634
- : { preResolved: undefined, wrapped: undefined };
635
- const autoLinked = await linkReferencesForIssue({
636
- issueUuid: resolvedId,
637
- identifier,
638
- description,
639
- includeComments,
640
- dryRun,
641
- graphQLService,
642
- linearService,
643
- preResolved: rewrite.preResolved,
644
- });
645
- let descriptionRewritten = false;
646
- if (rewriteDescription && rewrite.wrapped && !dryRun) {
647
- await pushDescriptionUpdate(resolvedId, rewrite.wrapped, graphQLService);
648
- descriptionRewritten = true;
649
- }
650
- outputSuccess({
651
- id: resolvedId,
652
- identifier,
653
- title: issue.title,
654
- autoLinked,
655
- ...(rewriteDescription
656
- ? {
657
- descriptionRewritten,
658
- ...(dryRun && rewrite.wrapped
659
- ? { descriptionPreview: rewrite.wrapped }
660
- : {}),
661
- }
662
- : {}),
663
- meta: {
664
- dryRun,
665
- includeComments,
666
- ...(rewriteDescription ? { rewriteDescription: true } : {}),
667
- },
668
- });
669
- }
670
- async function handleLinkReferencesBatch(teamInput, options, graphQLService, linearService) {
671
- const teamId = resolveTeam(teamInput);
672
- const limit = options.limit
673
- ? Number.parseInt(options.limit, 10)
674
- : 100;
675
- if (!Number.isFinite(limit) || limit <= 0) {
676
- throw new Error(`--limit must be a positive integer, got: ${options.limit}`);
677
- }
678
- const dryRun = Boolean(options.dryRun);
679
- const includeComments = Boolean(options.includeComments);
680
- const scan = await graphQLService.rawRequest(SCAN_ISSUES_QUERY, {
681
- filter: { team: { id: { eq: teamId } } },
682
- first: limit,
683
- });
684
- const issues = scan.issues?.nodes;
685
- const nodes = issues ?? [];
686
- const entries = [];
687
- for (const node of nodes) {
688
- const issueUuid = node.id;
689
- const identifier = node.identifier;
690
- const description = node.description ?? "";
691
- const autoLinked = await linkReferencesForIssue({
692
- issueUuid,
693
- identifier,
694
- description,
695
- includeComments,
696
- dryRun,
697
- graphQLService,
698
- linearService,
699
- });
700
- entries.push({ identifier, autoLinked });
701
- }
702
- // Summary counts across the batch
703
- const totalLinked = entries.reduce((n, e) => n + e.autoLinked.linked.length, 0);
704
- const totalSkipped = entries.reduce((n, e) => n + e.autoLinked.skipped.length, 0);
705
- const totalFailed = entries.reduce((n, e) => n + e.autoLinked.failed.length, 0);
706
- outputSuccess({
707
- data: entries,
708
- meta: {
709
- count: entries.length,
710
- team: teamInput,
711
- dryRun,
712
- includeComments,
713
- totals: {
714
- linked: totalLinked,
715
- skipped: totalSkipped,
716
- failed: totalFailed,
717
- },
718
- },
719
- });
720
- }
721
- async function handleLinkReferencesIssue(issueIdOrTeamFlag, options, command) {
722
- const rootOpts = getRootOpts(command);
723
- const graphQLService = await createGraphQLService(rootOpts);
724
- const linearService = await createLinearService(rootOpts);
725
- if (options.team) {
726
- if (issueIdOrTeamFlag) {
727
- throw new Error("Provide either an issueId positional argument OR --team, not both. With --team, omit the issueId.");
728
- }
729
- if (options.rewriteDescription) {
730
- throw new Error("--rewrite-description is single-issue only. Use it case by case (one issueId per invocation) — bulk description rewrites bot-author the whole team's history.");
731
- }
732
- return handleLinkReferencesBatch(options.team, options, graphQLService, linearService);
733
- }
734
- if (!issueIdOrTeamFlag) {
735
- throw new Error("issueId is required (or use --team <key> to scan a whole team)");
736
- }
737
- return handleLinkReferencesSingle(issueIdOrTeamFlag, options, graphQLService, linearService);
738
- }
739
474
  async function handleUpdateIssue(issueId, options, command) {
740
475
  // Normalize aliases
741
476
  if (options.state && !options.status) {
@@ -754,9 +489,10 @@ async function handleUpdateIssue(issueId, options, command) {
754
489
  const linearService = await createLinearService(rootOpts);
755
490
  if (options.appendDescription) {
756
491
  const resolved = await linearService.resolveIssueId(issueId);
757
- const current = await graphQLService.rawRequest("query($id: String!) { issue(id: $id) { description } }", { id: resolved });
758
- const issue = current.issue;
759
- const existing = issue?.description ?? "";
492
+ const current = await graphQLService.rawRequest("query($id: String!) { issue(id: $id) { description } }", {
493
+ id: resolved,
494
+ });
495
+ const existing = current.issue?.description ?? "";
760
496
  options.description = `${existing}\n${options.appendDescription}`;
761
497
  }
762
498
  if (options.title || options.description) {
@@ -799,6 +535,34 @@ async function handleUpdateIssue(issueId, options, command) {
799
535
  : undefined;
800
536
  outputSuccess(autoLinked ? { ...result, autoLinked } : result);
801
537
  }
538
+ async function handleArchiveIssue(issueId, _options, command) {
539
+ const rootOpts = getRootOpts(command);
540
+ const graphQLService = await createGraphQLService(rootOpts);
541
+ const linearService = await createLinearService(rootOpts);
542
+ const issuesService = new GraphQLIssuesService(graphQLService, linearService);
543
+ const result = await issuesService.archiveIssue(issueId);
544
+ outputSuccess({
545
+ success: true,
546
+ archived: true,
547
+ ...result,
548
+ });
549
+ }
550
+ async function handleDeleteIssue(issueId, options, command) {
551
+ const rootOpts = getRootOpts(command);
552
+ const graphQLService = await createGraphQLService(rootOpts);
553
+ const linearService = await createLinearService(rootOpts);
554
+ const issuesService = new GraphQLIssuesService(graphQLService, linearService);
555
+ const permanentlyDelete = Boolean(options.permanentlyDelete || options.hard);
556
+ const result = await issuesService.deleteIssue(issueId, {
557
+ permanentlyDelete,
558
+ });
559
+ outputSuccess({
560
+ success: true,
561
+ deleted: true,
562
+ permanentlyDeleted: permanentlyDelete,
563
+ ...result,
564
+ });
565
+ }
802
566
  async function handleRetrolink(options, command) {
803
567
  // 1. Read current branch
804
568
  let currentBranch;
@@ -1000,6 +764,23 @@ export function setupIssuesCommands(program) {
1000
764
  .option("--strict", "strict brand validation")
1001
765
  .option("--no-auto-link", "skip auto-linking issue references found in the description")
1002
766
  .action(handleAsyncCommand(handleUpdateIssue));
767
+ issues
768
+ .command("archive <issueId>")
769
+ .description("Archive an issue.")
770
+ .addHelpText("after", "\nBoth UUID and identifiers like ABC-123 are supported.")
771
+ .action(handleAsyncCommand(handleArchiveIssue));
772
+ issues
773
+ .command("delete <issueId>")
774
+ .description("Delete (trash) an issue.")
775
+ .addHelpText("after", "\nBoth UUID and identifiers like ABC-123 are supported. Use --permanently-delete only when you intend to skip Linear's grace period; Linear requires admin permissions.")
776
+ .option("--permanently-delete", "skip Linear's 30-day grace period (admin only)")
777
+ .option("--hard", "alias for --permanently-delete")
778
+ .action(handleAsyncCommand(handleDeleteIssue));
779
+ issues
780
+ .command("hard-delete <issueId>")
781
+ .description("Permanently delete an issue immediately (admin only).")
782
+ .addHelpText("after", "\nBoth UUID and identifiers like ABC-123 are supported.")
783
+ .action(handleAsyncCommand((issueId, options, command) => handleDeleteIssue(issueId, { ...options, permanentlyDelete: true }, command)));
1003
784
  issues
1004
785
  .command("history <issueId>")
1005
786
  .description("Show issue state transition history (time in each status).")
@@ -6,7 +6,7 @@ import { createGraphQLService } from "../utils/graphql-service.js";
6
6
  import { createLinearService } from "../utils/linear-service.js";
7
7
  import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
8
8
  import { getRootOpts } from "../utils/root-opts.js";
9
- import { validateHexColor } from "../utils/validators.js";
9
+ import { parsePositiveInt, validateHexColor } from "../utils/validators.js";
10
10
  async function handleCreateLabel(name, options, command) {
11
11
  const rootOpts = getRootOpts(command);
12
12
  const teamId = resolveTeam(options.team);
@@ -23,45 +23,35 @@ async function handleCreateLabel(name, options, command) {
23
23
  name: options.parent,
24
24
  teamId,
25
25
  });
26
- const issueLabels = parentResult.issueLabels;
27
- const nodes = issueLabels?.nodes;
28
- const parentLabel = nodes?.[0];
26
+ const parentLabel = parentResult.issueLabels.nodes[0];
29
27
  if (parentLabel) {
30
28
  input.parentId = parentLabel.id;
31
29
  }
32
30
  }
33
- const result = await graphQLService.rawRequest(CREATE_LABEL_MUTATION, {
34
- input,
35
- });
36
- const createResult = result.issueLabelCreate;
37
- if (!createResult?.success) {
31
+ const result = await graphQLService.rawRequest(CREATE_LABEL_MUTATION, { input });
32
+ if (!result.issueLabelCreate.success || !result.issueLabelCreate.issueLabel) {
38
33
  throw new Error(`Failed to create label "${name}"`);
39
34
  }
40
- outputSuccess(createResult.issueLabel);
35
+ outputSuccess(result.issueLabelCreate.issueLabel);
41
36
  }
42
37
  async function handleRetireLabel(labelId, _options, command) {
43
38
  const rootOpts = getRootOpts(command);
44
39
  const graphQLService = await createGraphQLService(rootOpts);
45
- const result = await graphQLService.rawRequest(RETIRE_LABEL_MUTATION, {
46
- id: labelId,
47
- });
48
- const retireResult = result.issueLabelRetire;
49
- if (!retireResult?.success) {
40
+ const result = await graphQLService.rawRequest(RETIRE_LABEL_MUTATION, { id: labelId });
41
+ if (!result.issueLabelRetire.success || !result.issueLabelRetire.issueLabel) {
50
42
  throw new Error(`Failed to retire label "${labelId}"`);
51
43
  }
52
- outputSuccess(retireResult.issueLabel);
44
+ outputSuccess(result.issueLabelRetire.issueLabel);
53
45
  }
54
46
  async function handleRestoreLabel(labelId, _options, command) {
55
47
  const rootOpts = getRootOpts(command);
56
48
  const graphQLService = await createGraphQLService(rootOpts);
57
- const result = await graphQLService.rawRequest(RESTORE_LABEL_MUTATION, {
58
- id: labelId,
59
- });
60
- const restoreResult = result.issueLabelRestore;
61
- if (!restoreResult?.success) {
49
+ const result = await graphQLService.rawRequest(RESTORE_LABEL_MUTATION, { id: labelId });
50
+ if (!result.issueLabelRestore.success ||
51
+ !result.issueLabelRestore.issueLabel) {
62
52
  throw new Error(`Failed to restore label "${labelId}"`);
63
53
  }
64
- outputSuccess(restoreResult.issueLabel);
54
+ outputSuccess(result.issueLabelRestore.issueLabel);
65
55
  }
66
56
  export function setupLabelsCommands(program) {
67
57
  const labels = program
@@ -79,7 +69,7 @@ export function setupLabelsCommands(program) {
79
69
  const rootOpts = getRootOpts(command);
80
70
  const teamFilter = options.team ? resolveTeam(options.team) : undefined;
81
71
  const nameFilter = options.name;
82
- const limit = Number.parseInt(options.limit, 10);
72
+ const limit = parsePositiveInt(options.limit, "--limit");
83
73
  const ttl = resolveCacheTTL({
84
74
  configTTL: loadConfig().cacheTTLSeconds,
85
75
  noCacheFlag: rootOpts.cache === false,
@@ -5,7 +5,7 @@ import { createLinearService, } from "../utils/linear-service.js";
5
5
  import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
6
6
  import { getRootOpts } from "../utils/root-opts.js";
7
7
  import { isUuid } from "../utils/uuid.js";
8
- import { validateIsoDate } from "../utils/validators.js";
8
+ import { parsePositiveInt, validateIsoDate } from "../utils/validators.js";
9
9
  async function resolveMilestoneId(milestoneNameOrId, graphQLService, linearService, projectNameOrId) {
10
10
  if (isUuid(milestoneNameOrId)) {
11
11
  return milestoneNameOrId;
@@ -17,22 +17,17 @@ async function resolveMilestoneId(milestoneNameOrId, graphQLService, linearServi
17
17
  name: milestoneNameOrId,
18
18
  projectId,
19
19
  });
20
- const projectData = findRes.project;
21
- const milestonesData = projectData?.projectMilestones;
22
- nodes = milestonesData?.nodes || [];
20
+ nodes = findRes.project?.projectMilestones.nodes ?? [];
23
21
  }
24
22
  if (nodes.length === 0) {
25
- const globalRes = await graphQLService.rawRequest(FIND_PROJECT_MILESTONE_BY_NAME_GLOBAL, {
26
- name: milestoneNameOrId,
27
- });
28
- const globalMilestones = globalRes.projectMilestones;
29
- nodes = globalMilestones?.nodes || [];
23
+ const globalRes = await graphQLService.rawRequest(FIND_PROJECT_MILESTONE_BY_NAME_GLOBAL, { name: milestoneNameOrId });
24
+ nodes = globalRes.projectMilestones.nodes;
30
25
  }
31
26
  if (nodes.length === 0) {
32
27
  throw notFoundError("Milestone", milestoneNameOrId);
33
28
  }
34
29
  if (nodes.length > 1) {
35
- const matches = nodes.map((m) => `"${m.name}" in project "${m.project?.name}"`);
30
+ const matches = nodes.map((m) => `"${m.name}" in project "${m.project.name}"`);
36
31
  throw multipleMatchesError("milestone", milestoneNameOrId, matches, "specify --project or use the milestone ID");
37
32
  }
38
33
  return nodes[0].id;
@@ -44,11 +39,9 @@ async function handleListMilestones(options, command) {
44
39
  const projectId = await linearService.resolveProjectId(options.project);
45
40
  const result = await graphQLService.rawRequest(LIST_PROJECT_MILESTONES_QUERY, {
46
41
  projectId,
47
- first: Number.parseInt(options.limit || "50", 10),
42
+ first: parsePositiveInt(options.limit || "50", "--limit"),
48
43
  });
49
- const projectData = result.project;
50
- const milestonesData = projectData?.projectMilestones;
51
- const nodes = milestonesData?.nodes || [];
44
+ const nodes = result.project?.projectMilestones.nodes ?? [];
52
45
  outputSuccess({ data: nodes, meta: { count: nodes.length } });
53
46
  }
54
47
  async function handleReadMilestone(milestoneIdOrName, options, command) {
@@ -76,11 +69,11 @@ async function handleCreateMilestone(name, options, command) {
76
69
  description: options.description,
77
70
  targetDate: options.targetDate,
78
71
  });
79
- const createData = result.projectMilestoneCreate;
80
- if (!createData?.success) {
72
+ if (!result.projectMilestoneCreate.success ||
73
+ !result.projectMilestoneCreate.projectMilestone) {
81
74
  throw new Error("Failed to create project milestone");
82
75
  }
83
- outputSuccess(createData.projectMilestone);
76
+ outputSuccess(result.projectMilestoneCreate.projectMilestone);
84
77
  }
85
78
  async function handleUpdateMilestone(milestoneIdOrName, options, command) {
86
79
  const rootOpts = getRootOpts(command);
@@ -102,11 +95,11 @@ async function handleUpdateMilestone(milestoneIdOrName, options, command) {
102
95
  updateVars.sortOrder = Number.parseFloat(options.sortOrder);
103
96
  }
104
97
  const result = await graphQLService.rawRequest(UPDATE_PROJECT_MILESTONE_MUTATION, updateVars);
105
- const updateData = result.projectMilestoneUpdate;
106
- if (!updateData?.success) {
98
+ if (!result.projectMilestoneUpdate.success ||
99
+ !result.projectMilestoneUpdate.projectMilestone) {
107
100
  throw new Error("Failed to update project milestone");
108
101
  }
109
- outputSuccess(updateData.projectMilestone);
102
+ outputSuccess(result.projectMilestoneUpdate.projectMilestone);
110
103
  }
111
104
  export function setupProjectMilestonesCommands(program) {
112
105
  const projectMilestones = program