instant-cli 1.0.33 → 1.0.34

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 (80) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/__tests__/multiSelect.test.ts +236 -0
  3. package/__tests__/select.test.ts +224 -0
  4. package/__tests__/webhooks.test.ts +728 -0
  5. package/dist/commands/webhooks/add.d.ts +9 -0
  6. package/dist/commands/webhooks/add.d.ts.map +1 -0
  7. package/dist/commands/webhooks/add.js +75 -0
  8. package/dist/commands/webhooks/add.js.map +1 -0
  9. package/dist/commands/webhooks/delete.d.ts +6 -0
  10. package/dist/commands/webhooks/delete.d.ts.map +1 -0
  11. package/dist/commands/webhooks/delete.js +17 -0
  12. package/dist/commands/webhooks/delete.js.map +1 -0
  13. package/dist/commands/webhooks/disable.d.ts +7 -0
  14. package/dist/commands/webhooks/disable.d.ts.map +1 -0
  15. package/dist/commands/webhooks/disable.js +18 -0
  16. package/dist/commands/webhooks/disable.js.map +1 -0
  17. package/dist/commands/webhooks/enable.d.ts +6 -0
  18. package/dist/commands/webhooks/enable.d.ts.map +1 -0
  19. package/dist/commands/webhooks/enable.js +18 -0
  20. package/dist/commands/webhooks/enable.js.map +1 -0
  21. package/dist/commands/webhooks/events/list.d.ts +7 -0
  22. package/dist/commands/webhooks/events/list.d.ts.map +1 -0
  23. package/dist/commands/webhooks/events/list.js +31 -0
  24. package/dist/commands/webhooks/events/list.js.map +1 -0
  25. package/dist/commands/webhooks/events/payload.d.ts +8 -0
  26. package/dist/commands/webhooks/events/payload.d.ts.map +1 -0
  27. package/dist/commands/webhooks/events/payload.js +39 -0
  28. package/dist/commands/webhooks/events/payload.js.map +1 -0
  29. package/dist/commands/webhooks/events/resend.d.ts +8 -0
  30. package/dist/commands/webhooks/events/resend.d.ts.map +1 -0
  31. package/dist/commands/webhooks/events/resend.js +43 -0
  32. package/dist/commands/webhooks/events/resend.js.map +1 -0
  33. package/dist/commands/webhooks/list.d.ts +8 -0
  34. package/dist/commands/webhooks/list.d.ts.map +1 -0
  35. package/dist/commands/webhooks/list.js +29 -0
  36. package/dist/commands/webhooks/list.js.map +1 -0
  37. package/dist/commands/webhooks/shared.d.ts +40 -0
  38. package/dist/commands/webhooks/shared.d.ts.map +1 -0
  39. package/dist/commands/webhooks/shared.js +248 -0
  40. package/dist/commands/webhooks/shared.js.map +1 -0
  41. package/dist/commands/webhooks/update.d.ts +10 -0
  42. package/dist/commands/webhooks/update.d.ts.map +1 -0
  43. package/dist/commands/webhooks/update.js +189 -0
  44. package/dist/commands/webhooks/update.js.map +1 -0
  45. package/dist/index.d.ts +45 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +141 -0
  48. package/dist/index.js.map +1 -1
  49. package/dist/layer.d.ts +2 -2
  50. package/dist/layer.d.ts.map +1 -1
  51. package/dist/layer.js +30 -1
  52. package/dist/layer.js.map +1 -1
  53. package/dist/lib/webhooks.d.ts +28 -0
  54. package/dist/lib/webhooks.d.ts.map +1 -0
  55. package/dist/lib/webhooks.js +102 -0
  56. package/dist/lib/webhooks.js.map +1 -0
  57. package/dist/ui/index.d.ts +39 -1
  58. package/dist/ui/index.d.ts.map +1 -1
  59. package/dist/ui/index.js +387 -25
  60. package/dist/ui/index.js.map +1 -1
  61. package/dist/ui/lib.d.ts +7 -0
  62. package/dist/ui/lib.d.ts.map +1 -1
  63. package/dist/ui/lib.js +40 -1
  64. package/dist/ui/lib.js.map +1 -1
  65. package/package.json +4 -4
  66. package/src/commands/webhooks/add.ts +111 -0
  67. package/src/commands/webhooks/delete.ts +23 -0
  68. package/src/commands/webhooks/disable.ts +24 -0
  69. package/src/commands/webhooks/enable.ts +24 -0
  70. package/src/commands/webhooks/events/list.ts +38 -0
  71. package/src/commands/webhooks/events/payload.ts +56 -0
  72. package/src/commands/webhooks/events/resend.ts +66 -0
  73. package/src/commands/webhooks/list.ts +41 -0
  74. package/src/commands/webhooks/shared.ts +339 -0
  75. package/src/commands/webhooks/update.ts +276 -0
  76. package/src/index.ts +242 -0
  77. package/src/layer.ts +33 -1
  78. package/src/lib/webhooks.ts +127 -0
  79. package/src/ui/index.ts +465 -32
  80. package/src/ui/lib.ts +41 -1
