linear-sdk-cli 0.1.0 → 0.1.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.
- package/CHANGELOG.md +46 -46
- package/COVERAGE.md +5 -5
- package/MIGRATING.md +69 -71
- package/README.md +88 -96
- package/package.json +8 -9
- package/skills/linear-sdk-cli/SKILL.md +3 -13
- package/skills/linear-sdk-cli/references/api.md +9 -9
- package/skills/linear-sdk-cli/references/auth.md +0 -18
- package/skills/linear-sdk-cli/references/commands.md +2 -2
- package/skills/linear-sdk-cli/references/config.md +11 -9
- package/skills/linear-sdk-cli/references/issue.md +12 -63
- package/skills/linear-sdk-cli/references/team.md +0 -16
- package/skills/linear-sdk-cli/scripts/generate-docs.ts +2 -6
- package/src/bin/linear.ts +6 -16
- package/src/cli.ts +1 -3
- package/src/commands/agent-session.ts +5 -13
- package/src/commands/api.ts +9 -15
- package/src/commands/attach.ts +4 -15
- package/src/commands/comment.ts +2 -6
- package/src/commands/completion.ts +6 -2
- package/src/commands/cycle.ts +1 -6
- package/src/commands/discover.ts +2 -6
- package/src/commands/document.ts +1 -4
- package/src/commands/initiative.ts +1 -5
- package/src/commands/issue.ts +88 -376
- package/src/commands/meta.ts +31 -139
- package/src/commands/milestone.ts +1 -4
- package/src/commands/notification.ts +6 -15
- package/src/commands/project.ts +4 -17
- package/src/commands/team.ts +0 -13
- package/src/commands/webhook.ts +2 -12
- package/src/config.ts +183 -500
- package/src/context.ts +4 -13
- package/src/git.ts +3 -28
- package/src/lib/body.ts +2 -26
- package/src/lib/errors.ts +7 -32
- package/src/lib/introspect.ts +7 -76
- package/src/lib/keyring.ts +9 -51
- package/src/lib/options.ts +12 -108
- package/src/lib/output-shapes.ts +9 -81
- package/src/lib/pagination.ts +17 -50
- package/src/lib/resolve.ts +7 -40
- package/src/lib/shape.ts +12 -16
- package/src/lib/upload.ts +1 -3
- package/src/output/format.ts +18 -96
- package/src/output/table.ts +7 -7
- package/src/services/agent-session.ts +3 -11
- package/src/services/attachment.ts +5 -14
- package/src/services/comment.ts +4 -12
- package/src/services/cycle.ts +4 -11
- package/src/services/document.ts +10 -29
- package/src/services/favorite.ts +9 -15
- package/src/services/initiative-update.ts +15 -16
- package/src/services/initiative.ts +10 -15
- package/src/services/issue.ts +18 -262
- package/src/services/label.ts +2 -12
- package/src/services/milestone.ts +5 -8
- package/src/services/notification.ts +2 -16
- package/src/services/organization.ts +18 -23
- package/src/services/project.ts +34 -55
- package/src/services/roadmap.ts +6 -7
- package/src/services/state.ts +3 -9
- package/src/services/team.ts +24 -67
- package/src/services/user.ts +2 -2
- package/src/services/webhook.ts +17 -28
- package/skills/linear-sdk-cli/references/open.md +0 -29
- package/src/commands/open.ts +0 -84
- package/src/lib/open.ts +0 -32
- package/src/output/color.ts +0 -52
- package/src/output/markdown.ts +0 -133
- package/src/output/pager.ts +0 -131
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", "
|
|
146
|
+
export const ISSUE_SORTS = ["priority", "updated", "created"] as const;
|
|
147
147
|
export type IssueSort = (typeof ISSUE_SORTS)[number];
|
|
148
148
|
|
|
149
149
|
type SortConfig = Pick<
|
|
@@ -237,9 +237,7 @@ 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(
|
|
241
|
-
asList(f.allTeams ? undefined : (f.team ?? defaultTeamKey)).map((k) => k.toUpperCase()),
|
|
242
|
-
),
|
|
240
|
+
...new Set(asList(f.allTeams ? undefined : (f.team ?? defaultTeamKey)).map((k) => k.toUpperCase())),
|
|
243
241
|
];
|
|
244
242
|
// One team keeps the exact `eq` shape it has always sent; several use `in`
|
|
245
243
|
// (the reference CLI spells the same thing as `or: [{ key: { eq } }, …]`).
|
|
@@ -371,11 +369,6 @@ export function sortSpec(sort: IssueSort = "priority"): Array<Record<string, unk
|
|
|
371
369
|
];
|
|
372
370
|
case "created":
|
|
373
371
|
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" } }];
|
|
379
372
|
case "updated":
|
|
380
373
|
return [{ updatedAt: { order: "Descending" } }];
|
|
381
374
|
}
|
|
@@ -493,57 +486,6 @@ function lifecycle(n: any) {
|
|
|
493
486
|
* flattened to strings here: `team: "TES Test-workspace-bla"` is not even
|
|
494
487
|
* parseable, since a team name can contain spaces.)
|
|
495
488
|
*/
|
|
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
|
-
|
|
547
489
|
export interface IssueDetail {
|
|
548
490
|
id: string;
|
|
549
491
|
identifier: string;
|
|
@@ -568,15 +510,9 @@ export interface IssueDetail {
|
|
|
568
510
|
project: { id: string; name: string } | null;
|
|
569
511
|
milestone: { id: string; name: string } | null;
|
|
570
512
|
cycle: { id: string; number: number; name: string | null } | null;
|
|
571
|
-
parent:
|
|
572
|
-
children: IssueDetailRef[];
|
|
513
|
+
parent: { id: string; identifier: string } | null;
|
|
573
514
|
labels: Array<{ id: string; name: string }>;
|
|
574
515
|
subscribers: Array<{ id: string; displayName: string }>;
|
|
575
|
-
attachments: IssueAttachmentDetail[];
|
|
576
|
-
documents: IssueDocumentDetail[];
|
|
577
|
-
relations: IssueRelationDetail[];
|
|
578
|
-
inverseRelations: IssueRelationDetail[];
|
|
579
|
-
comments: IssueCommentDetail[];
|
|
580
516
|
}
|
|
581
517
|
|
|
582
518
|
/** The detail's shape as `linear commands` advertises it; checked against `IssueDetail`. */
|
|
@@ -604,95 +540,9 @@ export const ISSUE_DETAIL_SHAPE = shape<IssueDetail>({
|
|
|
604
540
|
project: { nullable: { id: "string", name: "string" } },
|
|
605
541
|
milestone: { nullable: { id: "string", name: "string" } },
|
|
606
542
|
cycle: { nullable: { id: "string", number: "number", name: "string|null" } },
|
|
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
|
-
],
|
|
543
|
+
parent: { nullable: { id: "string", identifier: "string" } },
|
|
623
544
|
labels: [{ id: "string", name: "string" }],
|
|
624
545
|
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
|
-
],
|
|
696
546
|
});
|
|
697
547
|
|
|
698
548
|
/**
|
|
@@ -707,7 +557,7 @@ export const ISSUE_DETAIL_SHAPE = shape<IssueDetail>({
|
|
|
707
557
|
* depends on the API accepting human identifiers in `issue(id:)`.
|
|
708
558
|
*/
|
|
709
559
|
const DETAIL_QUERY = `
|
|
710
|
-
query CliIssueDetail($filter: IssueFilter
|
|
560
|
+
query CliIssueDetail($filter: IssueFilter!) {
|
|
711
561
|
issues(filter: $filter, first: 1, includeArchived: true) {
|
|
712
562
|
nodes {
|
|
713
563
|
id identifier title description priority priorityLabel estimate url branchName dueDate
|
|
@@ -718,39 +568,9 @@ query CliIssueDetail($filter: IssueFilter!, $includeComments: Boolean!) {
|
|
|
718
568
|
project { id name }
|
|
719
569
|
projectMilestone { id name }
|
|
720
570
|
cycle { id number name }
|
|
721
|
-
parent { id identifier
|
|
722
|
-
children(first: 250) { nodes { id identifier title state { id name type } } }
|
|
571
|
+
parent { id identifier }
|
|
723
572
|
labels(first: 50) { nodes { id name } }
|
|
724
573
|
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
|
-
}
|
|
754
574
|
}
|
|
755
575
|
}
|
|
756
576
|
}`;
|
|
@@ -761,28 +581,15 @@ const IDENTIFIER_RE = /^([a-zA-Z][a-zA-Z0-9]*)-(\d+)$/;
|
|
|
761
581
|
function detailFilter(input: string): { filter: Record<string, unknown>; label: string } {
|
|
762
582
|
if (isUuid(input)) return { filter: { id: { eq: input } }, label: input };
|
|
763
583
|
const match = input.match(IDENTIFIER_RE);
|
|
764
|
-
if (!match)
|
|
765
|
-
throw usageError(`'${input}' is not a valid issue id (expected e.g. TES-123 or a UUID).`);
|
|
584
|
+
if (!match) throw usageError(`'${input}' is not a valid issue id (expected e.g. TES-123 or a UUID).`);
|
|
766
585
|
const key = match[1]!.toUpperCase();
|
|
767
586
|
const number = Number.parseInt(match[2]!, 10);
|
|
768
|
-
return {
|
|
769
|
-
filter: { team: { key: { eq: key } }, number: { eq: number } },
|
|
770
|
-
label: `${key}-${number}`,
|
|
771
|
-
};
|
|
587
|
+
return { filter: { team: { key: { eq: key } }, number: { eq: number } }, label: `${key}-${number}` };
|
|
772
588
|
}
|
|
773
589
|
|
|
774
|
-
export async function getIssueDetail(
|
|
775
|
-
client: LinearClient,
|
|
776
|
-
idArg: string,
|
|
777
|
-
opts: { includeComments?: boolean } = {},
|
|
778
|
-
): Promise<IssueDetail> {
|
|
590
|
+
export async function getIssueDetail(client: LinearClient, idArg: string): Promise<IssueDetail> {
|
|
779
591
|
const { filter, label } = detailFilter(idArg);
|
|
780
|
-
const data: any = await withRetry(() =>
|
|
781
|
-
(client as any).client.rawRequest(DETAIL_QUERY, {
|
|
782
|
-
filter,
|
|
783
|
-
includeComments: opts.includeComments !== false,
|
|
784
|
-
}),
|
|
785
|
-
);
|
|
592
|
+
const data: any = await withRetry(() => (client as any).client.rawRequest(DETAIL_QUERY, { filter }));
|
|
786
593
|
const n = data.data?.issues?.nodes?.[0];
|
|
787
594
|
if (!n) throw notFound(`No issue ${label}.`);
|
|
788
595
|
return {
|
|
@@ -806,38 +613,8 @@ export async function getIssueDetail(
|
|
|
806
613
|
milestone: n.projectMilestone ?? null,
|
|
807
614
|
cycle: cycleRef(n.cycle),
|
|
808
615
|
parent: n.parent ?? null,
|
|
809
|
-
children: n.children?.nodes ?? [],
|
|
810
616
|
labels: n.labels?.nodes ?? [],
|
|
811
617
|
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,
|
|
841
618
|
};
|
|
842
619
|
}
|
|
843
620
|
|
|
@@ -896,8 +673,7 @@ export async function createIssue(
|
|
|
896
673
|
}
|
|
897
674
|
if (opts.milestone) {
|
|
898
675
|
// Resolved after the parent so a milestone can name one in the inherited project.
|
|
899
|
-
if (!input.projectId)
|
|
900
|
-
throw usageError("A milestone requires --project (or a --parent in a project).");
|
|
676
|
+
if (!input.projectId) throw usageError("A milestone requires --project (or a --parent in a project).");
|
|
901
677
|
input.projectMilestoneId = await resolveMilestoneId(client, input.projectId, opts.milestone);
|
|
902
678
|
}
|
|
903
679
|
if (opts.cycle) input.cycleId = await resolveCycleId(client, team.id, opts.cycle);
|
|
@@ -927,8 +703,6 @@ export interface UpdateOptions {
|
|
|
927
703
|
estimate?: number;
|
|
928
704
|
parent?: string;
|
|
929
705
|
dueDate?: string;
|
|
930
|
-
/** Replace the complete label set (including `[]` to clear it). */
|
|
931
|
-
label?: string[];
|
|
932
706
|
addLabel?: string[];
|
|
933
707
|
removeLabel?: string[];
|
|
934
708
|
/** Clear the assignee. Mutually exclusive with `assignee`. */
|
|
@@ -943,8 +717,6 @@ export async function updateIssue(client: LinearClient, idArg: string, opts: Upd
|
|
|
943
717
|
throw usageError("Pass either --assignee or --unassign, not both.");
|
|
944
718
|
if (opts.clearCycle && opts.cycle)
|
|
945
719
|
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.");
|
|
948
720
|
|
|
949
721
|
const issue = await resolveIssue(client, idArg);
|
|
950
722
|
const currentTeamId = (await issue.team)?.id;
|
|
@@ -973,9 +745,7 @@ export async function updateIssue(client: LinearClient, idArg: string, opts: Upd
|
|
|
973
745
|
if (opts.project) input.projectId = await resolveProjectId(client, opts.project);
|
|
974
746
|
if (opts.cycle && teamId) input.cycleId = await resolveCycleId(client, teamId, opts.cycle);
|
|
975
747
|
if (opts.parent) input.parentId = (await resolveIssue(client, opts.parent)).id;
|
|
976
|
-
if (opts.
|
|
977
|
-
if (opts.addLabel?.length)
|
|
978
|
-
input.addedLabelIds = await resolveLabelIds(client, opts.addLabel, teamId);
|
|
748
|
+
if (opts.addLabel?.length) input.addedLabelIds = await resolveLabelIds(client, opts.addLabel, teamId);
|
|
979
749
|
if (opts.removeLabel?.length)
|
|
980
750
|
input.removedLabelIds = await resolveLabelIds(client, opts.removeLabel, teamId);
|
|
981
751
|
if (opts.milestone) {
|
|
@@ -985,8 +755,7 @@ export async function updateIssue(client: LinearClient, idArg: string, opts: Upd
|
|
|
985
755
|
input.projectMilestoneId = await resolveMilestoneId(client, projectId, opts.milestone);
|
|
986
756
|
}
|
|
987
757
|
|
|
988
|
-
if (Object.keys(input).length === 0)
|
|
989
|
-
throw usageError("Nothing to update; pass at least one field.");
|
|
758
|
+
if (Object.keys(input).length === 0) throw usageError("Nothing to update; pass at least one field.");
|
|
990
759
|
// The updated issue comes from the payload, never from `issue` — falling back
|
|
991
760
|
// to the pre-mutation entity would print "Updated TES-1" for a write the API
|
|
992
761
|
// refused.
|
|
@@ -1008,19 +777,14 @@ export async function archiveIssue(client: LinearClient, idArg: string, unarchiv
|
|
|
1008
777
|
|
|
1009
778
|
export async function deleteIssue(client: LinearClient, idArg: string) {
|
|
1010
779
|
const issue = await resolveIssue(client, idArg);
|
|
1011
|
-
await assertMutation(
|
|
1012
|
-
withRetry(() => client.deleteIssue(issue.id)),
|
|
1013
|
-
"Issue deletion",
|
|
1014
|
-
);
|
|
780
|
+
await assertMutation(withRetry(() => client.deleteIssue(issue.id)), "Issue deletion");
|
|
1015
781
|
return issue;
|
|
1016
782
|
}
|
|
1017
783
|
|
|
1018
784
|
export async function setSubscription(client: LinearClient, idArg: string, subscribe: boolean) {
|
|
1019
785
|
const issue = await resolveIssue(client, idArg);
|
|
1020
786
|
await assertMutation(
|
|
1021
|
-
withRetry(() =>
|
|
1022
|
-
subscribe ? client.issueSubscribe(issue.id) : client.issueUnsubscribe(issue.id),
|
|
1023
|
-
),
|
|
787
|
+
withRetry(() => (subscribe ? client.issueSubscribe(issue.id) : client.issueUnsubscribe(issue.id))),
|
|
1024
788
|
subscribe ? "Issue subscribe" : "Issue unsubscribe",
|
|
1025
789
|
);
|
|
1026
790
|
return issue;
|
|
@@ -1047,10 +811,7 @@ export async function moveIssueState(client: LinearClient, issue: Issue, opts: S
|
|
|
1047
811
|
const stateId = opts.stateInput
|
|
1048
812
|
? await resolveStateId(client, teamId, opts.stateInput)
|
|
1049
813
|
: await firstStateOfType(client, teamId, "started");
|
|
1050
|
-
await assertMutation(
|
|
1051
|
-
withRetry(() => client.updateIssue(issue.id, { stateId })),
|
|
1052
|
-
"Issue update",
|
|
1053
|
-
);
|
|
814
|
+
await assertMutation(withRetry(() => client.updateIssue(issue.id, { stateId })), "Issue update");
|
|
1054
815
|
}
|
|
1055
816
|
|
|
1056
817
|
export async function startIssue(client: LinearClient, idArg: string, opts: StartMove) {
|
|
@@ -1074,11 +835,7 @@ export async function addRemoveRelation(
|
|
|
1074
835
|
if (op === "add") {
|
|
1075
836
|
await assertMutation(
|
|
1076
837
|
withRetry(() =>
|
|
1077
|
-
client.createIssueRelation({
|
|
1078
|
-
issueId: from.id,
|
|
1079
|
-
relatedIssueId: to.id,
|
|
1080
|
-
type: apiType as any,
|
|
1081
|
-
}),
|
|
838
|
+
client.createIssueRelation({ issueId: from.id, relatedIssueId: to.id, type: apiType as any }),
|
|
1082
839
|
),
|
|
1083
840
|
"Relation creation",
|
|
1084
841
|
);
|
|
@@ -1091,8 +848,7 @@ export async function addRemoveRelation(
|
|
|
1091
848
|
collect((await withRetry(() => from.inverseRelations())) as any, Infinity),
|
|
1092
849
|
]);
|
|
1093
850
|
const match = await findRelation([...direct, ...inverse] as any[], from.id, to.id, apiType);
|
|
1094
|
-
if (!match)
|
|
1095
|
-
throw notFound(`No ${type} relation between ${issue.identifier} and ${other.identifier}.`);
|
|
851
|
+
if (!match) throw notFound(`No ${type} relation between ${issue.identifier} and ${other.identifier}.`);
|
|
1096
852
|
await assertMutation(
|
|
1097
853
|
withRetry(() => client.deleteIssueRelation(match.id)),
|
|
1098
854
|
"Relation removal",
|
package/src/services/label.ts
CHANGED
|
@@ -78,14 +78,7 @@ export async function listLabels(
|
|
|
78
78
|
filter = { or: [{ team: { key: { eq: team.key } } }, { team: { null: true } }] };
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
return collectRawQuery<LabelRow>(
|
|
82
|
-
client as any,
|
|
83
|
-
LIST_QUERY,
|
|
84
|
-
{ filter },
|
|
85
|
-
"issueLabels",
|
|
86
|
-
limit,
|
|
87
|
-
toLabelRow,
|
|
88
|
-
);
|
|
81
|
+
return collectRawQuery<LabelRow>(client as any, LIST_QUERY, { filter }, "issueLabels", limit, toLabelRow);
|
|
89
82
|
}
|
|
90
83
|
|
|
91
84
|
/** Map a tailored-query label node to a display row. */
|
|
@@ -168,10 +161,7 @@ export async function deleteLabel(client: LinearClient, idArg: string) {
|
|
|
168
161
|
const id = await resolveLabel(client, idArg);
|
|
169
162
|
// Fetch the label first so we can report its name after deletion.
|
|
170
163
|
const label = await withRetry(() => client.issueLabel(id));
|
|
171
|
-
await assertMutation(
|
|
172
|
-
withRetry(() => client.deleteIssueLabel(id)),
|
|
173
|
-
"Label deletion",
|
|
174
|
-
);
|
|
164
|
+
await assertMutation(withRetry(() => client.deleteIssueLabel(id)), "Label deletion");
|
|
175
165
|
return label;
|
|
176
166
|
}
|
|
177
167
|
|
|
@@ -11,12 +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 {
|
|
15
|
-
collect,
|
|
16
|
-
inheritPaginationMetadata,
|
|
17
|
-
pageSize,
|
|
18
|
-
pageSizeForMore,
|
|
19
|
-
} from "../lib/pagination.js";
|
|
14
|
+
import { collect, pageSizeForMore } from "../lib/pagination.js";
|
|
20
15
|
import { usageError, notFound } from "../lib/errors.js";
|
|
21
16
|
import { assertMutation, unwrapMutation } from "../lib/mutation.js";
|
|
22
17
|
import { resolveProjectId, resolveMilestoneId, isUuid } from "../lib/resolve.js";
|
|
@@ -59,9 +54,11 @@ export async function listMilestones(
|
|
|
59
54
|
): Promise<MilestoneRow[]> {
|
|
60
55
|
const projectId = await resolveProjectId(client, projectInput);
|
|
61
56
|
const project = await withRetry(() => client.project(projectId));
|
|
62
|
-
const conn = await withRetry(() =>
|
|
57
|
+
const conn = await withRetry(() =>
|
|
58
|
+
project.projectMilestones({ first: limit === Infinity ? 100 : Math.min(limit, 100) }),
|
|
59
|
+
);
|
|
63
60
|
const nodes = await collect(conn as any, limit);
|
|
64
|
-
return
|
|
61
|
+
return nodes.map(toRow);
|
|
65
62
|
}
|
|
66
63
|
|
|
67
64
|
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
|
|
17
|
+
import { normalizeError } from "../lib/errors.js";
|
|
18
18
|
|
|
19
19
|
export interface NotificationRow {
|
|
20
20
|
id: string;
|
|
@@ -122,11 +122,7 @@ export interface MarkAllItem {
|
|
|
122
122
|
error?: string;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
export const MARK_ALL_ITEM_SHAPE = shape<MarkAllItem>({
|
|
126
|
-
id: "string",
|
|
127
|
-
read: "boolean",
|
|
128
|
-
"error?": "string",
|
|
129
|
-
});
|
|
125
|
+
export const MARK_ALL_ITEM_SHAPE = shape<MarkAllItem>({ id: "string", read: "boolean", "error?": "string" });
|
|
130
126
|
|
|
131
127
|
/**
|
|
132
128
|
* Mark all of the viewer's unread notifications as read. The SDK's
|
|
@@ -172,16 +168,6 @@ export async function archiveNotification(client: LinearClient, id: string): Pro
|
|
|
172
168
|
|
|
173
169
|
/** Snooze a notification until the given ISO timestamp. */
|
|
174
170
|
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
|
-
}
|
|
185
171
|
await assertMutation(
|
|
186
172
|
withRetry(() => client.updateNotification(id, { snoozedUntilAt: untilISO } as any)),
|
|
187
173
|
"Notification snooze",
|
|
@@ -11,7 +11,8 @@
|
|
|
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
|
|
14
|
+
import { collect } from "../lib/pagination.js";
|
|
15
|
+
import { pageSize } from "../lib/pagination.js";
|
|
15
16
|
|
|
16
17
|
export interface OrganizationDetail {
|
|
17
18
|
id: string;
|
|
@@ -82,17 +83,14 @@ export const ORGANIZATION_MEMBER_ROW_SHAPE = shape<MemberRow>({
|
|
|
82
83
|
export async function listMembers(client: LinearClient, limit: number): Promise<MemberRow[]> {
|
|
83
84
|
const conn = await withRetry(() => client.users({ first: pageSize(limit) }));
|
|
84
85
|
const nodes = await collect(conn as any, limit);
|
|
85
|
-
return
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
})),
|
|
94
|
-
nodes,
|
|
95
|
-
);
|
|
86
|
+
return 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
|
+
}));
|
|
96
94
|
}
|
|
97
95
|
|
|
98
96
|
export interface InviteRow {
|
|
@@ -130,15 +128,12 @@ export function inviteStatus(invite: { acceptedAt?: unknown; expiresAt?: unknown
|
|
|
130
128
|
export async function listInvites(client: LinearClient, limit: number): Promise<InviteRow[]> {
|
|
131
129
|
const conn = await withRetry(() => client.organizationInvites({ first: pageSize(limit) }));
|
|
132
130
|
const nodes = await collect(conn as any, limit);
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
})),
|
|
142
|
-
nodes,
|
|
143
|
-
);
|
|
131
|
+
return nodes.map((i: any) => ({
|
|
132
|
+
id: i.id,
|
|
133
|
+
email: i.email,
|
|
134
|
+
status: inviteStatus(i),
|
|
135
|
+
role: i.role ?? "member",
|
|
136
|
+
external: !!i.external,
|
|
137
|
+
createdAt: i.createdAt?.toISOString?.() ?? String(i.createdAt),
|
|
138
|
+
}));
|
|
144
139
|
}
|