linear-sdk-cli 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -46
- package/COVERAGE.md +5 -5
- package/MIGRATING.md +69 -71
- package/README.md +88 -96
- package/package.json +8 -9
- package/skills/linear-sdk-cli/SKILL.md +3 -13
- package/skills/linear-sdk-cli/references/api.md +9 -9
- package/skills/linear-sdk-cli/references/auth.md +0 -18
- package/skills/linear-sdk-cli/references/commands.md +2 -2
- package/skills/linear-sdk-cli/references/config.md +11 -9
- package/skills/linear-sdk-cli/references/issue.md +12 -63
- package/skills/linear-sdk-cli/references/team.md +0 -16
- package/skills/linear-sdk-cli/scripts/generate-docs.ts +2 -6
- package/src/bin/linear.ts +6 -16
- package/src/cli.ts +1 -3
- package/src/commands/agent-session.ts +5 -13
- package/src/commands/api.ts +9 -15
- package/src/commands/attach.ts +4 -15
- package/src/commands/comment.ts +2 -6
- package/src/commands/completion.ts +6 -2
- package/src/commands/cycle.ts +1 -6
- package/src/commands/discover.ts +2 -6
- package/src/commands/document.ts +1 -4
- package/src/commands/initiative.ts +1 -5
- package/src/commands/issue.ts +88 -376
- package/src/commands/meta.ts +31 -139
- package/src/commands/milestone.ts +1 -4
- package/src/commands/notification.ts +6 -15
- package/src/commands/project.ts +4 -17
- package/src/commands/team.ts +0 -13
- package/src/commands/webhook.ts +2 -12
- package/src/config.ts +183 -500
- package/src/context.ts +4 -13
- package/src/git.ts +3 -28
- package/src/lib/body.ts +2 -26
- package/src/lib/errors.ts +7 -32
- package/src/lib/introspect.ts +7 -76
- package/src/lib/keyring.ts +9 -51
- package/src/lib/options.ts +12 -108
- package/src/lib/output-shapes.ts +9 -81
- package/src/lib/pagination.ts +17 -50
- package/src/lib/resolve.ts +7 -40
- package/src/lib/shape.ts +12 -16
- package/src/lib/upload.ts +1 -3
- package/src/output/format.ts +18 -96
- package/src/output/table.ts +7 -7
- package/src/services/agent-session.ts +3 -11
- package/src/services/attachment.ts +5 -14
- package/src/services/comment.ts +4 -12
- package/src/services/cycle.ts +4 -11
- package/src/services/document.ts +10 -29
- package/src/services/favorite.ts +9 -15
- package/src/services/initiative-update.ts +15 -16
- package/src/services/initiative.ts +10 -15
- package/src/services/issue.ts +18 -262
- package/src/services/label.ts +2 -12
- package/src/services/milestone.ts +5 -8
- package/src/services/notification.ts +2 -16
- package/src/services/organization.ts +18 -23
- package/src/services/project.ts +34 -55
- package/src/services/roadmap.ts +6 -7
- package/src/services/state.ts +3 -9
- package/src/services/team.ts +24 -67
- package/src/services/user.ts +2 -2
- package/src/services/webhook.ts +17 -28
- package/skills/linear-sdk-cli/references/open.md +0 -29
- package/src/commands/open.ts +0 -84
- package/src/lib/open.ts +0 -32
- package/src/output/color.ts +0 -52
- package/src/output/markdown.ts +0 -133
- package/src/output/pager.ts +0 -131
package/src/output/table.ts
CHANGED
|
@@ -148,14 +148,15 @@ export function selectPairs(
|
|
|
148
148
|
export function renderTable<T>(
|
|
149
149
|
rows: T[],
|
|
150
150
|
columns: Column<T>[],
|
|
151
|
-
opts: { color?: boolean
|
|
151
|
+
opts: { color?: boolean } = {},
|
|
152
152
|
): string {
|
|
153
153
|
const color = opts.color ?? false;
|
|
154
|
-
|
|
155
|
-
if (rows.length === 0) return colors.dim(sanitizeForTerminal(opts.empty ?? "(no results)"));
|
|
154
|
+
if (rows.length === 0) return color ? pc.dim("(no results)") : "(no results)";
|
|
156
155
|
|
|
157
156
|
const headers = columns.map((c) => c.header ?? c.key);
|
|
158
|
-
const body = rows.map((row) =>
|
|
157
|
+
const body = rows.map((row) =>
|
|
158
|
+
columns.map((c) => truncate(cell(c.value(row)), c.max ?? 60)),
|
|
159
|
+
);
|
|
159
160
|
|
|
160
161
|
const widths = columns.map((_, i) => {
|
|
161
162
|
const header = headers[i] ?? "";
|
|
@@ -166,7 +167,7 @@ export function renderTable<T>(
|
|
|
166
167
|
const pad = (s: string, w: number) => s + " ".repeat(Math.max(0, w - displayWidth(s)));
|
|
167
168
|
|
|
168
169
|
const headerLine = headers
|
|
169
|
-
.map((h, i) => pad(
|
|
170
|
+
.map((h, i) => pad(color ? pc.bold(h) : h, widths[i] ?? 0))
|
|
170
171
|
.join(" ")
|
|
171
172
|
.trimEnd();
|
|
172
173
|
|
|
@@ -186,13 +187,12 @@ export function renderDetail(
|
|
|
186
187
|
opts: { color?: boolean } = {},
|
|
187
188
|
): string {
|
|
188
189
|
const color = opts.color ?? false;
|
|
189
|
-
const colors = pc.createColors(color);
|
|
190
190
|
const visible = pairs.filter(([, v]) => v !== undefined && v !== null);
|
|
191
191
|
const keyWidth = visible.reduce((m, [k]) => Math.max(m, k.length), 0);
|
|
192
192
|
return visible
|
|
193
193
|
.map(([k, v]) => {
|
|
194
194
|
const label = (k + ":").padEnd(keyWidth + 1);
|
|
195
|
-
return `${
|
|
195
|
+
return `${color ? pc.dim(label) : label} ${cell(v)}`;
|
|
196
196
|
})
|
|
197
197
|
.join("\n");
|
|
198
198
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import type { LinearClient } from "@linear/sdk";
|
|
15
15
|
import { withRetry } from "../client.js";
|
|
16
16
|
import { shape } from "../lib/shape.js";
|
|
17
|
-
import { collectRawQuery
|
|
17
|
+
import { collectRawQuery } from "../lib/pagination.js";
|
|
18
18
|
import { notFound } from "../lib/errors.js";
|
|
19
19
|
|
|
20
20
|
/** The statuses Linear reports for a session (AgentSessionStatus). */
|
|
@@ -35,11 +35,7 @@ export interface SessionUser {
|
|
|
35
35
|
displayName: string;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
const SESSION_USER_SHAPE = shape<SessionUser>({
|
|
39
|
-
id: "string",
|
|
40
|
-
name: "string",
|
|
41
|
-
displayName: "string",
|
|
42
|
-
});
|
|
38
|
+
const SESSION_USER_SHAPE = shape<SessionUser>({ id: "string", name: "string", displayName: "string" });
|
|
43
39
|
|
|
44
40
|
export interface AgentSessionRow {
|
|
45
41
|
id: string;
|
|
@@ -158,11 +154,7 @@ function finish(
|
|
|
158
154
|
const filtered = opts.status ? present.filter((r) => r.status === opts.status) : present;
|
|
159
155
|
// Newest first, whichever query found them (an issue's comments come oldest first).
|
|
160
156
|
filtered.sort((a, b) => (a.createdAt < b.createdAt ? 1 : a.createdAt > b.createdAt ? -1 : 0));
|
|
161
|
-
|
|
162
|
-
return setPaginationMetadata(
|
|
163
|
-
filtered.slice(0, limit),
|
|
164
|
-
filtered.length > limit || (!opts.status && filtered.length >= limit && hasMoreResults(rows)),
|
|
165
|
-
);
|
|
157
|
+
return limit === Infinity ? filtered : filtered.slice(0, limit);
|
|
166
158
|
}
|
|
167
159
|
|
|
168
160
|
function toRow(s: any): AgentSessionRow {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import type { LinearClient } from "@linear/sdk";
|
|
12
12
|
import { withRetry } from "../client.js";
|
|
13
13
|
import { shape } from "../lib/shape.js";
|
|
14
|
-
import { collect,
|
|
14
|
+
import { collect, pageSize } from "../lib/pagination.js";
|
|
15
15
|
import { notFound, usageError } from "../lib/errors.js";
|
|
16
16
|
import { assertMutation, unwrapMutation } from "../lib/mutation.js";
|
|
17
17
|
import { resolveIssue } from "../lib/resolve.js";
|
|
@@ -65,7 +65,7 @@ export async function listAttachments(
|
|
|
65
65
|
const issue = await resolveIssue(client, issueArg);
|
|
66
66
|
const conn = await withRetry(() => issue.attachments({ first: pageSize(limit) }));
|
|
67
67
|
const nodes = await collect(conn as any, limit);
|
|
68
|
-
return
|
|
68
|
+
return nodes.map(toRow);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
export interface CreateOptions {
|
|
@@ -128,15 +128,9 @@ export async function attachFiles(
|
|
|
128
128
|
issueArg: string,
|
|
129
129
|
paths: string[],
|
|
130
130
|
opts: AttachFilesOptions,
|
|
131
|
-
): Promise<{
|
|
132
|
-
issue: { id: string; identifier: string };
|
|
133
|
-
attachments: AttachedFile[];
|
|
134
|
-
comment?: { id: string; url: string };
|
|
135
|
-
}> {
|
|
131
|
+
): Promise<{ issue: { id: string; identifier: string }; attachments: AttachedFile[]; comment?: { id: string; url: string } }> {
|
|
136
132
|
if (opts.title !== undefined && paths.length > 1) {
|
|
137
|
-
throw usageError(
|
|
138
|
-
"--title names a single attachment; with several files, drop it to use each file's name.",
|
|
139
|
-
);
|
|
133
|
+
throw usageError("--title names a single attachment; with several files, drop it to use each file's name.");
|
|
140
134
|
}
|
|
141
135
|
validateUploads(paths, { public: opts.public });
|
|
142
136
|
const issue = await resolveIssue(client, issueArg);
|
|
@@ -184,10 +178,7 @@ export async function attachFiles(
|
|
|
184
178
|
export async function deleteAttachment(client: LinearClient, id: string) {
|
|
185
179
|
const attachment = await withRetry(() => client.attachment(id));
|
|
186
180
|
if (!attachment) throw notFound(`No attachment ${id}.`);
|
|
187
|
-
await assertMutation(
|
|
188
|
-
withRetry(() => client.deleteAttachment(id)),
|
|
189
|
-
"Attachment deletion",
|
|
190
|
-
);
|
|
181
|
+
await assertMutation(withRetry(() => client.deleteAttachment(id)), "Attachment deletion");
|
|
191
182
|
return { id: attachment.id, title: attachment.title };
|
|
192
183
|
}
|
|
193
184
|
|
package/src/services/comment.ts
CHANGED
|
@@ -210,12 +210,9 @@ export async function updateComment(
|
|
|
210
210
|
current?: string,
|
|
211
211
|
) {
|
|
212
212
|
if (body.trim() === "") {
|
|
213
|
-
throw usageError(
|
|
214
|
-
"Refusing to blank the comment body. To remove a comment, use 'comment delete'.",
|
|
215
|
-
);
|
|
213
|
+
throw usageError("Refusing to blank the comment body. To remove a comment, use 'comment delete'.");
|
|
216
214
|
}
|
|
217
|
-
if (current !== undefined && body === current)
|
|
218
|
-
throw usageError("Comment body unchanged; nothing to update.");
|
|
215
|
+
if (current !== undefined && body === current) throw usageError("Comment body unchanged; nothing to update.");
|
|
219
216
|
return unwrapMutation(
|
|
220
217
|
withRetry(() => client.updateComment(commentId, { body })),
|
|
221
218
|
"comment",
|
|
@@ -224,19 +221,14 @@ export async function updateComment(
|
|
|
224
221
|
}
|
|
225
222
|
|
|
226
223
|
export async function deleteComment(client: LinearClient, commentId: string) {
|
|
227
|
-
await assertMutation(
|
|
228
|
-
withRetry(() => client.deleteComment(commentId)),
|
|
229
|
-
"Comment deletion",
|
|
230
|
-
);
|
|
224
|
+
await assertMutation(withRetry(() => client.deleteComment(commentId)), "Comment deletion");
|
|
231
225
|
return { id: commentId };
|
|
232
226
|
}
|
|
233
227
|
|
|
234
228
|
/** Resolve (or unresolve) a comment thread. */
|
|
235
229
|
export async function setResolved(client: LinearClient, commentId: string, resolved: boolean) {
|
|
236
230
|
return unwrapMutation(
|
|
237
|
-
withRetry(() =>
|
|
238
|
-
resolved ? client.commentResolve(commentId) : client.commentUnresolve(commentId),
|
|
239
|
-
),
|
|
231
|
+
withRetry(() => (resolved ? client.commentResolve(commentId) : client.commentUnresolve(commentId))),
|
|
240
232
|
"comment",
|
|
241
233
|
`Comment ${resolved ? "resolve" : "unresolve"}`,
|
|
242
234
|
);
|
package/src/services/cycle.ts
CHANGED
|
@@ -8,7 +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 { collect, pageSize
|
|
11
|
+
import { collect, pageSize } from "../lib/pagination.js";
|
|
12
12
|
import { usageError, notFound } from "../lib/errors.js";
|
|
13
13
|
import { unwrapMutation } from "../lib/mutation.js";
|
|
14
14
|
import { resolveTeam, resolveCycleId, isUuid } from "../lib/resolve.js";
|
|
@@ -44,16 +44,11 @@ export async function listCycles(
|
|
|
44
44
|
const team = await resolveTeam(client, teamInput, defaultTeamKey);
|
|
45
45
|
const teamModel = await withRetry(() => client.team(team.id));
|
|
46
46
|
const conn = await withRetry(() => teamModel.cycles({ first: pageSize(limit) }));
|
|
47
|
-
|
|
48
|
-
// `--limit` means the newest cycles, not an arbitrary API prefix.
|
|
49
|
-
const nodes = await collect(conn as any, Infinity);
|
|
47
|
+
const nodes = await collect(conn as any, limit);
|
|
50
48
|
const rows = nodes.map(toRow);
|
|
51
49
|
// Surface most-recent (highest number) cycle first.
|
|
52
50
|
rows.sort((a, b) => b.number - a.number);
|
|
53
|
-
return
|
|
54
|
-
limit === Infinity ? rows : rows.slice(0, limit),
|
|
55
|
-
rows.length > limit,
|
|
56
|
-
);
|
|
51
|
+
return rows;
|
|
57
52
|
}
|
|
58
53
|
|
|
59
54
|
/** The team's currently active cycle, if any. */
|
|
@@ -179,9 +174,7 @@ async function resolveCycleArg(
|
|
|
179
174
|
if (isUuid(idArg)) return idArg;
|
|
180
175
|
const teamKey = teamInput ?? defaultTeamKey;
|
|
181
176
|
if (!teamKey)
|
|
182
|
-
throw usageError(
|
|
183
|
-
"Resolving a cycle by number or 'current' requires --team (or pass a cycle id).",
|
|
184
|
-
);
|
|
177
|
+
throw usageError("Resolving a cycle by number or 'current' requires --team (or pass a cycle id).");
|
|
185
178
|
const team = await resolveTeam(client, teamKey, undefined);
|
|
186
179
|
return resolveCycleId(client, team.id, idArg);
|
|
187
180
|
}
|
package/src/services/document.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import type { LinearClient } from "@linear/sdk";
|
|
20
20
|
import { withRetry } from "../client.js";
|
|
21
21
|
import { collectRawQuery } from "../lib/pagination.js";
|
|
22
|
-
import { usageError, notFound
|
|
22
|
+
import { usageError, notFound } from "../lib/errors.js";
|
|
23
23
|
import { assertMutation, unwrapMutation } from "../lib/mutation.js";
|
|
24
24
|
import {
|
|
25
25
|
resolveProjectId,
|
|
@@ -100,9 +100,7 @@ export async function resolveTarget(
|
|
|
100
100
|
case "cycle": {
|
|
101
101
|
const teamKey = sel.team ?? defaultTeamKey;
|
|
102
102
|
if (!teamKey)
|
|
103
|
-
throw usageError(
|
|
104
|
-
"--cycle needs a team to look the cycle up in: pass --team <KEY> or set a default team.",
|
|
105
|
-
);
|
|
103
|
+
throw usageError("--cycle needs a team to look the cycle up in: pass --team <KEY> or set a default team.");
|
|
106
104
|
const team = await resolveTeam(client, teamKey, undefined);
|
|
107
105
|
return { kind: "cycle", id: await resolveCycleId(client, team.id, sel.value) };
|
|
108
106
|
}
|
|
@@ -171,9 +169,7 @@ function targets(n: any): DocumentTargets {
|
|
|
171
169
|
initiative: n.initiative ?? null,
|
|
172
170
|
team: n.team ?? null,
|
|
173
171
|
cycle: n.cycle ? { id: n.cycle.id, number: n.cycle.number, name: n.cycle.name ?? null } : null,
|
|
174
|
-
release: n.release
|
|
175
|
-
? { id: n.release.id, name: n.release.name, version: n.release.version ?? null }
|
|
176
|
-
: null,
|
|
172
|
+
release: n.release ? { id: n.release.id, name: n.release.name, version: n.release.version ?? null } : null,
|
|
177
173
|
};
|
|
178
174
|
}
|
|
179
175
|
|
|
@@ -188,8 +184,7 @@ export function describeTarget(t: DocumentTargets): string | null {
|
|
|
188
184
|
if (t.initiative) return `Initiative: ${t.initiative.name}`;
|
|
189
185
|
if (t.team) return `Team: ${t.team.key} ${t.team.name}`;
|
|
190
186
|
if (t.cycle) return `Cycle: #${t.cycle.number}${t.cycle.name ? ` ${t.cycle.name}` : ""}`;
|
|
191
|
-
if (t.release)
|
|
192
|
-
return `Release: ${t.release.name}${t.release.version ? ` (${t.release.version})` : ""}`;
|
|
187
|
+
if (t.release) return `Release: ${t.release.name}${t.release.version ? ` (${t.release.version})` : ""}`;
|
|
193
188
|
return null;
|
|
194
189
|
}
|
|
195
190
|
|
|
@@ -238,15 +233,10 @@ query CliDocumentDetail($id: String!) {
|
|
|
238
233
|
}
|
|
239
234
|
}`;
|
|
240
235
|
|
|
241
|
-
export async function getDocumentDetail(
|
|
242
|
-
client: LinearClient,
|
|
243
|
-
idArg: string,
|
|
244
|
-
): Promise<DocumentDetail> {
|
|
236
|
+
export async function getDocumentDetail(client: LinearClient, idArg: string): Promise<DocumentDetail> {
|
|
245
237
|
// An unknown id is an API error ("Could not find referenced Document"), which
|
|
246
238
|
// the error boundary already reads as not-found (exit 3); a null is guarded too.
|
|
247
|
-
const data: any = await withRetry(() =>
|
|
248
|
-
(client as any).client.rawRequest(DETAIL_QUERY, { id: idArg }),
|
|
249
|
-
);
|
|
239
|
+
const data: any = await withRetry(() => (client as any).client.rawRequest(DETAIL_QUERY, { id: idArg }));
|
|
250
240
|
const n = data?.data?.document;
|
|
251
241
|
if (!n) throw notFound(`No document matching '${idArg}'.`);
|
|
252
242
|
return {
|
|
@@ -277,11 +267,7 @@ export interface CreateOptions extends DocumentTargetOptions {
|
|
|
277
267
|
* NOT a fallback here: the command decides that, so the service never turns
|
|
278
268
|
* "no target" into "the team from config" behind the caller's back.
|
|
279
269
|
*/
|
|
280
|
-
export async function createDocument(
|
|
281
|
-
client: LinearClient,
|
|
282
|
-
opts: CreateOptions,
|
|
283
|
-
defaultTeamKey?: string,
|
|
284
|
-
) {
|
|
270
|
+
export async function createDocument(client: LinearClient, opts: CreateOptions, defaultTeamKey?: string) {
|
|
285
271
|
const sel = selectTarget(opts);
|
|
286
272
|
if (!sel) throw usageError(`A document needs a target: pass one of ${TARGET_FLAGS}.`);
|
|
287
273
|
const input: Record<string, any> = { title: opts.title };
|
|
@@ -332,10 +318,7 @@ export async function updateDocument(
|
|
|
332
318
|
|
|
333
319
|
export async function deleteDocument(client: LinearClient, idArg: string) {
|
|
334
320
|
const document = await resolveDocument(client, idArg);
|
|
335
|
-
await assertMutation(
|
|
336
|
-
withRetry(() => client.deleteDocument(document.id)),
|
|
337
|
-
"Document deletion",
|
|
338
|
-
);
|
|
321
|
+
await assertMutation(withRetry(() => client.deleteDocument(document.id)), "Document deletion");
|
|
339
322
|
return document;
|
|
340
323
|
}
|
|
341
324
|
|
|
@@ -349,9 +332,7 @@ async function resolveDocument(client: LinearClient, idArg: string) {
|
|
|
349
332
|
// The SDK's document() also resolves a slugId; fall back to it directly.
|
|
350
333
|
try {
|
|
351
334
|
return await withRetry(() => client.document(idArg));
|
|
352
|
-
} catch
|
|
353
|
-
|
|
354
|
-
if (normalized.code === "not_found") throw notFound(`No document matching '${idArg}'.`);
|
|
355
|
-
throw normalized;
|
|
335
|
+
} catch {
|
|
336
|
+
throw notFound(`No document matching '${idArg}'.`);
|
|
356
337
|
}
|
|
357
338
|
}
|
package/src/services/favorite.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, pageSize } from "../lib/pagination.js";
|
|
17
17
|
import { usageError } from "../lib/errors.js";
|
|
18
18
|
import { assertMutation, unwrapMutation } from "../lib/mutation.js";
|
|
19
19
|
import { resolveIssue, resolveProjectId, isUuid } from "../lib/resolve.js";
|
|
@@ -69,16 +69,13 @@ export function entityLabel(entity: any): string {
|
|
|
69
69
|
export async function listFavorites(client: LinearClient, limit: number): Promise<FavoriteRow[]> {
|
|
70
70
|
const conn = await withRetry(() => client.favorites({ first: pageSize(limit) }));
|
|
71
71
|
const nodes = await collect(conn as any, limit);
|
|
72
|
-
return
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
})),
|
|
80
|
-
),
|
|
81
|
-
nodes,
|
|
72
|
+
return Promise.all(
|
|
73
|
+
nodes.map(async (f: any) => ({
|
|
74
|
+
id: f.id,
|
|
75
|
+
type: f.type,
|
|
76
|
+
name: await favoriteName(f),
|
|
77
|
+
url: f.url ?? null,
|
|
78
|
+
})),
|
|
82
79
|
);
|
|
83
80
|
}
|
|
84
81
|
|
|
@@ -139,9 +136,6 @@ export async function addFavorite(client: LinearClient, opts: AddOptions) {
|
|
|
139
136
|
|
|
140
137
|
/** Delete a favorite by id. */
|
|
141
138
|
export async function removeFavorite(client: LinearClient, id: string) {
|
|
142
|
-
await assertMutation(
|
|
143
|
-
withRetry(() => client.deleteFavorite(id)),
|
|
144
|
-
"Favorite removal",
|
|
145
|
-
);
|
|
139
|
+
await assertMutation(withRetry(() => client.deleteFavorite(id)), "Favorite removal");
|
|
146
140
|
return { id };
|
|
147
141
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { LinearClient } from "@linear/sdk";
|
|
9
9
|
import { withRetry } from "../client.js";
|
|
10
|
-
import { collect
|
|
10
|
+
import { collect } from "../lib/pagination.js";
|
|
11
11
|
import { normalizeUpdatePayload, type Health, type UpdateRow } from "../lib/status-update.js";
|
|
12
12
|
import { resolveInitiative } from "./initiative.js";
|
|
13
13
|
|
|
@@ -20,22 +20,21 @@ export async function listInitiativeUpdates(
|
|
|
20
20
|
limit: number,
|
|
21
21
|
): Promise<UpdateRow[]> {
|
|
22
22
|
const initiative = await resolveInitiative(client, idArg);
|
|
23
|
-
const conn = await withRetry(() =>
|
|
23
|
+
const conn = await withRetry(() =>
|
|
24
|
+
initiative.initiativeUpdates({ first: Math.min(limit === Infinity ? 100 : limit, 100) }),
|
|
25
|
+
);
|
|
24
26
|
const nodes = await collect(conn as any, limit);
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}),
|
|
37
|
-
),
|
|
38
|
-
nodes,
|
|
27
|
+
return Promise.all(
|
|
28
|
+
nodes.map(async (u: any) => {
|
|
29
|
+
const user = await u.user;
|
|
30
|
+
return {
|
|
31
|
+
id: u.id,
|
|
32
|
+
createdAt: u.createdAt?.toISOString?.() ?? String(u.createdAt),
|
|
33
|
+
user: user?.displayName ?? "unknown",
|
|
34
|
+
body: u.body ?? "",
|
|
35
|
+
health: u.health ?? null,
|
|
36
|
+
};
|
|
37
|
+
}),
|
|
39
38
|
);
|
|
40
39
|
}
|
|
41
40
|
|
|
@@ -164,9 +164,7 @@ export const INITIATIVE_DETAIL_SHAPE = shape<InitiativeDetail>({
|
|
|
164
164
|
archivedAt: "string|null",
|
|
165
165
|
owner: "string|null",
|
|
166
166
|
creator: "string|null",
|
|
167
|
-
projects: [
|
|
168
|
-
{ id: "string", name: "string", status: { nullable: { name: "string", type: "string" } } },
|
|
169
|
-
],
|
|
167
|
+
projects: [{ id: "string", name: "string", status: { nullable: { name: "string", type: "string" } } }],
|
|
170
168
|
});
|
|
171
169
|
|
|
172
170
|
/** How many linked projects `view` reads; more than this is unusual and shows a truncation note. */
|
|
@@ -189,11 +187,7 @@ export async function getInitiativeDetail(
|
|
|
189
187
|
const projectRows = await Promise.all(
|
|
190
188
|
(projects?.nodes ?? []).map(async (p: any) => {
|
|
191
189
|
const status = await p.status;
|
|
192
|
-
return {
|
|
193
|
-
id: p.id,
|
|
194
|
-
name: p.name,
|
|
195
|
-
status: status ? { name: status.name, type: status.type } : null,
|
|
196
|
-
};
|
|
190
|
+
return { id: p.id, name: p.name, status: status ? { name: status.name, type: status.type } : null };
|
|
197
191
|
}),
|
|
198
192
|
);
|
|
199
193
|
return {
|
|
@@ -263,7 +257,11 @@ export interface UpdateOptions {
|
|
|
263
257
|
color?: string;
|
|
264
258
|
}
|
|
265
259
|
|
|
266
|
-
export async function updateInitiative(
|
|
260
|
+
export async function updateInitiative(
|
|
261
|
+
client: LinearClient,
|
|
262
|
+
idArg: string,
|
|
263
|
+
opts: UpdateOptions,
|
|
264
|
+
) {
|
|
267
265
|
const initiative = await resolveInitiative(client, idArg);
|
|
268
266
|
const input: Record<string, any> = {};
|
|
269
267
|
if (opts.name !== undefined) input.name = opts.name;
|
|
@@ -422,11 +420,7 @@ export async function resolveInitiative(
|
|
|
422
420
|
const matches: any[] = [];
|
|
423
421
|
for (;;) {
|
|
424
422
|
const conn = await withRetry(() =>
|
|
425
|
-
client.initiatives({
|
|
426
|
-
first: 100,
|
|
427
|
-
after,
|
|
428
|
-
includeArchived: opts.includeArchived === true,
|
|
429
|
-
} as any),
|
|
423
|
+
client.initiatives({ first: 100, after, includeArchived: opts.includeArchived === true } as any),
|
|
430
424
|
);
|
|
431
425
|
for (const n of conn.nodes) {
|
|
432
426
|
if (n.name.toLowerCase() === lower) matches.push(n);
|
|
@@ -444,6 +438,7 @@ export async function resolveInitiative(
|
|
|
444
438
|
export function resolveStatus(input: string): string {
|
|
445
439
|
const lower = input.toLowerCase();
|
|
446
440
|
const match = STATUSES.find((s) => s.toLowerCase() === lower);
|
|
447
|
-
if (!match)
|
|
441
|
+
if (!match)
|
|
442
|
+
throw usageError(`Invalid status '${input}'. Valid: ${STATUSES.join(", ")}.`);
|
|
448
443
|
return match;
|
|
449
444
|
}
|