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.
Files changed (71) hide show
  1. package/CHANGELOG.md +46 -46
  2. package/COVERAGE.md +5 -5
  3. package/MIGRATING.md +69 -71
  4. package/README.md +88 -96
  5. package/package.json +8 -9
  6. package/skills/linear-sdk-cli/SKILL.md +3 -13
  7. package/skills/linear-sdk-cli/references/api.md +9 -9
  8. package/skills/linear-sdk-cli/references/auth.md +0 -18
  9. package/skills/linear-sdk-cli/references/commands.md +2 -2
  10. package/skills/linear-sdk-cli/references/config.md +11 -9
  11. package/skills/linear-sdk-cli/references/issue.md +12 -63
  12. package/skills/linear-sdk-cli/references/team.md +0 -16
  13. package/skills/linear-sdk-cli/scripts/generate-docs.ts +2 -6
  14. package/src/bin/linear.ts +6 -16
  15. package/src/cli.ts +1 -3
  16. package/src/commands/agent-session.ts +5 -13
  17. package/src/commands/api.ts +9 -15
  18. package/src/commands/attach.ts +4 -15
  19. package/src/commands/comment.ts +2 -6
  20. package/src/commands/completion.ts +6 -2
  21. package/src/commands/cycle.ts +1 -6
  22. package/src/commands/discover.ts +2 -6
  23. package/src/commands/document.ts +1 -4
  24. package/src/commands/initiative.ts +1 -5
  25. package/src/commands/issue.ts +88 -376
  26. package/src/commands/meta.ts +31 -139
  27. package/src/commands/milestone.ts +1 -4
  28. package/src/commands/notification.ts +6 -15
  29. package/src/commands/project.ts +4 -17
  30. package/src/commands/team.ts +0 -13
  31. package/src/commands/webhook.ts +2 -12
  32. package/src/config.ts +183 -500
  33. package/src/context.ts +4 -13
  34. package/src/git.ts +3 -28
  35. package/src/lib/body.ts +2 -26
  36. package/src/lib/errors.ts +7 -32
  37. package/src/lib/introspect.ts +7 -76
  38. package/src/lib/keyring.ts +9 -51
  39. package/src/lib/options.ts +12 -108
  40. package/src/lib/output-shapes.ts +9 -81
  41. package/src/lib/pagination.ts +17 -50
  42. package/src/lib/resolve.ts +7 -40
  43. package/src/lib/shape.ts +12 -16
  44. package/src/lib/upload.ts +1 -3
  45. package/src/output/format.ts +18 -96
  46. package/src/output/table.ts +7 -7
  47. package/src/services/agent-session.ts +3 -11
  48. package/src/services/attachment.ts +5 -14
  49. package/src/services/comment.ts +4 -12
  50. package/src/services/cycle.ts +4 -11
  51. package/src/services/document.ts +10 -29
  52. package/src/services/favorite.ts +9 -15
  53. package/src/services/initiative-update.ts +15 -16
  54. package/src/services/initiative.ts +10 -15
  55. package/src/services/issue.ts +18 -262
  56. package/src/services/label.ts +2 -12
  57. package/src/services/milestone.ts +5 -8
  58. package/src/services/notification.ts +2 -16
  59. package/src/services/organization.ts +18 -23
  60. package/src/services/project.ts +34 -55
  61. package/src/services/roadmap.ts +6 -7
  62. package/src/services/state.ts +3 -9
  63. package/src/services/team.ts +24 -67
  64. package/src/services/user.ts +2 -2
  65. package/src/services/webhook.ts +17 -28
  66. package/skills/linear-sdk-cli/references/open.md +0 -29
  67. package/src/commands/open.ts +0 -84
  68. package/src/lib/open.ts +0 -32
  69. package/src/output/color.ts +0 -52
  70. package/src/output/markdown.ts +0 -133
  71. package/src/output/pager.ts +0 -131
@@ -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(() => project.projectMilestones({ first: pageSize(limit) }));
394
- const nodes = await collect(conn as any, limit);
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(() => project.projectUpdates({ first: pageSize(limit) }));
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 inheritPaginationMetadata(
424
- await Promise.all(
425
- nodes.map(async (u: any) => {
426
- const user = await u.user;
427
- return {
428
- id: u.id,
429
- createdAt: u.createdAt?.toISOString?.() ?? String(u.createdAt),
430
- user: user?.displayName ?? "unknown",
431
- body: u.body ?? "",
432
- health: u.health ?? null,
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 conn = await withRetry(() => client.projectStatuses({ first: 250 }));
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 ? byName : statuses.filter((s) => s.type.toLowerCase() === lower);
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
  }
@@ -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, inheritPaginationMetadata, pageSize } from "../lib/pagination.js";
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(() => client.roadmaps({ first: pageSize(limit) }));
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 inheritPaginationMetadata(nodes.map(toRow), nodes);
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
 
@@ -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, setPaginationMetadata } from "../lib/pagination.js";
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
- // The API's connection order is unrelated to workflow position. Sort the
45
- // complete small state set, then apply the caller's limit.
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 {
@@ -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: pageSize(limit) }));
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 inheritPaginationMetadata(
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 inheritPaginationMetadata(
149
- nodes.map((u: any) => ({
150
- id: u.id,
151
- displayName: u.displayName,
152
- name: u.name,
153
- email: u.email,
154
- active: !!u.active,
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, Infinity);
185
- const rows = nodes
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 inheritPaginationMetadata(
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, Infinity);
252
- const rows = nodes
253
- .map((c: any) => ({
254
- id: c.id,
255
- number: c.number,
256
- name: c.name ?? null,
257
- startsAt: c.startsAt?.toISOString?.() ?? (c.startsAt ? String(c.startsAt) : null),
258
- endsAt: c.endsAt?.toISOString?.() ?? (c.endsAt ? String(c.endsAt) : null),
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
  }
@@ -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, inheritPaginationMetadata, pageSize } from "../lib/pagination.js";
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 inheritPaginationMetadata(nodes.map(toRow), nodes);
48
+ return nodes.map(toRow);
49
49
  }
50
50
 
51
51
  export interface UserDetail {
@@ -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, inheritPaginationMetadata, pageSize } from "../lib/pagination.js";
13
- import { usageError, notFound, normalizeError } from "../lib/errors.js";
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(() => client.webhooks({ first: pageSize(limit) }));
60
- const nodes = await collect(conn as any, limit);
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
- let webhook: WebhookDetail;
191
- try {
192
- webhook = await getWebhookDetail(client, id);
193
- } catch (err) {
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
- ```
@@ -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
- }
@@ -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
- }