linear-sdk-cli 0.1.0 → 0.1.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 +46 -46
- package/COVERAGE.md +5 -5
- package/MIGRATING.md +69 -71
- package/README.md +88 -96
- package/package.json +3 -4
- 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/project.ts
CHANGED
|
@@ -8,12 +8,7 @@
|
|
|
8
8
|
import type { LinearClient } from "@linear/sdk";
|
|
9
9
|
import { withRetry } from "../client.js";
|
|
10
10
|
import { shape } from "../lib/shape.js";
|
|
11
|
-
import {
|
|
12
|
-
collect,
|
|
13
|
-
collectRawQuery,
|
|
14
|
-
inheritPaginationMetadata,
|
|
15
|
-
pageSize,
|
|
16
|
-
} from "../lib/pagination.js";
|
|
11
|
+
import { collect, collectRawQuery } from "../lib/pagination.js";
|
|
17
12
|
import { usageError, notFound, ambiguous } from "../lib/errors.js";
|
|
18
13
|
import { assertMutation, unwrapMutation } from "../lib/mutation.js";
|
|
19
14
|
import {
|
|
@@ -211,10 +206,7 @@ query CliProjectDetail($filter: ProjectFilter!, $includeArchived: Boolean!) {
|
|
|
211
206
|
}
|
|
212
207
|
}`;
|
|
213
208
|
|
|
214
|
-
export async function getProjectDetail(
|
|
215
|
-
client: LinearClient,
|
|
216
|
-
idArg: string,
|
|
217
|
-
): Promise<ProjectDetail> {
|
|
209
|
+
export async function getProjectDetail(client: LinearClient, idArg: string): Promise<ProjectDetail> {
|
|
218
210
|
const byId = isUuid(idArg);
|
|
219
211
|
const filter = byId ? { id: { eq: idArg } } : { name: { eqIgnoreCase: idArg } };
|
|
220
212
|
const data: any = await withRetry(() =>
|
|
@@ -345,10 +337,7 @@ export async function updateProject(client: LinearClient, idArg: string, opts: U
|
|
|
345
337
|
export async function archiveProject(client: LinearClient, idArg: string) {
|
|
346
338
|
const projectId = await resolveProjectId(client, idArg);
|
|
347
339
|
const project = await withRetry(() => client.project(projectId));
|
|
348
|
-
await assertMutation(
|
|
349
|
-
withRetry(() => client.archiveProject(projectId)),
|
|
350
|
-
"Project archive",
|
|
351
|
-
);
|
|
340
|
+
await assertMutation(withRetry(() => client.archiveProject(projectId)), "Project archive");
|
|
352
341
|
return project;
|
|
353
342
|
}
|
|
354
343
|
|
|
@@ -361,10 +350,7 @@ export async function archiveProject(client: LinearClient, idArg: string) {
|
|
|
361
350
|
export async function deleteProject(client: LinearClient, idArg: string) {
|
|
362
351
|
const projectId = await resolveProjectId(client, idArg);
|
|
363
352
|
const project = await withRetry(() => client.project(projectId));
|
|
364
|
-
await assertMutation(
|
|
365
|
-
withRetry(() => client.deleteProject(projectId)),
|
|
366
|
-
"Project deletion",
|
|
367
|
-
);
|
|
353
|
+
await assertMutation(withRetry(() => client.deleteProject(projectId)), "Project deletion");
|
|
368
354
|
return project;
|
|
369
355
|
}
|
|
370
356
|
|
|
@@ -390,17 +376,16 @@ export async function listMilestones(
|
|
|
390
376
|
): Promise<MilestoneRow[]> {
|
|
391
377
|
const projectId = await resolveProjectId(client, idArg);
|
|
392
378
|
const project = await withRetry(() => client.project(projectId));
|
|
393
|
-
const conn = await withRetry(() =>
|
|
394
|
-
|
|
395
|
-
return inheritPaginationMetadata(
|
|
396
|
-
nodes.map((m: any) => ({
|
|
397
|
-
id: m.id,
|
|
398
|
-
name: m.name,
|
|
399
|
-
targetDate: m.targetDate ?? null,
|
|
400
|
-
progress: m.progress ?? null,
|
|
401
|
-
})),
|
|
402
|
-
nodes,
|
|
379
|
+
const conn = await withRetry(() =>
|
|
380
|
+
project.projectMilestones({ first: Math.min(limit === Infinity ? 100 : limit, 100) }),
|
|
403
381
|
);
|
|
382
|
+
const nodes = await collect(conn as any, limit);
|
|
383
|
+
return nodes.map((m: any) => ({
|
|
384
|
+
id: m.id,
|
|
385
|
+
name: m.name,
|
|
386
|
+
targetDate: m.targetDate ?? null,
|
|
387
|
+
progress: m.progress ?? null,
|
|
388
|
+
}));
|
|
404
389
|
}
|
|
405
390
|
|
|
406
391
|
export interface UpdateRow {
|
|
@@ -418,22 +403,21 @@ export async function listUpdates(
|
|
|
418
403
|
): Promise<UpdateRow[]> {
|
|
419
404
|
const projectId = await resolveProjectId(client, idArg);
|
|
420
405
|
const project = await withRetry(() => client.project(projectId));
|
|
421
|
-
const conn = await withRetry(() =>
|
|
406
|
+
const conn = await withRetry(() =>
|
|
407
|
+
project.projectUpdates({ first: Math.min(limit === Infinity ? 100 : limit, 100) }),
|
|
408
|
+
);
|
|
422
409
|
const nodes = await collect(conn as any, limit);
|
|
423
|
-
return
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}),
|
|
435
|
-
),
|
|
436
|
-
nodes,
|
|
410
|
+
return Promise.all(
|
|
411
|
+
nodes.map(async (u: any) => {
|
|
412
|
+
const user = await u.user;
|
|
413
|
+
return {
|
|
414
|
+
id: u.id,
|
|
415
|
+
createdAt: u.createdAt?.toISOString?.() ?? String(u.createdAt),
|
|
416
|
+
user: user?.displayName ?? "unknown",
|
|
417
|
+
body: u.body ?? "",
|
|
418
|
+
health: u.health ?? null,
|
|
419
|
+
};
|
|
420
|
+
}),
|
|
437
421
|
);
|
|
438
422
|
}
|
|
439
423
|
|
|
@@ -473,22 +457,17 @@ async function resolveTeamIds(
|
|
|
473
457
|
/** Resolve a project status (by name or type) to a status id. */
|
|
474
458
|
async function resolveStatusId(client: LinearClient, input: string): Promise<string> {
|
|
475
459
|
if (isUuid(input)) return input;
|
|
476
|
-
const
|
|
477
|
-
const statuses = (await collect(conn as any, Infinity)) as Array<{
|
|
478
|
-
id: string;
|
|
479
|
-
name: string;
|
|
480
|
-
type: string;
|
|
481
|
-
}>;
|
|
460
|
+
const statuses = await withRetry(() => client.projectStatuses({ first: 100 }));
|
|
482
461
|
const lower = input.toLowerCase();
|
|
483
|
-
const byName = statuses.filter((s) => s.name.toLowerCase() === lower);
|
|
484
|
-
const matches = byName.length
|
|
462
|
+
const byName = statuses.nodes.filter((s) => s.name.toLowerCase() === lower);
|
|
463
|
+
const matches = byName.length
|
|
464
|
+
? byName
|
|
465
|
+
: statuses.nodes.filter((s) => s.type.toLowerCase() === lower);
|
|
485
466
|
if (matches.length === 0)
|
|
486
467
|
throw notFound(
|
|
487
|
-
`No project status '${input}'. Available: ${statuses.map((s) => s.name).join(", ")}`,
|
|
468
|
+
`No project status '${input}'. Available: ${statuses.nodes.map((s) => s.name).join(", ")}`,
|
|
488
469
|
);
|
|
489
470
|
if (matches.length > 1)
|
|
490
|
-
throw ambiguous(
|
|
491
|
-
`Multiple project statuses match '${input}': ${matches.map((s) => s.name).join(", ")}`,
|
|
492
|
-
);
|
|
471
|
+
throw ambiguous(`Multiple project statuses match '${input}': ${matches.map((s) => s.name).join(", ")}`);
|
|
493
472
|
return matches[0]!.id;
|
|
494
473
|
}
|
package/src/services/roadmap.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import type { LinearClient } from "@linear/sdk";
|
|
14
14
|
import { withRetry } from "../client.js";
|
|
15
15
|
import { shape } from "../lib/shape.js";
|
|
16
|
-
import { collect
|
|
16
|
+
import { collect } from "../lib/pagination.js";
|
|
17
17
|
import { usageError, notFound, ambiguous } from "../lib/errors.js";
|
|
18
18
|
import { assertMutation, unwrapMutation } from "../lib/mutation.js";
|
|
19
19
|
import { resolveUserId, isUuid } from "../lib/resolve.js";
|
|
@@ -44,9 +44,11 @@ export function toRow(r: any): RoadmapRow {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export async function listRoadmaps(client: LinearClient, limit: number): Promise<RoadmapRow[]> {
|
|
47
|
-
const conn = await withRetry(() =>
|
|
47
|
+
const conn = await withRetry(() =>
|
|
48
|
+
client.roadmaps({ first: limit === Infinity ? 100 : Math.min(limit, 100) }),
|
|
49
|
+
);
|
|
48
50
|
const nodes = await collect(conn as any, limit);
|
|
49
|
-
return
|
|
51
|
+
return nodes.map(toRow);
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
export interface RoadmapDetail {
|
|
@@ -151,10 +153,7 @@ export async function updateRoadmap(client: LinearClient, idArg: string, opts: U
|
|
|
151
153
|
|
|
152
154
|
export async function deleteRoadmap(client: LinearClient, idArg: string) {
|
|
153
155
|
const roadmap = await resolveRoadmap(client, idArg);
|
|
154
|
-
await assertMutation(
|
|
155
|
-
withRetry(() => client.deleteRoadmap(roadmap.id)),
|
|
156
|
-
"Roadmap deletion",
|
|
157
|
-
);
|
|
156
|
+
await assertMutation(withRetry(() => client.deleteRoadmap(roadmap.id)), "Roadmap deletion");
|
|
158
157
|
return roadmap;
|
|
159
158
|
}
|
|
160
159
|
|
package/src/services/state.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import type { LinearClient } from "@linear/sdk";
|
|
11
11
|
import { withRetry } from "../client.js";
|
|
12
12
|
import { shape } from "../lib/shape.js";
|
|
13
|
-
import { collect, pageSize
|
|
13
|
+
import { collect, pageSize } from "../lib/pagination.js";
|
|
14
14
|
import { usageError } from "../lib/errors.js";
|
|
15
15
|
import { resolveTeam, resolveStateId, isUuid } from "../lib/resolve.js";
|
|
16
16
|
|
|
@@ -41,14 +41,8 @@ export async function listStates(
|
|
|
41
41
|
const team = await resolveTeam(client, teamInput, defaultTeamKey);
|
|
42
42
|
const teamModel = await withRetry(() => client.team(team.id));
|
|
43
43
|
const conn = await withRetry(() => teamModel.states({ first: pageSize(limit) }));
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const nodes = await collect(conn as any, Infinity);
|
|
47
|
-
const rows = nodes.map(toRow).sort((a, b) => a.position - b.position);
|
|
48
|
-
return setPaginationMetadata(
|
|
49
|
-
limit === Infinity ? rows : rows.slice(0, limit),
|
|
50
|
-
rows.length > limit,
|
|
51
|
-
);
|
|
44
|
+
const nodes = await collect(conn as any, limit);
|
|
45
|
+
return nodes.map(toRow).sort((a, b) => a.position - b.position);
|
|
52
46
|
}
|
|
53
47
|
|
|
54
48
|
export interface StateDetail {
|
package/src/services/team.ts
CHANGED
|
@@ -9,12 +9,7 @@
|
|
|
9
9
|
import type { LinearClient } from "@linear/sdk";
|
|
10
10
|
import { withRetry } from "../client.js";
|
|
11
11
|
import { shape } from "../lib/shape.js";
|
|
12
|
-
import {
|
|
13
|
-
collect,
|
|
14
|
-
inheritPaginationMetadata,
|
|
15
|
-
pageSize,
|
|
16
|
-
setPaginationMetadata,
|
|
17
|
-
} from "../lib/pagination.js";
|
|
12
|
+
import { collect, pageSize } from "../lib/pagination.js";
|
|
18
13
|
import { usageError } from "../lib/errors.js";
|
|
19
14
|
import { assertMutation, unwrapMutation } from "../lib/mutation.js";
|
|
20
15
|
import { resolveTeam, type ResolvedTeam } from "../lib/resolve.js";
|
|
@@ -28,23 +23,11 @@ export interface TeamRow {
|
|
|
28
23
|
/** The row's shape as `linear commands` advertises it (TES-610); checked against the interface. */
|
|
29
24
|
export const TEAM_ROW_SHAPE = shape<TeamRow>({ id: "string", key: "string", name: "string" });
|
|
30
25
|
|
|
31
|
-
/** Resolve the selected team to its canonical key, UUID, and name. */
|
|
32
|
-
export async function getTeamIdentity(
|
|
33
|
-
client: LinearClient,
|
|
34
|
-
keyArg: string | undefined,
|
|
35
|
-
defaultTeamKey: string | undefined,
|
|
36
|
-
): Promise<ResolvedTeam> {
|
|
37
|
-
return resolveTeam(client, keyArg, defaultTeamKey);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
26
|
/** All teams (key, name, id). */
|
|
41
27
|
export async function listTeams(client: LinearClient, limit: number): Promise<TeamRow[]> {
|
|
42
|
-
const conn = await withRetry(() => client.teams({ first:
|
|
28
|
+
const conn = await withRetry(() => client.teams({ first: limit === Infinity ? 100 : Math.min(limit, 250) }));
|
|
43
29
|
const nodes = await collect(conn as any, limit);
|
|
44
|
-
return
|
|
45
|
-
nodes.map((t: any) => ({ id: t.id, key: t.key, name: t.name })),
|
|
46
|
-
nodes,
|
|
47
|
-
);
|
|
30
|
+
return nodes.map((t: any) => ({ id: t.id, key: t.key, name: t.name }));
|
|
48
31
|
}
|
|
49
32
|
|
|
50
33
|
export interface TeamDetail {
|
|
@@ -145,16 +128,13 @@ export async function listMembers(
|
|
|
145
128
|
(await withRetry(() => team.members({ first: pageSize(limit), includeDisabled }))) as any,
|
|
146
129
|
limit,
|
|
147
130
|
);
|
|
148
|
-
return
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
})),
|
|
156
|
-
nodes,
|
|
157
|
-
);
|
|
131
|
+
return nodes.map((u: any) => ({
|
|
132
|
+
id: u.id,
|
|
133
|
+
displayName: u.displayName,
|
|
134
|
+
name: u.name,
|
|
135
|
+
email: u.email,
|
|
136
|
+
active: !!u.active,
|
|
137
|
+
}));
|
|
158
138
|
}
|
|
159
139
|
|
|
160
140
|
export interface StateRow {
|
|
@@ -181,8 +161,8 @@ export async function listStates(
|
|
|
181
161
|
): Promise<StateRow[]> {
|
|
182
162
|
const resolved = await resolveTeam(client, keyArg, defaultTeamKey);
|
|
183
163
|
const team = await withRetry(() => client.team(resolved.id));
|
|
184
|
-
const nodes = await collect((await withRetry(() => team.states())) as any,
|
|
185
|
-
|
|
164
|
+
const nodes = await collect((await withRetry(() => team.states())) as any, limit);
|
|
165
|
+
return nodes
|
|
186
166
|
.map((s: any) => ({
|
|
187
167
|
id: s.id,
|
|
188
168
|
name: s.name,
|
|
@@ -191,10 +171,6 @@ export async function listStates(
|
|
|
191
171
|
position: s.position,
|
|
192
172
|
}))
|
|
193
173
|
.sort((a: StateRow, b: StateRow) => a.position - b.position);
|
|
194
|
-
return setPaginationMetadata(
|
|
195
|
-
limit === Infinity ? rows : rows.slice(0, limit),
|
|
196
|
-
rows.length > limit,
|
|
197
|
-
);
|
|
198
174
|
}
|
|
199
175
|
|
|
200
176
|
export interface LabelRow {
|
|
@@ -203,11 +179,7 @@ export interface LabelRow {
|
|
|
203
179
|
color: string;
|
|
204
180
|
}
|
|
205
181
|
|
|
206
|
-
export const TEAM_LABEL_ROW_SHAPE = shape<LabelRow>({
|
|
207
|
-
id: "string",
|
|
208
|
-
name: "string",
|
|
209
|
-
color: "string",
|
|
210
|
-
});
|
|
182
|
+
export const TEAM_LABEL_ROW_SHAPE = shape<LabelRow>({ id: "string", name: "string", color: "string" });
|
|
211
183
|
|
|
212
184
|
export async function listLabels(
|
|
213
185
|
client: LinearClient,
|
|
@@ -218,10 +190,7 @@ export async function listLabels(
|
|
|
218
190
|
const resolved = await resolveTeam(client, keyArg, defaultTeamKey);
|
|
219
191
|
const team = await withRetry(() => client.team(resolved.id));
|
|
220
192
|
const nodes = await collect((await withRetry(() => team.labels())) as any, limit);
|
|
221
|
-
return
|
|
222
|
-
nodes.map((l: any) => ({ id: l.id, name: l.name, color: l.color })),
|
|
223
|
-
nodes,
|
|
224
|
-
);
|
|
193
|
+
return nodes.map((l: any) => ({ id: l.id, name: l.name, color: l.color }));
|
|
225
194
|
}
|
|
226
195
|
|
|
227
196
|
export interface CycleRow {
|
|
@@ -248,20 +217,14 @@ export async function listCycles(
|
|
|
248
217
|
): Promise<CycleRow[]> {
|
|
249
218
|
const resolved = await resolveTeam(client, keyArg, defaultTeamKey);
|
|
250
219
|
const team = await withRetry(() => client.team(resolved.id));
|
|
251
|
-
const nodes = await collect((await withRetry(() => team.cycles())) as any,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}))
|
|
260
|
-
.sort((a, b) => b.number - a.number);
|
|
261
|
-
return setPaginationMetadata(
|
|
262
|
-
limit === Infinity ? rows : rows.slice(0, limit),
|
|
263
|
-
rows.length > limit,
|
|
264
|
-
);
|
|
220
|
+
const nodes = await collect((await withRetry(() => team.cycles())) as any, limit);
|
|
221
|
+
return nodes.map((c: any) => ({
|
|
222
|
+
id: c.id,
|
|
223
|
+
number: c.number,
|
|
224
|
+
name: c.name ?? null,
|
|
225
|
+
startsAt: c.startsAt?.toISOString?.() ?? (c.startsAt ? String(c.startsAt) : null),
|
|
226
|
+
endsAt: c.endsAt?.toISOString?.() ?? (c.endsAt ? String(c.endsAt) : null),
|
|
227
|
+
}));
|
|
265
228
|
}
|
|
266
229
|
|
|
267
230
|
export interface CreateTeamOptions {
|
|
@@ -358,10 +321,7 @@ export async function moveTeamIssues(
|
|
|
358
321
|
to: ResolvedTeam,
|
|
359
322
|
): Promise<number> {
|
|
360
323
|
const team = await withRetry(() => client.team(from.id));
|
|
361
|
-
const issues = await collect(
|
|
362
|
-
(await withRetry(() => team.issues({ first: 100 }))) as any,
|
|
363
|
-
Infinity,
|
|
364
|
-
);
|
|
324
|
+
const issues = await collect((await withRetry(() => team.issues({ first: 100 }))) as any, Infinity);
|
|
365
325
|
const ids = issues.map((i: any) => i.id as string);
|
|
366
326
|
for (let i = 0; i < ids.length; i += MOVE_BATCH) {
|
|
367
327
|
const batch = ids.slice(i, i + MOVE_BATCH);
|
|
@@ -379,8 +339,5 @@ export async function moveTeamIssues(
|
|
|
379
339
|
* user as `feature_not_accessible` like every other plan gate.
|
|
380
340
|
*/
|
|
381
341
|
export async function deleteTeam(client: LinearClient, team: ResolvedTeam): Promise<void> {
|
|
382
|
-
await assertMutation(
|
|
383
|
-
withRetry(() => client.deleteTeam(team.id)),
|
|
384
|
-
"Team deletion",
|
|
385
|
-
);
|
|
342
|
+
await assertMutation(withRetry(() => client.deleteTeam(team.id)), "Team deletion");
|
|
386
343
|
}
|
package/src/services/user.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import type { LinearClient } from "@linear/sdk";
|
|
10
10
|
import { withRetry } from "../client.js";
|
|
11
11
|
import { shape } from "../lib/shape.js";
|
|
12
|
-
import { collect,
|
|
12
|
+
import { collect, pageSize } from "../lib/pagination.js";
|
|
13
13
|
import { resolveUserId } from "../lib/resolve.js";
|
|
14
14
|
|
|
15
15
|
export interface UserRow {
|
|
@@ -45,7 +45,7 @@ export async function listUsers(
|
|
|
45
45
|
): Promise<UserRow[]> {
|
|
46
46
|
const conn = await withRetry(() => client.users({ first: pageSize(limit), includeDisabled }));
|
|
47
47
|
const nodes = await collect(conn as any, limit);
|
|
48
|
-
return
|
|
48
|
+
return nodes.map(toRow);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export interface UserDetail {
|
package/src/services/webhook.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
import { type LinearClient, WebhookResourceType } from "@linear/sdk";
|
|
10
10
|
import { withRetry } from "../client.js";
|
|
11
11
|
import { shape } from "../lib/shape.js";
|
|
12
|
-
import { collect
|
|
13
|
-
import { usageError, notFound
|
|
12
|
+
import { collect } from "../lib/pagination.js";
|
|
13
|
+
import { usageError, notFound } from "../lib/errors.js";
|
|
14
14
|
import { assertMutation, unwrapMutation } from "../lib/mutation.js";
|
|
15
15
|
import { resolveTeam } from "../lib/resolve.js";
|
|
16
16
|
|
|
@@ -56,18 +56,17 @@ export const WEBHOOK_ROW_SHAPE = shape<WebhookRow>({
|
|
|
56
56
|
|
|
57
57
|
/** All webhooks in the workspace (relation-light, so no N+1). */
|
|
58
58
|
export async function listWebhooks(client: LinearClient, limit: number): Promise<WebhookRow[]> {
|
|
59
|
-
const conn = await withRetry(() =>
|
|
60
|
-
|
|
61
|
-
return inheritPaginationMetadata(
|
|
62
|
-
nodes.map((w: any) => ({
|
|
63
|
-
id: w.id,
|
|
64
|
-
url: w.url ?? null,
|
|
65
|
-
enabled: w.enabled,
|
|
66
|
-
resourceTypes: w.resourceTypes ?? [],
|
|
67
|
-
label: w.label ?? null,
|
|
68
|
-
})),
|
|
69
|
-
nodes,
|
|
59
|
+
const conn = await withRetry(() =>
|
|
60
|
+
client.webhooks({ first: limit === Infinity ? 100 : Math.min(limit, 100) }),
|
|
70
61
|
);
|
|
62
|
+
const nodes = await collect(conn as any, limit);
|
|
63
|
+
return nodes.map((w: any) => ({
|
|
64
|
+
id: w.id,
|
|
65
|
+
url: w.url ?? null,
|
|
66
|
+
enabled: w.enabled,
|
|
67
|
+
resourceTypes: w.resourceTypes ?? [],
|
|
68
|
+
label: w.label ?? null,
|
|
69
|
+
}));
|
|
71
70
|
}
|
|
72
71
|
|
|
73
72
|
export interface WebhookDetail {
|
|
@@ -175,9 +174,7 @@ export async function updateWebhook(client: LinearClient, id: string, opts: Upda
|
|
|
175
174
|
if (opts.secret !== undefined) input.secret = opts.secret;
|
|
176
175
|
|
|
177
176
|
if (Object.keys(input).length === 0)
|
|
178
|
-
throw usageError(
|
|
179
|
-
"Nothing to update; pass at least one of --url, --enabled/--disabled, --resource.",
|
|
180
|
-
);
|
|
177
|
+
throw usageError("Nothing to update; pass at least one of --url, --enabled/--disabled, --resource.");
|
|
181
178
|
|
|
182
179
|
return unwrapMutation(
|
|
183
180
|
withRetry(() => client.updateWebhook(id, input as any)),
|
|
@@ -187,17 +184,9 @@ export async function updateWebhook(client: LinearClient, id: string, opts: Upda
|
|
|
187
184
|
}
|
|
188
185
|
|
|
189
186
|
export async function deleteWebhook(client: LinearClient, id: string): Promise<WebhookDetail> {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const normalized = normalizeError(err);
|
|
195
|
-
if (normalized.code === "not_found") throw notFound(`No webhook matching '${id}'.`);
|
|
196
|
-
throw normalized;
|
|
197
|
-
}
|
|
198
|
-
await assertMutation(
|
|
199
|
-
withRetry(() => client.deleteWebhook(id)),
|
|
200
|
-
"Webhook deletion",
|
|
201
|
-
);
|
|
187
|
+
const webhook = await getWebhookDetail(client, id).catch(() => {
|
|
188
|
+
throw notFound(`No webhook matching '${id}'.`);
|
|
189
|
+
});
|
|
190
|
+
await assertMutation(withRetry(() => client.deleteWebhook(id)), "Webhook deletion");
|
|
202
191
|
return webhook;
|
|
203
192
|
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# linear open
|
|
2
|
-
|
|
3
|
-
> Open the workspace, an issue, team, project, or URL
|
|
4
|
-
|
|
5
|
-
_Generated from `linear commands --json`. `linear open --help` (or `<subcommand> --help`) is authoritative._
|
|
6
|
-
|
|
7
|
-
Every command also accepts the global flags `-j/--json`, `--no-input`, `-y/--yes`, `-q/--quiet`, `--workspace <slug>`, `--api-key <key>`, `-t/--team <key>`, `-n/--limit <n>`, `--all`, `-f/--fields <a,b,c>`, `--no-ansi` (alias `--no-color`), and `--debug`. Only command-specific options are listed below.
|
|
8
|
-
|
|
9
|
-
### `linear open`
|
|
10
|
-
|
|
11
|
-
Open the workspace, an issue, team, project, or URL
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
linear open [options] [target]
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
| Option | Description |
|
|
18
|
-
| ----------- | ----------------------------------------- |
|
|
19
|
-
| `--app` | open in Linear.app (macOS) |
|
|
20
|
-
| `-w, --web` | open in the default browser (the default) |
|
|
21
|
-
|
|
22
|
-
**Output (`--json`)**: a receipt object
|
|
23
|
-
|
|
24
|
-
```text
|
|
25
|
-
target: string
|
|
26
|
-
url: string
|
|
27
|
-
label: string
|
|
28
|
-
opened: boolean
|
|
29
|
-
```
|
package/src/commands/open.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/** `linear open` — open a workspace, team, project, issue, or explicit URL. */
|
|
2
|
-
|
|
3
|
-
import { Command } from "commander";
|
|
4
|
-
import { action } from "../lib/action.js";
|
|
5
|
-
import { usageError } from "../lib/errors.js";
|
|
6
|
-
import { openUrl } from "../lib/open.js";
|
|
7
|
-
import { normalizeIssueReference, resolveProjectId, resolveTeam } from "../lib/resolve.js";
|
|
8
|
-
import type { Context } from "../context.js";
|
|
9
|
-
import { getIssueDetail } from "../services/issue.js";
|
|
10
|
-
import { getOrganizationDetail } from "../services/organization.js";
|
|
11
|
-
import { getProjectDetail } from "../services/project.js";
|
|
12
|
-
|
|
13
|
-
const ISSUE_REFERENCE = /^[a-zA-Z][a-zA-Z0-9]*-\d+$/;
|
|
14
|
-
const URL = /^https?:\/\//i;
|
|
15
|
-
type OpenTarget = "workspace" | "team" | "project" | "issue" | "url";
|
|
16
|
-
|
|
17
|
-
export function registerOpen(program: Command): void {
|
|
18
|
-
program
|
|
19
|
-
.command("open [target]")
|
|
20
|
-
.description("Open the workspace, an issue, team, project, or URL")
|
|
21
|
-
.option("--app", "open in Linear.app (macOS)")
|
|
22
|
-
.option("-w, --web", "open in the default browser (the default)")
|
|
23
|
-
.addHelpText(
|
|
24
|
-
"after",
|
|
25
|
-
[
|
|
26
|
-
"",
|
|
27
|
-
"Targets: an issue id (TES-123), `team:<key>`, `project:<name>`, or an https URL.",
|
|
28
|
-
"With no target, opens the current workspace. A bare team key opens that team.",
|
|
29
|
-
].join("\n"),
|
|
30
|
-
)
|
|
31
|
-
.action(
|
|
32
|
-
action(async (ctx: Context, opts, target?: string) => {
|
|
33
|
-
if (opts.app && opts.web) throw usageError("Pass either --web or --app, not both.");
|
|
34
|
-
const resolved = await resolveOpenTarget(ctx, target);
|
|
35
|
-
await openUrl(resolved.url, { app: opts.app === true });
|
|
36
|
-
ctx.output.emit({ ...resolved, opened: true }, () =>
|
|
37
|
-
ctx.output.success(
|
|
38
|
-
`Opened ${resolved.label} in ${opts.app ? "Linear.app" : "the browser"}`,
|
|
39
|
-
),
|
|
40
|
-
);
|
|
41
|
-
}),
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function resolveOpenTarget(
|
|
46
|
-
ctx: Context,
|
|
47
|
-
target: string | undefined,
|
|
48
|
-
): Promise<{ target: OpenTarget; url: string; label: string }> {
|
|
49
|
-
if (!target) {
|
|
50
|
-
const org = await getOrganizationDetail(ctx.client);
|
|
51
|
-
return { target: "workspace", url: `https://linear.app/${org.urlKey}`, label: org.name };
|
|
52
|
-
}
|
|
53
|
-
if (URL.test(target)) return { target: "url", url: target, label: target };
|
|
54
|
-
if (target.startsWith("team:")) {
|
|
55
|
-
const team = await resolveTeam(ctx.client, target.slice("team:".length), ctx.defaultTeam);
|
|
56
|
-
const org = await getOrganizationDetail(ctx.client);
|
|
57
|
-
return {
|
|
58
|
-
target: "team",
|
|
59
|
-
url: `https://linear.app/${org.urlKey}/team/${team.key}/all`,
|
|
60
|
-
label: `${team.key} (${team.name})`,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
if (target.startsWith("project:")) {
|
|
64
|
-
const project = await getProjectDetail(
|
|
65
|
-
ctx.client,
|
|
66
|
-
await resolveProjectId(ctx.client, target.slice("project:".length)),
|
|
67
|
-
);
|
|
68
|
-
return { target: "project", url: project.url, label: project.name };
|
|
69
|
-
}
|
|
70
|
-
const issueReference = /^\d+$/.test(target)
|
|
71
|
-
? normalizeIssueReference(target, ctx.defaultTeam)
|
|
72
|
-
: target;
|
|
73
|
-
if (ISSUE_REFERENCE.test(issueReference)) {
|
|
74
|
-
const issue = await getIssueDetail(ctx.client, issueReference, { includeComments: false });
|
|
75
|
-
return { target: "issue", url: issue.url, label: issue.identifier };
|
|
76
|
-
}
|
|
77
|
-
const team = await resolveTeam(ctx.client, target, ctx.defaultTeam);
|
|
78
|
-
const org = await getOrganizationDetail(ctx.client);
|
|
79
|
-
return {
|
|
80
|
-
target: "team",
|
|
81
|
-
url: `https://linear.app/${org.urlKey}/team/${team.key}/all`,
|
|
82
|
-
label: `${team.key} (${team.name})`,
|
|
83
|
-
};
|
|
84
|
-
}
|
package/src/lib/open.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/** Open a Linear URL in the system browser or the macOS Linear app. */
|
|
2
|
-
|
|
3
|
-
import { execFile } from "node:child_process";
|
|
4
|
-
import { CliError, usageError } from "./errors.js";
|
|
5
|
-
|
|
6
|
-
export async function openUrl(url: string, opts: { app?: boolean } = {}): Promise<void> {
|
|
7
|
-
if (opts.app && process.platform !== "darwin") {
|
|
8
|
-
throw usageError("--app currently requires macOS with Linear.app installed; use --web here.");
|
|
9
|
-
}
|
|
10
|
-
const command =
|
|
11
|
-
process.platform === "darwin"
|
|
12
|
-
? "open"
|
|
13
|
-
: process.platform === "win32"
|
|
14
|
-
? "rundll32.exe"
|
|
15
|
-
: "xdg-open";
|
|
16
|
-
const args =
|
|
17
|
-
process.platform === "darwin"
|
|
18
|
-
? opts.app
|
|
19
|
-
? ["-a", "Linear", url]
|
|
20
|
-
: [url]
|
|
21
|
-
: process.platform === "win32"
|
|
22
|
-
? ["url.dll,FileProtocolHandler", url]
|
|
23
|
-
: [url];
|
|
24
|
-
|
|
25
|
-
await new Promise<void>((resolve, reject) => {
|
|
26
|
-
execFile(command, args, { encoding: "utf8" }, (err, _stdout, stderr) => {
|
|
27
|
-
if (!err) return resolve();
|
|
28
|
-
const detail = stderr.trim();
|
|
29
|
-
reject(new CliError(detail || `Failed to open ${url}: ${err.message}`, "runtime"));
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
}
|
package/src/output/color.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolve terminal colour and debug policy in one place. Both the per-command
|
|
3
|
-
* Context and the top-level error boundary use these helpers, so a parse-time
|
|
4
|
-
* failure behaves exactly like an API result.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export interface ColorPolicy {
|
|
8
|
-
/** An explicit CLI opt-out (`--no-ansi` / `--no-color`). */
|
|
9
|
-
disabled?: boolean;
|
|
10
|
-
/** JSON is a byte-oriented machine format and never carries terminal ANSI. */
|
|
11
|
-
json?: boolean;
|
|
12
|
-
/** TTY state of the stream that will receive the text. */
|
|
13
|
-
isTTY?: boolean;
|
|
14
|
-
env?: NodeJS.ProcessEnv;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Values conventionally used to turn a forcing variable off. */
|
|
18
|
-
function isFalse(value: string): boolean {
|
|
19
|
-
return value === "0" || value.toLowerCase() === "false";
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Whether human output should contain ANSI colour.
|
|
24
|
-
*
|
|
25
|
-
* Precedence is deliberately conservative:
|
|
26
|
-
*
|
|
27
|
-
* CLI opt-out / JSON > NO_COLOR > FORCE_COLOR > CLICOLOR_FORCE > TTY
|
|
28
|
-
*
|
|
29
|
-
* `FORCE_COLOR=0` is also an explicit opt-out, matching the convention used by
|
|
30
|
-
* Node tooling. `CLICOLOR_FORCE=0` merely means "do not force". This lets a
|
|
31
|
-
* caller keep colour while stdout is connected to a pager without ever
|
|
32
|
-
* putting ANSI into JSON.
|
|
33
|
-
*/
|
|
34
|
-
export function shouldUseColor(policy: ColorPolicy): boolean {
|
|
35
|
-
if (policy.disabled === true || policy.json === true) return false;
|
|
36
|
-
|
|
37
|
-
const env = policy.env ?? process.env;
|
|
38
|
-
if (env.NO_COLOR !== undefined) return false;
|
|
39
|
-
|
|
40
|
-
if (env.FORCE_COLOR !== undefined) {
|
|
41
|
-
return !isFalse(env.FORCE_COLOR);
|
|
42
|
-
}
|
|
43
|
-
if (env.CLICOLOR_FORCE !== undefined && !isFalse(env.CLICOLOR_FORCE)) return true;
|
|
44
|
-
|
|
45
|
-
return policy.isTTY === true;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** `--debug` and LINEAR_DEBUG are equivalent, with the flag always additive. */
|
|
49
|
-
export function isDebugEnabled(explicit: boolean | undefined, env = process.env): boolean {
|
|
50
|
-
const value = env.LINEAR_DEBUG?.toLowerCase();
|
|
51
|
-
return explicit === true || value === "1" || value === "true";
|
|
52
|
-
}
|