linear-sdk-cli 0.1.0
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/.claude-plugin/marketplace.json +20 -0
- package/.claude-plugin/plugin.json +9 -0
- package/CHANGELOG.md +807 -0
- package/COVERAGE.md +495 -0
- package/LICENSE +21 -0
- package/MIGRATING.md +220 -0
- package/README.md +524 -0
- package/package.json +87 -0
- package/skills/linear-sdk-cli/SKILL.md +534 -0
- package/skills/linear-sdk-cli/references/api.md +27 -0
- package/skills/linear-sdk-cli/references/attachment.md +82 -0
- package/skills/linear-sdk-cli/references/auth.md +174 -0
- package/skills/linear-sdk-cli/references/commands.md +37 -0
- package/skills/linear-sdk-cli/references/comment.md +154 -0
- package/skills/linear-sdk-cli/references/completion.md +17 -0
- package/skills/linear-sdk-cli/references/config.md +99 -0
- package/skills/linear-sdk-cli/references/cycle.md +131 -0
- package/skills/linear-sdk-cli/references/document.md +180 -0
- package/skills/linear-sdk-cli/references/favorite.md +78 -0
- package/skills/linear-sdk-cli/references/initiative-update.md +67 -0
- package/skills/linear-sdk-cli/references/initiative.md +256 -0
- package/skills/linear-sdk-cli/references/issue.md +928 -0
- package/skills/linear-sdk-cli/references/label.md +112 -0
- package/skills/linear-sdk-cli/references/milestone.md +139 -0
- package/skills/linear-sdk-cli/references/notification.md +123 -0
- package/skills/linear-sdk-cli/references/open.md +29 -0
- package/skills/linear-sdk-cli/references/organization.md +99 -0
- package/skills/linear-sdk-cli/references/project-update.md +67 -0
- package/skills/linear-sdk-cli/references/project.md +234 -0
- package/skills/linear-sdk-cli/references/roadmap.md +150 -0
- package/skills/linear-sdk-cli/references/schema.md +21 -0
- package/skills/linear-sdk-cli/references/state.md +61 -0
- package/skills/linear-sdk-cli/references/team.md +227 -0
- package/skills/linear-sdk-cli/references/user.md +103 -0
- package/skills/linear-sdk-cli/references/webhook.md +151 -0
- package/skills/linear-sdk-cli/references/whoami.md +26 -0
- package/skills/linear-sdk-cli/scripts/generate-docs.ts +339 -0
- package/src/bin/linear.ts +97 -0
- package/src/cli.ts +232 -0
- package/src/client.ts +172 -0
- package/src/commands/agent-session.ts +121 -0
- package/src/commands/api.ts +317 -0
- package/src/commands/attach.ts +90 -0
- package/src/commands/attachment.ts +74 -0
- package/src/commands/comment.ts +225 -0
- package/src/commands/completion.ts +69 -0
- package/src/commands/cycle.ts +125 -0
- package/src/commands/discover.ts +170 -0
- package/src/commands/document.ts +211 -0
- package/src/commands/favorite.ts +72 -0
- package/src/commands/initiative-update.ts +47 -0
- package/src/commands/initiative.ts +261 -0
- package/src/commands/issue.ts +1263 -0
- package/src/commands/label.ts +119 -0
- package/src/commands/meta.ts +534 -0
- package/src/commands/milestone.ts +163 -0
- package/src/commands/notification.ts +110 -0
- package/src/commands/open.ts +84 -0
- package/src/commands/organization.ts +79 -0
- package/src/commands/project-update.ts +47 -0
- package/src/commands/project.ts +339 -0
- package/src/commands/roadmap.ts +131 -0
- package/src/commands/state.ts +58 -0
- package/src/commands/team.ts +243 -0
- package/src/commands/user.ts +107 -0
- package/src/commands/webhook.ts +156 -0
- package/src/config.ts +1236 -0
- package/src/context.ts +117 -0
- package/src/git.ts +174 -0
- package/src/index.ts +10 -0
- package/src/lib/action.ts +26 -0
- package/src/lib/body.ts +172 -0
- package/src/lib/errors.ts +195 -0
- package/src/lib/introspect.ts +150 -0
- package/src/lib/keyring.ts +270 -0
- package/src/lib/mutation.ts +53 -0
- package/src/lib/open.ts +32 -0
- package/src/lib/options.ts +640 -0
- package/src/lib/output-shapes.ts +562 -0
- package/src/lib/pagination.ts +145 -0
- package/src/lib/prompt.ts +91 -0
- package/src/lib/resolve.ts +596 -0
- package/src/lib/shape.ts +273 -0
- package/src/lib/status-update.ts +91 -0
- package/src/lib/upload.ts +329 -0
- package/src/output/color.ts +52 -0
- package/src/output/format.ts +238 -0
- package/src/output/markdown.ts +133 -0
- package/src/output/pager.ts +131 -0
- package/src/output/sanitize.ts +76 -0
- package/src/output/table.ts +198 -0
- package/src/services/agent-session.ts +289 -0
- package/src/services/attachment.ts +198 -0
- package/src/services/comment.ts +262 -0
- package/src/services/cycle.ts +224 -0
- package/src/services/document.ts +357 -0
- package/src/services/favorite.ts +147 -0
- package/src/services/initiative-update.ts +59 -0
- package/src/services/initiative.ts +449 -0
- package/src/services/issue.ts +1153 -0
- package/src/services/label.ts +193 -0
- package/src/services/milestone.ts +262 -0
- package/src/services/notification.ts +190 -0
- package/src/services/organization.ts +144 -0
- package/src/services/project-update.ts +36 -0
- package/src/services/project.ts +494 -0
- package/src/services/roadmap.ts +177 -0
- package/src/services/state.ts +127 -0
- package/src/services/team.ts +386 -0
- package/src/services/user.ts +137 -0
- package/src/services/webhook.ts +203 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Iterate a Linear SDK connection up to `limit`, following `fetchNext`.
|
|
3
|
+
*
|
|
4
|
+
* The SDK's connection objects expose `.nodes` and `.pageInfo.hasNextPage`,
|
|
5
|
+
* and `.fetchNext()` mutates the connection to append the next page's nodes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { fetchNextWithRetry, withRetry } from "../client.js";
|
|
9
|
+
|
|
10
|
+
/** Linear accepts at most 250 nodes per connection page. */
|
|
11
|
+
export const MAX_PAGE_SIZE = 250;
|
|
12
|
+
|
|
13
|
+
export interface Connection<T> {
|
|
14
|
+
nodes: T[];
|
|
15
|
+
pageInfo: { hasNextPage: boolean };
|
|
16
|
+
fetchNext: () => Promise<Connection<T>>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Non-enumerable metadata carried by list arrays without changing their JSON contract. */
|
|
20
|
+
const HAS_MORE = Symbol.for("linear-sdk-cli.pagination.has-more");
|
|
21
|
+
|
|
22
|
+
type PagedArray<T> = T[] & { [HAS_MORE]?: boolean };
|
|
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[]> {
|
|
45
|
+
let conn = first;
|
|
46
|
+
// Each fetchNext() appends to conn.nodes in place.
|
|
47
|
+
while (conn.nodes.length < limit && conn.pageInfo.hasNextPage) {
|
|
48
|
+
// Page two and later are independent requests and can be rate-limited just
|
|
49
|
+
// like page one. Keep them behind the same bounded retry policy.
|
|
50
|
+
conn = await fetchNextWithRetry(conn);
|
|
51
|
+
}
|
|
52
|
+
if (limit === Infinity) return withMoreMetadata(conn.nodes, false);
|
|
53
|
+
const hasMore =
|
|
54
|
+
conn.nodes.length > limit || (conn.nodes.length >= limit && conn.pageInfo.hasNextPage);
|
|
55
|
+
return withMoreMetadata(conn.nodes.slice(0, limit), hasMore);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** How many to request per page given a desired total limit. */
|
|
59
|
+
export function pageSize(limit: number): number {
|
|
60
|
+
if (limit === Infinity) return MAX_PAGE_SIZE;
|
|
61
|
+
return Math.min(Math.max(limit, 1), MAX_PAGE_SIZE);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* `collect`, plus an honest answer to "were there more?".
|
|
66
|
+
*
|
|
67
|
+
* The connection cannot answer that after the fact: `fetchNext()` mutates the
|
|
68
|
+
* connection in place, so once collection stops at `limit` the object's
|
|
69
|
+
* `pageInfo.hasNextPage` describes the *last page fetched*, not the items the
|
|
70
|
+
* limit hid. Reading it post-collection reports "nothing was hidden" precisely
|
|
71
|
+
* when the final page happened to be the last one — which is the common case.
|
|
72
|
+
*
|
|
73
|
+
* So we ask for one more than we need and let the extra item be the evidence.
|
|
74
|
+
* Callers should size the first page with `pageSizeForMore(limit)` so the
|
|
75
|
+
* spare slot costs no extra request.
|
|
76
|
+
*/
|
|
77
|
+
export async function collectWithMore<T>(
|
|
78
|
+
conn: Connection<T>,
|
|
79
|
+
limit: number,
|
|
80
|
+
): Promise<{ items: T[]; hasMore: boolean }> {
|
|
81
|
+
const nodes = await collect(conn, limit + 1);
|
|
82
|
+
return { items: nodes.slice(0, limit), hasMore: nodes.length > limit };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Page size for a `collectWithMore(limit)`: room for the sentinel item. */
|
|
86
|
+
export function pageSizeForMore(limit: number): number {
|
|
87
|
+
return pageSize(limit === Infinity ? Infinity : limit + 1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A GraphQL connection page as returned by a raw query. */
|
|
91
|
+
interface RawConnection {
|
|
92
|
+
nodes: any[];
|
|
93
|
+
pageInfo: { hasNextPage: boolean; endCursor?: string };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** A client exposing the low-level GraphQL `rawRequest`. */
|
|
97
|
+
interface RawClient {
|
|
98
|
+
client: { rawRequest: (query: string, variables: Record<string, any>) => Promise<{ data: any }> };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Run a cursor-paginated raw GraphQL query, collecting transformed nodes up to
|
|
103
|
+
* `limit`. The query must take `$first: Int!` and `$after: String` and return a
|
|
104
|
+
* connection (`{ nodes, pageInfo { hasNextPage endCursor } }`) reachable at
|
|
105
|
+
* `connectionPath` (a dot path) within `data.data`. Each call is wrapped in
|
|
106
|
+
* `withRetry`; pagination stops at `limit` or when `hasNextPage` is false.
|
|
107
|
+
*
|
|
108
|
+
* Mirrors the hand-rolled loops in the list services exactly: page size is
|
|
109
|
+
* `limit === Infinity ? 250 : Math.min(limit, 250)`, and rows are capped at
|
|
110
|
+
* `limit` mid-page.
|
|
111
|
+
*/
|
|
112
|
+
export async function collectRawQuery<T>(
|
|
113
|
+
client: RawClient,
|
|
114
|
+
query: string,
|
|
115
|
+
variables: Record<string, any>,
|
|
116
|
+
connectionPath: string,
|
|
117
|
+
limit: number,
|
|
118
|
+
transform: (node: any) => T,
|
|
119
|
+
): Promise<T[]> {
|
|
120
|
+
const pageLimit = pageSize(limit);
|
|
121
|
+
const rows: T[] = [];
|
|
122
|
+
let after: string | undefined;
|
|
123
|
+
let hasMore = false;
|
|
124
|
+
|
|
125
|
+
for (;;) {
|
|
126
|
+
const data: any = await withRetry(() =>
|
|
127
|
+
client.client.rawRequest(query, { ...variables, first: pageLimit, after }),
|
|
128
|
+
);
|
|
129
|
+
const conn = connectionPath.split(".").reduce((acc: any, key) => acc?.[key], data.data) as
|
|
130
|
+
RawConnection | null | undefined;
|
|
131
|
+
// A nested path (`issue.comments`) can land on null when the parent entity
|
|
132
|
+
// is gone; that is an empty list, not a crash.
|
|
133
|
+
if (!conn) break;
|
|
134
|
+
for (const [index, n] of conn.nodes.entries()) {
|
|
135
|
+
rows.push(transform(n));
|
|
136
|
+
if (rows.length >= limit) {
|
|
137
|
+
hasMore = index < conn.nodes.length - 1 || conn.pageInfo.hasNextPage;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (rows.length >= limit || !conn.pageInfo.hasNextPage) break;
|
|
142
|
+
after = conn.pageInfo.endCursor;
|
|
143
|
+
}
|
|
144
|
+
return withMoreMetadata(rows, hasMore);
|
|
145
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive prompt helpers. Every prompt is TTY-gated: when stdin is not a
|
|
3
|
+
* TTY (or --no-input / --yes was passed) prompting is refused with a usage
|
|
4
|
+
* error so the CLI never hangs in scripts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
confirm as inquirerConfirm,
|
|
9
|
+
input as inquirerInput,
|
|
10
|
+
password as inquirerPassword,
|
|
11
|
+
select as inquirerSelect,
|
|
12
|
+
} from "@inquirer/prompts";
|
|
13
|
+
import { usageError } from "./errors.js";
|
|
14
|
+
import type { Context } from "../context.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Exit code for "the user declined a confirmation prompt".
|
|
18
|
+
*
|
|
19
|
+
* Deliberately outside the 0–5 range of `ExitCode` (lib/errors.ts): declining
|
|
20
|
+
* is neither success nor any of the failures, and a script needs to tell the
|
|
21
|
+
* three apart. Kept next to the prompt that produces it; see `ExitCode` for the
|
|
22
|
+
* rest of the table.
|
|
23
|
+
*/
|
|
24
|
+
export const EXIT_CANCELLED = 6;
|
|
25
|
+
|
|
26
|
+
export function ensureInteractive(ctx: Context, what: string): void {
|
|
27
|
+
if (!ctx.isTTY) {
|
|
28
|
+
throw usageError(`${what} requires interactive input; provide it via flags or run in a TTY.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function confirmDestructive(ctx: Context, message: string): Promise<boolean> {
|
|
33
|
+
if (ctx.options.yes) return true;
|
|
34
|
+
if (!ctx.isTTY) {
|
|
35
|
+
throw usageError(`Refusing destructive action without confirmation; pass --yes. (${message})`);
|
|
36
|
+
}
|
|
37
|
+
if (await inquirerConfirm({ message, default: false })) return true;
|
|
38
|
+
// Every gated command answers `false` with a bare `return`, so before this the
|
|
39
|
+
// decline produced exit 0 and no output at all — indistinguishable from a
|
|
40
|
+
// successful delete, which made `linear issue delete X && rm …` run the `&&`
|
|
41
|
+
// side after the user had just said no. The receipt and the exit code are
|
|
42
|
+
// emitted here rather than at each of the ~14 call sites precisely so they
|
|
43
|
+
// cannot drift apart between commands.
|
|
44
|
+
ctx.output.cancelled(message);
|
|
45
|
+
process.exitCode = EXIT_CANCELLED;
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function promptInput(
|
|
50
|
+
ctx: Context,
|
|
51
|
+
message: string,
|
|
52
|
+
opts: { required?: boolean; default?: string } = {},
|
|
53
|
+
): Promise<string> {
|
|
54
|
+
ensureInteractive(ctx, message);
|
|
55
|
+
return inquirerInput({
|
|
56
|
+
message,
|
|
57
|
+
default: opts.default,
|
|
58
|
+
validate: (v) => (opts.required && !v.trim() ? "Required" : true),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Prompt for a secret.
|
|
64
|
+
*
|
|
65
|
+
* `promptInput` echoes, which for an API key means the credential is on screen
|
|
66
|
+
* during entry and then in the terminal's scrollback for as long as the user
|
|
67
|
+
* keeps that window — recoverable by anything that can read it, and easy to
|
|
68
|
+
* paste into a screenshot or a bug report by accident. inquirer's `password`
|
|
69
|
+
* prompt reads the same value without rendering it.
|
|
70
|
+
*/
|
|
71
|
+
export async function promptSecret(
|
|
72
|
+
ctx: Context,
|
|
73
|
+
message: string,
|
|
74
|
+
opts: { required?: boolean } = {},
|
|
75
|
+
): Promise<string> {
|
|
76
|
+
ensureInteractive(ctx, message);
|
|
77
|
+
return inquirerPassword({
|
|
78
|
+
message,
|
|
79
|
+
mask: true,
|
|
80
|
+
validate: (v) => (opts.required && !v.trim() ? "Required" : true),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function promptSelect<T>(
|
|
85
|
+
ctx: Context,
|
|
86
|
+
message: string,
|
|
87
|
+
choices: Array<{ name: string; value: T }>,
|
|
88
|
+
): Promise<T> {
|
|
89
|
+
ensureInteractive(ctx, message);
|
|
90
|
+
return inquirerSelect({ message, choices });
|
|
91
|
+
}
|