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
package/src/index.ts CHANGED
@@ -36,6 +36,15 @@ import { authOriginAddCmd } from './commands/auth/origin/add.ts';
36
36
  import { link } from './logging.ts';
37
37
  import { appListCommand } from './commands/app/list.ts';
38
38
  import { appDeleteCommand } from './commands/app/delete.ts';
39
+ import { webhooksListCmd } from './commands/webhooks/list.ts';
40
+ import { webhooksAddCmd } from './commands/webhooks/add.ts';
41
+ import { webhooksUpdateCmd } from './commands/webhooks/update.ts';
42
+ import { webhooksDeleteCmd } from './commands/webhooks/delete.ts';
43
+ import { webhooksEnableCmd } from './commands/webhooks/enable.ts';
44
+ import { webhooksDisableCmd } from './commands/webhooks/disable.ts';
45
+ import { webhooksEventsListCmd } from './commands/webhooks/events/list.ts';
46
+ import { webhooksEventsPayloadCmd } from './commands/webhooks/events/payload.ts';
47
+ import { webhooksEventsResendCmd } from './commands/webhooks/events/resend.ts';
39
48
 
40
49
  export type OptsFromCommand<C> =
41
50
  C extends Command<any, infer R, any> ? R : never;
@@ -392,6 +401,239 @@ export const authOriginDeleteDef = authOrigin
392
401
  );
393
402
  });
394
403
 
