linear-sdk-cli 0.3.3 → 0.5.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.
- package/CHANGELOG.md +15 -0
- package/COVERAGE.md +13 -9
- package/MIGRATING.md +6 -0
- package/README.md +280 -86
- package/package.json +5 -5
- package/skills/linear-sdk-cli/SKILL.md +10 -0
- package/skills/linear-sdk-cli/references/auth.md +1 -0
- package/skills/linear-sdk-cli/references/config.md +10 -7
- package/skills/linear-sdk-cli/references/custom-view.md +205 -0
- package/skills/linear-sdk-cli/references/initiative.md +10 -5
- package/skills/linear-sdk-cli/references/issue.md +222 -77
- package/skills/linear-sdk-cli/references/notification.md +3 -3
- package/skills/linear-sdk-cli/references/project.md +9 -4
- package/src/cli.ts +2 -0
- package/src/commands/custom-view.ts +279 -0
- package/src/commands/initiative.ts +23 -6
- package/src/commands/issue.ts +313 -12
- package/src/commands/meta.ts +55 -9
- package/src/commands/notification.ts +5 -3
- package/src/commands/project.ts +21 -3
- package/src/config.ts +111 -10
- package/src/lib/delegation.ts +240 -0
- package/src/lib/json-input.ts +46 -0
- package/src/lib/options.ts +23 -3
- package/src/lib/output-shapes.ts +102 -3
- package/src/output/lifecycle.ts +10 -0
- package/src/services/custom-view.ts +376 -0
- package/src/services/initiative.ts +12 -3
- package/src/services/issue.ts +498 -21
- package/src/services/project.ts +14 -3
package/src/lib/output-shapes.ts
CHANGED
|
@@ -25,6 +25,11 @@ import { AGENT_SESSION_DETAIL_SHAPE, AGENT_SESSION_ROW_SHAPE } from "../services
|
|
|
25
25
|
import { ATTACHMENT_ROW_SHAPE } from "../services/attachment.js";
|
|
26
26
|
import { COMMENT_ROW_SHAPE } from "../services/comment.js";
|
|
27
27
|
import { CYCLE_DETAIL_SHAPE, CYCLE_ROW_SHAPE } from "../services/cycle.js";
|
|
28
|
+
import {
|
|
29
|
+
CUSTOM_VIEW_DETAIL_SHAPE,
|
|
30
|
+
CUSTOM_VIEW_RESULT_ROW_SHAPE,
|
|
31
|
+
CUSTOM_VIEW_ROW_SHAPE,
|
|
32
|
+
} from "../services/custom-view.js";
|
|
28
33
|
import type { DocumentDetail, DocumentRow, DocumentTargets } from "../services/document.js";
|
|
29
34
|
import { FAVORITE_ROW_SHAPE } from "../services/favorite.js";
|
|
30
35
|
import {
|
|
@@ -149,6 +154,7 @@ const SETTING_ORIGIN_SHAPE = shape<SettingOrigin>({
|
|
|
149
154
|
source: "string",
|
|
150
155
|
"path?": "string",
|
|
151
156
|
"key?": "string",
|
|
157
|
+
"workspace?": "string",
|
|
152
158
|
});
|
|
153
159
|
const SETTING_ORIGINS_SHAPE = shape<SettingOrigins>({
|
|
154
160
|
team: SETTING_ORIGIN_SHAPE,
|
|
@@ -163,6 +169,7 @@ const CONFIG_SHOW_SHAPE: ObjectFields = {
|
|
|
163
169
|
accessToken: "string",
|
|
164
170
|
accessTokenSource: "string",
|
|
165
171
|
credentialWorkspace: "string|null",
|
|
172
|
+
workspaceProfile: "string|null",
|
|
166
173
|
team: "string|null",
|
|
167
174
|
workspace: "string|null",
|
|
168
175
|
sort: "string",
|
|
@@ -203,6 +210,48 @@ export const COMMAND_NODE_SHAPE = shape<CommandNode>({
|
|
|
203
210
|
// Receipts shared by several commands.
|
|
204
211
|
const ISSUE_REF: ObjectFields = { id: "string", identifier: "string" };
|
|
205
212
|
const ISSUE_URL_RECEIPT: ObjectFields = { ...ISSUE_REF, url: "string" };
|
|
213
|
+
const ISSUE_ASSIGNEE_SHAPE: FieldShape = {
|
|
214
|
+
nullable: { id: "string", displayName: "string", email: "string" },
|
|
215
|
+
};
|
|
216
|
+
const ISSUE_DELEGATE_SHAPE: FieldShape = {
|
|
217
|
+
nullable: { id: "string", displayName: "string", name: "string" },
|
|
218
|
+
};
|
|
219
|
+
const ISSUE_DELEGATION_RECEIPT: ObjectFields = {
|
|
220
|
+
...ISSUE_URL_RECEIPT,
|
|
221
|
+
assignee: ISSUE_ASSIGNEE_SHAPE,
|
|
222
|
+
delegate: ISSUE_DELEGATE_SHAPE,
|
|
223
|
+
};
|
|
224
|
+
const ISSUE_MUTATION_PREVIEW: ObjectFields = {
|
|
225
|
+
operation: "string",
|
|
226
|
+
dryRun: "boolean",
|
|
227
|
+
target: { nullable: ISSUE_REF },
|
|
228
|
+
input: "object",
|
|
229
|
+
};
|
|
230
|
+
const ISSUE_DELEGATION_FULL_RESULT: ObjectFields = {
|
|
231
|
+
receipt: ISSUE_DELEGATION_RECEIPT,
|
|
232
|
+
resource: ISSUE_DETAIL_SHAPE,
|
|
233
|
+
verified: "boolean",
|
|
234
|
+
};
|
|
235
|
+
const ISSUE_LABEL_REF: ObjectFields = { id: "string", name: "string" };
|
|
236
|
+
const ISSUE_LABEL_GROUPS: FieldShape = [{ group: ISSUE_LABEL_REF, label: ISSUE_LABEL_REF }];
|
|
237
|
+
const ISSUE_LABEL_GROUP_RECEIPT: ObjectFields = {
|
|
238
|
+
...ISSUE_REF,
|
|
239
|
+
changed: "boolean",
|
|
240
|
+
mutationSent: "boolean",
|
|
241
|
+
groups: ISSUE_LABEL_GROUPS,
|
|
242
|
+
};
|
|
243
|
+
const ISSUE_LABEL_GROUP_PREVIEW: ObjectFields = {
|
|
244
|
+
...ISSUE_MUTATION_PREVIEW,
|
|
245
|
+
changed: "boolean",
|
|
246
|
+
mutationSent: "boolean",
|
|
247
|
+
groups: ISSUE_LABEL_GROUPS,
|
|
248
|
+
};
|
|
249
|
+
const ISSUE_LABEL_GROUP_FULL_RESULT: ObjectFields = {
|
|
250
|
+
receipt: ISSUE_LABEL_GROUP_RECEIPT,
|
|
251
|
+
resource: ISSUE_DETAIL_SHAPE,
|
|
252
|
+
priorState: [{ group: ISSUE_LABEL_REF, labels: [ISSUE_LABEL_REF] }],
|
|
253
|
+
verified: "boolean",
|
|
254
|
+
};
|
|
206
255
|
const NAMED_URL_RECEIPT: ObjectFields = { id: "string", name: "string", url: "string" };
|
|
207
256
|
const NAMED_DELETED: ObjectFields = { id: "string", name: "string", deleted: "boolean" };
|
|
208
257
|
const NAMED_ARCHIVED: ObjectFields = { id: "string", name: "string", archived: "boolean" };
|
|
@@ -286,6 +335,7 @@ export const OUTPUT_SHAPES: Record<string, OutputShape | null> = {
|
|
|
286
335
|
removed: "boolean",
|
|
287
336
|
revocation: "string",
|
|
288
337
|
fallbackCredentialType: "string|null",
|
|
338
|
+
teamMetadataRemoved: "boolean",
|
|
289
339
|
}),
|
|
290
340
|
"auth migrate": receipt({ success: "boolean", migrated: ["string"], path: "string" }),
|
|
291
341
|
"auth status": object({
|
|
@@ -319,7 +369,13 @@ export const OUTPUT_SHAPES: Record<string, OutputShape | null> = {
|
|
|
319
369
|
|
|
320
370
|
config: runs("config show", object(CONFIG_SHOW_SHAPE)),
|
|
321
371
|
"config init": receipt({ success: "boolean", path: "string", team: "string", "sort?": "string" }),
|
|
322
|
-
"config set": receipt({
|
|
372
|
+
"config set": receipt({
|
|
373
|
+
success: "boolean",
|
|
374
|
+
path: "string",
|
|
375
|
+
key: "string",
|
|
376
|
+
value: "string",
|
|
377
|
+
"workspace?": "string",
|
|
378
|
+
}),
|
|
323
379
|
"config show": object(CONFIG_SHOW_SHAPE),
|
|
324
380
|
|
|
325
381
|
cycle: null,
|
|
@@ -329,6 +385,26 @@ export const OUTPUT_SHAPES: Record<string, OutputShape | null> = {
|
|
|
329
385
|
"cycle update": receipt({ id: "string", number: "number" }),
|
|
330
386
|
"cycle view": CYCLE_VIEW,
|
|
331
387
|
|
|
388
|
+
"custom-view": runs("custom-view view", object(CUSTOM_VIEW_DETAIL_SHAPE)),
|
|
389
|
+
"custom-view create": receipt({
|
|
390
|
+
id: "string",
|
|
391
|
+
name: "string",
|
|
392
|
+
type: "string",
|
|
393
|
+
shared: "boolean",
|
|
394
|
+
slugId: "string",
|
|
395
|
+
}),
|
|
396
|
+
"custom-view delete": receipt(NAMED_DELETED),
|
|
397
|
+
"custom-view list": list(CUSTOM_VIEW_ROW_SHAPE),
|
|
398
|
+
"custom-view results": list(CUSTOM_VIEW_RESULT_ROW_SHAPE),
|
|
399
|
+
"custom-view update": receipt({
|
|
400
|
+
id: "string",
|
|
401
|
+
name: "string",
|
|
402
|
+
type: "string",
|
|
403
|
+
shared: "boolean",
|
|
404
|
+
slugId: "string",
|
|
405
|
+
}),
|
|
406
|
+
"custom-view view": object(CUSTOM_VIEW_DETAIL_SHAPE),
|
|
407
|
+
|
|
332
408
|
document: runs("document view", object(DOCUMENT_DETAIL_SHAPE)),
|
|
333
409
|
"document create": receipt({ id: "string", title: "string", url: "string" }),
|
|
334
410
|
"document delete": receipt({ id: "string", title: "string", deleted: "boolean" }),
|
|
@@ -383,6 +459,12 @@ export const OUTPUT_SHAPES: Record<string, OutputShape | null> = {
|
|
|
383
459
|
},
|
|
384
460
|
),
|
|
385
461
|
"issue assign": receipt(ISSUE_REF),
|
|
462
|
+
"issue delegate": receipt(ISSUE_DELEGATION_RECEIPT, {
|
|
463
|
+
variants: {
|
|
464
|
+
"--dry-run": receipt(ISSUE_MUTATION_PREVIEW),
|
|
465
|
+
"--full-result": receipt(ISSUE_DELEGATION_FULL_RESULT),
|
|
466
|
+
},
|
|
467
|
+
}),
|
|
386
468
|
"issue attach": list(
|
|
387
469
|
{
|
|
388
470
|
id: "string",
|
|
@@ -413,6 +495,10 @@ export const OUTPUT_SHAPES: Record<string, OutputShape | null> = {
|
|
|
413
495
|
checkedOut: "boolean",
|
|
414
496
|
stateChanged: "boolean",
|
|
415
497
|
}),
|
|
498
|
+
"--delegate": receipt(ISSUE_DELEGATION_RECEIPT),
|
|
499
|
+
"--clear-delegate": receipt(ISSUE_DELEGATION_RECEIPT),
|
|
500
|
+
"--dry-run": receipt(ISSUE_MUTATION_PREVIEW),
|
|
501
|
+
"--full-result": receipt(ISSUE_DELEGATION_FULL_RESULT),
|
|
416
502
|
},
|
|
417
503
|
}),
|
|
418
504
|
"issue delete": receipt(
|
|
@@ -444,7 +530,13 @@ export const OUTPUT_SHAPES: Record<string, OutputShape | null> = {
|
|
|
444
530
|
},
|
|
445
531
|
),
|
|
446
532
|
"issue id": receipt({ id: "string" }, { note: "`id` is the identifier (TES-123), not the UUID" }),
|
|
447
|
-
"issue label": receipt(ISSUE_REF
|
|
533
|
+
"issue label": receipt(ISSUE_REF, {
|
|
534
|
+
variants: {
|
|
535
|
+
"--set-group": receipt(ISSUE_LABEL_GROUP_RECEIPT),
|
|
536
|
+
"--dry-run": receipt(ISSUE_LABEL_GROUP_PREVIEW),
|
|
537
|
+
"--full-result": receipt(ISSUE_LABEL_GROUP_FULL_RESULT),
|
|
538
|
+
},
|
|
539
|
+
}),
|
|
448
540
|
"issue list": ISSUE_LIST,
|
|
449
541
|
"issue mine": ISSUE_LIST,
|
|
450
542
|
"issue pull-request": receipt(
|
|
@@ -474,7 +566,14 @@ export const OUTPUT_SHAPES: Record<string, OutputShape | null> = {
|
|
|
474
566
|
"issue title": receipt({ title: "string" }),
|
|
475
567
|
"issue unarchive": receipt({ ...ISSUE_REF, archived: "boolean" }),
|
|
476
568
|
"issue unsubscribe": receipt({ ...ISSUE_REF, subscribed: "boolean" }),
|
|
477
|
-
"issue update": receipt(ISSUE_URL_RECEIPT
|
|
569
|
+
"issue update": receipt(ISSUE_URL_RECEIPT, {
|
|
570
|
+
variants: {
|
|
571
|
+
"--delegate": receipt(ISSUE_DELEGATION_RECEIPT),
|
|
572
|
+
"--clear-delegate": receipt(ISSUE_DELEGATION_RECEIPT),
|
|
573
|
+
"--dry-run": receipt(ISSUE_MUTATION_PREVIEW),
|
|
574
|
+
"--full-result": receipt(ISSUE_DELEGATION_FULL_RESULT),
|
|
575
|
+
},
|
|
576
|
+
}),
|
|
478
577
|
"issue url": receipt({ url: "string" }),
|
|
479
578
|
"issue view": ISSUE_VIEW,
|
|
480
579
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Lifecycle fields exposed by Linear on historically listable resources. */
|
|
2
|
+
export interface LifecycleFields {
|
|
3
|
+
archivedAt: string | null;
|
|
4
|
+
trashed?: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** A visible suffix for mixed live/historical tables. Trashed wins over archived. */
|
|
8
|
+
export function lifecycleSuffix(resource: LifecycleFields): string {
|
|
9
|
+
return resource.trashed ? " (trashed)" : resource.archivedAt ? " (archived)" : "";
|
|
10
|
+
}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom-view service: typed @linear/sdk access, input construction, reference
|
|
3
|
+
* validation, pagination, and stable result shaping.
|
|
4
|
+
*
|
|
5
|
+
* Linear's `customViews` query deliberately excludes views scoped to a project
|
|
6
|
+
* or initiative. UUID lookup remains complete, so every single-view operation
|
|
7
|
+
* requires a UUID rather than offering a name lookup over an incomplete set.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CustomView, LinearClient } from "@linear/sdk";
|
|
11
|
+
import { withRetry } from "../client.js";
|
|
12
|
+
import { usageError } from "../lib/errors.js";
|
|
13
|
+
import { assertMutation, unwrapMutation } from "../lib/mutation.js";
|
|
14
|
+
import { collect, inheritPaginationMetadata, pageSize } from "../lib/pagination.js";
|
|
15
|
+
import { isUuid, resolveProjectId, resolveTeam, resolveUserId } from "../lib/resolve.js";
|
|
16
|
+
import { shape } from "../lib/shape.js";
|
|
17
|
+
import { resolveInitiative } from "./initiative.js";
|
|
18
|
+
|
|
19
|
+
export const CUSTOM_VIEW_TYPES = ["issue", "project", "initiative"] as const;
|
|
20
|
+
export type CustomViewType = (typeof CUSTOM_VIEW_TYPES)[number];
|
|
21
|
+
type CustomViewCreateInput = Parameters<LinearClient["createCustomView"]>[0];
|
|
22
|
+
type CustomViewUpdateInput = Parameters<LinearClient["updateCustomView"]>[1];
|
|
23
|
+
|
|
24
|
+
export interface CustomViewPerson {
|
|
25
|
+
id: string;
|
|
26
|
+
displayName: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CustomViewTeam {
|
|
30
|
+
id: string;
|
|
31
|
+
key: string;
|
|
32
|
+
name: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CustomViewRow {
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
type: string;
|
|
39
|
+
shared: boolean;
|
|
40
|
+
owner: CustomViewPerson | null;
|
|
41
|
+
team: CustomViewTeam | null;
|
|
42
|
+
slugId: string;
|
|
43
|
+
updatedAt: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const CUSTOM_VIEW_ROW_SHAPE = shape<CustomViewRow>({
|
|
47
|
+
id: "string",
|
|
48
|
+
name: "string",
|
|
49
|
+
type: "string",
|
|
50
|
+
shared: "boolean",
|
|
51
|
+
owner: { nullable: { id: "string", displayName: "string" } },
|
|
52
|
+
team: { nullable: { id: "string", key: "string", name: "string" } },
|
|
53
|
+
slugId: "string",
|
|
54
|
+
updatedAt: "string",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export interface CustomViewDetail extends CustomViewRow {
|
|
58
|
+
description: string | null;
|
|
59
|
+
filter: Record<string, unknown>;
|
|
60
|
+
color: string | null;
|
|
61
|
+
icon: string | null;
|
|
62
|
+
creator: CustomViewPerson | null;
|
|
63
|
+
createdAt: string;
|
|
64
|
+
archivedAt: string | null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const CUSTOM_VIEW_DETAIL_SHAPE = shape<CustomViewDetail>({
|
|
68
|
+
...CUSTOM_VIEW_ROW_SHAPE,
|
|
69
|
+
description: "string|null",
|
|
70
|
+
filter: "object",
|
|
71
|
+
color: "string|null",
|
|
72
|
+
icon: "string|null",
|
|
73
|
+
creator: { nullable: { id: "string", displayName: "string" } },
|
|
74
|
+
createdAt: "string",
|
|
75
|
+
archivedAt: "string|null",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export interface CustomViewResultRow {
|
|
79
|
+
type: CustomViewType;
|
|
80
|
+
id: string;
|
|
81
|
+
identifier: string | null;
|
|
82
|
+
name: string;
|
|
83
|
+
url: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const CUSTOM_VIEW_RESULT_ROW_SHAPE = shape<CustomViewResultRow>({
|
|
87
|
+
type: "string",
|
|
88
|
+
id: "string",
|
|
89
|
+
identifier: "string|null",
|
|
90
|
+
name: "string",
|
|
91
|
+
url: "string",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
function iso(value: Date | string): string {
|
|
95
|
+
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Stable CLI spelling of the API's modelName. */
|
|
99
|
+
export function customViewType(modelName: string): string {
|
|
100
|
+
switch (modelName.toLowerCase()) {
|
|
101
|
+
case "issue":
|
|
102
|
+
return "issue";
|
|
103
|
+
case "project":
|
|
104
|
+
return "project";
|
|
105
|
+
case "initiative":
|
|
106
|
+
return "initiative";
|
|
107
|
+
case "feeditem":
|
|
108
|
+
return "feed-item";
|
|
109
|
+
default:
|
|
110
|
+
return modelName;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function supportedType(modelName: string): CustomViewType {
|
|
115
|
+
const type = customViewType(modelName);
|
|
116
|
+
if ((CUSTOM_VIEW_TYPES as readonly string[]).includes(type)) return type as CustomViewType;
|
|
117
|
+
throw usageError(
|
|
118
|
+
`Custom view type '${type}' has no matched-entity command in the typed SDK. Supported: ${CUSTOM_VIEW_TYPES.join(
|
|
119
|
+
", ",
|
|
120
|
+
)}.`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function person(user: any): CustomViewPerson | null {
|
|
125
|
+
return user ? { id: user.id, displayName: user.displayName } : null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function teamRow(team: any): CustomViewTeam | null {
|
|
129
|
+
return team ? { id: team.id, key: team.key, name: team.name } : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Project a model to a row, resolving the two displayed relations. */
|
|
133
|
+
export async function toRow(view: CustomView | any): Promise<CustomViewRow> {
|
|
134
|
+
const [owner, team] = await Promise.all([view.owner, view.team]);
|
|
135
|
+
return {
|
|
136
|
+
id: view.id,
|
|
137
|
+
name: view.name,
|
|
138
|
+
type: customViewType(view.modelName),
|
|
139
|
+
shared: view.shared,
|
|
140
|
+
owner: person(owner),
|
|
141
|
+
team: teamRow(team),
|
|
142
|
+
slugId: view.slugId,
|
|
143
|
+
updatedAt: iso(view.updatedAt),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function listCustomViews(
|
|
148
|
+
client: LinearClient,
|
|
149
|
+
limit: number,
|
|
150
|
+
): Promise<CustomViewRow[]> {
|
|
151
|
+
const conn = await withRetry(() => client.customViews({ first: pageSize(limit) }));
|
|
152
|
+
const nodes = await collect(conn as any, limit);
|
|
153
|
+
return inheritPaginationMetadata(await Promise.all(nodes.map(toRow)), nodes);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** UUID-only by design: the list query is not a complete namespace for names. */
|
|
157
|
+
export async function resolveCustomView(client: LinearClient, reference: string) {
|
|
158
|
+
if (!isUuid(reference)) {
|
|
159
|
+
throw usageError(
|
|
160
|
+
`A custom view must be referenced by UUID; names are not unique and scoped views are absent from 'custom-view list'.`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return withRetry(() => client.customView(reference));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function filterOf(view: CustomView | any): Record<string, unknown> {
|
|
167
|
+
switch (customViewType(view.modelName)) {
|
|
168
|
+
case "project":
|
|
169
|
+
return (view.projectFilterData ?? {}) as Record<string, unknown>;
|
|
170
|
+
case "initiative":
|
|
171
|
+
return (view.initiativeFilterData ?? {}) as Record<string, unknown>;
|
|
172
|
+
case "feed-item":
|
|
173
|
+
return (view.feedItemFilterData ?? {}) as Record<string, unknown>;
|
|
174
|
+
default:
|
|
175
|
+
return (view.filterData ?? {}) as Record<string, unknown>;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function getCustomViewDetail(
|
|
180
|
+
client: LinearClient,
|
|
181
|
+
reference: string,
|
|
182
|
+
): Promise<CustomViewDetail> {
|
|
183
|
+
const view = await resolveCustomView(client, reference);
|
|
184
|
+
const [row, creator] = await Promise.all([toRow(view), view.creator]);
|
|
185
|
+
return {
|
|
186
|
+
...row,
|
|
187
|
+
description: view.description ?? null,
|
|
188
|
+
filter: filterOf(view),
|
|
189
|
+
color: view.color ?? null,
|
|
190
|
+
icon: view.icon ?? null,
|
|
191
|
+
creator: person(creator),
|
|
192
|
+
createdAt: iso(view.createdAt),
|
|
193
|
+
archivedAt: view.archivedAt ? iso(view.archivedAt) : null,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function listCustomViewResults(
|
|
198
|
+
client: LinearClient,
|
|
199
|
+
reference: string,
|
|
200
|
+
limit: number,
|
|
201
|
+
): Promise<CustomViewResultRow[]> {
|
|
202
|
+
const view = await resolveCustomView(client, reference);
|
|
203
|
+
const type = supportedType(view.modelName);
|
|
204
|
+
const first = pageSize(limit);
|
|
205
|
+
const conn =
|
|
206
|
+
type === "issue"
|
|
207
|
+
? await withRetry(() => view.issues({ first }))
|
|
208
|
+
: type === "project"
|
|
209
|
+
? await withRetry(() => view.projects({ first }))
|
|
210
|
+
: await withRetry(() => view.initiatives({ first }));
|
|
211
|
+
const nodes = await collect<any>(conn as any, limit);
|
|
212
|
+
const rows = nodes.map((entity: any): CustomViewResultRow => ({
|
|
213
|
+
type,
|
|
214
|
+
id: entity.id,
|
|
215
|
+
identifier: type === "issue" ? entity.identifier : null,
|
|
216
|
+
name: type === "issue" ? entity.title : entity.name,
|
|
217
|
+
url: entity.url,
|
|
218
|
+
}));
|
|
219
|
+
return inheritPaginationMetadata(rows, nodes);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export interface CreateCustomViewOptions {
|
|
223
|
+
name: string;
|
|
224
|
+
type: CustomViewType;
|
|
225
|
+
filter?: Record<string, unknown>;
|
|
226
|
+
description?: string;
|
|
227
|
+
color?: string;
|
|
228
|
+
icon?: string;
|
|
229
|
+
owner?: string;
|
|
230
|
+
shared?: boolean;
|
|
231
|
+
scopeTeam?: string;
|
|
232
|
+
scopeProject?: string;
|
|
233
|
+
scopeInitiative?: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function applyFilter(
|
|
237
|
+
input: CustomViewCreateInput | CustomViewUpdateInput,
|
|
238
|
+
type: CustomViewType,
|
|
239
|
+
filter: Record<string, unknown>,
|
|
240
|
+
): void {
|
|
241
|
+
switch (type) {
|
|
242
|
+
case "issue":
|
|
243
|
+
input.filterData = filter as NonNullable<CustomViewCreateInput["filterData"]>;
|
|
244
|
+
break;
|
|
245
|
+
case "project":
|
|
246
|
+
input.projectFilterData = filter as NonNullable<CustomViewCreateInput["projectFilterData"]>;
|
|
247
|
+
break;
|
|
248
|
+
case "initiative":
|
|
249
|
+
input.initiativeFilterData = filter as NonNullable<
|
|
250
|
+
CustomViewCreateInput["initiativeFilterData"]
|
|
251
|
+
>;
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function createScopeInput(
|
|
257
|
+
client: LinearClient,
|
|
258
|
+
opts: Pick<CreateCustomViewOptions, "scopeTeam" | "scopeProject" | "scopeInitiative">,
|
|
259
|
+
): Promise<Partial<Pick<CustomViewCreateInput, "teamId" | "projectId" | "initiativeId">>> {
|
|
260
|
+
const scopes = [opts.scopeTeam, opts.scopeProject, opts.scopeInitiative].filter(
|
|
261
|
+
(value) => value !== undefined,
|
|
262
|
+
);
|
|
263
|
+
if (scopes.length > 1) {
|
|
264
|
+
throw usageError("Pass at most one of --scope-team, --scope-project, or --scope-initiative.");
|
|
265
|
+
}
|
|
266
|
+
if (opts.scopeTeam) {
|
|
267
|
+
return { teamId: (await resolveTeam(client, opts.scopeTeam, undefined)).id };
|
|
268
|
+
}
|
|
269
|
+
if (opts.scopeProject) return { projectId: await resolveProjectId(client, opts.scopeProject) };
|
|
270
|
+
if (opts.scopeInitiative) {
|
|
271
|
+
return { initiativeId: (await resolveInitiative(client, opts.scopeInitiative)).id };
|
|
272
|
+
}
|
|
273
|
+
return {};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Exported for tests: constructs exactly the SDK input sent by create. */
|
|
277
|
+
export async function buildCreateCustomViewInput(
|
|
278
|
+
client: LinearClient,
|
|
279
|
+
opts: CreateCustomViewOptions,
|
|
280
|
+
): Promise<CustomViewCreateInput> {
|
|
281
|
+
const input: CustomViewCreateInput = { name: opts.name };
|
|
282
|
+
applyFilter(input, opts.type, opts.filter ?? {});
|
|
283
|
+
if (opts.description !== undefined) input.description = opts.description;
|
|
284
|
+
if (opts.color !== undefined) input.color = opts.color;
|
|
285
|
+
if (opts.icon !== undefined) input.icon = opts.icon;
|
|
286
|
+
if (opts.owner) input.ownerId = await resolveUserId(client, opts.owner);
|
|
287
|
+
if (opts.shared !== undefined) input.shared = opts.shared;
|
|
288
|
+
Object.assign(input, await createScopeInput(client, opts));
|
|
289
|
+
return input;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export async function createCustomView(client: LinearClient, opts: CreateCustomViewOptions) {
|
|
293
|
+
const input = await buildCreateCustomViewInput(client, opts);
|
|
294
|
+
return unwrapMutation(
|
|
295
|
+
withRetry(() => client.createCustomView(input)),
|
|
296
|
+
"customView",
|
|
297
|
+
"Custom view creation",
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export interface UpdateCustomViewOptions {
|
|
302
|
+
name?: string;
|
|
303
|
+
filter?: Record<string, unknown>;
|
|
304
|
+
description?: string;
|
|
305
|
+
clearDescription?: boolean;
|
|
306
|
+
color?: string;
|
|
307
|
+
clearColor?: boolean;
|
|
308
|
+
icon?: string;
|
|
309
|
+
clearIcon?: boolean;
|
|
310
|
+
owner?: string;
|
|
311
|
+
shared?: boolean;
|
|
312
|
+
scopeTeam?: string;
|
|
313
|
+
clearTeamScope?: boolean;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Exported for tests: omits every field the caller did not explicitly set. */
|
|
317
|
+
export async function buildUpdateCustomViewInput(
|
|
318
|
+
client: LinearClient,
|
|
319
|
+
view: CustomView | any,
|
|
320
|
+
opts: UpdateCustomViewOptions,
|
|
321
|
+
): Promise<CustomViewUpdateInput> {
|
|
322
|
+
const input: CustomViewUpdateInput = {};
|
|
323
|
+
if (opts.name !== undefined) input.name = opts.name;
|
|
324
|
+
if (opts.filter !== undefined) applyFilter(input, supportedType(view.modelName), opts.filter);
|
|
325
|
+
if (opts.description !== undefined && opts.clearDescription) {
|
|
326
|
+
throw usageError("Pass either --description or --clear-description, not both.");
|
|
327
|
+
}
|
|
328
|
+
if (opts.description !== undefined) input.description = opts.description;
|
|
329
|
+
if (opts.clearDescription) input.description = null;
|
|
330
|
+
if (opts.color !== undefined && opts.clearColor) {
|
|
331
|
+
throw usageError("Pass either --color or --clear-color, not both.");
|
|
332
|
+
}
|
|
333
|
+
if (opts.color !== undefined) input.color = opts.color;
|
|
334
|
+
if (opts.clearColor) input.color = null;
|
|
335
|
+
if (opts.icon !== undefined && opts.clearIcon) {
|
|
336
|
+
throw usageError("Pass either --icon or --clear-icon, not both.");
|
|
337
|
+
}
|
|
338
|
+
if (opts.icon !== undefined) input.icon = opts.icon;
|
|
339
|
+
if (opts.clearIcon) input.icon = null;
|
|
340
|
+
if (opts.owner) input.ownerId = await resolveUserId(client, opts.owner);
|
|
341
|
+
if (opts.shared !== undefined) input.shared = opts.shared;
|
|
342
|
+
if (opts.scopeTeam !== undefined && opts.clearTeamScope) {
|
|
343
|
+
throw usageError("Pass either --scope-team or --clear-team-scope, not both.");
|
|
344
|
+
}
|
|
345
|
+
if (opts.scopeTeam !== undefined) {
|
|
346
|
+
input.teamId = (await resolveTeam(client, opts.scopeTeam, undefined)).id;
|
|
347
|
+
}
|
|
348
|
+
if (opts.clearTeamScope) input.teamId = null;
|
|
349
|
+
if (Object.keys(input).length === 0) {
|
|
350
|
+
throw usageError("Nothing to update; pass at least one field.");
|
|
351
|
+
}
|
|
352
|
+
return input;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export async function updateCustomView(
|
|
356
|
+
client: LinearClient,
|
|
357
|
+
reference: string,
|
|
358
|
+
opts: UpdateCustomViewOptions,
|
|
359
|
+
) {
|
|
360
|
+
const view = await resolveCustomView(client, reference);
|
|
361
|
+
const input = await buildUpdateCustomViewInput(client, view, opts);
|
|
362
|
+
return unwrapMutation(
|
|
363
|
+
withRetry(() => client.updateCustomView(view.id, input)),
|
|
364
|
+
"customView",
|
|
365
|
+
"Custom view update",
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export async function deleteCustomView(client: LinearClient, reference: string) {
|
|
370
|
+
const view = await resolveCustomView(client, reference);
|
|
371
|
+
await assertMutation(
|
|
372
|
+
withRetry(() => client.deleteCustomView(view.id)),
|
|
373
|
+
"Custom view deletion",
|
|
374
|
+
);
|
|
375
|
+
return view;
|
|
376
|
+
}
|
|
@@ -51,6 +51,8 @@ export interface InitiativeRow {
|
|
|
51
51
|
targetDate: string | null;
|
|
52
52
|
health: string | null;
|
|
53
53
|
url: string;
|
|
54
|
+
archivedAt: string | null;
|
|
55
|
+
trashed: boolean;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
/** The row's shape as `linear commands` advertises it (TES-610); checked against the interface. */
|
|
@@ -62,13 +64,15 @@ export const INITIATIVE_ROW_SHAPE = shape<InitiativeRow>({
|
|
|
62
64
|
targetDate: "string|null",
|
|
63
65
|
health: "string|null",
|
|
64
66
|
url: "string",
|
|
67
|
+
archivedAt: "string|null",
|
|
68
|
+
trashed: "boolean",
|
|
65
69
|
});
|
|
66
70
|
|
|
67
71
|
const LIST_QUERY = `
|
|
68
72
|
query CliInitiatives($filter: InitiativeFilter, $first: Int!, $after: String, $includeArchived: Boolean) {
|
|
69
73
|
initiatives(filter: $filter, first: $first, after: $after, includeArchived: $includeArchived) {
|
|
70
74
|
nodes {
|
|
71
|
-
id name status priority targetDate health url
|
|
75
|
+
id name status priority targetDate health url archivedAt trashed
|
|
72
76
|
}
|
|
73
77
|
pageInfo { hasNextPage endCursor }
|
|
74
78
|
}
|
|
@@ -80,7 +84,7 @@ export interface ListFilters {
|
|
|
80
84
|
/** me|email|name|id — resolved to a user id. */
|
|
81
85
|
owner?: string;
|
|
82
86
|
/** Include archived initiatives (the API excludes them by default). */
|
|
83
|
-
|
|
87
|
+
includeArchived?: boolean;
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
/** Build an InitiativeFilter from human options, resolving names to ids. Exported for tests. */
|
|
@@ -104,7 +108,7 @@ export async function listInitiatives(
|
|
|
104
108
|
return collectRawQuery<InitiativeRow>(
|
|
105
109
|
client as any,
|
|
106
110
|
LIST_QUERY,
|
|
107
|
-
{ filter, includeArchived: filters.
|
|
111
|
+
{ filter, includeArchived: filters.includeArchived === true },
|
|
108
112
|
"initiatives",
|
|
109
113
|
limit,
|
|
110
114
|
(n) => ({
|
|
@@ -115,6 +119,8 @@ export async function listInitiatives(
|
|
|
115
119
|
targetDate: n.targetDate ?? null,
|
|
116
120
|
health: n.health ?? null,
|
|
117
121
|
url: n.url,
|
|
122
|
+
archivedAt: n.archivedAt ?? null,
|
|
123
|
+
trashed: n.trashed === true,
|
|
118
124
|
}),
|
|
119
125
|
);
|
|
120
126
|
}
|
|
@@ -137,6 +143,7 @@ export interface InitiativeDetail {
|
|
|
137
143
|
startedAt: string | null;
|
|
138
144
|
completedAt: string | null;
|
|
139
145
|
archivedAt: string | null;
|
|
146
|
+
trashed: boolean;
|
|
140
147
|
owner: string | null;
|
|
141
148
|
creator: string | null;
|
|
142
149
|
/** The projects linked to the initiative (`initiative add-project`), API order. */
|
|
@@ -162,6 +169,7 @@ export const INITIATIVE_DETAIL_SHAPE = shape<InitiativeDetail>({
|
|
|
162
169
|
startedAt: "string|null",
|
|
163
170
|
completedAt: "string|null",
|
|
164
171
|
archivedAt: "string|null",
|
|
172
|
+
trashed: "boolean",
|
|
165
173
|
owner: "string|null",
|
|
166
174
|
creator: "string|null",
|
|
167
175
|
projects: [
|
|
@@ -214,6 +222,7 @@ export async function getInitiativeDetail(
|
|
|
214
222
|
startedAt: initiative.startedAt ? initiative.startedAt.toISOString() : null,
|
|
215
223
|
completedAt: initiative.completedAt ? initiative.completedAt.toISOString() : null,
|
|
216
224
|
archivedAt: initiative.archivedAt ? initiative.archivedAt.toISOString() : null,
|
|
225
|
+
trashed: initiative.trashed === true,
|
|
217
226
|
owner: owner?.displayName ?? null,
|
|
218
227
|
creator: creator?.displayName ?? null,
|
|
219
228
|
projects: projectRows,
|