linear-sdk-cli 0.4.0 → 0.5.1
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 +17 -0
- package/COVERAGE.md +8 -3
- package/MIGRATING.md +6 -0
- package/README.md +193 -37
- package/package.json +8 -7
- package/skills/linear-sdk-cli/SKILL.md +11 -0
- package/skills/linear-sdk-cli/references/auth.md +4 -1
- package/skills/linear-sdk-cli/references/comment.md +2 -0
- package/skills/linear-sdk-cli/references/config.md +10 -7
- package/skills/linear-sdk-cli/references/initiative.md +10 -5
- package/skills/linear-sdk-cli/references/issue.md +224 -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 +1 -0
- package/src/commands/comment.ts +9 -7
- package/src/commands/initiative.ts +23 -6
- package/src/commands/issue.ts +313 -12
- package/src/commands/meta.ts +85 -13
- package/src/commands/notification.ts +5 -3
- package/src/commands/open.ts +2 -1
- package/src/commands/project.ts +21 -3
- package/src/config.ts +124 -39
- package/src/context.ts +21 -1
- package/src/lib/action.ts +8 -0
- package/src/lib/delegation.ts +240 -0
- package/src/lib/options.ts +17 -3
- package/src/lib/output-shapes.ts +78 -3
- package/src/lib/resolve.ts +53 -39
- package/src/output/lifecycle.ts +10 -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/options.ts
CHANGED
|
@@ -210,6 +210,20 @@ export function readAlias<T = string>(
|
|
|
210
210
|
return (canonicalValue ?? aliasValue) as T | undefined;
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Add the canonical lifecycle-listing option. Linear may return both archived
|
|
215
|
+
* and trashed records when this is true, so the help does not imply an
|
|
216
|
+
* archived-only filter.
|
|
217
|
+
*/
|
|
218
|
+
export function addIncludeArchivedOption(cmd: Command): Command {
|
|
219
|
+
return cmd.addOption(
|
|
220
|
+
new Option(
|
|
221
|
+
"--include-archived",
|
|
222
|
+
"include archived resources (the API may also return trashed records)",
|
|
223
|
+
),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
213
227
|
/**
|
|
214
228
|
* A `--no-…` global registered as an ordinary boolean flag rather than as
|
|
215
229
|
* commander's *negation* of a positive option. Two bugs came out of leaving
|
|
@@ -603,7 +617,7 @@ export function addCoreFilterOptions(cmd: Command, set: FilterOptionSet = {}): C
|
|
|
603
617
|
cmd.addOption(new Option("-a, --assignee <who>", "filter by assignee (me|email|name)"));
|
|
604
618
|
cmd.addOption(new Option("-U, --unassigned", "only issues with no assignee"));
|
|
605
619
|
}
|
|
606
|
-
|
|
620
|
+
cmd
|
|
607
621
|
.addOption(new Option("-p, --project <name>", "filter by project"))
|
|
608
622
|
.addOption(
|
|
609
623
|
new Option("--project-label <name>", "filter by the project's label (excludes --project)"),
|
|
@@ -622,8 +636,8 @@ export function addCoreFilterOptions(cmd: Command, set: FilterOptionSet = {}): C
|
|
|
622
636
|
.addOption(
|
|
623
637
|
new Option("--updated-after <date>", "only issues updated at/after a date (YYYY-MM-DD)"),
|
|
624
638
|
)
|
|
625
|
-
.addOption(new Option("--all-teams", "search every team, ignoring the default team"))
|
|
626
|
-
|
|
639
|
+
.addOption(new Option("--all-teams", "search every team, ignoring the default team"));
|
|
640
|
+
return addIncludeArchivedOption(cmd);
|
|
627
641
|
}
|
|
628
642
|
|
|
629
643
|
/**
|
package/src/lib/output-shapes.ts
CHANGED
|
@@ -154,6 +154,7 @@ const SETTING_ORIGIN_SHAPE = shape<SettingOrigin>({
|
|
|
154
154
|
source: "string",
|
|
155
155
|
"path?": "string",
|
|
156
156
|
"key?": "string",
|
|
157
|
+
"workspace?": "string",
|
|
157
158
|
});
|
|
158
159
|
const SETTING_ORIGINS_SHAPE = shape<SettingOrigins>({
|
|
159
160
|
team: SETTING_ORIGIN_SHAPE,
|
|
@@ -168,6 +169,7 @@ const CONFIG_SHOW_SHAPE: ObjectFields = {
|
|
|
168
169
|
accessToken: "string",
|
|
169
170
|
accessTokenSource: "string",
|
|
170
171
|
credentialWorkspace: "string|null",
|
|
172
|
+
workspaceProfile: "string|null",
|
|
171
173
|
team: "string|null",
|
|
172
174
|
workspace: "string|null",
|
|
173
175
|
sort: "string",
|
|
@@ -208,6 +210,48 @@ export const COMMAND_NODE_SHAPE = shape<CommandNode>({
|
|
|
208
210
|
// Receipts shared by several commands.
|
|
209
211
|
const ISSUE_REF: ObjectFields = { id: "string", identifier: "string" };
|
|
210
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
|
+
};
|
|
211
255
|
const NAMED_URL_RECEIPT: ObjectFields = { id: "string", name: "string", url: "string" };
|
|
212
256
|
const NAMED_DELETED: ObjectFields = { id: "string", name: "string", deleted: "boolean" };
|
|
213
257
|
const NAMED_ARCHIVED: ObjectFields = { id: "string", name: "string", archived: "boolean" };
|
|
@@ -284,6 +328,7 @@ export const OUTPUT_SHAPES: Record<string, OutputShape | null> = {
|
|
|
284
328
|
"scopes?": ["string"],
|
|
285
329
|
"expiresAt?": "string",
|
|
286
330
|
path: "string",
|
|
331
|
+
projectConfigPath: "string|null",
|
|
287
332
|
}),
|
|
288
333
|
"auth logout": receipt({
|
|
289
334
|
success: "boolean",
|
|
@@ -291,6 +336,7 @@ export const OUTPUT_SHAPES: Record<string, OutputShape | null> = {
|
|
|
291
336
|
removed: "boolean",
|
|
292
337
|
revocation: "string",
|
|
293
338
|
fallbackCredentialType: "string|null",
|
|
339
|
+
teamMetadataRemoved: "boolean",
|
|
294
340
|
}),
|
|
295
341
|
"auth migrate": receipt({ success: "boolean", migrated: ["string"], path: "string" }),
|
|
296
342
|
"auth status": object({
|
|
@@ -324,7 +370,13 @@ export const OUTPUT_SHAPES: Record<string, OutputShape | null> = {
|
|
|
324
370
|
|
|
325
371
|
config: runs("config show", object(CONFIG_SHOW_SHAPE)),
|
|
326
372
|
"config init": receipt({ success: "boolean", path: "string", team: "string", "sort?": "string" }),
|
|
327
|
-
"config set": receipt({
|
|
373
|
+
"config set": receipt({
|
|
374
|
+
success: "boolean",
|
|
375
|
+
path: "string",
|
|
376
|
+
key: "string",
|
|
377
|
+
value: "string",
|
|
378
|
+
"workspace?": "string",
|
|
379
|
+
}),
|
|
328
380
|
"config show": object(CONFIG_SHOW_SHAPE),
|
|
329
381
|
|
|
330
382
|
cycle: null,
|
|
@@ -408,6 +460,12 @@ export const OUTPUT_SHAPES: Record<string, OutputShape | null> = {
|
|
|
408
460
|
},
|
|
409
461
|
),
|
|
410
462
|
"issue assign": receipt(ISSUE_REF),
|
|
463
|
+
"issue delegate": receipt(ISSUE_DELEGATION_RECEIPT, {
|
|
464
|
+
variants: {
|
|
465
|
+
"--dry-run": receipt(ISSUE_MUTATION_PREVIEW),
|
|
466
|
+
"--full-result": receipt(ISSUE_DELEGATION_FULL_RESULT),
|
|
467
|
+
},
|
|
468
|
+
}),
|
|
411
469
|
"issue attach": list(
|
|
412
470
|
{
|
|
413
471
|
id: "string",
|
|
@@ -438,6 +496,10 @@ export const OUTPUT_SHAPES: Record<string, OutputShape | null> = {
|
|
|
438
496
|
checkedOut: "boolean",
|
|
439
497
|
stateChanged: "boolean",
|
|
440
498
|
}),
|
|
499
|
+
"--delegate": receipt(ISSUE_DELEGATION_RECEIPT),
|
|
500
|
+
"--clear-delegate": receipt(ISSUE_DELEGATION_RECEIPT),
|
|
501
|
+
"--dry-run": receipt(ISSUE_MUTATION_PREVIEW),
|
|
502
|
+
"--full-result": receipt(ISSUE_DELEGATION_FULL_RESULT),
|
|
441
503
|
},
|
|
442
504
|
}),
|
|
443
505
|
"issue delete": receipt(
|
|
@@ -469,7 +531,13 @@ export const OUTPUT_SHAPES: Record<string, OutputShape | null> = {
|
|
|
469
531
|
},
|
|
470
532
|
),
|
|
471
533
|
"issue id": receipt({ id: "string" }, { note: "`id` is the identifier (TES-123), not the UUID" }),
|
|
472
|
-
"issue label": receipt(ISSUE_REF
|
|
534
|
+
"issue label": receipt(ISSUE_REF, {
|
|
535
|
+
variants: {
|
|
536
|
+
"--set-group": receipt(ISSUE_LABEL_GROUP_RECEIPT),
|
|
537
|
+
"--dry-run": receipt(ISSUE_LABEL_GROUP_PREVIEW),
|
|
538
|
+
"--full-result": receipt(ISSUE_LABEL_GROUP_FULL_RESULT),
|
|
539
|
+
},
|
|
540
|
+
}),
|
|
473
541
|
"issue list": ISSUE_LIST,
|
|
474
542
|
"issue mine": ISSUE_LIST,
|
|
475
543
|
"issue pull-request": receipt(
|
|
@@ -499,7 +567,14 @@ export const OUTPUT_SHAPES: Record<string, OutputShape | null> = {
|
|
|
499
567
|
"issue title": receipt({ title: "string" }),
|
|
500
568
|
"issue unarchive": receipt({ ...ISSUE_REF, archived: "boolean" }),
|
|
501
569
|
"issue unsubscribe": receipt({ ...ISSUE_REF, subscribed: "boolean" }),
|
|
502
|
-
"issue update": receipt(ISSUE_URL_RECEIPT
|
|
570
|
+
"issue update": receipt(ISSUE_URL_RECEIPT, {
|
|
571
|
+
variants: {
|
|
572
|
+
"--delegate": receipt(ISSUE_DELEGATION_RECEIPT),
|
|
573
|
+
"--clear-delegate": receipt(ISSUE_DELEGATION_RECEIPT),
|
|
574
|
+
"--dry-run": receipt(ISSUE_MUTATION_PREVIEW),
|
|
575
|
+
"--full-result": receipt(ISSUE_DELEGATION_FULL_RESULT),
|
|
576
|
+
},
|
|
577
|
+
}),
|
|
503
578
|
"issue url": receipt({ url: "string" }),
|
|
504
579
|
"issue view": ISSUE_VIEW,
|
|
505
580
|
|
package/src/lib/resolve.ts
CHANGED
|
@@ -209,46 +209,60 @@ export async function resolveLabelIds(
|
|
|
209
209
|
names: string[],
|
|
210
210
|
teamId?: string,
|
|
211
211
|
): Promise<string[]> {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
212
|
+
// Share in-flight searches only within this call. Later calls must observe
|
|
213
|
+
// renamed/deleted labels, and exact spelling matters for case preference.
|
|
214
|
+
const lookups = new Map<string, Promise<any[]>>();
|
|
215
|
+
const lookup = (name: string): Promise<any[]> => {
|
|
216
|
+
let pending = lookups.get(name);
|
|
217
|
+
if (!pending) {
|
|
218
|
+
pending = (async () => {
|
|
219
|
+
const conn = await withRetry(() =>
|
|
220
|
+
client.issueLabels({
|
|
221
|
+
filter: { name: { eqIgnoreCase: name } } as any,
|
|
222
|
+
first: RESOLVE_PAGE,
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
// Share the complete scan too: concurrent consumers must not advance
|
|
226
|
+
// the same mutable SDK connection independently.
|
|
227
|
+
return scanAll<any>(conn as any, "labels", "linear label list");
|
|
228
|
+
})();
|
|
229
|
+
lookups.set(name, pending);
|
|
217
230
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
231
|
+
return pending;
|
|
232
|
+
};
|
|
233
|
+
return Promise.all(
|
|
234
|
+
names.map(async (name) => {
|
|
235
|
+
if (isUuid(name)) return name;
|
|
236
|
+
const nodes = await lookup(name);
|
|
237
|
+
if (nodes.length === 0)
|
|
238
|
+
throw notFound(`No label matching '${name}'. Run 'linear label list' to see the options.`);
|
|
239
|
+
|
|
240
|
+
// Narrow to this team's labels + workspace-level labels when a team is known.
|
|
241
|
+
// If a team is known and nothing is in scope, that's not-found (do NOT fall
|
|
242
|
+
// back to an out-of-scope label from another team).
|
|
243
|
+
// Label groups are containers, not labels that can be applied to an issue.
|
|
244
|
+
let candidates: any[] = nodes.filter((label: any) => !label.isGroup);
|
|
245
|
+
if (teamId) {
|
|
246
|
+
// The SDK includes teamId in the label response; accessing team fetches it again.
|
|
247
|
+
candidates = candidates.filter((label) => !label.teamId || label.teamId === teamId);
|
|
248
|
+
if (candidates.length === 0)
|
|
249
|
+
throw notFound(`No label '${name}' in this team or workspace.`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (candidates.length === 0)
|
|
253
|
+
throw notFound(`No label matching '${name}'. Run 'linear label list' to see the options.`);
|
|
254
|
+
|
|
255
|
+
// Prefer an exact (case-sensitive) match.
|
|
256
|
+
const exact = candidates.filter((l: any) => l.name === name);
|
|
257
|
+
const finalists = exact.length ? exact : candidates;
|
|
258
|
+
if (finalists.length > 1) {
|
|
259
|
+
throw ambiguous(
|
|
260
|
+
`Multiple labels named '${name}'${teamId ? " in scope" : ""}; pass the label id instead.`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return finalists[0]!.id;
|
|
264
|
+
}),
|
|
265
|
+
);
|
|
252
266
|
}
|
|
253
267
|
|
|
254
268
|
/**
|
|
@@ -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
|
+
}
|
|
@@ -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,
|