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/lib/output-shapes.ts
CHANGED
|
@@ -130,10 +130,7 @@ export const UPDATE_ROW_SHAPE = shape<UpdateRow>({
|
|
|
130
130
|
body: "string",
|
|
131
131
|
health: "string|null",
|
|
132
132
|
});
|
|
133
|
-
const UPDATE_RECEIPT_SHAPE = shape<UpdateRow & { url: string }>({
|
|
134
|
-
...UPDATE_ROW_SHAPE,
|
|
135
|
-
url: "string",
|
|
136
|
-
});
|
|
133
|
+
const UPDATE_RECEIPT_SHAPE = shape<UpdateRow & { url: string }>({ ...UPDATE_ROW_SHAPE, url: "string" });
|
|
137
134
|
|
|
138
135
|
/** `whoami` / `auth whoami`. */
|
|
139
136
|
const WHOAMI_SHAPE: ObjectFields = {
|
|
@@ -145,15 +142,12 @@ const WHOAMI_SHAPE: ObjectFields = {
|
|
|
145
142
|
organization: { id: "string", name: "string", urlKey: "string" },
|
|
146
143
|
};
|
|
147
144
|
|
|
148
|
-
const SETTING_ORIGIN_SHAPE = shape<SettingOrigin>({
|
|
149
|
-
source: "string",
|
|
150
|
-
"path?": "string",
|
|
151
|
-
"key?": "string",
|
|
152
|
-
});
|
|
145
|
+
const SETTING_ORIGIN_SHAPE = shape<SettingOrigin>({ source: "string", "path?": "string" });
|
|
153
146
|
const SETTING_ORIGINS_SHAPE = shape<SettingOrigins>({
|
|
154
147
|
team: SETTING_ORIGIN_SHAPE,
|
|
155
148
|
workspace: SETTING_ORIGIN_SHAPE,
|
|
156
149
|
sort: SETTING_ORIGIN_SHAPE,
|
|
150
|
+
vcs: SETTING_ORIGIN_SHAPE,
|
|
157
151
|
});
|
|
158
152
|
|
|
159
153
|
/** `config show`: every resolved setting, redacted, with its provenance. */
|
|
@@ -164,6 +158,7 @@ const CONFIG_SHOW_SHAPE: ObjectFields = {
|
|
|
164
158
|
team: "string|null",
|
|
165
159
|
workspace: "string|null",
|
|
166
160
|
sort: "string",
|
|
161
|
+
vcs: "string",
|
|
167
162
|
origins: SETTING_ORIGINS_SHAPE,
|
|
168
163
|
userConfigPath: "string",
|
|
169
164
|
projectConfigPath: "string|null",
|
|
@@ -176,20 +171,7 @@ export const COMMAND_NODE_SHAPE = shape<CommandNode>({
|
|
|
176
171
|
description: "string",
|
|
177
172
|
aliases: ["string"],
|
|
178
173
|
arguments: [{ name: "string", required: "boolean", variadic: "boolean" }],
|
|
179
|
-
options: [
|
|
180
|
-
{
|
|
181
|
-
flags: "string",
|
|
182
|
-
description: "string",
|
|
183
|
-
attribute: "string",
|
|
184
|
-
valueRequired: "boolean",
|
|
185
|
-
valueOptional: "boolean",
|
|
186
|
-
variadic: "boolean",
|
|
187
|
-
"choices?": ["string"],
|
|
188
|
-
"defaultValue?": "unknown",
|
|
189
|
-
global: "boolean",
|
|
190
|
-
applicable: "boolean",
|
|
191
|
-
},
|
|
192
|
-
],
|
|
174
|
+
options: [{ flags: "string", description: "string" }],
|
|
193
175
|
"output?": {
|
|
194
176
|
kind: "string",
|
|
195
177
|
"fields?": "object",
|
|
@@ -228,8 +210,8 @@ const RELATION_ROW: ObjectFields = { type: "string", issue: "string", title: "st
|
|
|
228
210
|
|
|
229
211
|
const ISSUE_VIEW = object(ISSUE_DETAIL_SHAPE, {
|
|
230
212
|
variants: {
|
|
213
|
+
"--comments": object({ ...ISSUE_DETAIL_SHAPE, comments: [COMMENT_ROW_SHAPE] }),
|
|
231
214
|
"--web": receipt({ ...ISSUE_URL_RECEIPT, opened: "boolean" }),
|
|
232
|
-
"--app": receipt({ ...ISSUE_URL_RECEIPT, opened: "boolean" }),
|
|
233
215
|
},
|
|
234
216
|
});
|
|
235
217
|
const ISSUE_LIST = list(ISSUE_ROW_SHAPE);
|
|
@@ -254,13 +236,6 @@ export const OUTPUT_SHAPES: Record<string, OutputShape | null> = {
|
|
|
254
236
|
"attachment list": list(ATTACHMENT_ROW_SHAPE),
|
|
255
237
|
|
|
256
238
|
auth: null,
|
|
257
|
-
"auth adopt": receipt({
|
|
258
|
-
success: "boolean",
|
|
259
|
-
workspace: "string",
|
|
260
|
-
user: { id: "string", name: "string", email: "string" },
|
|
261
|
-
storage: "string",
|
|
262
|
-
path: "string",
|
|
263
|
-
}),
|
|
264
239
|
"auth default": receipt({ success: "boolean", default_workspace: "string", path: "string" }),
|
|
265
240
|
"auth list": list({ slug: "string", isDefault: "boolean", storage: "string" }),
|
|
266
241
|
"auth login": receipt({
|
|
@@ -341,28 +316,7 @@ export const OUTPUT_SHAPES: Record<string, OutputShape | null> = {
|
|
|
341
316
|
"issue agent-session": null,
|
|
342
317
|
"issue agent-session list": list(AGENT_SESSION_ROW_SHAPE),
|
|
343
318
|
"issue agent-session view": object(AGENT_SESSION_DETAIL_SHAPE),
|
|
344
|
-
"issue archive": receipt(
|
|
345
|
-
{ ...ISSUE_REF, archived: "boolean" },
|
|
346
|
-
{
|
|
347
|
-
variants: {
|
|
348
|
-
"--bulk": receipt({
|
|
349
|
-
action: "string",
|
|
350
|
-
results: [
|
|
351
|
-
{
|
|
352
|
-
input: "string",
|
|
353
|
-
"id?": "string",
|
|
354
|
-
"identifier?": "string",
|
|
355
|
-
"archived?": "boolean",
|
|
356
|
-
"deleted?": "boolean",
|
|
357
|
-
"error?": { message: "string", code: "string" },
|
|
358
|
-
},
|
|
359
|
-
],
|
|
360
|
-
succeeded: "number",
|
|
361
|
-
failed: "number",
|
|
362
|
-
}),
|
|
363
|
-
},
|
|
364
|
-
},
|
|
365
|
-
),
|
|
319
|
+
"issue archive": receipt({ ...ISSUE_REF, archived: "boolean" }),
|
|
366
320
|
"issue assign": receipt(ISSUE_REF),
|
|
367
321
|
"issue attach": list(
|
|
368
322
|
{
|
|
@@ -396,33 +350,10 @@ export const OUTPUT_SHAPES: Record<string, OutputShape | null> = {
|
|
|
396
350
|
}),
|
|
397
351
|
},
|
|
398
352
|
}),
|
|
399
|
-
"issue delete": receipt(
|
|
400
|
-
{ ...ISSUE_REF, deleted: "boolean" },
|
|
401
|
-
{
|
|
402
|
-
variants: {
|
|
403
|
-
"--bulk": receipt({
|
|
404
|
-
action: "string",
|
|
405
|
-
results: [
|
|
406
|
-
{
|
|
407
|
-
input: "string",
|
|
408
|
-
"id?": "string",
|
|
409
|
-
"identifier?": "string",
|
|
410
|
-
"archived?": "boolean",
|
|
411
|
-
"deleted?": "boolean",
|
|
412
|
-
"error?": { message: "string", code: "string" },
|
|
413
|
-
},
|
|
414
|
-
],
|
|
415
|
-
succeeded: "number",
|
|
416
|
-
failed: "number",
|
|
417
|
-
}),
|
|
418
|
-
},
|
|
419
|
-
},
|
|
420
|
-
),
|
|
353
|
+
"issue delete": receipt({ ...ISSUE_REF, deleted: "boolean" }),
|
|
421
354
|
"issue describe": receipt(
|
|
422
355
|
{ identifier: "string", title: "string", url: "string", trailer: "string", message: "string" },
|
|
423
|
-
{
|
|
424
|
-
note: "`trailer` is the magic-word phrase (Fixes TES-1); `message` the full commit text as printed",
|
|
425
|
-
},
|
|
356
|
+
{ note: "`trailer` is the magic-word phrase (Fixes TES-1); `message` the full commit text as printed" },
|
|
426
357
|
),
|
|
427
358
|
"issue id": receipt({ id: "string" }, { note: "`id` is the identifier (TES-123), not the UUID" }),
|
|
428
359
|
"issue label": receipt(ISSUE_REF),
|
|
@@ -485,8 +416,6 @@ export const OUTPUT_SHAPES: Record<string, OutputShape | null> = {
|
|
|
485
416
|
"notification snooze": receipt({ id: "string", snoozedUntilAt: "string" }),
|
|
486
417
|
"notification unread": receipt({ id: "string", read: "boolean" }),
|
|
487
418
|
|
|
488
|
-
open: receipt({ target: "string", url: "string", label: "string", opened: "boolean" }),
|
|
489
|
-
|
|
490
419
|
organization: runs("organization view", object(ORGANIZATION_DETAIL_SHAPE)),
|
|
491
420
|
"organization invites": list(INVITE_ROW_SHAPE),
|
|
492
421
|
"organization members": list(ORGANIZATION_MEMBER_ROW_SHAPE),
|
|
@@ -530,7 +459,6 @@ export const OUTPUT_SHAPES: Record<string, OutputShape | null> = {
|
|
|
530
459
|
movedIssues: "number",
|
|
531
460
|
movedTo: { nullable: TEAM_ROW_SHAPE },
|
|
532
461
|
}),
|
|
533
|
-
"team id": receipt(TEAM_ROW_SHAPE),
|
|
534
462
|
"team labels": list(TEAM_LABEL_ROW_SHAPE),
|
|
535
463
|
"team list": list(TEAM_ROW_SHAPE),
|
|
536
464
|
"team members": list(TEAM_MEMBER_ROW_SHAPE),
|
package/src/lib/pagination.ts
CHANGED
|
@@ -5,10 +5,7 @@
|
|
|
5
5
|
* and `.fetchNext()` mutates the connection to append the next page's nodes.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
/** Linear accepts at most 250 nodes per connection page. */
|
|
11
|
-
export const MAX_PAGE_SIZE = 250;
|
|
8
|
+
import { withRetry } from "../client.js";
|
|
12
9
|
|
|
13
10
|
export interface Connection<T> {
|
|
14
11
|
nodes: T[];
|
|
@@ -16,49 +13,22 @@ export interface Connection<T> {
|
|
|
16
13
|
fetchNext: () => Promise<Connection<T>>;
|
|
17
14
|
}
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
function withMoreMetadata<T>(items: T[], hasMore: boolean): T[] {
|
|
25
|
-
Object.defineProperty(items, HAS_MORE, { value: hasMore, enumerable: false });
|
|
26
|
-
return items;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Attach an explicit pagination result to an array without changing its JSON value. */
|
|
30
|
-
export function setPaginationMetadata<T>(items: T[], hasMore: boolean): T[] {
|
|
31
|
-
return withMoreMetadata(items, hasMore);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Whether a service list stopped at its caller-supplied limit. */
|
|
35
|
-
export function hasMoreResults(items: readonly unknown[]): boolean {
|
|
36
|
-
return (items as PagedArray<unknown>)[HAS_MORE] === true;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Carry pagination metadata through a service's map/filter/sort result. */
|
|
40
|
-
export function inheritPaginationMetadata<T>(items: T[], source: readonly unknown[]): T[] {
|
|
41
|
-
return withMoreMetadata(items, hasMoreResults(source));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export async function collect<T>(first: Connection<T>, limit: number): Promise<T[]> {
|
|
16
|
+
export async function collect<T>(
|
|
17
|
+
first: Connection<T>,
|
|
18
|
+
limit: number,
|
|
19
|
+
): Promise<T[]> {
|
|
45
20
|
let conn = first;
|
|
46
21
|
// Each fetchNext() appends to conn.nodes in place.
|
|
47
22
|
while (conn.nodes.length < limit && conn.pageInfo.hasNextPage) {
|
|
48
|
-
|
|
49
|
-
// like page one. Keep them behind the same bounded retry policy.
|
|
50
|
-
conn = await fetchNextWithRetry(conn);
|
|
23
|
+
conn = await conn.fetchNext();
|
|
51
24
|
}
|
|
52
|
-
|
|
53
|
-
const hasMore =
|
|
54
|
-
conn.nodes.length > limit || (conn.nodes.length >= limit && conn.pageInfo.hasNextPage);
|
|
55
|
-
return withMoreMetadata(conn.nodes.slice(0, limit), hasMore);
|
|
25
|
+
return limit === Infinity ? conn.nodes : conn.nodes.slice(0, limit);
|
|
56
26
|
}
|
|
57
27
|
|
|
58
28
|
/** How many to request per page given a desired total limit. */
|
|
59
29
|
export function pageSize(limit: number): number {
|
|
60
|
-
if (limit === Infinity) return
|
|
61
|
-
return Math.min(Math.max(limit, 1),
|
|
30
|
+
if (limit === Infinity) return 100;
|
|
31
|
+
return Math.min(Math.max(limit, 1), 100);
|
|
62
32
|
}
|
|
63
33
|
|
|
64
34
|
/**
|
|
@@ -106,7 +76,7 @@ interface RawClient {
|
|
|
106
76
|
* `withRetry`; pagination stops at `limit` or when `hasNextPage` is false.
|
|
107
77
|
*
|
|
108
78
|
* Mirrors the hand-rolled loops in the list services exactly: page size is
|
|
109
|
-
* `limit === Infinity ?
|
|
79
|
+
* `limit === Infinity ? 100 : Math.min(limit, 100)`, and rows are capped at
|
|
110
80
|
* `limit` mid-page.
|
|
111
81
|
*/
|
|
112
82
|
export async function collectRawQuery<T>(
|
|
@@ -117,29 +87,26 @@ export async function collectRawQuery<T>(
|
|
|
117
87
|
limit: number,
|
|
118
88
|
transform: (node: any) => T,
|
|
119
89
|
): Promise<T[]> {
|
|
120
|
-
const pageLimit =
|
|
90
|
+
const pageLimit = limit === Infinity ? 100 : Math.min(limit, 100);
|
|
121
91
|
const rows: T[] = [];
|
|
122
92
|
let after: string | undefined;
|
|
123
|
-
let hasMore = false;
|
|
124
93
|
|
|
125
94
|
for (;;) {
|
|
126
95
|
const data: any = await withRetry(() =>
|
|
127
96
|
client.client.rawRequest(query, { ...variables, first: pageLimit, after }),
|
|
128
97
|
);
|
|
129
|
-
const conn = connectionPath
|
|
130
|
-
|
|
98
|
+
const conn = connectionPath
|
|
99
|
+
.split(".")
|
|
100
|
+
.reduce((acc: any, key) => acc?.[key], data.data) as RawConnection | null | undefined;
|
|
131
101
|
// A nested path (`issue.comments`) can land on null when the parent entity
|
|
132
102
|
// is gone; that is an empty list, not a crash.
|
|
133
103
|
if (!conn) break;
|
|
134
|
-
for (const
|
|
104
|
+
for (const n of conn.nodes) {
|
|
135
105
|
rows.push(transform(n));
|
|
136
|
-
if (rows.length >= limit)
|
|
137
|
-
hasMore = index < conn.nodes.length - 1 || conn.pageInfo.hasNextPage;
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
106
|
+
if (rows.length >= limit) break;
|
|
140
107
|
}
|
|
141
108
|
if (rows.length >= limit || !conn.pageInfo.hasNextPage) break;
|
|
142
109
|
after = conn.pageInfo.endCursor;
|
|
143
110
|
}
|
|
144
|
-
return
|
|
111
|
+
return rows;
|
|
145
112
|
}
|
package/src/lib/resolve.ts
CHANGED
|
@@ -15,27 +15,6 @@ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
|
|
15
15
|
export const isUuid = (v: string): boolean => UUID_RE.test(v);
|
|
16
16
|
|
|
17
17
|
const IDENTIFIER_RE = /^([a-zA-Z][a-zA-Z0-9]*)-(\d+)$/;
|
|
18
|
-
const ISSUE_NUMBER_RE = /^\d+$/;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Expand a bare issue number with the configured team (`42` + `TES` →
|
|
22
|
-
* `TES-42`). This is deliberately pure: commands can normalize every place an
|
|
23
|
-
* issue reference appears before handing it to the existing resolvers.
|
|
24
|
-
*/
|
|
25
|
-
export function normalizeIssueReference(input: string, defaultTeamKey?: string): string {
|
|
26
|
-
if (!ISSUE_NUMBER_RE.test(input)) return input;
|
|
27
|
-
if (!defaultTeamKey) {
|
|
28
|
-
throw usageError(
|
|
29
|
-
`Bare issue number '${input}' needs a default team. Pass --team <KEY> or set one in config.`,
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(defaultTeamKey)) {
|
|
33
|
-
throw usageError(
|
|
34
|
-
`Cannot expand issue number '${input}' with default team '${defaultTeamKey}'; configure a team key such as TES.`,
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
return `${defaultTeamKey.toUpperCase()}-${Number.parseInt(input, 10)}`;
|
|
38
|
-
}
|
|
39
18
|
|
|
40
19
|
/** Known workflow-state types, used to decide name-vs-type filtering. */
|
|
41
20
|
export const STATE_TYPES = ["triage", "backlog", "unstarted", "started", "completed", "canceled"];
|
|
@@ -122,15 +101,13 @@ export async function resolveTeam(
|
|
|
122
101
|
"linear team list",
|
|
123
102
|
)}`,
|
|
124
103
|
);
|
|
125
|
-
if (matches.length > 1)
|
|
126
|
-
throw ambiguous(`Multiple teams match '${value}': ${matches.map((t) => t.key).join(", ")}`);
|
|
104
|
+
if (matches.length > 1) throw ambiguous(`Multiple teams match '${value}': ${matches.map((t) => t.key).join(", ")}`);
|
|
127
105
|
const team = matches[0]!;
|
|
128
106
|
return { id: team.id, key: team.key, name: team.name };
|
|
129
107
|
}
|
|
130
108
|
|
|
131
109
|
/** The spellings that mean "the authenticated user": ours (`me`, `@me`) and the reference CLI's (`self`). */
|
|
132
|
-
export const isSelf = (input: string): boolean =>
|
|
133
|
-
input === "me" || input === "@me" || input === "self";
|
|
110
|
+
export const isSelf = (input: string): boolean => input === "me" || input === "@me" || input === "self";
|
|
134
111
|
|
|
135
112
|
/**
|
|
136
113
|
* Resolve an assignee reference (`me`, email, name, or id) to a user id.
|
|
@@ -161,9 +138,7 @@ export async function resolveUserId(client: LinearClient, input: string): Promis
|
|
|
161
138
|
if (nodes.length === 0)
|
|
162
139
|
throw notFound(`No user matching '${input}'. Run 'linear user list' to see workspace members.`);
|
|
163
140
|
if (nodes.length > 1)
|
|
164
|
-
throw ambiguous(
|
|
165
|
-
`Multiple users match '${input}': ${nodes.map((u: any) => u.email).join(", ")}`,
|
|
166
|
-
);
|
|
141
|
+
throw ambiguous(`Multiple users match '${input}': ${nodes.map((u: any) => u.email).join(", ")}`);
|
|
167
142
|
return nodes[0]!.id;
|
|
168
143
|
}
|
|
169
144
|
|
|
@@ -228,8 +203,7 @@ export async function resolveLabelIds(
|
|
|
228
203
|
// Narrow to this team's labels + workspace-level labels when a team is known.
|
|
229
204
|
// If a team is known and nothing is in scope, that's not-found (do NOT fall
|
|
230
205
|
// back to an out-of-scope label from another team).
|
|
231
|
-
|
|
232
|
-
let candidates: any[] = nodes.filter((label: any) => !label.isGroup);
|
|
206
|
+
let candidates: any[] = nodes;
|
|
233
207
|
if (teamId) {
|
|
234
208
|
const scoped = await Promise.all(
|
|
235
209
|
nodes.map(async (l: any) => ({ label: l, team: await l.team })),
|
|
@@ -360,9 +334,7 @@ export async function resolveProjectId(client: LinearClient, input: string): Pro
|
|
|
360
334
|
if (nodes.length === 0)
|
|
361
335
|
throw notFound(`No project matching '${input}'. Run 'linear project list' to see the options.`);
|
|
362
336
|
if (nodes.length > 1)
|
|
363
|
-
throw ambiguous(
|
|
364
|
-
`Multiple projects match '${input}': ${nodes.map((p: any) => p.name).join(", ")}`,
|
|
365
|
-
);
|
|
337
|
+
throw ambiguous(`Multiple projects match '${input}': ${nodes.map((p: any) => p.name).join(", ")}`);
|
|
366
338
|
return nodes[0]!.id;
|
|
367
339
|
}
|
|
368
340
|
|
|
@@ -513,11 +485,7 @@ export async function resolveReleaseId(client: LinearClient, input: string): Pro
|
|
|
513
485
|
first: RESOLVE_PAGE,
|
|
514
486
|
}),
|
|
515
487
|
);
|
|
516
|
-
const nodes = await scanAll<any>(
|
|
517
|
-
conn as any,
|
|
518
|
-
"releases",
|
|
519
|
-
"linear api '{ releases { nodes { id name version } } }'",
|
|
520
|
-
);
|
|
488
|
+
const nodes = await scanAll<any>(conn as any, "releases", "linear api '{ releases { nodes { id name version } } }'");
|
|
521
489
|
if (nodes.length === 0) throw notFound(`No release named or versioned '${input}'.`);
|
|
522
490
|
if (nodes.length > 1)
|
|
523
491
|
throw ambiguous(
|
|
@@ -579,8 +547,7 @@ export async function resolveTemplateId(
|
|
|
579
547
|
export async function resolveIssue(client: LinearClient, input: string): Promise<Issue> {
|
|
580
548
|
if (isUuid(input)) return withRetry(() => client.issue(input));
|
|
581
549
|
const match = input.match(IDENTIFIER_RE);
|
|
582
|
-
if (!match)
|
|
583
|
-
throw usageError(`'${input}' is not a valid issue id (expected e.g. TES-123 or a UUID).`);
|
|
550
|
+
if (!match) throw usageError(`'${input}' is not a valid issue id (expected e.g. TES-123 or a UUID).`);
|
|
584
551
|
const key = match[1]!.toUpperCase();
|
|
585
552
|
const number = Number.parseInt(match[2]!, 10);
|
|
586
553
|
// includeArchived so archive/unarchive/delete can operate on archived issues.
|
package/src/lib/shape.ts
CHANGED
|
@@ -66,13 +66,11 @@ type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
|
66
66
|
type ObjectShapeOf<T> = string extends keyof T
|
|
67
67
|
? "object"
|
|
68
68
|
: {
|
|
69
|
-
[
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
: K & string
|
|
75
|
-
]-?: ShapeOf<Exclude<T[K], undefined>>;
|
|
69
|
+
[K in keyof T as IsAny<T[K]> extends true
|
|
70
|
+
? K & string
|
|
71
|
+
: undefined extends T[K]
|
|
72
|
+
? `${K & string}?`
|
|
73
|
+
: K & string]-?: ShapeOf<Exclude<T[K], undefined>>;
|
|
76
74
|
};
|
|
77
75
|
|
|
78
76
|
export type ShapeOf<T> =
|
|
@@ -121,7 +119,10 @@ export type ObjectFields = Record<string, FieldShape>;
|
|
|
121
119
|
|
|
122
120
|
function isNullableWrapper(s: FieldShape): s is { readonly nullable: FieldShape } {
|
|
123
121
|
return (
|
|
124
|
-
typeof s === "object" &&
|
|
122
|
+
typeof s === "object" &&
|
|
123
|
+
!Array.isArray(s) &&
|
|
124
|
+
Object.keys(s).length === 1 &&
|
|
125
|
+
"nullable" in s
|
|
125
126
|
);
|
|
126
127
|
}
|
|
127
128
|
|
|
@@ -159,9 +160,7 @@ export function matchesShape(
|
|
|
159
160
|
}
|
|
160
161
|
if (Array.isArray(shape)) {
|
|
161
162
|
if (!Array.isArray(value)) return [`${path}: expected an array, got ${describe(value)}`];
|
|
162
|
-
return value.flatMap((item, i) =>
|
|
163
|
-
matchesShape(item, shape[0], `${path}[${i}]`, strictNullable),
|
|
164
|
-
);
|
|
163
|
+
return value.flatMap((item, i) => matchesShape(item, shape[0], `${path}[${i}]`, strictNullable));
|
|
165
164
|
}
|
|
166
165
|
if (isNullableWrapper(shape)) {
|
|
167
166
|
if (value === undefined) return [`${path}: expected an object or null, got undefined`];
|
|
@@ -185,8 +184,7 @@ export function matchesShape(
|
|
|
185
184
|
problems.push(...matchesShape(record[key], sub, `${path}.${key}`, strictNullable));
|
|
186
185
|
}
|
|
187
186
|
for (const key of Object.keys(record)) {
|
|
188
|
-
if (!declared.has(key) && record[key] !== undefined)
|
|
189
|
-
problems.push(`${path}.${key}: not in the declared shape`);
|
|
187
|
+
if (!declared.has(key) && record[key] !== undefined) problems.push(`${path}.${key}: not in the declared shape`);
|
|
190
188
|
}
|
|
191
189
|
return problems;
|
|
192
190
|
}
|
|
@@ -205,9 +203,7 @@ export function renderShape(shape: FieldShape): string {
|
|
|
205
203
|
if (typeof shape === "string") return shape.replace("|null", " | null");
|
|
206
204
|
if (Array.isArray(shape)) {
|
|
207
205
|
const inner = renderShape(shape[0]);
|
|
208
|
-
return typeof shape[0] === "string" && !shape[0].includes("|")
|
|
209
|
-
? `${inner}[]`
|
|
210
|
-
: `Array<${inner}>`;
|
|
206
|
+
return typeof shape[0] === "string" && !shape[0].includes("|") ? `${inner}[]` : `Array<${inner}>`;
|
|
211
207
|
}
|
|
212
208
|
if (isNullableWrapper(shape)) return `${renderShape(shape.nullable)} | null`;
|
|
213
209
|
const parts = Object.entries(shape).map(([k, v]) => `${k}: ${renderShape(v)}`);
|
package/src/lib/upload.ts
CHANGED
|
@@ -294,9 +294,7 @@ function redact(text: string, uploadUrl: string): string {
|
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
/** Markdown for a comment body: images embed inline, anything else links. */
|
|
297
|
-
export function formatEmbed(
|
|
298
|
-
r: Pick<UploadResult, "filename" | "assetUrl" | "contentType">,
|
|
299
|
-
): string {
|
|
297
|
+
export function formatEmbed(r: Pick<UploadResult, "filename" | "assetUrl" | "contentType">): string {
|
|
300
298
|
return `${isImage(r.contentType) ? "!" : ""}[${r.filename}](${r.assetUrl})`;
|
|
301
299
|
}
|
|
302
300
|
|
package/src/output/format.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - lists → bare array, single resource → bare object, mutations → affected
|
|
7
7
|
* object (or {success,id} when there is no body)
|
|
8
8
|
* - status/progress/pagination notes → stderr
|
|
9
|
-
* - errors → {"error":{"message","code"
|
|
9
|
+
* - errors → {"error":{"message","code"}} on stderr + non-zero exit
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import pc from "picocolors";
|
|
@@ -20,9 +20,6 @@ import {
|
|
|
20
20
|
type Column,
|
|
21
21
|
} from "./table.js";
|
|
22
22
|
import { sanitizeForTerminal } from "./sanitize.js";
|
|
23
|
-
import { renderMarkdown } from "./markdown.js";
|
|
24
|
-
import { pageOutput, shouldUsePager } from "./pager.js";
|
|
25
|
-
import { hasMoreResults } from "../lib/pagination.js";
|
|
26
23
|
|
|
27
24
|
export interface OutputOptions {
|
|
28
25
|
json: boolean;
|
|
@@ -31,38 +28,10 @@ export interface OutputOptions {
|
|
|
31
28
|
debug: boolean;
|
|
32
29
|
/** `--fields`: table columns / detail lines (human), top-level keys (json). */
|
|
33
30
|
fields?: string[];
|
|
34
|
-
/** Injectable terminal facts; default to stdout's live state. */
|
|
35
|
-
isTTY?: boolean;
|
|
36
|
-
terminalRows?: number;
|
|
37
|
-
terminalColumns?: number;
|
|
38
|
-
/** Used only to resolve PAGER when long-form output is eligible. */
|
|
39
|
-
env?: NodeJS.ProcessEnv;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface ListOutputOptions {
|
|
43
|
-
/** JSON rows may be richer/differently shaped than the human table rows. */
|
|
44
|
-
jsonRows?: unknown[];
|
|
45
|
-
/** Context shown instead of the default `(no results)` in human mode. */
|
|
46
|
-
empty?: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface MarkdownOutputOptions {
|
|
50
|
-
/** Leave Markdown syntax intact (terminal controls are still removed). */
|
|
51
|
-
raw?: boolean;
|
|
52
|
-
/** Commands with `--no-pager` pass false. Defaults to enabled. */
|
|
53
|
-
pager?: boolean;
|
|
54
31
|
}
|
|
55
32
|
|
|
56
33
|
export class Output {
|
|
57
|
-
private readonly opts: OutputOptions
|
|
58
|
-
private readonly colors: ReturnType<typeof pc.createColors>;
|
|
59
|
-
|
|
60
|
-
constructor(opts: OutputOptions) {
|
|
61
|
-
this.opts = opts;
|
|
62
|
-
// Do not rely on picocolors' module-load-time environment detection:
|
|
63
|
-
// CLICOLOR_FORCE and per-stream TTY policy are resolved by Context.
|
|
64
|
-
this.colors = pc.createColors(opts.color);
|
|
65
|
-
}
|
|
34
|
+
constructor(private readonly opts: OutputOptions) {}
|
|
66
35
|
|
|
67
36
|
get json(): boolean {
|
|
68
37
|
return this.opts.json;
|
|
@@ -85,28 +54,13 @@ export class Output {
|
|
|
85
54
|
* Emit a list as a table (human) or bare array (json). `--fields` narrows
|
|
86
55
|
* both: the table's columns (or any row key), and the JSON objects' keys.
|
|
87
56
|
*/
|
|
88
|
-
list<T>(
|
|
89
|
-
rows: T[],
|
|
90
|
-
columns: Column<T>[],
|
|
91
|
-
jsonRowsOrOptions?: unknown[] | ListOutputOptions,
|
|
92
|
-
legacyOptions?: Pick<ListOutputOptions, "empty">,
|
|
93
|
-
): void {
|
|
94
|
-
const options: ListOutputOptions = Array.isArray(jsonRowsOrOptions)
|
|
95
|
-
? { jsonRows: jsonRowsOrOptions, ...legacyOptions }
|
|
96
|
-
: (jsonRowsOrOptions ?? {});
|
|
97
|
-
if (hasMoreResults(rows)) {
|
|
98
|
-
this.info(
|
|
99
|
-
`Showing ${rows.length} results; more exist. Use --all or increase --limit to see them.`,
|
|
100
|
-
);
|
|
101
|
-
}
|
|
57
|
+
list<T>(rows: T[], columns: Column<T>[], jsonRows?: unknown[]): void {
|
|
102
58
|
if (this.opts.json) {
|
|
103
|
-
this.writeJson(projectFields(
|
|
59
|
+
this.writeJson(projectFields(jsonRows ?? rows, this.opts.fields));
|
|
104
60
|
return;
|
|
105
61
|
}
|
|
106
62
|
const cols = selectColumns(columns, this.opts.fields, rows[0]);
|
|
107
|
-
process.stdout.write(
|
|
108
|
-
renderTable(rows, cols, { color: this.opts.color, empty: options.empty }) + "\n",
|
|
109
|
-
);
|
|
63
|
+
process.stdout.write(renderTable(rows, cols, { color: this.opts.color }) + "\n");
|
|
110
64
|
}
|
|
111
65
|
|
|
112
66
|
/**
|
|
@@ -130,41 +84,6 @@ export class Output {
|
|
|
130
84
|
if (!this.opts.json) process.stdout.write(sanitizeForTerminal(text) + "\n");
|
|
131
85
|
}
|
|
132
86
|
|
|
133
|
-
/**
|
|
134
|
-
* Render a long Markdown body for a human, optionally through a pager.
|
|
135
|
-
*
|
|
136
|
-
* Non-TTY output keeps the original Markdown so pipes/files receive stable,
|
|
137
|
-
* reusable text. JSON mode writes nothing — callers normally put this inside
|
|
138
|
-
* `emit(value, () => output.markdown(...))`, but the guard prevents an
|
|
139
|
-
* accidental second stdout value. Both human paths remain terminal-sanitized.
|
|
140
|
-
*/
|
|
141
|
-
markdown(text: string, options: MarkdownOutputOptions = {}): void {
|
|
142
|
-
if (this.opts.json) return;
|
|
143
|
-
|
|
144
|
-
const isTTY = this.opts.isTTY ?? process.stdout.isTTY === true;
|
|
145
|
-
const columns = this.opts.terminalColumns ?? process.stdout.columns ?? 80;
|
|
146
|
-
const rows = this.opts.terminalRows ?? process.stdout.rows;
|
|
147
|
-
const content =
|
|
148
|
-
isTTY && options.raw !== true
|
|
149
|
-
? renderMarkdown(text, { color: this.opts.color, width: columns })
|
|
150
|
-
: sanitizeForTerminal(text);
|
|
151
|
-
const output = content.endsWith("\n") ? content : `${content}\n`;
|
|
152
|
-
|
|
153
|
-
if (
|
|
154
|
-
shouldUsePager(output, {
|
|
155
|
-
enabled: options.pager,
|
|
156
|
-
json: this.opts.json,
|
|
157
|
-
isTTY,
|
|
158
|
-
rows,
|
|
159
|
-
columns,
|
|
160
|
-
}) &&
|
|
161
|
-
pageOutput(output, { env: this.opts.env })
|
|
162
|
-
) {
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
process.stdout.write(output);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
87
|
/** Status/progress to stderr. Suppressed by --quiet. */
|
|
169
88
|
info(text: string): void {
|
|
170
89
|
if (!this.opts.quiet) process.stderr.write(sanitizeForTerminal(text) + "\n");
|
|
@@ -172,11 +91,13 @@ export class Output {
|
|
|
172
91
|
|
|
173
92
|
success(text: string): void {
|
|
174
93
|
if (!this.opts.quiet)
|
|
175
|
-
process.stderr.write(
|
|
94
|
+
process.stderr.write(
|
|
95
|
+
(this.opts.color ? pc.green("✓ ") : "✓ ") + sanitizeForTerminal(text) + "\n",
|
|
96
|
+
);
|
|
176
97
|
}
|
|
177
98
|
|
|
178
99
|
warn(text: string): void {
|
|
179
|
-
process.stderr.write(this.
|
|
100
|
+
process.stderr.write((this.opts.color ? pc.yellow("! ") : "! ") + sanitizeForTerminal(text) + "\n");
|
|
180
101
|
}
|
|
181
102
|
|
|
182
103
|
/**
|
|
@@ -196,7 +117,7 @@ export class Output {
|
|
|
196
117
|
}
|
|
197
118
|
if (!this.opts.quiet) {
|
|
198
119
|
process.stderr.write(
|
|
199
|
-
this.
|
|
120
|
+
(this.opts.color ? pc.yellow("! ") : "! ") + `Cancelled: ${sanitizeForTerminal(action)}\n`,
|
|
200
121
|
);
|
|
201
122
|
}
|
|
202
123
|
}
|
|
@@ -216,19 +137,20 @@ export class Output {
|
|
|
216
137
|
// `message` and `code` keep their positions; `detail` is additive, and
|
|
217
138
|
// absent entirely without --debug.
|
|
218
139
|
const error: Record<string, unknown> = { message: err.message, code: err.code };
|
|
219
|
-
if (err.suggestion !== undefined) error.suggestion = err.suggestion;
|
|
220
140
|
if (showDetail) error.detail = err.detail;
|
|
221
141
|
process.stderr.write(JSON.stringify({ error }) + "\n");
|
|
222
142
|
return;
|
|
223
143
|
}
|
|
224
144
|
// An error message can quote API data (a name that matched several, …).
|
|
225
|
-
process.stderr.write(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
145
|
+
process.stderr.write(
|
|
146
|
+
(this.opts.color ? pc.red("error: ") : "error: ") + sanitizeForTerminal(err.message) + "\n",
|
|
147
|
+
);
|
|
229
148
|
if (showDetail) {
|
|
230
|
-
|
|
231
|
-
|
|
149
|
+
process.stderr.write(
|
|
150
|
+
(this.opts.color ? pc.dim("detail: ") : "detail: ") +
|
|
151
|
+
JSON.stringify(err.detail, null, 2) +
|
|
152
|
+
"\n",
|
|
153
|
+
);
|
|
232
154
|
}
|
|
233
155
|
}
|
|
234
156
|
|