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,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `linear team` (alias `t`) — inspect and manage teams.
|
|
3
|
+
*
|
|
4
|
+
* The team key argument is optional on the sub-resource and view commands and
|
|
5
|
+
* falls back to the configured default team (ctx.defaultTeam) when omitted.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import { action } from "../lib/action.js";
|
|
10
|
+
import { confirmDestructive, promptInput } from "../lib/prompt.js";
|
|
11
|
+
import type { Context } from "../context.js";
|
|
12
|
+
import * as svc from "../services/team.js";
|
|
13
|
+
import { noteAllIsPagination, ALL_VS_INCLUDE_DISABLED_HELP } from "./user.js";
|
|
14
|
+
import type { Column } from "../output/table.js";
|
|
15
|
+
|
|
16
|
+
const TEAM_COLUMNS: Column<svc.TeamRow>[] = [
|
|
17
|
+
{ key: "key", header: "Key", value: (r) => r.key },
|
|
18
|
+
{ key: "name", header: "Name", value: (r) => r.name, max: 40 },
|
|
19
|
+
{ key: "id", header: "ID", value: (r) => r.id },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const MEMBER_COLUMNS: Column<svc.MemberRow>[] = [
|
|
23
|
+
{ key: "name", header: "Name", value: (r) => r.displayName, max: 24 },
|
|
24
|
+
{ key: "email", header: "Email", value: (r) => r.email, max: 40 },
|
|
25
|
+
{ key: "active", header: "Active", value: (r) => (r.active ? "yes" : "no") },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const STATE_COLUMNS: Column<svc.StateRow>[] = [
|
|
29
|
+
{ key: "name", header: "Name", value: (r) => r.name, max: 24 },
|
|
30
|
+
{ key: "type", header: "Type", value: (r) => r.type },
|
|
31
|
+
{ key: "color", header: "Color", value: (r) => r.color },
|
|
32
|
+
{ key: "position", header: "Pos", value: (r) => r.position },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const LABEL_COLUMNS: Column<svc.LabelRow>[] = [
|
|
36
|
+
{ key: "name", header: "Name", value: (r) => r.name, max: 30 },
|
|
37
|
+
{ key: "color", header: "Color", value: (r) => r.color },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const CYCLE_COLUMNS: Column<svc.CycleRow>[] = [
|
|
41
|
+
{ key: "number", header: "#", value: (r) => r.number },
|
|
42
|
+
{ key: "name", header: "Name", value: (r) => r.name ?? "—", max: 30 },
|
|
43
|
+
{ key: "startsAt", header: "Starts", value: (r) => (r.startsAt ? r.startsAt.slice(0, 10) : "—") },
|
|
44
|
+
{ key: "endsAt", header: "Ends", value: (r) => (r.endsAt ? r.endsAt.slice(0, 10) : "—") },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
export function registerTeam(program: Command): void {
|
|
48
|
+
const team = program.command("team").alias("t").description("Inspect and manage teams");
|
|
49
|
+
|
|
50
|
+
// list --------------------------------------------------------------------
|
|
51
|
+
team
|
|
52
|
+
.command("list")
|
|
53
|
+
.alias("ls")
|
|
54
|
+
.description("List all teams")
|
|
55
|
+
.action(
|
|
56
|
+
action(async (ctx: Context) => {
|
|
57
|
+
const rows = await svc.listTeams(ctx.client, ctx.limit);
|
|
58
|
+
ctx.output.list(rows, TEAM_COLUMNS, rows);
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// id ----------------------------------------------------------------------
|
|
63
|
+
team
|
|
64
|
+
.command("id [key]")
|
|
65
|
+
.description("Print a team's UUID (defaults to the configured team)")
|
|
66
|
+
.action(
|
|
67
|
+
action(async (ctx: Context, _opts, keyArg?: string) => {
|
|
68
|
+
const resolved = await svc.getTeamIdentity(ctx.client, keyArg, ctx.defaultTeam);
|
|
69
|
+
// Human mode is deliberately a single UUID for command substitution;
|
|
70
|
+
// JSON retains the resolved key and name as useful context.
|
|
71
|
+
ctx.output.emit(resolved, () => process.stdout.write(resolved.id + "\n"));
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// view --------------------------------------------------------------------
|
|
76
|
+
team
|
|
77
|
+
.command("view [key]")
|
|
78
|
+
.description("Show a team (defaults to the configured team)")
|
|
79
|
+
.action(
|
|
80
|
+
action(async (ctx: Context, _opts, keyArg?: string) => {
|
|
81
|
+
const detail = await svc.getTeamDetail(ctx.client, keyArg, ctx.defaultTeam);
|
|
82
|
+
ctx.output.detail(detail, [
|
|
83
|
+
["Team", `${detail.key} ${detail.name}`],
|
|
84
|
+
["ID", detail.id],
|
|
85
|
+
["Description", detail.description],
|
|
86
|
+
["Members", detail.memberCount],
|
|
87
|
+
["Issues", detail.issueCount],
|
|
88
|
+
["Cycles enabled", detail.cyclesEnabled ? "yes" : "no"],
|
|
89
|
+
["Private", detail.private ? "yes" : "no"],
|
|
90
|
+
["Timezone", detail.timezone],
|
|
91
|
+
["Color", detail.color],
|
|
92
|
+
["Created", detail.createdAt],
|
|
93
|
+
["Updated", detail.updatedAt],
|
|
94
|
+
]);
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// members -----------------------------------------------------------------
|
|
99
|
+
team
|
|
100
|
+
.command("members [key]")
|
|
101
|
+
.description("List a team's members")
|
|
102
|
+
.option("--include-disabled", "include deactivated users (excluded by default)")
|
|
103
|
+
.addHelpText("after", ALL_VS_INCLUDE_DISABLED_HELP)
|
|
104
|
+
.action(
|
|
105
|
+
action(async (ctx: Context, opts, keyArg?: string) => {
|
|
106
|
+
// schpet's `--all` here means "inactive too"; ours is pagination. Say so.
|
|
107
|
+
noteAllIsPagination(ctx, !!opts.includeDisabled);
|
|
108
|
+
const rows = await svc.listMembers(
|
|
109
|
+
ctx.client,
|
|
110
|
+
keyArg,
|
|
111
|
+
ctx.defaultTeam,
|
|
112
|
+
ctx.limit,
|
|
113
|
+
!!opts.includeDisabled,
|
|
114
|
+
);
|
|
115
|
+
ctx.output.list(rows, MEMBER_COLUMNS, rows);
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// states ------------------------------------------------------------------
|
|
120
|
+
team
|
|
121
|
+
.command("states [key]")
|
|
122
|
+
.description("List a team's workflow states")
|
|
123
|
+
.action(
|
|
124
|
+
action(async (ctx: Context, _opts, keyArg?: string) => {
|
|
125
|
+
const rows = await svc.listStates(ctx.client, keyArg, ctx.defaultTeam, ctx.limit);
|
|
126
|
+
ctx.output.list(rows, STATE_COLUMNS, rows);
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// labels ------------------------------------------------------------------
|
|
131
|
+
team
|
|
132
|
+
.command("labels [key]")
|
|
133
|
+
.description("List a team's labels")
|
|
134
|
+
.action(
|
|
135
|
+
action(async (ctx: Context, _opts, keyArg?: string) => {
|
|
136
|
+
const rows = await svc.listLabels(ctx.client, keyArg, ctx.defaultTeam, ctx.limit);
|
|
137
|
+
ctx.output.list(rows, LABEL_COLUMNS, rows);
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// cycles ------------------------------------------------------------------
|
|
142
|
+
team
|
|
143
|
+
.command("cycles [key]")
|
|
144
|
+
.description("List a team's cycles")
|
|
145
|
+
.action(
|
|
146
|
+
action(async (ctx: Context, _opts, keyArg?: string) => {
|
|
147
|
+
const rows = await svc.listCycles(ctx.client, keyArg, ctx.defaultTeam, ctx.limit);
|
|
148
|
+
ctx.output.list(rows, CYCLE_COLUMNS, rows);
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// create ------------------------------------------------------------------
|
|
153
|
+
team
|
|
154
|
+
.command("create")
|
|
155
|
+
.alias("new")
|
|
156
|
+
.description("Create a new team")
|
|
157
|
+
.option("--name <name>", "team name")
|
|
158
|
+
.option("--key <key>", "team key (e.g. ENG); generated from the name if omitted")
|
|
159
|
+
.option("-d, --description <text>", "team description")
|
|
160
|
+
.option("--private", "make the team private (members only)")
|
|
161
|
+
.action(
|
|
162
|
+
action(async (ctx: Context, opts) => {
|
|
163
|
+
let name: string | undefined = opts.name;
|
|
164
|
+
if (!name) name = await promptInput(ctx, "Name:", { required: true });
|
|
165
|
+
const created = await svc.createTeam(ctx.client, {
|
|
166
|
+
name,
|
|
167
|
+
key: opts.key,
|
|
168
|
+
description: opts.description,
|
|
169
|
+
private: !!opts.private,
|
|
170
|
+
});
|
|
171
|
+
ctx.output.emit({ id: created.id, key: created.key, name: created.name }, () =>
|
|
172
|
+
ctx.output.success(`Created team ${created.key} (${created.name})`),
|
|
173
|
+
);
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// update ------------------------------------------------------------------
|
|
178
|
+
team
|
|
179
|
+
.command("update [key]")
|
|
180
|
+
.alias("edit")
|
|
181
|
+
.description("Update a team (defaults to the configured team)")
|
|
182
|
+
.option("--name <name>", "new team name")
|
|
183
|
+
.option("--key <key>", "new team key")
|
|
184
|
+
.option("-d, --description <text>", "new team description")
|
|
185
|
+
.action(
|
|
186
|
+
action(async (ctx: Context, opts, keyArg?: string) => {
|
|
187
|
+
const updated = await svc.updateTeam(ctx.client, keyArg, ctx.defaultTeam, {
|
|
188
|
+
name: opts.name,
|
|
189
|
+
key: opts.key,
|
|
190
|
+
description: opts.description,
|
|
191
|
+
});
|
|
192
|
+
ctx.output.emit({ id: updated.id, key: updated.key, name: updated.name }, () =>
|
|
193
|
+
ctx.output.success(`Updated team ${updated.key}`),
|
|
194
|
+
);
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// delete ------------------------------------------------------------------
|
|
199
|
+
// The key is required here, unlike the other team commands: a delete that
|
|
200
|
+
// silently fell back to the configured default team would be a disaster.
|
|
201
|
+
team
|
|
202
|
+
.command("delete <key>")
|
|
203
|
+
.alias("rm")
|
|
204
|
+
.description("Delete a team (admin); its issues go with it unless --move-issues")
|
|
205
|
+
.option("--move-issues <team>", "move the team's issues to another team first")
|
|
206
|
+
.addHelpText(
|
|
207
|
+
"after",
|
|
208
|
+
[
|
|
209
|
+
"",
|
|
210
|
+
"Examples:",
|
|
211
|
+
" linear team delete OLD --move-issues ENG",
|
|
212
|
+
" linear team delete SCRATCH --yes",
|
|
213
|
+
].join("\n"),
|
|
214
|
+
)
|
|
215
|
+
.action(
|
|
216
|
+
action(async (ctx: Context, opts, keyArg: string) => {
|
|
217
|
+
const plan = await svc.planDeleteTeam(ctx.client, keyArg, opts.moveIssues);
|
|
218
|
+
const { team: t, issueCount, moveTo } = plan;
|
|
219
|
+
const issues = `${issueCount} issue${issueCount === 1 ? "" : "s"}`;
|
|
220
|
+
const question = moveTo
|
|
221
|
+
? `Delete team ${t.key} (${t.name}), moving its ${issues} to ${moveTo.key} first?`
|
|
222
|
+
: `Delete team ${t.key} (${t.name}) and its ${issues}?`;
|
|
223
|
+
if (!(await confirmDestructive(ctx, question))) return;
|
|
224
|
+
let moved = 0;
|
|
225
|
+
if (moveTo && issueCount > 0) {
|
|
226
|
+
moved = await svc.moveTeamIssues(ctx.client, t, moveTo);
|
|
227
|
+
ctx.output.info(`Moved ${moved} issue${moved === 1 ? "" : "s"} to ${moveTo.key}`);
|
|
228
|
+
}
|
|
229
|
+
await svc.deleteTeam(ctx.client, t);
|
|
230
|
+
ctx.output.emit(
|
|
231
|
+
{
|
|
232
|
+
id: t.id,
|
|
233
|
+
key: t.key,
|
|
234
|
+
name: t.name,
|
|
235
|
+
deleted: true,
|
|
236
|
+
movedIssues: moved,
|
|
237
|
+
movedTo: moveTo ? { id: moveTo.id, key: moveTo.key, name: moveTo.name } : null,
|
|
238
|
+
},
|
|
239
|
+
() => ctx.output.success(`Deleted team ${t.key} (${t.name})`),
|
|
240
|
+
);
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `linear user` (alias `u`) — inspect workspace users. Read-only group.
|
|
3
|
+
*
|
|
4
|
+
* `view` accepts `me`, an email, a display/full name, or an id (all handled by
|
|
5
|
+
* `resolveUserId`); `me` is a convenience alias for `view me`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import { action } from "../lib/action.js";
|
|
10
|
+
import type { Context } from "../context.js";
|
|
11
|
+
import * as svc from "../services/user.js";
|
|
12
|
+
import type { Column } from "../output/table.js";
|
|
13
|
+
|
|
14
|
+
const ROW_COLUMNS: Column<svc.UserRow>[] = [
|
|
15
|
+
{ key: "displayName", header: "Name", value: (r) => r.displayName, max: 24 },
|
|
16
|
+
{ key: "email", header: "Email", value: (r) => r.email, max: 40 },
|
|
17
|
+
{ key: "active", header: "Active", value: (r) => (r.active ? "yes" : "no") },
|
|
18
|
+
{ key: "admin", header: "Admin", value: (r) => (r.admin ? "yes" : "no") },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* `--all` on a member listing, without `--include-disabled`.
|
|
23
|
+
*
|
|
24
|
+
* schpet/linear-cli spells "include inactive members" as `-a/--all` on `user
|
|
25
|
+
* list` and `team members`. Ours is the global `--all` — exhaust pagination —
|
|
26
|
+
* so a transplanted `linear user list --all` succeeds, and the deactivated
|
|
27
|
+
* users it was asking for are quietly missing (TES-637 item 1). The global
|
|
28
|
+
* keeps its one meaning (making it mean "and deactivated" here alone would be a
|
|
29
|
+
* second meaning for one flag, the thing ALIGNMENT.md refuses); instead the
|
|
30
|
+
* combination says what it did and names the flag that does the other thing.
|
|
31
|
+
* A warning, not `info`: it survives `--quiet`, because a script is exactly
|
|
32
|
+
* where a wrong result set goes unnoticed. Shared with `team members`.
|
|
33
|
+
*/
|
|
34
|
+
export function noteAllIsPagination(ctx: Context, includeDisabled: boolean): void {
|
|
35
|
+
if (ctx.options.all === true && !includeDisabled) {
|
|
36
|
+
ctx.output.warn(
|
|
37
|
+
"--all exhausts pagination here; deactivated users are still excluded — pass --include-disabled to list them.",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The help footer both member listings carry, so the difference is loud before it is hit. */
|
|
43
|
+
export const ALL_VS_INCLUDE_DISABLED_HELP = [
|
|
44
|
+
"",
|
|
45
|
+
"Note: --all is the global 'fetch every page'. Deactivated users need --include-disabled",
|
|
46
|
+
"(schpet/linear-cli's --all).",
|
|
47
|
+
].join("\n");
|
|
48
|
+
|
|
49
|
+
export function registerUser(program: Command): void {
|
|
50
|
+
const user = program.command("user").alias("u").description("Inspect workspace users");
|
|
51
|
+
|
|
52
|
+
// list --------------------------------------------------------------------
|
|
53
|
+
user
|
|
54
|
+
.command("list")
|
|
55
|
+
.alias("ls")
|
|
56
|
+
.description("List workspace users")
|
|
57
|
+
.option("--include-disabled", "include deactivated users (excluded by default)")
|
|
58
|
+
.addHelpText("after", ALL_VS_INCLUDE_DISABLED_HELP)
|
|
59
|
+
.action(
|
|
60
|
+
action(async (ctx: Context, opts) => {
|
|
61
|
+
noteAllIsPagination(ctx, !!opts.includeDisabled);
|
|
62
|
+
const rows = await svc.listUsers(ctx.client, ctx.limit, !!opts.includeDisabled);
|
|
63
|
+
ctx.output.list(rows, ROW_COLUMNS, rows);
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// view --------------------------------------------------------------------
|
|
68
|
+
user
|
|
69
|
+
.command("view <who>")
|
|
70
|
+
.description("Show a user (me, email, name, or id)")
|
|
71
|
+
.action(
|
|
72
|
+
action(async (ctx: Context, _opts, who: string) => {
|
|
73
|
+
const detail = await svc.getUserDetail(ctx.client, who);
|
|
74
|
+
emitDetail(ctx, detail);
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// me ----------------------------------------------------------------------
|
|
79
|
+
user
|
|
80
|
+
.command("me")
|
|
81
|
+
.description("Show the authenticated viewer")
|
|
82
|
+
.action(
|
|
83
|
+
action(async (ctx: Context) => {
|
|
84
|
+
const detail = await svc.getViewer(ctx.client);
|
|
85
|
+
emitDetail(ctx, detail);
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function emitDetail(ctx: Context, detail: svc.UserDetail): void {
|
|
91
|
+
ctx.output.detail(detail, [
|
|
92
|
+
["User", `${detail.displayName} ${detail.name}`],
|
|
93
|
+
["ID", detail.id],
|
|
94
|
+
["Email", detail.email],
|
|
95
|
+
["Active", detail.active ? "yes" : "no"],
|
|
96
|
+
["Admin", detail.admin ? "yes" : "no"],
|
|
97
|
+
["Guest", detail.guest ? "yes" : "no"],
|
|
98
|
+
["You", detail.isMe ? "yes" : null],
|
|
99
|
+
["Status", detail.statusLabel],
|
|
100
|
+
["Description", detail.description],
|
|
101
|
+
["Timezone", detail.timezone],
|
|
102
|
+
["Last seen", detail.lastSeen],
|
|
103
|
+
["URL", detail.url],
|
|
104
|
+
["Created", detail.createdAt],
|
|
105
|
+
["Updated", detail.updatedAt],
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `linear webhook` (alias `wh`) — manage workspace webhooks.
|
|
3
|
+
*
|
|
4
|
+
* Webhooks are workspace-scoped; `create` may scope one to a single team via the
|
|
5
|
+
* global -t/--team (read from ctx.defaultTeam — there is no local --team option,
|
|
6
|
+
* which would collide). `--resource` is repeatable (collectArray) and required.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Command } from "commander";
|
|
10
|
+
import { action } from "../lib/action.js";
|
|
11
|
+
import { collectArray } from "../lib/options.js";
|
|
12
|
+
import { confirmDestructive } from "../lib/prompt.js";
|
|
13
|
+
import { usageError } from "../lib/errors.js";
|
|
14
|
+
import type { Context } from "../context.js";
|
|
15
|
+
import * as svc from "../services/webhook.js";
|
|
16
|
+
import type { Column } from "../output/table.js";
|
|
17
|
+
|
|
18
|
+
const ROW_COLUMNS: Column<svc.WebhookRow>[] = [
|
|
19
|
+
{ key: "id", header: "ID", value: (r) => r.id },
|
|
20
|
+
{ key: "url", header: "URL", value: (r) => r.url ?? "—", max: 40 },
|
|
21
|
+
{ key: "enabled", header: "Enabled", value: (r) => (r.enabled ? "yes" : "no") },
|
|
22
|
+
{
|
|
23
|
+
key: "resourceTypes",
|
|
24
|
+
header: "Resources",
|
|
25
|
+
value: (r) => (r.resourceTypes.length ? r.resourceTypes.join(", ") : "—"),
|
|
26
|
+
max: 40,
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export function registerWebhook(program: Command): void {
|
|
31
|
+
const webhook = program.command("webhook").alias("wh").description("Manage workspace webhooks");
|
|
32
|
+
|
|
33
|
+
// list --------------------------------------------------------------------
|
|
34
|
+
webhook
|
|
35
|
+
.command("list")
|
|
36
|
+
.alias("ls")
|
|
37
|
+
.description("List webhooks")
|
|
38
|
+
.action(
|
|
39
|
+
action(async (ctx: Context) => {
|
|
40
|
+
const rows = await svc.listWebhooks(ctx.client, ctx.limit);
|
|
41
|
+
ctx.output.list(rows, ROW_COLUMNS, rows);
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// view --------------------------------------------------------------------
|
|
46
|
+
webhook
|
|
47
|
+
.command("view <id>", { isDefault: true })
|
|
48
|
+
.alias("show")
|
|
49
|
+
.description("Show a webhook")
|
|
50
|
+
.action(
|
|
51
|
+
action(async (ctx: Context, _opts, idArg: string) => {
|
|
52
|
+
const detail = await svc.getWebhookDetail(ctx.client, idArg);
|
|
53
|
+
ctx.output.detail(detail, [
|
|
54
|
+
["Webhook", detail.id],
|
|
55
|
+
["URL", detail.url],
|
|
56
|
+
["Enabled", detail.enabled ? "yes" : "no"],
|
|
57
|
+
["Resources", detail.resourceTypes.length ? detail.resourceTypes.join(", ") : null],
|
|
58
|
+
["Label", detail.label],
|
|
59
|
+
["All public teams", detail.allPublicTeams ? "yes" : "no"],
|
|
60
|
+
["Team", detail.team],
|
|
61
|
+
["Creator", detail.creator],
|
|
62
|
+
["Created", detail.createdAt],
|
|
63
|
+
["Updated", detail.updatedAt],
|
|
64
|
+
]);
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// create ------------------------------------------------------------------
|
|
69
|
+
webhook
|
|
70
|
+
.command("create")
|
|
71
|
+
.alias("new")
|
|
72
|
+
.description("Create a webhook (scope to the global --team, or --all-public)")
|
|
73
|
+
.option("--url <url>", "destination URL that receives event payloads")
|
|
74
|
+
.option("--resource <type...>", "resource type to subscribe to (repeatable)", collectArray)
|
|
75
|
+
.option("--label <label>", "human-readable label for the webhook")
|
|
76
|
+
.option("--all-public", "subscribe to all public teams in the workspace")
|
|
77
|
+
.option("--secret <secret>", "secret used to sign webhook payloads")
|
|
78
|
+
.action(
|
|
79
|
+
action(async (ctx: Context, opts) => {
|
|
80
|
+
if (!opts.url) throw usageError("A webhook needs a --url.");
|
|
81
|
+
const resourceTypes: string[] = opts.resource ?? [];
|
|
82
|
+
if (!resourceTypes.length)
|
|
83
|
+
throw usageError("Pass at least one --resource (e.g. Issue, Comment, Project).");
|
|
84
|
+
const created = await svc.createWebhook(ctx.client, {
|
|
85
|
+
url: opts.url,
|
|
86
|
+
resourceTypes,
|
|
87
|
+
// The global -t/--team scopes the webhook; never combined with --all-public.
|
|
88
|
+
team: opts.allPublic ? undefined : ctx.defaultTeam,
|
|
89
|
+
label: opts.label,
|
|
90
|
+
allPublicTeams: opts.allPublic,
|
|
91
|
+
secret: opts.secret,
|
|
92
|
+
});
|
|
93
|
+
ctx.output.emit(
|
|
94
|
+
{
|
|
95
|
+
id: created.id,
|
|
96
|
+
url: created.url,
|
|
97
|
+
enabled: created.enabled,
|
|
98
|
+
resourceTypes: created.resourceTypes,
|
|
99
|
+
},
|
|
100
|
+
() => ctx.output.success(`Created webhook ${created.id}`),
|
|
101
|
+
);
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// update ------------------------------------------------------------------
|
|
106
|
+
webhook
|
|
107
|
+
.command("update <id>")
|
|
108
|
+
.alias("edit")
|
|
109
|
+
.description("Update a webhook")
|
|
110
|
+
.option("--url <url>", "new destination URL")
|
|
111
|
+
.option("--enabled", "enable the webhook")
|
|
112
|
+
.option("--disabled", "disable the webhook")
|
|
113
|
+
.option("--resource <type...>", "set resource types (repeatable)", collectArray)
|
|
114
|
+
.option("--label <label>", "new label")
|
|
115
|
+
.option("--secret <secret>", "new signing secret")
|
|
116
|
+
.action(
|
|
117
|
+
action(async (ctx: Context, opts, idArg: string) => {
|
|
118
|
+
if (opts.enabled && opts.disabled)
|
|
119
|
+
throw usageError("Pass only one of --enabled / --disabled.");
|
|
120
|
+
const enabled = opts.enabled ? true : opts.disabled ? false : undefined;
|
|
121
|
+
const updated = await svc.updateWebhook(ctx.client, idArg, {
|
|
122
|
+
url: opts.url,
|
|
123
|
+
enabled,
|
|
124
|
+
resourceTypes: opts.resource,
|
|
125
|
+
label: opts.label,
|
|
126
|
+
secret: opts.secret,
|
|
127
|
+
});
|
|
128
|
+
ctx.output.emit(
|
|
129
|
+
{
|
|
130
|
+
id: updated.id,
|
|
131
|
+
url: updated.url,
|
|
132
|
+
enabled: updated.enabled,
|
|
133
|
+
resourceTypes: updated.resourceTypes,
|
|
134
|
+
},
|
|
135
|
+
() => ctx.output.success(`Updated webhook ${updated.id}`),
|
|
136
|
+
);
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// delete ------------------------------------------------------------------
|
|
141
|
+
webhook
|
|
142
|
+
.command("delete <id>")
|
|
143
|
+
.alias("rm")
|
|
144
|
+
.description("Delete a webhook")
|
|
145
|
+
.action(
|
|
146
|
+
action(async (ctx: Context, _opts, idArg: string) => {
|
|
147
|
+
const wh = await svc.getWebhookDetail(ctx.client, idArg);
|
|
148
|
+
if (!(await confirmDestructive(ctx, `Delete webhook ${wh.id} (${wh.url ?? "no url"})?`)))
|
|
149
|
+
return;
|
|
150
|
+
const deleted = await svc.deleteWebhook(ctx.client, wh.id);
|
|
151
|
+
ctx.output.emit({ id: deleted.id, deleted: true }, () =>
|
|
152
|
+
ctx.output.success(`Deleted webhook ${deleted.id}`),
|
|
153
|
+
);
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
}
|