404
+ const webhooks = program
405
+ .command('webhook')
406
+ .description('Manage webhooks for an app');
407
+
408
+ export const webhooksListDef = webhooks
409
+ .command('list')
410
+ .description('List webhooks for an app')
411
+ .option(
412
+ '-a --app <app-id>',
413
+ 'App ID to list webhooks for. Defaults to *_INSTANT_APP_ID in .env',
414
+ )
415
+ .option('--json', 'Enable JSON output')
416
+ .action((opts) => {
417
+ return runCommandEffect(
418
+ webhooksListCmd(opts).pipe(
419
+ Effect.provide(
420
+ WithAppLayer({
421
+ coerce: false,
422
+ coerceAuth: false,
423
+ appId: opts.app,
424
+ allowAdminToken: false,
425
+ }).pipe(Layer.annotateLogs('silent', !!opts.json)),
426
+ ),
427
+ ),
428
+ );
429
+ });
430
+
431
+ export const webhooksAddDef = webhooks
432
+ .command('add')
433
+ .description('Add a webhook to an app')
434
+ .option('--url <url>', 'HTTPS endpoint to deliver events to')
435
+ .option(
436
+ '--namespaces <e1,e2>',
437
+ 'Comma-separated list of namespaces to listen on',
438
+ )
439
+ .option(
440
+ '--actions <a1,a2>',
441
+ 'Comma-separated list of actions (create, update, delete)',
442
+ )
443
+ .option(
444
+ '-a --app <app-id>',
445
+ 'App ID to add a webhook to. Defaults to *_INSTANT_APP_ID in .env',
446
+ )
447
+ .action((opts) => {
448
+ return runCommandEffect(
449
+ webhooksAddCmd(opts).pipe(
450
+ Effect.provide(
451
+ WithAppLayer({
452
+ coerce: false,
453
+ coerceAuth: false,
454
+ appId: opts.app,
455
+ allowAdminToken: false,
456
+ }),
457
+ ),
458
+ ),
459
+ );
460
+ });
461
+
462
+ export const webhooksUpdateDef = webhooks
463
+ .command('update')
464
+ .description('Update a webhook')
465
+ .option('--id <webhook-id>', 'Webhook ID to update')
466
+ .option('--url <url>', 'New HTTPS endpoint')
467
+ .option('--namespaces <e1,e2>', 'New comma-separated namespaces')
468
+ .option(
469
+ '--actions <a1,a2>',
470
+ 'New comma-separated actions (create, update, delete)',
471
+ )
472
+ .option(
473
+ '-a --app <app-id>',
474
+ 'App ID the webhook belongs to. Defaults to *_INSTANT_APP_ID in .env',
475
+ )
476
+ .action((opts) => {
477
+ return runCommandEffect(
478
+ webhooksUpdateCmd(opts).pipe(
479
+ Effect.provide(
480
+ WithAppLayer({
481
+ coerce: false,
482
+ coerceAuth: false,
483
+ appId: opts.app,
484
+ allowAdminToken: false,
485
+ }),
486
+ ),
487
+ ),
488
+ );
489
+ });
490
+
491
+ export const webhooksDeleteDef = webhooks
492
+ .command('delete')
493
+ .description('Delete a webhook')
494
+ .option('--id <webhook-id>', 'Webhook ID to delete')
495
+ .option(
496
+ '-a --app <app-id>',
497
+ 'App ID the webhook belongs to. Defaults to *_INSTANT_APP_ID in .env',
498
+ )
499
+ .action((opts) => {
500
+ return runCommandEffect(
501
+ webhooksDeleteCmd(opts).pipe(
502
+ Effect.provide(
503
+ WithAppLayer({
504
+ coerce: false,
505
+ coerceAuth: false,
506
+ appId: opts.app,
507
+ allowAdminToken: false,
508
+ }),
509
+ ),
510
+ ),
511
+ );
512
+ });
513
+
514
+ export const webhooksEnableDef = webhooks
515
+ .command('enable')
516
+ .description('Re-enable a disabled webhook')
517
+ .option('--id <webhook-id>', 'Webhook ID to enable')
518
+ .option(
519
+ '-a --app <app-id>',
520
+ 'App ID the webhook belongs to. Defaults to *_INSTANT_APP_ID in .env',
521
+ )
522
+ .action((opts) => {
523
+ return runCommandEffect(
524
+ webhooksEnableCmd(opts).pipe(
525
+ Effect.provide(
526
+ WithAppLayer({
527
+ coerce: false,
528
+ coerceAuth: false,
529
+ appId: opts.app,
530
+ allowAdminToken: false,
531
+ }),
532
+ ),
533
+ ),
534
+ );
535
+ });
536
+
537
+ export const webhooksDisableDef = webhooks
538
+ .command('disable')
539
+ .description('Disable an active webhook')
540
+ .option('--id <webhook-id>', 'Webhook ID to disable')
541
+ .option('--reason <reason>', 'Human-readable reason stored on the webhook')
542
+ .option(
543
+ '-a --app <app-id>',
544
+ 'App ID the webhook belongs to. Defaults to *_INSTANT_APP_ID in .env',
545
+ )
546
+ .action((opts) => {
547
+ return runCommandEffect(
548
+ webhooksDisableCmd(opts).pipe(
549
+ Effect.provide(
550
+ WithAppLayer({
551
+ coerce: false,
552
+ coerceAuth: false,
553
+ appId: opts.app,
554
+ allowAdminToken: false,
555
+ }),
556
+ ),
557
+ ),
558
+ );
559
+ });
560
+
561
+ const webhooksEvents = webhooks
562
+ .command('event')
563
+ .description('Inspect and resend webhook events');
564
+
565
+ export const webhooksEventsListDef = webhooksEvents
566
+ .command('list')
567
+ .description('List recent events for a webhook (up to 100, newest first)')
568
+ .option('--webhook-id <webhook-id>', 'Webhook ID to inspect')
569
+ .option('--json', 'Enable JSON output')
570
+ .option(
571
+ '-a --app <app-id>',
572
+ 'App ID the webhook belongs to. Defaults to *_INSTANT_APP_ID in .env',
573
+ )
574
+ .action((opts) => {
575
+ return runCommandEffect(
576
+ webhooksEventsListCmd(opts).pipe(
577
+ Effect.provide(
578
+ WithAppLayer({
579
+ coerce: false,
580
+ coerceAuth: false,
581
+ appId: opts.app,
582
+ allowAdminToken: false,
583
+ }).pipe(Layer.annotateLogs('silent', !!opts.json)),
584
+ ),
585
+ ),
586
+ );
587
+ });
588
+
589
+ export const webhooksEventsResendDef = webhooksEvents
590
+ .command('resend')
591
+ .description('Re-queue a webhook event for delivery')
592
+ .option('--webhook-id <webhook-id>', 'Webhook ID the event belongs to')
593
+ .option('--isn <isn>', 'Event ISN to resend')
594
+ .option(
595
+ '-a --app <app-id>',
596
+ 'App ID the webhook belongs to. Defaults to *_INSTANT_APP_ID in .env',
597
+ )
598
+ .action((opts) => {
599
+ return runCommandEffect(
600
+ webhooksEventsResendCmd(opts).pipe(
601
+ Effect.provide(
602
+ WithAppLayer({
603
+ coerce: false,
604
+ coerceAuth: false,
605
+ appId: opts.app,
606
+ allowAdminToken: false,
607
+ }),
608
+ ),
609
+ ),
610
+ );
611
+ });
612
+
613
+ export const webhooksEventsPayloadDef = webhooksEvents
614
+ .command('payload')
615
+ .description('Print the JSON payload for a webhook event')
616
+ .option('--webhook-id <webhook-id>', 'Webhook ID the event belongs to')
617
+ .option('--isn <isn>', 'Event ISN to fetch')
618
+ .option(
619
+ '-a --app <app-id>',
620
+ 'App ID the webhook belongs to. Defaults to *_INSTANT_APP_ID in .env',
621
+ )
622
+ .action((opts) => {
623
+ return runCommandEffect(
624
+ webhooksEventsPayloadCmd(opts).pipe(
625
+ Effect.provide(
626
+ WithAppLayer({
627
+ coerce: false,
628
+ coerceAuth: false,
629
+ appId: opts.app,
630
+ allowAdminToken: false,
631
+ }),
632
+ ),
633
+ ),
634
+ );
635
+ });
636
+
395
637
  export const initWithoutFilesDef = program
