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.
Files changed (111) hide show
  1. package/.claude-plugin/marketplace.json +20 -0
  2. package/.claude-plugin/plugin.json +9 -0
  3. package/CHANGELOG.md +807 -0
  4. package/COVERAGE.md +495 -0
  5. package/LICENSE +21 -0
  6. package/MIGRATING.md +220 -0
  7. package/README.md +524 -0
  8. package/package.json +87 -0
  9. package/skills/linear-sdk-cli/SKILL.md +534 -0
  10. package/skills/linear-sdk-cli/references/api.md +27 -0
  11. package/skills/linear-sdk-cli/references/attachment.md +82 -0
  12. package/skills/linear-sdk-cli/references/auth.md +174 -0
  13. package/skills/linear-sdk-cli/references/commands.md +37 -0
  14. package/skills/linear-sdk-cli/references/comment.md +154 -0
  15. package/skills/linear-sdk-cli/references/completion.md +17 -0
  16. package/skills/linear-sdk-cli/references/config.md +99 -0
  17. package/skills/linear-sdk-cli/references/cycle.md +131 -0
  18. package/skills/linear-sdk-cli/references/document.md +180 -0
  19. package/skills/linear-sdk-cli/references/favorite.md +78 -0
  20. package/skills/linear-sdk-cli/references/initiative-update.md +67 -0
  21. package/skills/linear-sdk-cli/references/initiative.md +256 -0
  22. package/skills/linear-sdk-cli/references/issue.md +928 -0
  23. package/skills/linear-sdk-cli/references/label.md +112 -0
  24. package/skills/linear-sdk-cli/references/milestone.md +139 -0
  25. package/skills/linear-sdk-cli/references/notification.md +123 -0
  26. package/skills/linear-sdk-cli/references/open.md +29 -0
  27. package/skills/linear-sdk-cli/references/organization.md +99 -0
  28. package/skills/linear-sdk-cli/references/project-update.md +67 -0
  29. package/skills/linear-sdk-cli/references/project.md +234 -0
  30. package/skills/linear-sdk-cli/references/roadmap.md +150 -0
  31. package/skills/linear-sdk-cli/references/schema.md +21 -0
  32. package/skills/linear-sdk-cli/references/state.md +61 -0
  33. package/skills/linear-sdk-cli/references/team.md +227 -0
  34. package/skills/linear-sdk-cli/references/user.md +103 -0
  35. package/skills/linear-sdk-cli/references/webhook.md +151 -0
  36. package/skills/linear-sdk-cli/references/whoami.md +26 -0
  37. package/skills/linear-sdk-cli/scripts/generate-docs.ts +339 -0
  38. package/src/bin/linear.ts +97 -0
  39. package/src/cli.ts +232 -0
  40. package/src/client.ts +172 -0
  41. package/src/commands/agent-session.ts +121 -0
  42. package/src/commands/api.ts +317 -0
  43. package/src/commands/attach.ts +90 -0
  44. package/src/commands/attachment.ts +74 -0
  45. package/src/commands/comment.ts +225 -0
  46. package/src/commands/completion.ts +69 -0
  47. package/src/commands/cycle.ts +125 -0
  48. package/src/commands/discover.ts +170 -0
  49. package/src/commands/document.ts +211 -0
  50. package/src/commands/favorite.ts +72 -0
  51. package/src/commands/initiative-update.ts +47 -0
  52. package/src/commands/initiative.ts +261 -0
  53. package/src/commands/issue.ts +1263 -0
  54. package/src/commands/label.ts +119 -0
  55. package/src/commands/meta.ts +534 -0
  56. package/src/commands/milestone.ts +163 -0
  57. package/src/commands/notification.ts +110 -0
  58. package/src/commands/open.ts +84 -0
  59. package/src/commands/organization.ts +79 -0
  60. package/src/commands/project-update.ts +47 -0
  61. package/src/commands/project.ts +339 -0
  62. package/src/commands/roadmap.ts +131 -0
  63. package/src/commands/state.ts +58 -0
  64. package/src/commands/team.ts +243 -0
  65. package/src/commands/user.ts +107 -0
  66. package/src/commands/webhook.ts +156 -0
  67. package/src/config.ts +1236 -0
  68. package/src/context.ts +117 -0
  69. package/src/git.ts +174 -0
  70. package/src/index.ts +10 -0
  71. package/src/lib/action.ts +26 -0
  72. package/src/lib/body.ts +172 -0
  73. package/src/lib/errors.ts +195 -0
  74. package/src/lib/introspect.ts +150 -0
  75. package/src/lib/keyring.ts +270 -0
  76. package/src/lib/mutation.ts +53 -0
  77. package/src/lib/open.ts +32 -0
  78. package/src/lib/options.ts +640 -0
  79. package/src/lib/output-shapes.ts +562 -0
  80. package/src/lib/pagination.ts +145 -0
  81. package/src/lib/prompt.ts +91 -0
  82. package/src/lib/resolve.ts +596 -0
  83. package/src/lib/shape.ts +273 -0
  84. package/src/lib/status-update.ts +91 -0
  85. package/src/lib/upload.ts +329 -0
  86. package/src/output/color.ts +52 -0
  87. package/src/output/format.ts +238 -0
  88. package/src/output/markdown.ts +133 -0
  89. package/src/output/pager.ts +131 -0
  90. package/src/output/sanitize.ts +76 -0
  91. package/src/output/table.ts +198 -0
  92. package/src/services/agent-session.ts +289 -0
  93. package/src/services/attachment.ts +198 -0
  94. package/src/services/comment.ts +262 -0
  95. package/src/services/cycle.ts +224 -0
  96. package/src/services/document.ts +357 -0
  97. package/src/services/favorite.ts +147 -0
  98. package/src/services/initiative-update.ts +59 -0
  99. package/src/services/initiative.ts +449 -0
  100. package/src/services/issue.ts +1153 -0
  101. package/src/services/label.ts +193 -0
  102. package/src/services/milestone.ts +262 -0
  103. package/src/services/notification.ts +190 -0
  104. package/src/services/organization.ts +144 -0
  105. package/src/services/project-update.ts +36 -0
  106. package/src/services/project.ts +494 -0
  107. package/src/services/roadmap.ts +177 -0
  108. package/src/services/state.ts +127 -0
  109. package/src/services/team.ts +386 -0
  110. package/src/services/user.ts +137 -0
  111. 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
+ }