@@ -0,0 +1,339 @@
1
+ import boxen from 'boxen';
2
+ import chalk from 'chalk';
3
+ import { Effect } from 'effect';
4
+ import type {
5
+ WebhookAction,
6
+ WebhookEventInfo,
7
+ WebhookEventStatus,
8
+ WebhookInfo,
9
+ } from '@instantdb/platform';
10
+ import { GlobalOpts } from '../../context/globalOpts.ts';
11
+ import { BadArgsError } from '../../errors.ts';
12
+ import { runUIEffect } from '../../lib/ui.ts';
13
+ import {
14
+ buildWebhooksManager,
15
+ fetchRecentEvents,
16
+ useWebhooksManager,
17
+ WEBHOOK_ACTIONS,
18
+ } from '../../lib/webhooks.ts';
19
+ import { UI } from '../../ui/index.ts';
20
+
21
+ type PickerParams = {
22
+ promptText: string;
23
+ emptyMessage: string;
24
+ filter?: (w: WebhookInfo) => boolean;
25
+ };
26
+
27
+ export const joinNamespaces = (namespaces: readonly string[]) =>
28
+ [...namespaces].sort().join(', ');
29
+
30
+ /**
31
+ * Validates a webhook URL: must be non-empty after trim, parse as a URL, and
32
+ * use the `https:` scheme (matches the server's requirement). Returns an error
33
+ * message or undefined. Pass to `Args.prompt({ validate })` for inline
34
+ * feedback, and `Args.validate(...)` to also cover flag-supplied values.
35
+ */
36
+ export const validateWebhookUrl = (raw: string): string | undefined => {
37
+ const trimmed = raw.trim();
38
+ if (!trimmed) return 'URL cannot be empty';
39
+ let parsed: URL;
40
+ try {
41
+ parsed = new URL(trimmed);
42
+ } catch {
43
+ return `Invalid URL: ${raw}`;
44
+ }
45
+ if (parsed.protocol !== 'https:') {
46
+ return 'URL must use https://';
47
+ }
48
+ return undefined;
49
+ };
50
+
51
+ export const joinActions = (actions: readonly string[]) =>
52
+ [...actions]
53
+ .sort(
54
+ (a, b) =>
55
+ WEBHOOK_ACTIONS.indexOf(a as WebhookAction) -
56
+ WEBHOOK_ACTIONS.indexOf(b as WebhookAction),
57
+ )
58
+ .join(', ');
59
+
60
+ const renderWebhookLabel = (w: WebhookInfo) =>
61
+ [
62
+ `${w.sink.url} ${chalk.dim(`(${w.id})`)}`,
63
+ chalk.dim(` status: ${w.status}`),
64
+ chalk.dim(` actions: ${joinActions(w.actions)}`),
65
+ chalk.dim(` namespaces: ${joinNamespaces(w.namespaces)}`),
66
+ ].join('\n');
67
+
68
+ const pickWebhook = (params: PickerParams) =>
69
+ Effect.gen(function* () {
70
+ const all = yield* useWebhooksManager(
71
+ (m) => m.list(),
72
+ 'Error listing webhooks',
73
+ );
74
+ const webhooks = params.filter ? all.filter(params.filter) : all;
75
+ if (webhooks.length === 0) {
76
+ yield* Effect.log(params.emptyMessage);
77
+ return undefined;
78
+ }
79
+ return yield* runUIEffect(
80
+ new UI.Select<WebhookInfo>({
81
+ options: webhooks.map((w) => ({
82
+ label: renderWebhookLabel(w),
83
+ value: w,
84
+ })),
85
+ promptText: params.promptText,
86
+ modifyOutput: UI.modifiers.vanishOnComplete,
87
+ }),
88
+ );
89
+ });
90
+
91
+ export const resolveWebhookId = (params: {
92
+ id: string | undefined;
93
+ picker: PickerParams;
94
+ flagName?: string;
95
+ }) =>
96
+ Effect.gen(function* () {
97
+ if (params.id) return params.id;
98
+ const { yes } = yield* GlobalOpts;
99
+ if (yes) {
100
+ return yield* BadArgsError.make({
101
+ message: `Must specify ${params.flagName ?? '--id'}`,
102
+ });
103
+ }
104
+ const picked = yield* pickWebhook(params.picker);
105
+ return picked?.id;
106
+ });
107
+
108
+ export const logWebhookEvent = (action: string, webhook: WebhookInfo) => {
109
+ const lines = [
110
+ `Webhook ${action}: ${webhook.sink.url}`,
111
+ `ID: ${webhook.id}`,
112
+ `Status: ${webhook.status}`,
113
+ `Actions: ${joinActions(webhook.actions)}`,
114
+ `Namespaces: ${joinNamespaces(webhook.namespaces)}`,
115
+ ];
116
+ if (webhook.disabledReason) {
117
+ lines.push(`Disabled reason: ${webhook.disabledReason}`);
118
+ }
119
+ return Effect.log(
120
+ '\n' +
121
+ boxen(lines.join('\n'), {
122
+ dimBorder: true,
123
+ padding: { right: 1, left: 1 },
124
+ }),
125
+ );
126
+ };
127
+
128
+ export const colorStatus = (status: WebhookEventStatus): string => {
129
+ switch (status) {
130
+ case 'success':
131
+ return chalk.green(status);
132
+ case 'failed':
133
+ return chalk.red(status);
134
+ case 'error':
135
+ return chalk.yellow(status);
136
+ case 'processing':
137
+ return chalk.cyan(status);
138
+ case 'pending':
139
+ return chalk.dim(status);
140
+ }
141
+ };
142
+
143
+ const lastAttemptSummary = (event: WebhookEventInfo): string => {
144
+ if (!event.attempts || event.attempts.length === 0) return 'no attempts';
145
+ const last = event.attempts[event.attempts.length - 1]!;
146
+ const code =
147
+ last.statusCode != null
148
+ ? String(last.statusCode)
149
+ : (last.errorType ?? 'error');
150
+ const dur = last.durationMs != null ? `, ${last.durationMs}ms` : '';
151
+ return `${code}${dur}`;
152
+ };
153
+
154
+ export const renderEventLabel = (e: WebhookEventInfo): string => {
155
+ const attempts = e.attempts?.length ?? 0;
156
+ const attemptsPart =
157
+ attempts === 0 ? '—' : `${attempts} attempt${attempts === 1 ? '' : 's'}`;
158
+ return `${e.isn} ${colorStatus(e.status)} ${chalk.dim(`${attemptsPart} · last: ${lastAttemptSummary(e)}`)}`;
159
+ };
160
+
161
+ const fmtTime = (d: Date | null) => (d ? d.toLocaleString() : 'n/a');
162
+
163
+ export const logEventDetail = (e: WebhookEventInfo): string[] => {
164
+ const lines = [
165
+ `${chalk.cyan(e.isn)}`,
166
+ ` Status: ${colorStatus(e.status)}`,
167
+ ` Created: ${fmtTime(e.createdAt)}`,
168
+ ` Updated: ${fmtTime(e.updatedAt)}`,
169
+ ` Attempts: ${e.attempts?.length ?? 0}${
170
+ e.attempts && e.attempts.length > 0
171
+ ? ` (last: ${lastAttemptSummary(e)})`
172
+ : ''
173
+ }`,
174
+ ];
175
+ if (e.nextAttemptAfter) {
176
+ lines.push(` Next attempt: ${fmtTime(e.nextAttemptAfter)}`);
177
+ }
178
+ return lines;
179
+ };
180
+
181
+ const formatExpandedEvent = (
182
+ e: WebhookEventInfo,
183
+ maxAttempts?: number,
184
+ ): string => {
185
+ const lines: string[] = [];
186
+ lines.push(chalk.dim(` Created: ${fmtTime(e.createdAt)}`));
187
+ lines.push(chalk.dim(` Updated: ${fmtTime(e.updatedAt)}`));
188
+ if (e.nextAttemptAfter) {
189
+ lines.push(chalk.dim(` Next attempt: ${fmtTime(e.nextAttemptAfter)}`));
190
+ }
191
+ if (e.attempts && e.attempts.length > 0) {
192
+ const total = e.attempts.length;
193
+ const cap = maxAttempts ?? total;
194
+ const hiddenCount = Math.max(0, total - cap);
195
+ const shown = e.attempts.slice(-cap);
196
+ lines.push(
197
+ chalk.dim(
198
+ hiddenCount > 0
199
+ ? ` Attempts: showing last ${shown.length} of ${total}`
200
+ : ` Attempts:`,
201
+ ),
202
+ );
203
+ shown.forEach((a, i) => {
204
+ const realIdx = hiddenCount + i + 1;
205
+ const code =
206
+ a.statusCode != null ? String(a.statusCode) : (a.errorType ?? 'error');
207
+ const dur = a.durationMs != null ? `${a.durationMs}ms` : '—';
208
+ const ok = a.success === true;
209
+ const codeColored = ok
210
+ ? chalk.green(code)
211
+ : a.success === false
212
+ ? chalk.red(code)
213
+ : chalk.yellow(code);
214
+ lines.push(
215
+ chalk.dim(` ${realIdx}. ${fmtTime(a.attemptAt)} → `) +
216
+ codeColored +
217
+ chalk.dim(` (${dur})`),
218
+ );
219
+ if (a.errorMessage) {
220
+ lines.push(chalk.dim(` ${a.errorMessage}`));
221
+ }
222
+ if (a.responseText) {
223
+ const trimmed = a.responseText.replace(/\s+/g, ' ').trim();
224
+ // 13 reserves the visible prefix on the printed line: 9 spaces of
225
+ // indent + 'body: ' (4 chars including the trailing space).
226
+ const max = Math.max(20, (process.stdout.columns ?? 80) - 13);
227
+ const body =
228
+ trimmed.length > max ? trimmed.slice(0, max - 1) + '…' : trimmed;
229
+ lines.push(chalk.dim(` body: ${body}`));
230
+ }
231
+ });
232
+ } else {
233
+ lines.push(chalk.dim(` Attempts: none yet`));
234
+ }
235
+ return lines.join('\n');
236
+ };
237
+
238
+ export const pickEvent = (params: {
239
+ webhookId: string;
240
+ limit?: number;
241
+ promptText: string;
242
+ emptyMessage: string;
243
+ }) =>
244
+ Effect.gen(function* () {
245
+ const events = yield* fetchRecentEvents(
246
+ params.webhookId,
247
+ params.limit ?? 25,
248
+ );
249
+ if (events.length === 0) {
250
+ yield* Effect.log(params.emptyMessage);
251
+ return undefined;
252
+ }
253
+ const manager = yield* buildWebhooksManager;
254
+ return yield* runUIEffect(
255
+ new UI.Select<WebhookEventInfo>({
256
+ options: events.map((e) => ({
257
+ label: renderEventLabel(e),
258
+ expandableLabel: async () => {
259
+ // Total budget = terminal rows minus picker chrome (prompt + one
260
+ // line per option + hint + safety).
261
+ const rows = process.stdout.rows ?? 24;
262
+ const chrome = events.length + 4;
263
+ const totalBudget = Math.max(12, rows - chrome);
264
+
265
+ // Split: reserve roughly half for payload (floor 6, ceil 30) and
266
+ // give the rest to attempts. Static timestamp lines + payload
267
+ // header are accounted for in `staticOverhead`.
268
+ const staticOverhead = 4; // 3 timestamp-ish lines + Payload: header
269
+ const payloadBudget = Math.min(
270
+ 30,
271
+ Math.max(6, Math.floor((totalBudget - staticOverhead) / 2)),
272
+ );
273
+ const attemptsBudget = Math.max(
274
+ 2,
275
+ totalBudget - staticOverhead - payloadBudget,
276
+ );
277
+ // Each attempt is 1-3 lines (line + optional errorMessage +
278
+ // optional body). Conservative average: 2 lines per attempt.
279
+ const maxAttempts = Math.max(1, Math.floor(attemptsBudget / 2));
280
+
281
+ const base = formatExpandedEvent(e, maxAttempts);
282
+
283
+ try {
284
+ const payload = await manager.getPayload(params.webhookId, e.isn);
285
+ const json = JSON.stringify(payload, null, 2);
286
+ const indented = json.split('\n').map((l) => ' ' + l);
287
+
288
+ let payloadLines = indented;
289
+ let truncationHint = '';
290
+ if (payloadLines.length > payloadBudget) {
291
+ const omitted = payloadLines.length - (payloadBudget - 1);
292
+ payloadLines = payloadLines.slice(0, payloadBudget - 1);
293
+ truncationHint =
294
+ '\n' +
295
+ chalk.dim(
296
+ ` … ${omitted} more lines · run \`instant-cli webhook event payload --webhook-id ${params.webhookId} --isn ${e.isn}\` for full output`,
297
+ );
298
+ }
299
+
300
+ return `${base}\n${chalk.dim(' Payload:')}\n${chalk.dim(payloadLines.join('\n'))}${truncationHint}`;
301
+ } catch (err: any) {
302
+ return `${base}\n${chalk.red(` Payload error: ${err instanceof Error ? err.message : String(err)}`)}`;
303
+ }
304
+ },
305
+ value: e,
306
+ })),
307
+ promptText: params.promptText,
308
+ modifyOutput: UI.modifiers.vanishOnComplete,
309
+ }),
310
+ );
311
+ });
312
+
313
+ export const resolveWebhook = (params: {
314
+ id: string | undefined;
315
+ picker: PickerParams;
316
+ flagName?: string;
317
+ }) =>
318
+ Effect.gen(function* () {
319
+ if (params.id) {
320
+ const all = yield* useWebhooksManager(
321
+ (m) => m.list(),
322
+ 'Error listing webhooks',
323
+ );
324
+ const found = all.find((w) => w.id === params.id);
325
+ if (!found) {
326
+ return yield* BadArgsError.make({
327
+ message: `No webhook found with id ${params.id}`,
328
+ });
329
+ }
330
+ return found;
331
+ }
332
+ const { yes } = yield* GlobalOpts;
333
+ if (yes) {
334
+ return yield* BadArgsError.make({
335
+ message: `Must specify ${params.flagName ?? '--id'}`,
336
+ });
337
+ }
338
+ return yield* pickWebhook(params.picker);
339
+ });
@@ -0,0 +1,276 @@
1
+ import chalk from 'chalk';
2
+ import { Effect } from 'effect';
3
+ import type {
4
+ UpdateWebhookParams,
5
+ WebhookAction,
6
+ WebhookInfo,
7
+ } from '@instantdb/platform';
8
+ import type { OptsFromCommand, webhooksUpdateDef } from '../../index.ts';
9
+ import { GlobalOpts } from '../../context/globalOpts.ts';
10
+ import { BadArgsError } from '../../errors.ts';
11
+ import { runUIEffect } from '../../lib/ui.ts';
12
+ import {
13
+ getRemoteNamespaces,
14
+ parseActions,
15
+ parseNamespaces,
16
+ useWebhooksManager,
17
+ WEBHOOK_ACTIONS,
18
+ } from '../../lib/webhooks.ts';
19
+ import { UI } from '../../ui/index.ts';
20
+ import {
21
+ joinActions,
22
+ joinNamespaces,
23
+ logWebhookEvent,
24
+ resolveWebhook,
25
+ resolveWebhookId,
26
+ validateWebhookUrl,
27
+ } from './shared.ts';
28
+
29
+ type MenuChoice = 'url' | 'namespaces' | 'actions' | 'save' | 'cancel';
30
+
31
+ const buildUpdateWebhookParams = Effect.fn(function* (
32
+ url: string | undefined,
33
+ namespaces: string[] | undefined,
34
+ actions: WebhookAction[] | undefined,
35
+ ) {
36
+ const params: UpdateWebhookParams<any> = {};
37
+ if (url !== undefined) {
38
+ const trimmed = url.trim();
39
+ const err = validateWebhookUrl(trimmed);
40
+ if (err) return yield* BadArgsError.make({ message: err });
41
+ params.url = trimmed;
42
+ }
43
+ if (namespaces) params.namespaces = namespaces;
44
+ if (actions) params.actions = actions;
45
+ return params;
46
+ });
47
+
48
+ const sortedEq = (a: readonly string[], b: readonly string[]) => {
49
+ if (a.length !== b.length) return false;
50
+ const aa = [...a].sort();
51
+ const bb = [...b].sort();
52
+ return aa.every((v, i) => v === bb[i]);
53
+ };
54
+
55
+ const fmtScalar = (current: string, pending: string | undefined) =>
56
+ pending !== undefined && pending !== current
57
+ ? `${chalk.green(pending)} ${chalk.dim(`(was ${current})`)}`
58
+ : chalk.dim(current);
59
+
60
+ const fmtList = (
61
+ current: readonly string[],
62
+ pending: readonly string[] | undefined,
63
+ join: (xs: readonly string[]) => string = (xs) => xs.join(', '),
64
+ ) =>
65
+ pending !== undefined && !sortedEq(current, pending)
66
+ ? `${chalk.green(join(pending))} ${chalk.dim(`(was ${join(current)})`)}`
67
+ : chalk.dim(join(current));
68
+
69
+ export const webhooksUpdateCmd = Effect.fn(
70
+ function* (opts: OptsFromCommand<typeof webhooksUpdateDef>) {
71
+ const { yes } = yield* GlobalOpts;
72
+ const optsNamespaces = yield* parseNamespaces(opts.namespaces);
73
+ const optsActions = yield* parseActions(opts.actions);
74
+ const hasAnyFieldFlag =
75
+ opts.url !== undefined || !!optsNamespaces || !!optsActions;
76
+
77
+ if (yes) {
78
+ if (!opts.id) {
79
+ return yield* BadArgsError.make({ message: 'Must specify --id' });
80
+ }
81
+ if (!hasAnyFieldFlag) {
82
+ return yield* BadArgsError.make({
83
+ message:
84
+ 'Must specify at least one of --url, --namespaces, or --actions',
85
+ });
86
+ }
87
+ const params = yield* buildUpdateWebhookParams(
88
+ opts.url,
89
+ optsNamespaces,
90
+ optsActions,
91
+ );
92
+ const webhook = yield* useWebhooksManager(
93
+ (m) => m.update(opts.id!, params),
94
+ 'Error updating webhook',
95
+ );
96
+ yield* logUpdated(webhook);
97
+ return;
98
+ }
99
+
100
+ if (hasAnyFieldFlag) {
101
+ const id = yield* resolveWebhookId({
102
+ id: opts.id,
103
+ picker: {
104
+ promptText: 'Select a webhook to update:',
105
+ emptyMessage: 'No webhooks configured.',
106
+ },
107
+ });
108
+ if (!id) return;
109
+ const params = yield* buildUpdateWebhookParams(
110
+ opts.url,
111
+ optsNamespaces,
112
+ optsActions,
113
+ );
114
+ const webhook = yield* useWebhooksManager(
115
+ (m) => m.update(id, params),
116
+ 'Error updating webhook',
117
+ );
118
+ yield* logUpdated(webhook);
119
+ return;
120
+ }
121
+
122
+ const current = yield* resolveWebhook({
123
+ id: opts.id,
124
+ picker: {
125
+ promptText: 'Select a webhook to update:',
126
+ emptyMessage: 'No webhooks configured.',
127
+ },
128
+ });
129
+ if (!current) return;
130
+
131
+ const pending: UpdateWebhookParams<any> = {};
132
+ let cursor: MenuChoice = 'url';
133
+
134
+ while (true) {
135
+ const choice: MenuChoice = yield* runUIEffect(
136
+ new UI.Select<MenuChoice>({
137
+ promptText: `Editing webhook ${chalk.cyan(current.sink.url)} ${chalk.dim(`(${current.id})`)}`,
138
+ defaultValue: cursor,
139
+ modifyOutput: UI.modifiers.vanishOnComplete,
140
+ options: [
141
+ {
142
+ value: 'url',
143
+ label: `URL: ${fmtScalar(current.sink.url, pending.url)}`,
144
+ },
145
+ {
146
+ value: 'actions',
147
+ label: `Actions: ${fmtList(current.actions, pending.actions, joinActions)}`,
148
+ },
149
+ {
150
+ value: 'namespaces',
151
+ label: `Namespaces: ${fmtList(current.namespaces, pending.namespaces, joinNamespaces)}`,
152
+ },
153
+ { value: 'save', label: 'Save changes', secondary: true },
154
+ { value: 'cancel', label: 'Cancel', secondary: true },
155
+ ],
156
+ }),
157
+ );
158
+
159
+ if (choice === 'cancel') {
160
+ yield* Effect.log('Cancelled.');
161
+ return;
162
+ }
163
+ if (choice === 'save') {
164
+ if (!hasPending(pending, current)) {
165
+ yield* Effect.log('No changes to save.');
166
+ return;
167
+ }
168
+ break;
169
+ }
170
+
171
+ cursor = choice;
172
+
173
+ if (choice === 'url') {
174
+ const seed = pending.url ?? current.sink.url;
175
+ const rawUrl = yield* runUIEffect(
176
+ new UI.TextInput({
177
+ prompt: 'Webhook URL:',
178
+ defaultValue: seed,
179
+ placeholder: seed,
180
+ validate: (v) => validateWebhookUrl(v.trim()),
181
+ modifyOutput: UI.modifiers.piped([
182
+ UI.modifiers.topPadding,
183
+ UI.modifiers.vanishOnComplete,
184
+ ]),
185
+ }),
186
+ );
187
+ pending.url = rawUrl.trim();
188
+ } else if (choice === 'namespaces') {
189
+ pending.namespaces = yield* promptNamespaces(
190
+ pending.namespaces ?? current.namespaces,
191
+ );
192
+ } else if (choice === 'actions') {
193
+ pending.actions = yield* runUIEffect(
194
+ new UI.MultiSelect<WebhookAction>({
195
+ options: WEBHOOK_ACTIONS.map((a) => ({ value: a, label: a })),
196
+ promptText: 'Actions to trigger on:',
197
+ initialSelected: pending.actions ?? current.actions,
198
+ minSelected: 1,
199
+ modifyOutput: UI.modifiers.vanishOnComplete,
200
+ }),
201
+ );
202
+ }
203
+ }
204
+
205
+ const webhook = yield* useWebhooksManager(
206
+ (m) => m.update(current.id, pending),
207
+ 'Error updating webhook',
208
+ );
209
+ yield* logUpdated(webhook);
210
+ },
211
+ Effect.catchTag('BadArgsError', (e) =>
212
+ Effect.gen(function* () {
213
+ yield* Effect.logError(e.message);
214
+ yield* Effect.log(
215
+ chalk.dim(
216
+ 'hint: run `instant-cli webhook update --help` for available arguments',
217
+ ),
218
+ );
219
+ }),
220
+ ),
221
+ );
222
+
223
+ const promptNamespaces = Effect.fn(function* (initial: readonly string[]) {
224
+ const available = yield* getRemoteNamespaces;
225
+ if (available && available.length > 0) {
226
+ return yield* runUIEffect(
227
+ new UI.MultiSelect<string>({
228
+ options: available.map((name) => ({ value: name, label: name })),
229
+ promptText: 'Namespaces to listen to:',
230
+ initialSelected: [...initial],
231
+ minSelected: 1,
232
+ modifyOutput: UI.modifiers.vanishOnComplete,
233
+ }),
234
+ );
235
+ }
236
+ const raw = yield* runUIEffect(
237
+ new UI.TextInput({
238
+ prompt: 'Namespaces (comma-separated):',
239
+ defaultValue: initial.join(','),
240
+ placeholder: initial.join(','),
241
+ modifyOutput: UI.modifiers.piped([
242
+ UI.modifiers.topPadding,
243
+ UI.modifiers.vanishOnComplete,
244
+ ]),
245
+ }),
246
+ );
247
+ const parsed = yield* parseNamespaces(raw);
248
+ if (!parsed) {
249
+ return yield* BadArgsError.make({
250
+ message: '--namespaces must include at least one namespace',
251
+ });
252
+ }
253
+ return parsed;
254
+ });
255
+
256
+ const hasPending = (
257
+ pending: UpdateWebhookParams<any>,
258
+ current: WebhookInfo,
259
+ ) => {
260
+ if (pending.url !== undefined && pending.url !== current.sink.url)
261
+ return true;
262
+ if (
263
+ pending.namespaces !== undefined &&
264
+ !sortedEq(pending.namespaces, current.namespaces)
265
+ )
266
+ return true;
267
+ if (
268
+ pending.actions !== undefined &&
269
+ !sortedEq(pending.actions, current.actions)
270
+ )
271
+ return true;
272
+ return false;
273
+ };
274
+
275
+ const logUpdated = (webhook: WebhookInfo) =>
276
+ logWebhookEvent('updated', webhook);