396
638
  .command('init-without-files')
397
639
  .description('Generate a new app id and admin token pair without any files.')
package/src/layer.ts CHANGED
@@ -2,11 +2,12 @@ import * as NodeContext from '@effect/platform-node/NodeContext';
2
2
  import * as NodeHttpClient from '@effect/platform-node/NodeHttpClient';
3
3
  import { Cause, Effect, Layer, ManagedRuntime } from 'effect';
4
4
  import { UnknownException } from 'effect/Cause';
5
+ import { inspect } from 'node:util';
5
6
  import chalk from 'chalk';
6
7
  import { AuthTokenLive } from './context/authToken.ts';
7
8
  import { CurrentAppLive } from './context/currentApp.ts';
8
9
  import { GlobalOptsLive } from './context/globalOpts.ts';
9
- import { PlatformApi } from './context/platformApi.ts';
10
+ import { PlatformApi, PlatformApiError } from './context/platformApi.ts';
10
11
  import {
11
12
  PACKAGE_ALIAS_AND_FULL_NAMES,
12
13
  ProjectInfoLive,
@@ -21,6 +22,19 @@ import { RequestError } from '@effect/platform/HttpClientError';
21
22
 
22
23
  const runtime = ManagedRuntime.make(SimpleLogLayer);
23
24
 
25
+ // Best-effort stringification for unknown error causes. Tries JSON first
26
+ // (compact, readable) and falls back to util.inspect for circular refs or
27
+ // values JSON can't serialize (functions, BigInt, etc.).
28
+ function serializeCause(cause: unknown): string {
29
+ try {
30
+ const json = JSON.stringify(cause);
31
+ if (json !== undefined) return json;
32
+ } catch {
33
+ // fall through
34
+ }
35
+ return inspect(cause, { depth: 2, breakLength: Infinity });
36
+ }
37
+
24
38
  export const runCommandEffect = <A, E, R extends never>(
25
39
  effect: Effect.Effect<A, E, R>,
26
40
  ): Promise<A> => runtime.runPromise(effect.pipe(printRedErrors) as any);
@@ -48,6 +62,24 @@ export const printRedErrors = Effect.catchAllCause((cause) =>
48
62
  return process.exit(1);
49
63
  }
50
64
 
65
+ // Surface server-side validation messages instead of dumping a stack with
66
+ // the useful detail buried in [cause].
67
+ if (theError instanceof PlatformApiError) {
68
+ const cause = theError.cause;
69
+ const causeMessage =
70
+ cause instanceof Error
71
+ ? cause.message
72
+ : cause != null
73
+ ? serializeCause(cause)
74
+ : '';
75
+ yield* Effect.logError(
76
+ causeMessage
77
+ ? `${theError.message}: ${causeMessage}`
78
+ : theError.message,
79
+ );
80
+ return process.exit(1);
81
+ }
82
+
51
83
  // Special error handling for specific error types
52
84
  if (theError instanceof InstantHttpError) {
53
85
  if (theError?.message) {
@@ -0,0 +1,127 @@
1
+ import { Effect } from 'effect';
2
+ import {
3
+ PlatformApi as InstantPlatformApi,
4
+ type WebhookAction,
5
+ type WebhookEventInfo,
6
+ type WebhooksManager,
7
+ } from '@instantdb/platform';
8
+ import { AuthToken } from '../context/authToken.ts';
9
+ import { CurrentApp } from '../context/currentApp.ts';
10
+ import { PlatformApiError } from '../context/platformApi.ts';
11
+ import { BadArgsError } from '../errors.ts';
12
+ import { getBaseUrl } from './http.ts';
13
+
14
+ const getAuthedPlatformApi = Effect.gen(function* () {
15
+ const apiURI = yield* getBaseUrl;
16
+ const authToken = yield* AuthToken;
17
+ const token = yield* authToken.getAuthToken;
18
+ return new InstantPlatformApi({ apiURI, auth: { token } });
19
+ });
20
+
21
+ export const WEBHOOK_ACTIONS: readonly WebhookAction[] = [
22
+ 'create',
23
+ 'update',
24
+ 'delete',
25
+ ] as const;
26
+
27
+ export const useWebhooksManager = <R>(
28
+ fun: (manager: WebhooksManager<any>) => Promise<R>,
29
+ errorMessage?: string,
30
+ ) =>
31
+ Effect.gen(function* () {
32
+ const api = yield* getAuthedPlatformApi;
33
+ const { appId } = yield* CurrentApp;
34
+ return yield* Effect.tryPromise({
35
+ try: () => fun(api.webhooks(appId).manager),
36
+ catch: (e) =>
37
+ new PlatformApiError({
38
+ message: errorMessage ?? 'Error using webhooks api',
39
+ cause: e,
40
+ }),
41
+ });
42
+ });
43
+
44
+ /**
45
+ * Yields a `WebhooksManager` instance scoped to the current app. Use when you
46
+ * need to hold on to the manager outside an Effect (e.g. to call from inside
47
+ * an async UI callback).
48
+ */
49
+ export const buildWebhooksManager = Effect.gen(function* () {
50
+ const api = yield* getAuthedPlatformApi;
51
+ const { appId } = yield* CurrentApp;
52
+ return api.webhooks(appId).manager;
53
+ });
54
+
55
+ /**
56
+ * Fetches the app's schema and returns the sorted list of namespace names. Returns
57
+ * `null` if the schema can't be fetched (network, auth, missing app, etc.) so
58
+ * callers can fall back to a plain text prompt.
59
+ */
60
+ export const getRemoteNamespaces = Effect.gen(function* () {
61
+ const api = yield* getAuthedPlatformApi;
62
+ const { appId } = yield* CurrentApp;
63
+ const result = yield* Effect.tryPromise(() => api.getSchema(appId)).pipe(
64
+ Effect.orElseSucceed(() => null),
65
+ );
66
+ if (!result) return null;
67
+ const entities = result.schema?.entities ?? {};
68
+ return Object.keys(entities).sort();
69
+ });
70
+
71
+ /**
72
+ * Pages through `manager.listEvents` until we have `limit` events or the server
73
+ * runs out. Returns the events in their natural (newest-first) order.
74
+ */
75
+ export const fetchRecentEvents = (webhookId: string, limit: number) =>
76
+ Effect.gen(function* () {
77
+ const collected: WebhookEventInfo[] = [];
78
+ let after: string | undefined;
79
+ while (collected.length < limit) {
80
+ const page = yield* useWebhooksManager(
81
+ (m) => m.listEvents(webhookId, after ? { after } : undefined),
82
+ 'Error listing webhook events',
83
+ );
84
+ collected.push(...page.events);
85
+ if (!page.pageInfo.hasNextPage || !page.pageInfo.endCursor) break;
86
+ after = page.pageInfo.endCursor;
87
+ }
88
+ return collected.slice(0, limit);
89
+ });
90
+
91
+ const splitCsv = (s: string) =>
92
+ s
93
+ .split(',')
94
+ .map((t) => t.trim())
95
+ .filter((t) => t.length > 0);
96
+
97
+ export const parseNamespaces = (raw: string | undefined) =>
98
+ Effect.gen(function* () {
99
+ if (raw === undefined) return undefined;
100
+ const namespaces = splitCsv(raw);
101
+ if (namespaces.length === 0) {
102
+ return yield* BadArgsError.make({
103
+ message: '--namespaces must include at least one namespace',
104
+ });
105
+ }
106
+ return namespaces;
107
+ });
108
+
109
+ export const parseActions = (raw: string | undefined) =>
110
+ Effect.gen(function* () {
111
+ if (raw === undefined) return undefined;
112
+ const tokens = splitCsv(raw);
113
+ if (tokens.length === 0) {
114
+ return yield* BadArgsError.make({
115
+ message: '--actions must include at least one action',
116
+ });
117
+ }
118
+ const invalid = tokens.filter(
119
+ (t): t is string => !WEBHOOK_ACTIONS.includes(t as WebhookAction),
120
+ );
121
+ if (invalid.length > 0) {
122
+ return yield* BadArgsError.make({
123
+ message: `Invalid action${invalid.length === 1 ? '' : 's'}: ${invalid.join(', ')}. Must be one of: ${WEBHOOK_ACTIONS.join(', ')}`,
124
+ });
125
+ }
126
+ return tokens as WebhookAction[];
127
+ });