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,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User service: all SDK access for users lives here so commands stay thin.
|
|
3
|
+
*
|
|
4
|
+
* The workspace user list goes through the typed `client.users()` connection;
|
|
5
|
+
* single `view`/`me` resolve a user id via `resolveUserId` (handling me / email /
|
|
6
|
+
* name / id) and read the typed SDK model. This group is read-only.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { LinearClient } from "@linear/sdk";
|
|
10
|
+
import { withRetry } from "../client.js";
|
|
11
|
+
import { shape } from "../lib/shape.js";
|
|
12
|
+
import { collect, inheritPaginationMetadata, pageSize } from "../lib/pagination.js";
|
|
13
|
+
import { resolveUserId } from "../lib/resolve.js";
|
|
14
|
+
|
|
15
|
+
export interface UserRow {
|
|
16
|
+
id: string;
|
|
17
|
+
displayName: string;
|
|
18
|
+
name: string;
|
|
19
|
+
email: string;
|
|
20
|
+
active: boolean;
|
|
21
|
+
admin: boolean;
|
|
22
|
+
guest: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The row's shape as `linear commands` advertises it (TES-610); checked against the interface. */
|
|
26
|
+
export const USER_ROW_SHAPE = shape<UserRow>({
|
|
27
|
+
id: "string",
|
|
28
|
+
displayName: "string",
|
|
29
|
+
name: "string",
|
|
30
|
+
email: "string",
|
|
31
|
+
active: "boolean",
|
|
32
|
+
admin: "boolean",
|
|
33
|
+
guest: "boolean",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* List the workspace's users. Linear defaults `includeDisabled` to false, so
|
|
38
|
+
* deactivated users are invisible (and the `active` column constantly true)
|
|
39
|
+
* unless the caller opts in.
|
|
40
|
+
*/
|
|
41
|
+
export async function listUsers(
|
|
42
|
+
client: LinearClient,
|
|
43
|
+
limit: number,
|
|
44
|
+
includeDisabled = false,
|
|
45
|
+
): Promise<UserRow[]> {
|
|
46
|
+
const conn = await withRetry(() => client.users({ first: pageSize(limit), includeDisabled }));
|
|
47
|
+
const nodes = await collect(conn as any, limit);
|
|
48
|
+
return inheritPaginationMetadata(nodes.map(toRow), nodes);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface UserDetail {
|
|
52
|
+
id: string;
|
|
53
|
+
displayName: string;
|
|
54
|
+
name: string;
|
|
55
|
+
email: string;
|
|
56
|
+
active: boolean;
|
|
57
|
+
admin: boolean;
|
|
58
|
+
guest: boolean;
|
|
59
|
+
isMe: boolean;
|
|
60
|
+
description: string | null;
|
|
61
|
+
statusLabel: string | null;
|
|
62
|
+
timezone: string | null;
|
|
63
|
+
url: string;
|
|
64
|
+
avatarUrl: string | null;
|
|
65
|
+
lastSeen: string | null;
|
|
66
|
+
createdAt: string;
|
|
67
|
+
updatedAt: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The detail's shape; checked against `UserDetail`. */
|
|
71
|
+
export const USER_DETAIL_SHAPE = shape<UserDetail>({
|
|
72
|
+
id: "string",
|
|
73
|
+
displayName: "string",
|
|
74
|
+
name: "string",
|
|
75
|
+
email: "string",
|
|
76
|
+
active: "boolean",
|
|
77
|
+
admin: "boolean",
|
|
78
|
+
guest: "boolean",
|
|
79
|
+
isMe: "boolean",
|
|
80
|
+
description: "string|null",
|
|
81
|
+
statusLabel: "string|null",
|
|
82
|
+
timezone: "string|null",
|
|
83
|
+
url: "string",
|
|
84
|
+
avatarUrl: "string|null",
|
|
85
|
+
lastSeen: "string|null",
|
|
86
|
+
createdAt: "string",
|
|
87
|
+
updatedAt: "string",
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve a user reference (`me`, email, name, or id) and return its detail.
|
|
92
|
+
* `resolveUserId` handles the `me`/email/name/id ergonomics.
|
|
93
|
+
*/
|
|
94
|
+
export async function getUserDetail(client: LinearClient, idArg: string): Promise<UserDetail> {
|
|
95
|
+
const userId = await resolveUserId(client, idArg);
|
|
96
|
+
const user = await withRetry(() => client.user(userId));
|
|
97
|
+
return toDetail(user);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The authenticated viewer. */
|
|
101
|
+
export async function getViewer(client: LinearClient): Promise<UserDetail> {
|
|
102
|
+
const user = await withRetry(() => client.viewer);
|
|
103
|
+
return toDetail(user);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function toRow(u: any): UserRow {
|
|
107
|
+
return {
|
|
108
|
+
id: u.id,
|
|
109
|
+
displayName: u.displayName,
|
|
110
|
+
name: u.name,
|
|
111
|
+
email: u.email,
|
|
112
|
+
active: !!u.active,
|
|
113
|
+
admin: !!u.admin,
|
|
114
|
+
guest: !!u.guest,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function toDetail(u: any): UserDetail {
|
|
119
|
+
return {
|
|
120
|
+
id: u.id,
|
|
121
|
+
displayName: u.displayName,
|
|
122
|
+
name: u.name,
|
|
123
|
+
email: u.email,
|
|
124
|
+
active: !!u.active,
|
|
125
|
+
admin: !!u.admin,
|
|
126
|
+
guest: !!u.guest,
|
|
127
|
+
isMe: !!u.isMe,
|
|
128
|
+
description: u.description ?? null,
|
|
129
|
+
statusLabel: u.statusLabel ?? null,
|
|
130
|
+
timezone: u.timezone ?? null,
|
|
131
|
+
url: u.url,
|
|
132
|
+
avatarUrl: u.avatarUrl ?? null,
|
|
133
|
+
lastSeen: u.lastSeen?.toISOString?.() ?? (u.lastSeen ? String(u.lastSeen) : null),
|
|
134
|
+
createdAt: u.createdAt?.toISOString?.() ?? String(u.createdAt),
|
|
135
|
+
updatedAt: u.updatedAt?.toISOString?.() ?? String(u.updatedAt),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook service: all SDK access for webhooks lives here so commands stay thin.
|
|
3
|
+
*
|
|
4
|
+
* Webhooks are workspace-scoped (optionally team-scoped). The list uses the typed
|
|
5
|
+
* SDK connection (relation-light: no per-row fetches), `view` reads a single model,
|
|
6
|
+
* and mutations unwrap the `{ success, webhook }` payload.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { type LinearClient, WebhookResourceType } from "@linear/sdk";
|
|
10
|
+
import { withRetry } from "../client.js";
|
|
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";
|
|
14
|
+
import { assertMutation, unwrapMutation } from "../lib/mutation.js";
|
|
15
|
+
import { resolveTeam } from "../lib/resolve.js";
|
|
16
|
+
|
|
17
|
+
/** The valid webhook resource-type strings (e.g. "Issue", "IssueSLA"). */
|
|
18
|
+
const RESOURCE_TYPES = Object.values(WebhookResourceType);
|
|
19
|
+
const RESOURCE_BY_LOWER = new Map(RESOURCE_TYPES.map((t) => [t.toLowerCase(), t]));
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validate and normalize user-supplied resource types against the SDK enum,
|
|
23
|
+
* case-insensitively, so a typo or `issue` becomes a clean usage error (with the
|
|
24
|
+
* valid list) instead of an opaque API validation failure.
|
|
25
|
+
*/
|
|
26
|
+
export function normalizeResourceTypes(input: string[]): string[] {
|
|
27
|
+
const out: string[] = [];
|
|
28
|
+
for (const raw of input) {
|
|
29
|
+
const match = RESOURCE_BY_LOWER.get(raw.toLowerCase());
|
|
30
|
+
if (!match) {
|
|
31
|
+
throw usageError(
|
|
32
|
+
`Unknown webhook resource type '${raw}'. Valid types: ${RESOURCE_TYPES.join(", ")}`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (!out.includes(match)) out.push(match);
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface WebhookRow {
|
|
41
|
+
id: string;
|
|
42
|
+
url: string | null;
|
|
43
|
+
enabled: boolean;
|
|
44
|
+
resourceTypes: string[];
|
|
45
|
+
label: string | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The row's shape as `linear commands` advertises it (TES-610); checked against the interface. */
|
|
49
|
+
export const WEBHOOK_ROW_SHAPE = shape<WebhookRow>({
|
|
50
|
+
id: "string",
|
|
51
|
+
url: "string|null",
|
|
52
|
+
enabled: "boolean",
|
|
53
|
+
resourceTypes: ["string"],
|
|
54
|
+
label: "string|null",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/** All webhooks in the workspace (relation-light, so no N+1). */
|
|
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,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface WebhookDetail {
|
|
74
|
+
id: string;
|
|
75
|
+
url: string | null;
|
|
76
|
+
enabled: boolean;
|
|
77
|
+
resourceTypes: string[];
|
|
78
|
+
label: string | null;
|
|
79
|
+
allPublicTeams: boolean;
|
|
80
|
+
team: string | null;
|
|
81
|
+
creator: string | null;
|
|
82
|
+
createdAt: string;
|
|
83
|
+
updatedAt: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The detail's shape; checked against `WebhookDetail`. */
|
|
87
|
+
export const WEBHOOK_DETAIL_SHAPE = shape<WebhookDetail>({
|
|
88
|
+
id: "string",
|
|
89
|
+
url: "string|null",
|
|
90
|
+
enabled: "boolean",
|
|
91
|
+
resourceTypes: ["string"],
|
|
92
|
+
label: "string|null",
|
|
93
|
+
allPublicTeams: "boolean",
|
|
94
|
+
team: "string|null",
|
|
95
|
+
creator: "string|null",
|
|
96
|
+
createdAt: "string",
|
|
97
|
+
updatedAt: "string",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
export async function getWebhookDetail(client: LinearClient, id: string): Promise<WebhookDetail> {
|
|
101
|
+
const webhook = await withRetry(() => client.webhook(id));
|
|
102
|
+
const [team, creator] = await Promise.all([webhook.team, webhook.creator]);
|
|
103
|
+
return {
|
|
104
|
+
id: webhook.id,
|
|
105
|
+
url: webhook.url ?? null,
|
|
106
|
+
enabled: webhook.enabled,
|
|
107
|
+
resourceTypes: webhook.resourceTypes ?? [],
|
|
108
|
+
label: webhook.label ?? null,
|
|
109
|
+
allPublicTeams: webhook.allPublicTeams,
|
|
110
|
+
team: team ? `${team.key} ${team.name}` : null,
|
|
111
|
+
creator: creator?.displayName ?? null,
|
|
112
|
+
createdAt: webhook.createdAt.toISOString(),
|
|
113
|
+
updatedAt: webhook.updatedAt.toISOString(),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface CreateOptions {
|
|
118
|
+
url: string;
|
|
119
|
+
resourceTypes: string[];
|
|
120
|
+
/** Team to scope the webhook to (key/name/id); from the global --team. */
|
|
121
|
+
team?: string;
|
|
122
|
+
label?: string;
|
|
123
|
+
allPublicTeams?: boolean;
|
|
124
|
+
secret?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Build a WebhookCreateInput, resolving the team reference (if any) to an id. */
|
|
128
|
+
export async function createWebhook(client: LinearClient, opts: CreateOptions) {
|
|
129
|
+
if (!opts.url) throw usageError("A webhook needs a --url.");
|
|
130
|
+
if (!opts.resourceTypes?.length)
|
|
131
|
+
throw usageError("A webhook needs at least one --resource (e.g. Issue, Comment, Project).");
|
|
132
|
+
|
|
133
|
+
// A webhook must be scoped to a team or explicitly to all public teams.
|
|
134
|
+
if (!opts.team && !opts.allPublicTeams) {
|
|
135
|
+
throw usageError(
|
|
136
|
+
"A webhook needs a scope: pass --team <KEY> (or set a default team) or --all-public.",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const input: Record<string, any> = {
|
|
141
|
+
url: opts.url,
|
|
142
|
+
resourceTypes: normalizeResourceTypes(opts.resourceTypes),
|
|
143
|
+
};
|
|
144
|
+
// --all-public and a specific team are mutually exclusive; prefer all-public.
|
|
145
|
+
if (opts.allPublicTeams) input.allPublicTeams = true;
|
|
146
|
+
else if (opts.team) input.teamId = (await resolveTeam(client, opts.team, undefined)).id;
|
|
147
|
+
if (opts.label !== undefined) input.label = opts.label;
|
|
148
|
+
if (opts.secret !== undefined) input.secret = opts.secret;
|
|
149
|
+
|
|
150
|
+
return unwrapMutation(
|
|
151
|
+
withRetry(() => client.createWebhook(input as any)),
|
|
152
|
+
"webhook",
|
|
153
|
+
"Webhook creation",
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface UpdateOptions {
|
|
158
|
+
url?: string;
|
|
159
|
+
enabled?: boolean;
|
|
160
|
+
resourceTypes?: string[];
|
|
161
|
+
label?: string;
|
|
162
|
+
secret?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function updateWebhook(client: LinearClient, id: string, opts: UpdateOptions) {
|
|
166
|
+
const input: Record<string, any> = {};
|
|
167
|
+
if (opts.url !== undefined) input.url = opts.url;
|
|
168
|
+
if (opts.enabled !== undefined) input.enabled = opts.enabled;
|
|
169
|
+
if (opts.resourceTypes !== undefined) {
|
|
170
|
+
if (!opts.resourceTypes.length)
|
|
171
|
+
throw usageError("--resource needs at least one resource type.");
|
|
172
|
+
input.resourceTypes = normalizeResourceTypes(opts.resourceTypes);
|
|
173
|
+
}
|
|
174
|
+
if (opts.label !== undefined) input.label = opts.label;
|
|
175
|
+
if (opts.secret !== undefined) input.secret = opts.secret;
|
|
176
|
+
|
|
177
|
+
if (Object.keys(input).length === 0)
|
|
178
|
+
throw usageError(
|
|
179
|
+
"Nothing to update; pass at least one of --url, --enabled/--disabled, --resource.",
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return unwrapMutation(
|
|
183
|
+
withRetry(() => client.updateWebhook(id, input as any)),
|
|
184
|
+
"webhook",
|
|
185
|
+
"Webhook update",
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
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
|
+
);
|
|
202
|
+
return webhook;
|
|
203
|
+
}
|