linear-sdk-cli 0.1.3 → 0.2.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +96 -58
- package/COVERAGE.md +5 -5
- package/MIGRATING.md +71 -69
- package/README.md +115 -90
- package/package.json +6 -7
- package/skills/README.md +15 -0
- package/skills/linear-sdk-cli/SKILL.md +27 -3
- package/skills/linear-sdk-cli/references/api.md +9 -9
- package/skills/linear-sdk-cli/references/auth.md +18 -0
- package/skills/linear-sdk-cli/references/commands.md +2 -2
- package/skills/linear-sdk-cli/references/comment.md +9 -6
- package/skills/linear-sdk-cli/references/config.md +9 -11
- package/skills/linear-sdk-cli/references/issue.md +72 -18
- package/skills/linear-sdk-cli/references/open.md +29 -0
- package/skills/linear-sdk-cli/references/team.md +16 -0
- package/skills/linear-sdk-cli/scripts/generate-docs.ts +6 -2
- package/skills/linear-sdk-cli-maintenance/SKILL.md +97 -0
- package/src/bin/linear.ts +16 -6
- package/src/cli.ts +3 -1
- package/src/commands/agent-session.ts +13 -5
- package/src/commands/api.ts +15 -9
- package/src/commands/attach.ts +15 -4
- package/src/commands/comment.ts +50 -11
- package/src/commands/completion.ts +2 -6
- package/src/commands/cycle.ts +6 -1
- package/src/commands/discover.ts +6 -2
- package/src/commands/document.ts +4 -1
- package/src/commands/initiative.ts +5 -1
- package/src/commands/issue.ts +392 -91
- package/src/commands/meta.ts +139 -31
- package/src/commands/milestone.ts +4 -1
- package/src/commands/notification.ts +15 -6
- package/src/commands/open.ts +84 -0
- package/src/commands/project.ts +17 -4
- package/src/commands/team.ts +13 -0
- package/src/commands/user.ts +2 -2
- package/src/commands/webhook.ts +12 -2
- package/src/config.ts +501 -184
- package/src/context.ts +13 -4
- package/src/git.ts +28 -3
- package/src/lib/body.ts +26 -2
- package/src/lib/errors.ts +33 -8
- package/src/lib/introspect.ts +76 -7
- package/src/lib/keyring.ts +51 -9
- package/src/lib/mentions.ts +49 -0
- package/src/lib/open.ts +32 -0
- package/src/lib/options.ts +108 -12
- package/src/lib/output-shapes.ts +81 -9
- package/src/lib/pagination.ts +50 -17
- package/src/lib/resolve.ts +40 -7
- package/src/lib/shape.ts +16 -12
- package/src/lib/upload.ts +3 -1
- package/src/output/color.ts +52 -0
- package/src/output/format.ts +96 -18
- package/src/output/markdown.ts +133 -0
- package/src/output/pager.ts +131 -0
- package/src/output/sanitize.ts +1 -1
- package/src/output/table.ts +7 -7
- package/src/services/agent-session.ts +11 -3
- package/src/services/attachment.ts +14 -5
- package/src/services/comment.ts +37 -9
- package/src/services/cycle.ts +11 -4
- package/src/services/document.ts +29 -10
- package/src/services/favorite.ts +15 -9
- package/src/services/initiative-update.ts +16 -15
- package/src/services/initiative.ts +15 -10
- package/src/services/issue.ts +263 -19
- package/src/services/label.ts +12 -2
- package/src/services/milestone.ts +8 -5
- package/src/services/notification.ts +16 -2
- package/src/services/organization.ts +23 -18
- package/src/services/project.ts +55 -34
- package/src/services/roadmap.ts +7 -6
- package/src/services/state.ts +9 -3
- package/src/services/team.ts +67 -24
- package/src/services/user.ts +2 -2
- package/src/services/webhook.ts +28 -17
package/src/services/issue.ts
CHANGED
|
@@ -143,7 +143,7 @@ export interface ListFilters {
|
|
|
143
143
|
export const MINE_STATE_TYPES = ["unstarted"];
|
|
144
144
|
|
|
145
145
|
/** The sort orders `issue list` accepts, in `--sort`, env, and config alike. */
|
|
146
|
-
export const ISSUE_SORTS = ["priority", "updated", "created"] as const;
|
|
146
|
+
export const ISSUE_SORTS = ["priority", "manual", "updated", "created"] as const;
|
|
147
147
|
export type IssueSort = (typeof ISSUE_SORTS)[number];
|
|
148
148
|
|
|
149
149
|
type SortConfig = Pick<
|
|
@@ -237,7 +237,9 @@ export async function buildFilter(
|
|
|
237
237
|
// --all-teams wins over both the flag and the configured default.
|
|
238
238
|
// Uppercase first, then dedupe: `--team tes --team TES` is one team.
|
|
239
239
|
const teamKeys = [
|
|
240
|
-
...new Set(
|
|
240
|
+
...new Set(
|
|
241
|
+
asList(f.allTeams ? undefined : (f.team ?? defaultTeamKey)).map((k) => k.toUpperCase()),
|
|
242
|
+
),
|
|
241
243
|
];
|
|
242
244
|
// One team keeps the exact `eq` shape it has always sent; several use `in`
|
|
243
245
|
// (the reference CLI spells the same thing as `or: [{ key: { eq } }, …]`).
|
|
@@ -358,7 +360,7 @@ export function sortSpec(sort: IssueSort = "priority"): Array<Record<string, unk
|
|
|
358
360
|
// against the API, where Descending returns Backlog BEFORE In Progress.
|
|
359
361
|
// The reference CLI hardcodes Descending, so a Low-priority backlog issue
|
|
360
362
|
// sorts above an Urgent in-progress one there; we deliberately diverge
|
|
361
|
-
//
|
|
363
|
+
// because workflow state should take precedence over backlog urgency.
|
|
362
364
|
//
|
|
363
365
|
// Descending priority is urgency order (Urgent…Low); `nulls: "last"` keeps
|
|
364
366
|
// "No priority" at the bottom, which is what `noPriorityFirst: false` did.
|
|
@@ -369,6 +371,11 @@ export function sortSpec(sort: IssueSort = "priority"): Array<Record<string, unk
|
|
|
369
371
|
];
|
|
370
372
|
case "created":
|
|
371
373
|
return [{ createdAt: { order: "Descending" } }];
|
|
374
|
+
case "manual":
|
|
375
|
+
// Linear's board order, without injecting workflow-state or priority
|
|
376
|
+
// grouping. This is the reference CLI's `issue_sort = "manual"` and is
|
|
377
|
+
// useful when a carefully curated backlog should read exactly as arranged.
|
|
378
|
+
return [{ manual: { nulls: "last", order: "Ascending" } }];
|
|
372
379
|
case "updated":
|
|
373
380
|
return [{ updatedAt: { order: "Descending" } }];
|
|
374
381
|
}
|
|
@@ -486,6 +493,57 @@ function lifecycle(n: any) {
|
|
|
486
493
|
* flattened to strings here: `team: "TES Test-workspace-bla"` is not even
|
|
487
494
|
* parseable, since a team name can contain spaces.)
|
|
488
495
|
*/
|
|
496
|
+
export interface IssueDetailRef {
|
|
497
|
+
id: string;
|
|
498
|
+
identifier: string;
|
|
499
|
+
title: string;
|
|
500
|
+
state: { id: string; name: string; type: string } | null;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export interface IssueAttachmentDetail {
|
|
504
|
+
id: string;
|
|
505
|
+
title: string;
|
|
506
|
+
url: string;
|
|
507
|
+
subtitle: string | null;
|
|
508
|
+
sourceType: string | null;
|
|
509
|
+
createdAt: string;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export interface IssueDocumentDetail {
|
|
513
|
+
id: string;
|
|
514
|
+
title: string;
|
|
515
|
+
slugId: string;
|
|
516
|
+
url: string;
|
|
517
|
+
createdAt: string;
|
|
518
|
+
updatedAt: string;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export interface IssueRelationDetail {
|
|
522
|
+
id: string;
|
|
523
|
+
type: string;
|
|
524
|
+
issue: IssueDetailRef;
|
|
525
|
+
relatedIssue: IssueDetailRef;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* A comment embedded in `IssueDetail`. It is a superset of the shared comment
|
|
530
|
+
* row: external authors and resolution metadata are retained so a caller can
|
|
531
|
+
* reconstruct threads without another request.
|
|
532
|
+
*/
|
|
533
|
+
export interface IssueCommentDetail {
|
|
534
|
+
id: string;
|
|
535
|
+
body: string;
|
|
536
|
+
url: string;
|
|
537
|
+
createdAt: string;
|
|
538
|
+
editedAt: string | null;
|
|
539
|
+
resolvedAt: string | null;
|
|
540
|
+
parent: { id: string } | null;
|
|
541
|
+
user: { id: string; displayName: string } | null;
|
|
542
|
+
externalUser: { id: string; displayName: string } | null;
|
|
543
|
+
resolvingCommentId: string | null;
|
|
544
|
+
resolvingUser: { id: string; displayName: string } | null;
|
|
545
|
+
}
|
|
546
|
+
|
|
489
547
|
export interface IssueDetail {
|
|
490
548
|
id: string;
|
|
491
549
|
identifier: string;
|
|
@@ -510,9 +568,15 @@ export interface IssueDetail {
|
|
|
510
568
|
project: { id: string; name: string } | null;
|
|
511
569
|
milestone: { id: string; name: string } | null;
|
|
512
570
|
cycle: { id: string; number: number; name: string | null } | null;
|
|
513
|
-
parent:
|
|
571
|
+
parent: IssueDetailRef | null;
|
|
572
|
+
children: IssueDetailRef[];
|
|
514
573
|
labels: Array<{ id: string; name: string }>;
|
|
515
574
|
subscribers: Array<{ id: string; displayName: string }>;
|
|
575
|
+
attachments: IssueAttachmentDetail[];
|
|
576
|
+
documents: IssueDocumentDetail[];
|
|
577
|
+
relations: IssueRelationDetail[];
|
|
578
|
+
inverseRelations: IssueRelationDetail[];
|
|
579
|
+
comments: IssueCommentDetail[];
|
|
516
580
|
}
|
|
517
581
|
|
|
518
582
|
/** The detail's shape as `linear commands` advertises it; checked against `IssueDetail`. */
|
|
@@ -540,9 +604,95 @@ export const ISSUE_DETAIL_SHAPE = shape<IssueDetail>({
|
|
|
540
604
|
project: { nullable: { id: "string", name: "string" } },
|
|
541
605
|
milestone: { nullable: { id: "string", name: "string" } },
|
|
542
606
|
cycle: { nullable: { id: "string", number: "number", name: "string|null" } },
|
|
543
|
-
parent: {
|
|
607
|
+
parent: {
|
|
608
|
+
nullable: {
|
|
609
|
+
id: "string",
|
|
610
|
+
identifier: "string",
|
|
611
|
+
title: "string",
|
|
612
|
+
state: { nullable: { id: "string", name: "string", type: "string" } },
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
children: [
|
|
616
|
+
{
|
|
617
|
+
id: "string",
|
|
618
|
+
identifier: "string",
|
|
619
|
+
title: "string",
|
|
620
|
+
state: { nullable: { id: "string", name: "string", type: "string" } },
|
|
621
|
+
},
|
|
622
|
+
],
|
|
544
623
|
labels: [{ id: "string", name: "string" }],
|
|
545
624
|
subscribers: [{ id: "string", displayName: "string" }],
|
|
625
|
+
attachments: [
|
|
626
|
+
{
|
|
627
|
+
id: "string",
|
|
628
|
+
title: "string",
|
|
629
|
+
url: "string",
|
|
630
|
+
subtitle: "string|null",
|
|
631
|
+
sourceType: "string|null",
|
|
632
|
+
createdAt: "string",
|
|
633
|
+
},
|
|
634
|
+
],
|
|
635
|
+
documents: [
|
|
636
|
+
{
|
|
637
|
+
id: "string",
|
|
638
|
+
title: "string",
|
|
639
|
+
slugId: "string",
|
|
640
|
+
url: "string",
|
|
641
|
+
createdAt: "string",
|
|
642
|
+
updatedAt: "string",
|
|
643
|
+
},
|
|
644
|
+
],
|
|
645
|
+
relations: [
|
|
646
|
+
{
|
|
647
|
+
id: "string",
|
|
648
|
+
type: "string",
|
|
649
|
+
issue: {
|
|
650
|
+
id: "string",
|
|
651
|
+
identifier: "string",
|
|
652
|
+
title: "string",
|
|
653
|
+
state: { nullable: { id: "string", name: "string", type: "string" } },
|
|
654
|
+
},
|
|
655
|
+
relatedIssue: {
|
|
656
|
+
id: "string",
|
|
657
|
+
identifier: "string",
|
|
658
|
+
title: "string",
|
|
659
|
+
state: { nullable: { id: "string", name: "string", type: "string" } },
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
],
|
|
663
|
+
inverseRelations: [
|
|
664
|
+
{
|
|
665
|
+
id: "string",
|
|
666
|
+
type: "string",
|
|
667
|
+
issue: {
|
|
668
|
+
id: "string",
|
|
669
|
+
identifier: "string",
|
|
670
|
+
title: "string",
|
|
671
|
+
state: { nullable: { id: "string", name: "string", type: "string" } },
|
|
672
|
+
},
|
|
673
|
+
relatedIssue: {
|
|
674
|
+
id: "string",
|
|
675
|
+
identifier: "string",
|
|
676
|
+
title: "string",
|
|
677
|
+
state: { nullable: { id: "string", name: "string", type: "string" } },
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
],
|
|
681
|
+
comments: [
|
|
682
|
+
{
|
|
683
|
+
id: "string",
|
|
684
|
+
body: "string",
|
|
685
|
+
url: "string",
|
|
686
|
+
createdAt: "string",
|
|
687
|
+
editedAt: "string|null",
|
|
688
|
+
resolvedAt: "string|null",
|
|
689
|
+
parent: { nullable: { id: "string" } },
|
|
690
|
+
user: { nullable: { id: "string", displayName: "string" } },
|
|
691
|
+
externalUser: { nullable: { id: "string", displayName: "string" } },
|
|
692
|
+
resolvingCommentId: "string|null",
|
|
693
|
+
resolvingUser: { nullable: { id: "string", displayName: "string" } },
|
|
694
|
+
},
|
|
695
|
+
],
|
|
546
696
|
});
|
|
547
697
|
|
|
548
698
|
/**
|
|
@@ -557,7 +707,7 @@ export const ISSUE_DETAIL_SHAPE = shape<IssueDetail>({
|
|
|
557
707
|
* depends on the API accepting human identifiers in `issue(id:)`.
|
|
558
708
|
*/
|
|
559
709
|
const DETAIL_QUERY = `
|
|
560
|
-
query CliIssueDetail($filter: IssueFilter!) {
|
|
710
|
+
query CliIssueDetail($filter: IssueFilter!, $includeComments: Boolean!) {
|
|
561
711
|
issues(filter: $filter, first: 1, includeArchived: true) {
|
|
562
712
|
nodes {
|
|
563
713
|
id identifier title description priority priorityLabel estimate url branchName dueDate
|
|
@@ -568,9 +718,39 @@ query CliIssueDetail($filter: IssueFilter!) {
|
|
|
568
718
|
project { id name }
|
|
569
719
|
projectMilestone { id name }
|
|
570
720
|
cycle { id number name }
|
|
571
|
-
parent { id identifier }
|
|
721
|
+
parent { id identifier title state { id name type } }
|
|
722
|
+
children(first: 250) { nodes { id identifier title state { id name type } } }
|
|
572
723
|
labels(first: 50) { nodes { id name } }
|
|
573
724
|
subscribers(first: 50) { nodes { id displayName } }
|
|
725
|
+
attachments(first: 50) {
|
|
726
|
+
nodes { id title url subtitle sourceType createdAt }
|
|
727
|
+
}
|
|
728
|
+
documents(first: 50) {
|
|
729
|
+
nodes { id title slugId url createdAt updatedAt }
|
|
730
|
+
}
|
|
731
|
+
relations(first: 50) {
|
|
732
|
+
nodes {
|
|
733
|
+
id type
|
|
734
|
+
issue { id identifier title state { id name type } }
|
|
735
|
+
relatedIssue { id identifier title state { id name type } }
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
inverseRelations(first: 50) {
|
|
739
|
+
nodes {
|
|
740
|
+
id type
|
|
741
|
+
issue { id identifier title state { id name type } }
|
|
742
|
+
relatedIssue { id identifier title state { id name type } }
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
comments(first: 50, orderBy: createdAt) @include(if: $includeComments) {
|
|
746
|
+
nodes {
|
|
747
|
+
id body url createdAt editedAt resolvedAt resolvingCommentId
|
|
748
|
+
parent { id }
|
|
749
|
+
user { id displayName }
|
|
750
|
+
externalUser { id displayName }
|
|
751
|
+
resolvingUser { id displayName }
|
|
752
|
+
}
|
|
753
|
+
}
|
|
574
754
|
}
|
|
575
755
|
}
|
|
576
756
|
}`;
|
|
@@ -581,15 +761,28 @@ const IDENTIFIER_RE = /^([a-zA-Z][a-zA-Z0-9]*)-(\d+)$/;
|
|
|
581
761
|
function detailFilter(input: string): { filter: Record<string, unknown>; label: string } {
|
|
582
762
|
if (isUuid(input)) return { filter: { id: { eq: input } }, label: input };
|
|
583
763
|
const match = input.match(IDENTIFIER_RE);
|
|
584
|
-
if (!match)
|
|
764
|
+
if (!match)
|
|
765
|
+
throw usageError(`'${input}' is not a valid issue id (expected e.g. TES-123 or a UUID).`);
|
|
585
766
|
const key = match[1]!.toUpperCase();
|
|
586
767
|
const number = Number.parseInt(match[2]!, 10);
|
|
587
|
-
return {
|
|
768
|
+
return {
|
|
769
|
+
filter: { team: { key: { eq: key } }, number: { eq: number } },
|
|
770
|
+
label: `${key}-${number}`,
|
|
771
|
+
};
|
|
588
772
|
}
|
|
589
773
|
|
|
590
|
-
export async function getIssueDetail(
|
|
774
|
+
export async function getIssueDetail(
|
|
775
|
+
client: LinearClient,
|
|
776
|
+
idArg: string,
|
|
777
|
+
opts: { includeComments?: boolean } = {},
|
|
778
|
+
): Promise<IssueDetail> {
|
|
591
779
|
const { filter, label } = detailFilter(idArg);
|
|
592
|
-
const data: any = await withRetry(() =>
|
|
780
|
+
const data: any = await withRetry(() =>
|
|
781
|
+
(client as any).client.rawRequest(DETAIL_QUERY, {
|
|
782
|
+
filter,
|
|
783
|
+
includeComments: opts.includeComments !== false,
|
|
784
|
+
}),
|
|
785
|
+
);
|
|
593
786
|
const n = data.data?.issues?.nodes?.[0];
|
|
594
787
|
if (!n) throw notFound(`No issue ${label}.`);
|
|
595
788
|
return {
|
|
@@ -613,8 +806,38 @@ export async function getIssueDetail(client: LinearClient, idArg: string): Promi
|
|
|
613
806
|
milestone: n.projectMilestone ?? null,
|
|
614
807
|
cycle: cycleRef(n.cycle),
|
|
615
808
|
parent: n.parent ?? null,
|
|
809
|
+
children: n.children?.nodes ?? [],
|
|
616
810
|
labels: n.labels?.nodes ?? [],
|
|
617
811
|
subscribers: n.subscribers?.nodes ?? [],
|
|
812
|
+
attachments: (n.attachments?.nodes ?? []).map((a: any) => ({
|
|
813
|
+
id: a.id,
|
|
814
|
+
title: a.title,
|
|
815
|
+
url: a.url,
|
|
816
|
+
subtitle: a.subtitle ?? null,
|
|
817
|
+
sourceType: a.sourceType ?? null,
|
|
818
|
+
createdAt: a.createdAt,
|
|
819
|
+
})),
|
|
820
|
+
documents: n.documents?.nodes ?? [],
|
|
821
|
+
relations: n.relations?.nodes ?? [],
|
|
822
|
+
inverseRelations: n.inverseRelations?.nodes ?? [],
|
|
823
|
+
comments: (n.comments?.nodes ?? []).map(toIssueCommentDetail),
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/** Normalize nullable comment relations while preserving thread metadata. */
|
|
828
|
+
function toIssueCommentDetail(c: any): IssueCommentDetail {
|
|
829
|
+
return {
|
|
830
|
+
id: c.id,
|
|
831
|
+
body: c.body,
|
|
832
|
+
url: c.url,
|
|
833
|
+
createdAt: c.createdAt,
|
|
834
|
+
editedAt: c.editedAt ?? null,
|
|
835
|
+
resolvedAt: c.resolvedAt ?? null,
|
|
836
|
+
parent: c.parent ?? null,
|
|
837
|
+
user: c.user ?? null,
|
|
838
|
+
externalUser: c.externalUser ?? null,
|
|
839
|
+
resolvingCommentId: c.resolvingCommentId ?? null,
|
|
840
|
+
resolvingUser: c.resolvingUser ?? null,
|
|
618
841
|
};
|
|
619
842
|
}
|
|
620
843
|
|
|
@@ -673,7 +896,8 @@ export async function createIssue(
|
|
|
673
896
|
}
|
|
674
897
|
if (opts.milestone) {
|
|
675
898
|
// Resolved after the parent so a milestone can name one in the inherited project.
|
|
676
|
-
if (!input.projectId)
|
|
899
|
+
if (!input.projectId)
|
|
900
|
+
throw usageError("A milestone requires --project (or a --parent in a project).");
|
|
677
901
|
input.projectMilestoneId = await resolveMilestoneId(client, input.projectId, opts.milestone);
|
|
678
902
|
}
|
|
679
903
|
if (opts.cycle) input.cycleId = await resolveCycleId(client, team.id, opts.cycle);
|
|
@@ -703,6 +927,8 @@ export interface UpdateOptions {
|
|
|
703
927
|
estimate?: number;
|
|
704
928
|
parent?: string;
|
|
705
929
|
dueDate?: string;
|
|
930
|
+
/** Replace the complete label set (including `[]` to clear it). */
|
|
931
|
+
label?: string[];
|
|
706
932
|
addLabel?: string[];
|
|
707
933
|
removeLabel?: string[];
|
|
708
934
|
/** Clear the assignee. Mutually exclusive with `assignee`. */
|
|
@@ -717,6 +943,8 @@ export async function updateIssue(client: LinearClient, idArg: string, opts: Upd
|
|
|
717
943
|
throw usageError("Pass either --assignee or --unassign, not both.");
|
|
718
944
|
if (opts.clearCycle && opts.cycle)
|
|
719
945
|
throw usageError("Pass either --cycle or --clear-cycle, not both.");
|
|
946
|
+
if (opts.label !== undefined && (opts.addLabel !== undefined || opts.removeLabel !== undefined))
|
|
947
|
+
throw usageError("Pass either --label (replace all) or --add-label/--remove-label, not both.");
|
|
720
948
|
|
|
721
949
|
const issue = await resolveIssue(client, idArg);
|
|
722
950
|
const currentTeamId = (await issue.team)?.id;
|
|
@@ -745,7 +973,9 @@ export async function updateIssue(client: LinearClient, idArg: string, opts: Upd
|
|
|
745
973
|
if (opts.project) input.projectId = await resolveProjectId(client, opts.project);
|
|
746
974
|
if (opts.cycle && teamId) input.cycleId = await resolveCycleId(client, teamId, opts.cycle);
|
|
747
975
|
if (opts.parent) input.parentId = (await resolveIssue(client, opts.parent)).id;
|
|
748
|
-
if (opts.
|
|
976
|
+
if (opts.label !== undefined) input.labelIds = await resolveLabelIds(client, opts.label, teamId);
|
|
977
|
+
if (opts.addLabel?.length)
|
|
978
|
+
input.addedLabelIds = await resolveLabelIds(client, opts.addLabel, teamId);
|
|
749
979
|
if (opts.removeLabel?.length)
|
|
750
980
|
input.removedLabelIds = await resolveLabelIds(client, opts.removeLabel, teamId);
|
|
751
981
|
if (opts.milestone) {
|
|
@@ -755,7 +985,8 @@ export async function updateIssue(client: LinearClient, idArg: string, opts: Upd
|
|
|
755
985
|
input.projectMilestoneId = await resolveMilestoneId(client, projectId, opts.milestone);
|
|
756
986
|
}
|
|
757
987
|
|
|
758
|
-
if (Object.keys(input).length === 0)
|
|
988
|
+
if (Object.keys(input).length === 0)
|
|
989
|
+
throw usageError("Nothing to update; pass at least one field.");
|
|
759
990
|
// The updated issue comes from the payload, never from `issue` — falling back
|
|
760
991
|
// to the pre-mutation entity would print "Updated TES-1" for a write the API
|
|
761
992
|
// refused.
|
|
@@ -777,14 +1008,19 @@ export async function archiveIssue(client: LinearClient, idArg: string, unarchiv
|
|
|
777
1008
|
|
|
778
1009
|
export async function deleteIssue(client: LinearClient, idArg: string) {
|
|
779
1010
|
const issue = await resolveIssue(client, idArg);
|
|
780
|
-
await assertMutation(
|
|
1011
|
+
await assertMutation(
|
|
1012
|
+
withRetry(() => client.deleteIssue(issue.id)),
|
|
1013
|
+
"Issue deletion",
|
|
1014
|
+
);
|
|
781
1015
|
return issue;
|
|
782
1016
|
}
|
|
783
1017
|
|
|
784
1018
|
export async function setSubscription(client: LinearClient, idArg: string, subscribe: boolean) {
|
|
785
1019
|
const issue = await resolveIssue(client, idArg);
|
|
786
1020
|
await assertMutation(
|
|
787
|
-
withRetry(() =>
|
|
1021
|
+
withRetry(() =>
|
|
1022
|
+
subscribe ? client.issueSubscribe(issue.id) : client.issueUnsubscribe(issue.id),
|
|
1023
|
+
),
|
|
788
1024
|
subscribe ? "Issue subscribe" : "Issue unsubscribe",
|
|
789
1025
|
);
|
|
790
1026
|
return issue;
|
|
@@ -811,7 +1047,10 @@ export async function moveIssueState(client: LinearClient, issue: Issue, opts: S
|
|
|
811
1047
|
const stateId = opts.stateInput
|
|
812
1048
|
? await resolveStateId(client, teamId, opts.stateInput)
|
|
813
1049
|
: await firstStateOfType(client, teamId, "started");
|
|
814
|
-
await assertMutation(
|
|
1050
|
+
await assertMutation(
|
|
1051
|
+
withRetry(() => client.updateIssue(issue.id, { stateId })),
|
|
1052
|
+
"Issue update",
|
|
1053
|
+
);
|
|
815
1054
|
}
|
|
816
1055
|
|
|
817
1056
|
export async function startIssue(client: LinearClient, idArg: string, opts: StartMove) {
|
|
@@ -835,7 +1074,11 @@ export async function addRemoveRelation(
|
|
|
835
1074
|
if (op === "add") {
|
|
836
1075
|
await assertMutation(
|
|
837
1076
|
withRetry(() =>
|
|
838
|
-
client.createIssueRelation({
|
|
1077
|
+
client.createIssueRelation({
|
|
1078
|
+
issueId: from.id,
|
|
1079
|
+
relatedIssueId: to.id,
|
|
1080
|
+
type: apiType as any,
|
|
1081
|
+
}),
|
|
839
1082
|
),
|
|
840
1083
|
"Relation creation",
|
|
841
1084
|
);
|
|
@@ -848,7 +1091,8 @@ export async function addRemoveRelation(
|
|
|
848
1091
|
collect((await withRetry(() => from.inverseRelations())) as any, Infinity),
|
|
849
1092
|
]);
|
|
850
1093
|
const match = await findRelation([...direct, ...inverse] as any[], from.id, to.id, apiType);
|
|
851
|
-
if (!match)
|
|
1094
|
+
if (!match)
|
|
1095
|
+
throw notFound(`No ${type} relation between ${issue.identifier} and ${other.identifier}.`);
|
|
852
1096
|
await assertMutation(
|
|
853
1097
|
withRetry(() => client.deleteIssueRelation(match.id)),
|
|
854
1098
|
"Relation removal",
|
package/src/services/label.ts
CHANGED
|
@@ -78,7 +78,14 @@ export async function listLabels(
|
|
|
78
78
|
filter = { or: [{ team: { key: { eq: team.key } } }, { team: { null: true } }] };
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
return collectRawQuery<LabelRow>(
|
|
81
|
+
return collectRawQuery<LabelRow>(
|
|
82
|
+
client as any,
|
|
83
|
+
LIST_QUERY,
|
|
84
|
+
{ filter },
|
|
85
|
+
"issueLabels",
|
|
86
|
+
limit,
|
|
87
|
+
toLabelRow,
|
|
88
|
+
);
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
/** Map a tailored-query label node to a display row. */
|
|
@@ -161,7 +168,10 @@ export async function deleteLabel(client: LinearClient, idArg: string) {
|
|
|
161
168
|
const id = await resolveLabel(client, idArg);
|
|
162
169
|
// Fetch the label first so we can report its name after deletion.
|
|
163
170
|
const label = await withRetry(() => client.issueLabel(id));
|
|
164
|
-
await assertMutation(
|
|
171
|
+
await assertMutation(
|
|
172
|
+
withRetry(() => client.deleteIssueLabel(id)),
|
|
173
|
+
"Label deletion",
|
|
174
|
+
);
|
|
165
175
|
return label;
|
|
166
176
|
}
|
|
167
177
|
|
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
import type { LinearClient } from "@linear/sdk";
|
|
12
12
|
import { withRetry } from "../client.js";
|
|
13
13
|
import { shape } from "../lib/shape.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
collect,
|
|
16
|
+
inheritPaginationMetadata,
|
|
17
|
+
pageSize,
|
|
18
|
+
pageSizeForMore,
|
|
19
|
+
} from "../lib/pagination.js";
|
|
15
20
|
import { usageError, notFound } from "../lib/errors.js";
|
|
16
21
|
import { assertMutation, unwrapMutation } from "../lib/mutation.js";
|
|
17
22
|
import { resolveProjectId, resolveMilestoneId, isUuid } from "../lib/resolve.js";
|
|
@@ -54,11 +59,9 @@ export async function listMilestones(
|
|
|
54
59
|
): Promise<MilestoneRow[]> {
|
|
55
60
|
const projectId = await resolveProjectId(client, projectInput);
|
|
56
61
|
const project = await withRetry(() => client.project(projectId));
|
|
57
|
-
const conn = await withRetry(() =>
|
|
58
|
-
project.projectMilestones({ first: limit === Infinity ? 100 : Math.min(limit, 100) }),
|
|
59
|
-
);
|
|
62
|
+
const conn = await withRetry(() => project.projectMilestones({ first: pageSize(limit) }));
|
|
60
63
|
const nodes = await collect(conn as any, limit);
|
|
61
|
-
return nodes.map(toRow);
|
|
64
|
+
return inheritPaginationMetadata(nodes.map(toRow), nodes);
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
export interface MilestoneDetail {
|
|
@@ -14,7 +14,7 @@ import { withRetry } from "../client.js";
|
|
|
14
14
|
import { shape } from "../lib/shape.js";
|
|
15
15
|
import { collectRawQuery } from "../lib/pagination.js";
|
|
16
16
|
import { assertMutation } from "../lib/mutation.js";
|
|
17
|
-
import { normalizeError } from "../lib/errors.js";
|
|
17
|
+
import { normalizeError, usageError } from "../lib/errors.js";
|
|
18
18
|
|
|
19
19
|
export interface NotificationRow {
|
|
20
20
|
id: string;
|
|
@@ -122,7 +122,11 @@ export interface MarkAllItem {
|
|
|
122
122
|
error?: string;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
export const MARK_ALL_ITEM_SHAPE = shape<MarkAllItem>({
|
|
125
|
+
export const MARK_ALL_ITEM_SHAPE = shape<MarkAllItem>({
|
|
126
|
+
id: "string",
|
|
127
|
+
read: "boolean",
|
|
128
|
+
"error?": "string",
|
|
129
|
+
});
|
|
126
130
|
|
|
127
131
|
/**
|
|
128
132
|
* Mark all of the viewer's unread notifications as read. The SDK's
|
|
@@ -168,6 +172,16 @@ export async function archiveNotification(client: LinearClient, id: string): Pro
|
|
|
168
172
|
|
|
169
173
|
/** Snooze a notification until the given ISO timestamp. */
|
|
170
174
|
export async function snoozeNotification(client: LinearClient, id: string, untilISO: string) {
|
|
175
|
+
if (
|
|
176
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
177
|
+
untilISO,
|
|
178
|
+
) ||
|
|
179
|
+
!Number.isFinite(Date.parse(untilISO))
|
|
180
|
+
) {
|
|
181
|
+
throw usageError(
|
|
182
|
+
`Invalid snooze timestamp '${untilISO}'. Use ISO 8601 with a timezone, e.g. 2026-07-01T09:00:00Z.`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
171
185
|
await assertMutation(
|
|
172
186
|
withRetry(() => client.updateNotification(id, { snoozedUntilAt: untilISO } as any)),
|
|
173
187
|
"Notification snooze",
|
|
@@ -11,8 +11,7 @@
|
|
|
11
11
|
import type { LinearClient } from "@linear/sdk";
|
|
12
12
|
import { withRetry } from "../client.js";
|
|
13
13
|
import { shape } from "../lib/shape.js";
|
|
14
|
-
import { collect } from "../lib/pagination.js";
|
|
15
|
-
import { pageSize } from "../lib/pagination.js";
|
|
14
|
+
import { collect, inheritPaginationMetadata, pageSize } from "../lib/pagination.js";
|
|
16
15
|
|
|
17
16
|
export interface OrganizationDetail {
|
|
18
17
|
id: string;
|
|
@@ -83,14 +82,17 @@ export const ORGANIZATION_MEMBER_ROW_SHAPE = shape<MemberRow>({
|
|
|
83
82
|
export async function listMembers(client: LinearClient, limit: number): Promise<MemberRow[]> {
|
|
84
83
|
const conn = await withRetry(() => client.users({ first: pageSize(limit) }));
|
|
85
84
|
const nodes = await collect(conn as any, limit);
|
|
86
|
-
return
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
85
|
+
return inheritPaginationMetadata(
|
|
86
|
+
nodes.map((u: any) => ({
|
|
87
|
+
id: u.id,
|
|
88
|
+
displayName: u.displayName,
|
|
89
|
+
name: u.name,
|
|
90
|
+
email: u.email,
|
|
91
|
+
admin: !!u.admin,
|
|
92
|
+
active: !!u.active,
|
|
93
|
+
})),
|
|
94
|
+
nodes,
|
|
95
|
+
);
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
export interface InviteRow {
|
|
@@ -128,12 +130,15 @@ export function inviteStatus(invite: { acceptedAt?: unknown; expiresAt?: unknown
|
|
|
128
130
|
export async function listInvites(client: LinearClient, limit: number): Promise<InviteRow[]> {
|
|
129
131
|
const conn = await withRetry(() => client.organizationInvites({ first: pageSize(limit) }));
|
|
130
132
|
const nodes = await collect(conn as any, limit);
|
|
131
|
-
return
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
133
|
+
return inheritPaginationMetadata(
|
|
134
|
+
nodes.map((i: any) => ({
|
|
135
|
+
id: i.id,
|
|
136
|
+
email: i.email,
|
|
137
|
+
status: inviteStatus(i),
|
|
138
|
+
role: i.role ?? "member",
|
|
139
|
+
external: !!i.external,
|
|
140
|
+
createdAt: i.createdAt?.toISOString?.() ?? String(i.createdAt),
|
|
141
|
+
})),
|
|
142
|
+
nodes,
|
|
143
|
+
);
|
|
139
144
|
}
|