attio 0.0.1-experimental.20241219 → 0.0.1-experimental.20250101

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 (54) hide show
  1. package/lib/api/create-version.js +2 -2
  2. package/lib/attio-logo.js +24 -0
  3. package/lib/attio.js +17 -8
  4. package/lib/commands/build.js +18 -35
  5. package/lib/commands/connection/add.js +51 -168
  6. package/lib/commands/connection/index.js +9 -1
  7. package/lib/commands/connection/list.js +17 -47
  8. package/lib/commands/connection/remove.js +17 -65
  9. package/lib/commands/create.js +27 -126
  10. package/lib/commands/dev.js +17 -106
  11. package/lib/commands/version/create.js +17 -68
  12. package/lib/commands/version/index.js +11 -1
  13. package/lib/commands/version/invite.js +40 -84
  14. package/lib/commands/version/list.js +18 -40
  15. package/lib/commands/version/publish.js +40 -64
  16. package/lib/machines/actions.js +7 -0
  17. package/lib/machines/actors.js +7 -1
  18. package/lib/machines/add-connection-machine.js +169 -201
  19. package/lib/machines/build-machine.js +35 -4
  20. package/lib/machines/create-machine.js +95 -70
  21. package/lib/machines/create-version-machine.js +121 -4
  22. package/lib/machines/dev-machine.js +173 -53
  23. package/lib/machines/generate-invite-machine.js +64 -10
  24. package/lib/machines/list-connections-machine.js +57 -2
  25. package/lib/machines/list-versions-machine.js +33 -0
  26. package/lib/machines/publish-version-machine.js +45 -16
  27. package/lib/machines/remove-connection-machine.js +64 -17
  28. package/lib/machines/ts-machine.js +4 -1
  29. package/lib/schema.js +2 -2
  30. package/lib/util/clear-terminal.js +4 -0
  31. package/lib/util/load-developer-config.js +2 -2
  32. package/lib/util/print-install-instructions.js +32 -0
  33. package/lib/util/print-message.js +9 -0
  34. package/lib/util/set-terminal-title.js +8 -0
  35. package/lib/util/text-gradient.js +28 -0
  36. package/lib/util/typescript.js +25 -0
  37. package/package.json +13 -12
  38. package/schema.graphql +8 -1
  39. package/lib/components/BuildError.js +0 -46
  40. package/lib/components/BuildLog.js +0 -6
  41. package/lib/components/CodeGenErrors.js +0 -22
  42. package/lib/components/Disclaimer.js +0 -9
  43. package/lib/components/InitialInstructions.js +0 -69
  44. package/lib/components/Log.js +0 -69
  45. package/lib/components/Logo.js +0 -10
  46. package/lib/components/MultiSelect.js +0 -65
  47. package/lib/components/ScrollBox.js +0 -87
  48. package/lib/components/ScrollBox.store.js +0 -36
  49. package/lib/components/ScrollBox.util.js +0 -27
  50. package/lib/components/Select.js +0 -6
  51. package/lib/components/Table.js +0 -33
  52. package/lib/components/TypeScriptErrors.js +0 -38
  53. package/lib/hooks/useFullScreen.js +0 -22
  54. package/lib/hooks/useTerminalTitle.js +0 -11
@@ -1,6 +1,5 @@
1
1
  import chokidar from "chokidar";
2
2
  import open from "open";
3
- import readline from "readline";
4
3
  import { assign, fromCallback, setup, enqueueActions } from "xstate";
5
4
  import { APP } from "../env.js";
6
5
  import { completeBundleUpload } from "../api/complete-bundle-upload.js";
@@ -9,13 +8,27 @@ import { startGraphqlServer } from "../api/start-graphql-server.js";
9
8
  import { startUpload } from "../api/start-upload.js";
10
9
  import { loadAppConfigFile } from "../util/app-config.js";
11
10
  import { loadDeveloperConfig, loadInitialDeveloperConfig, } from "../util/load-developer-config.js";
11
+ import notifier from "node-notifier";
12
12
  import { loadEnv } from "../util/load-env.js";
13
13
  import { codeGenMachine } from "./code-gen-machine.js";
14
14
  import { envMachine } from "./env-machine.js";
15
15
  import { jsMachine } from "./js-machine.js";
16
16
  import { tsMachine } from "./ts-machine.js";
17
- import { error as logError } from "../components/Log.js";
18
17
  import { fetchInstallation } from "../api/fetch-installation.js";
18
+ import Spinner from "tiny-spinner";
19
+ import { printMessage } from "../util/print-message.js";
20
+ import { printError } from "../util/typescript.js";
21
+ import { clearTerminal } from "../util/clear-terminal.js";
22
+ import { setTerminalTitle } from "../util/set-terminal-title.js";
23
+ import { printInstallInstructions } from "../util/print-install-instructions.js";
24
+ import { printLogo } from "./actions.js";
25
+ process.on("SIGINT", () => {
26
+ process.stdout.write("\x1B[?25h");
27
+ process.exit();
28
+ });
29
+ process.on("exit", () => {
30
+ process.stdout.write("\x1B[?25h");
31
+ });
19
32
  export const devMachine = setup({
20
33
  types: {
21
34
  context: {},
@@ -24,6 +37,7 @@ export const devMachine = setup({
24
37
  },
25
38
  guards: {
26
39
  "have dev version": ({ context }) => Boolean(context.devVersion),
40
+ "have typescript errors": (_, params) => Boolean(params.typeScriptErrors?.length),
27
41
  },
28
42
  actors: {
29
43
  "javascript": jsMachine,
@@ -38,45 +52,54 @@ export const devMachine = setup({
38
52
  }
39
53
  sendBack({ type: "Initialized", config });
40
54
  }),
41
- "listenForGraphqlOpen": fromCallback(({ input }) => {
42
- readline.emitKeypressEvents(process.stdin);
43
- if (process.stdin.isTTY) {
44
- process.stdin.setRawMode(true);
45
- }
46
- const handleKeyPress = (_, key) => {
47
- if (key.name?.toLowerCase() === "o") {
48
- open(`http://localhost:${input.graphqlPort}/graphql`);
49
- }
50
- };
51
- process.stdin.on("keypress", handleKeyPress);
52
- return () => {
53
- process.stdin.removeListener("keypress", handleKeyPress);
55
+ "keyboard": fromCallback(({ sendBack }) => {
56
+ if (!process.stdin.isTTY)
57
+ return;
58
+ const originalIsRaw = process.stdin.isRaw;
59
+ const originalIsTTY = process.stdin.isTTY;
60
+ const ensureRawMode = () => {
54
61
  if (process.stdin.isTTY) {
55
- process.stdin.setRawMode(false);
62
+ process.stdin.setRawMode(true);
56
63
  }
57
64
  };
58
- }),
59
- "listenForInstallOpen": fromCallback(({ input, sendBack }) => {
60
- readline.emitKeypressEvents(process.stdin);
61
- if (process.stdin.isTTY) {
62
- process.stdin.setRawMode(true);
63
- }
64
- const handleKeyPress = (_, key) => {
65
- if (key.name?.toLowerCase() === "i") {
66
- open(`${APP}/_/settings/apps/${input.appSlug}`);
67
- sendBack({ type: "Install Opened" });
65
+ const handleData = (data) => {
66
+ ensureRawMode();
67
+ const str = data.toString();
68
+ if (str === "\u0003") {
69
+ process.stdout.write("\x1B[?25h");
70
+ process.stdin.setRawMode(originalIsRaw);
71
+ process.exit();
72
+ }
73
+ switch (str) {
74
+ case "o":
75
+ sendBack({ type: "Open GraphQL Explorer" });
76
+ break;
77
+ case "i":
78
+ sendBack({ type: "Install Opened" });
79
+ break;
68
80
  }
69
81
  };
70
- process.stdin.on("keypress", handleKeyPress);
82
+ process.stdin.setRawMode(true);
83
+ process.stdin.resume();
84
+ process.stdin.on("data", handleData);
85
+ process.stdin.on("focus", ensureRawMode);
86
+ process.stdout.on("focus", ensureRawMode);
87
+ process.on("focus", ensureRawMode);
71
88
  return () => {
72
- process.stdin.removeListener("keypress", handleKeyPress);
73
- if (process.stdin.isTTY) {
74
- process.stdin.setRawMode(false);
89
+ process.stdin.removeListener("data", handleData);
90
+ process.stdin.removeListener("focus", ensureRawMode);
91
+ process.stdout.removeListener("focus", ensureRawMode);
92
+ process.removeListener("focus", ensureRawMode);
93
+ process.stdin.setRawMode(originalIsRaw);
94
+ if (originalIsTTY) {
95
+ process.stdin.pause();
75
96
  }
76
97
  };
77
98
  }),
78
99
  "prepareUpload": fromCallback(({ sendBack }) => {
100
+ const spinner = new Spinner();
79
101
  const prepareUpload = async () => {
102
+ spinner.start("Preparing upload...");
80
103
  const config = await loadDeveloperConfig();
81
104
  if (typeof config === "string")
82
105
  throw config;
@@ -93,18 +116,24 @@ export const devMachine = setup({
93
116
  targetWorkspaceId: config.target_workspace_id,
94
117
  environmentVariables,
95
118
  });
119
+ spinner.success("Upload prepared");
96
120
  sendBack({
97
121
  type: "Upload Prepared",
98
122
  devVersion: { ...devVersion, app_slug: app.slug },
99
123
  });
100
124
  };
101
- prepareUpload().catch((error) => sendBack({
102
- type: "Upload Error",
103
- error: typeof error === "string" ? new Error(error) : error,
104
- }));
125
+ prepareUpload().catch((error) => {
126
+ spinner.error(`Upload failed: ${error.message}`);
127
+ sendBack({
128
+ type: "Upload Error",
129
+ error: typeof error === "string" ? new Error(error) : error,
130
+ });
131
+ });
105
132
  }),
106
133
  "upload": fromCallback(({ sendBack, input: { config: { token, developer_slug: developerSlug }, contents, devVersion: { app_id: appId, app_dev_version_id: devVersionId }, }, }) => {
134
+ const spinner = new Spinner();
107
135
  const upload = async () => {
136
+ spinner.start("Uploading...");
108
137
  const { client_bundle_upload_url, server_bundle_upload_url, app_dev_version_bundle_id: bundleId, } = await startUpload({
109
138
  token,
110
139
  developerSlug,
@@ -130,7 +159,7 @@ export const devMachine = setup({
130
159
  },
131
160
  }),
132
161
  ]).catch((error) => {
133
- logError("Upload Error", error);
162
+ process.stderr.write(`Upload Error: ${error}`);
134
163
  });
135
164
  await completeBundleUpload({
136
165
  token,
@@ -139,9 +168,21 @@ export const devMachine = setup({
139
168
  devVersionId,
140
169
  bundleId,
141
170
  });
171
+ spinner.success(`Upload completed at ${new Date().toLocaleString()}.`);
142
172
  sendBack({ type: "Upload Complete" });
173
+ notifier.notify({
174
+ title: "Upload Complete",
175
+ message: "New bundle uploaded to Attio",
176
+ });
143
177
  };
144
- upload().catch((error) => sendBack({ type: "Upload Error", error }));
178
+ upload().catch((error) => {
179
+ spinner.error(`Upload failed: ${error.message}`);
180
+ sendBack({ type: "Upload Error", error });
181
+ notifier.notify({
182
+ title: "Upload Failed",
183
+ message: "Bundle upload to Attio failed",
184
+ });
185
+ });
145
186
  }),
146
187
  "watch": fromCallback(({ sendBack }) => {
147
188
  const watcher = chokidar.watch(["src", ".env"], {
@@ -188,6 +229,44 @@ export const devMachine = setup({
188
229
  },
189
230
  actions: {
190
231
  clearUploadError: assign({ uploadError: undefined }),
232
+ showConfigInstructions: ({ context }) => {
233
+ printInstallInstructions(context.configError);
234
+ },
235
+ printLogo: (_) => {
236
+ process.stdout.write("\x1B[?25l");
237
+ printLogo();
238
+ },
239
+ printWatching: (_) => {
240
+ printMessage("👀 Watching for changes...");
241
+ },
242
+ printTypeScriptErrors: (_, params) => {
243
+ if (params.errors.length) {
244
+ clearTerminal();
245
+ params.errors.forEach(printError);
246
+ notifier.notify({
247
+ title: "TypeScript Errors",
248
+ message: `There were ${params.errors.length === 1 ? "one error" : `${params.errors.length} errors`} in your TypeScript code`,
249
+ });
250
+ }
251
+ },
252
+ promptToInstall: (_) => {
253
+ process.stdout.write(`\n\n🚨 IMPORTANT: You will need to install your app in your workspace. Press "i" to open the app settings page, and then click "Install".\n\n`);
254
+ },
255
+ promptToOpenGraphQLExplorer: (_) => {
256
+ process.stdout.write(`\n\n Press "o" to open GraphQL Explorer.\n\n`);
257
+ },
258
+ openGraphQLExplorer: ({ context }) => {
259
+ open(`http://localhost:${context.graphqlPort}/graphql`);
260
+ },
261
+ openInstallPage: ({ context }) => {
262
+ open(`${APP}/_/settings/apps/${context.devVersion?.app_slug}`);
263
+ },
264
+ saveTypeScriptErrors: assign({
265
+ typeScriptErrors: (_, params) => params.errors,
266
+ }),
267
+ clearTypeScriptErrors: assign({
268
+ typeScriptErrors: () => [],
269
+ }),
191
270
  sendChange: enqueueActions(({ enqueue, event }) => {
192
271
  enqueue.sendTo("javascript", event);
193
272
  enqueue.sendTo("typescript", event);
@@ -209,6 +288,11 @@ export const devMachine = setup({
209
288
  setGraphqlPort: assign({
210
289
  graphqlPort: (_, params) => params.port,
211
290
  }),
291
+ setTerminalTitle: ({ context }) => {
292
+ setTerminalTitle(context.devVersion?.app_id
293
+ ? `attio dev – ${context.devVersion.app_id}`
294
+ : `attio dev`);
295
+ },
212
296
  setSuccess: assign({
213
297
  jsContents: (_, params) => params.contents,
214
298
  lastSuccessfulJavaScriptBuild: (_, params) => params.time,
@@ -245,6 +329,9 @@ export const devMachine = setup({
245
329
  input: ({ self }) => ({ parentRef: self }),
246
330
  },
247
331
  { src: "watch" },
332
+ {
333
+ src: "keyboard",
334
+ },
248
335
  ],
249
336
  states: {
250
337
  JavaScript: {
@@ -264,7 +351,9 @@ export const devMachine = setup({
264
351
  },
265
352
  },
266
353
  },
267
- "Watching": {},
354
+ "Watching": {
355
+ entry: "printWatching",
356
+ },
268
357
  "Uploading": {
269
358
  on: {
270
359
  "Upload Complete": "Watching",
@@ -286,13 +375,15 @@ export const devMachine = setup({
286
375
  },
287
376
  "Upload When Ready": {
288
377
  always: {
289
- target: "Uploading",
378
+ target: "Can Upload?",
290
379
  guard: "have dev version",
380
+ reenter: true,
291
381
  },
292
382
  on: {
293
383
  "Upload Prepared": {
294
- target: "Uploading",
384
+ target: "Can Upload?",
295
385
  actions: { type: "setDevVersion", params: ({ event }) => event },
386
+ reenter: true,
296
387
  },
297
388
  },
298
389
  },
@@ -307,12 +398,28 @@ export const devMachine = setup({
307
398
  },
308
399
  },
309
400
  },
401
+ "Can Upload?": {
402
+ always: [
403
+ {
404
+ target: "Watching",
405
+ guard: {
406
+ type: "have typescript errors",
407
+ params: ({ context }) => context,
408
+ },
409
+ reenter: true,
410
+ },
411
+ "Uploading",
412
+ ],
413
+ },
310
414
  },
311
415
  initial: "Building",
312
416
  on: {
313
417
  "Upload Prepared": {
314
418
  target: "JavaScript",
315
- actions: { type: "setDevVersion", params: ({ event }) => event },
419
+ actions: [
420
+ { type: "setDevVersion", params: ({ event }) => event },
421
+ "setTerminalTitle",
422
+ ],
316
423
  },
317
424
  "Change": {
318
425
  target: "JavaScript",
@@ -331,9 +438,19 @@ export const devMachine = setup({
331
438
  states: {
332
439
  Validating: {
333
440
  on: {
334
- "TypeScript Success": "Watching",
441
+ "TypeScript Success": {
442
+ target: "Watching",
443
+ actions: "clearTypeScriptErrors",
444
+ },
335
445
  "TypeScript Error": {
336
446
  target: "Watching",
447
+ actions: [
448
+ {
449
+ type: "printTypeScriptErrors",
450
+ params: ({ event }) => event,
451
+ },
452
+ { type: "saveTypeScriptErrors", params: ({ event }) => event },
453
+ ],
337
454
  },
338
455
  },
339
456
  },
@@ -369,9 +486,11 @@ export const devMachine = setup({
369
486
  },
370
487
  },
371
488
  "Started": {
372
- invoke: {
373
- src: "listenForGraphqlOpen",
374
- input: ({ context }) => ({ graphqlPort: context.graphqlPort }),
489
+ on: {
490
+ "Open GraphQL Explorer": {
491
+ target: "Started",
492
+ actions: "openGraphQLExplorer",
493
+ },
375
494
  },
376
495
  },
377
496
  },
@@ -396,15 +515,7 @@ export const devMachine = setup({
396
515
  type: "final",
397
516
  },
398
517
  "Prompt to Install": {
399
- invoke: {
400
- src: "listenForInstallOpen",
401
- input: ({ context }) => ({
402
- appSlug: context.devVersion.app_slug,
403
- }),
404
- },
405
- on: {
406
- "Install Opened": "Poll For Installation",
407
- },
518
+ entry: "promptToInstall",
408
519
  },
409
520
  "Poll For Installation": {
410
521
  invoke: {
@@ -428,9 +539,16 @@ export const devMachine = setup({
428
539
  },
429
540
  },
430
541
  initial: "Waiting for App ID",
542
+ on: {
543
+ "Install Opened": {
544
+ target: ".Poll For Installation",
545
+ actions: "openInstallPage",
546
+ },
547
+ },
431
548
  },
432
549
  },
433
550
  type: "parallel",
551
+ entry: "promptToOpenGraphQLExplorer",
434
552
  },
435
553
  "Read Config": {
436
554
  on: {
@@ -452,6 +570,8 @@ export const devMachine = setup({
452
570
  },
453
571
  "No Config": {
454
572
  type: "final",
573
+ entry: "showConfigInstructions",
455
574
  },
456
575
  },
576
+ entry: "printLogo",
457
577
  });
@@ -1,8 +1,13 @@
1
1
  import { assign, setup, fromCallback } from "xstate";
2
+ import clipboard from "clipboardy";
2
3
  import { fetchUnpublishedVersions } from "../api/fetch-versions.js";
3
4
  import { generateInviteLink } from "../api/generate-invite-link.js";
4
5
  import { emptyConfig } from "../schema.js";
5
- import { loadAppConfig, loadDeveloperConfig } from "./actors.js";
6
+ import { askWithTypedChoices, loadAppConfig, loadDeveloperConfig } from "./actors.js";
7
+ import { printLogo, showError } from "./actions.js";
8
+ import { printInstallInstructions } from "../util/print-install-instructions.js";
9
+ import Spinner from "tiny-spinner";
10
+ import chalk from "chalk";
6
11
  export const connectionTypes = [
7
12
  { value: "secret", label: "Secret" },
8
13
  { value: "oauth2-code", label: "OAuth2" },
@@ -18,19 +23,27 @@ export const generateInviteMachine = setup({
18
23
  input: {},
19
24
  },
20
25
  actors: {
26
+ askForVersion: askWithTypedChoices(),
21
27
  fetchVersions: fromCallback(({ sendBack, input: { developer: { token, slug: devSlug }, config, }, }) => {
22
28
  const getVersions = async () => {
29
+ const spinner = new Spinner();
30
+ spinner.start("Loading versions...");
23
31
  const versions = await fetchUnpublishedVersions({
24
32
  token,
25
33
  devSlug,
26
34
  appId: config.id,
27
35
  });
36
+ spinner.success("Versions loaded");
28
37
  sendBack({ type: "Versions Fetched", versions });
29
38
  };
30
39
  getVersions();
31
40
  }),
32
- generateInviteLink: fromCallback(({ sendBack, input: { developer: { token, slug: devSlug }, config, major, minor, }, }) => {
41
+ generateInviteLink: fromCallback(({ sendBack, input: { developer: { token, slug: devSlug }, config, versionProvided, major, minor, }, }) => {
33
42
  const add = async () => {
43
+ const spinner = new Spinner();
44
+ if (!versionProvided) {
45
+ spinner.start("Generating invite link...");
46
+ }
34
47
  try {
35
48
  const inviteLink = await generateInviteLink({
36
49
  token,
@@ -39,9 +52,15 @@ export const generateInviteMachine = setup({
39
52
  major: major,
40
53
  minor: minor,
41
54
  });
55
+ if (!versionProvided) {
56
+ spinner.success("Invite link generated");
57
+ }
42
58
  sendBack({ type: "Invite Link Generated", inviteLink });
43
59
  }
44
60
  catch (error) {
61
+ if (!versionProvided) {
62
+ spinner.error("Error generating invite link");
63
+ }
45
64
  sendBack({ type: "Error", error: error.message });
46
65
  }
47
66
  };
@@ -51,6 +70,27 @@ export const generateInviteMachine = setup({
51
70
  loadAppConfig,
52
71
  },
53
72
  actions: {
73
+ printLogo,
74
+ showError,
75
+ showConfigInstructions: ({ context }) => {
76
+ printLogo();
77
+ printInstallInstructions(context.configError);
78
+ },
79
+ showNoUnpublishedVersions: () => {
80
+ process.stdout.write("No unpublished versions found.");
81
+ },
82
+ printInviteLink: ({ context }) => {
83
+ if (context.versionProvided) {
84
+ process.stdout.write(context.inviteLink);
85
+ return;
86
+ }
87
+ process.stdout.write(`\nInvite link for version ${context.major}.${context.minor} successfully generated. It will expire in 24 hours.\n\n`);
88
+ process.stdout.write(chalk.blue(context.inviteLink));
89
+ process.stdout.write("\n\n📋 It's already on your clipboard!\n");
90
+ },
91
+ copyToInviteLinkToClipboard: ({ context }) => {
92
+ clipboard.writeSync(context.inviteLink);
93
+ },
54
94
  clearError: assign({
55
95
  error: () => undefined,
56
96
  }),
@@ -70,8 +110,8 @@ export const generateInviteMachine = setup({
70
110
  inviteLink: (_, params) => params.inviteLink,
71
111
  }),
72
112
  setVersion: assign({
73
- major: (_, params) => params.version.major,
74
- minor: (_, params) => params.version.minor,
113
+ major: (_, params) => params.output.major,
114
+ minor: (_, params) => params.output.minor,
75
115
  }),
76
116
  setVersions: assign({
77
117
  versions: (_, params) => params.versions,
@@ -86,6 +126,7 @@ export const generateInviteMachine = setup({
86
126
  developer: { slug: "", token: "" },
87
127
  config: emptyConfig,
88
128
  versions: [],
129
+ versionProvided: input.major !== undefined && input.minor !== undefined,
89
130
  major: input.major,
90
131
  minor: input.minor,
91
132
  }),
@@ -108,6 +149,7 @@ export const generateInviteMachine = setup({
108
149
  },
109
150
  "Show config instructions": {
110
151
  type: "final",
152
+ entry: "showConfigInstructions",
111
153
  },
112
154
  "Loading App Config": {
113
155
  invoke: {
@@ -127,9 +169,11 @@ export const generateInviteMachine = setup({
127
169
  },
128
170
  "Error": {
129
171
  type: "final",
172
+ entry: { type: "showError", params: ({ context }) => ({ error: context.error }) },
130
173
  },
131
174
  "Display Invite Link": {
132
175
  type: "final",
176
+ entry: ["copyToInviteLinkToClipboard", "printInviteLink"],
133
177
  },
134
178
  "Fetching Versions": {
135
179
  on: {
@@ -158,6 +202,7 @@ export const generateInviteMachine = setup({
158
202
  {
159
203
  target: "Fetching Versions",
160
204
  reenter: true,
205
+ actions: "printLogo",
161
206
  },
162
207
  ],
163
208
  },
@@ -178,12 +223,6 @@ export const generateInviteMachine = setup({
178
223
  },
179
224
  },
180
225
  "Ask for version": {
181
- on: {
182
- "Select Version": {
183
- target: "Generating Invite Link",
184
- actions: { type: "setVersion", params: ({ event }) => event },
185
- },
186
- },
187
226
  always: {
188
227
  target: "No unpublished versions",
189
228
  guard: {
@@ -191,9 +230,24 @@ export const generateInviteMachine = setup({
191
230
  params: ({ context }) => context,
192
231
  },
193
232
  },
233
+ invoke: {
234
+ src: "askForVersion",
235
+ input: ({ context }) => ({
236
+ choices: context.versions.map((version) => ({
237
+ name: `${version.major}.${version.minor}`,
238
+ value: version,
239
+ })),
240
+ message: "Select an unpublished version to invite someone to:",
241
+ }),
242
+ onDone: {
243
+ target: "Generating Invite Link",
244
+ actions: { type: "setVersion", params: ({ event }) => event },
245
+ },
246
+ },
194
247
  },
195
248
  "No unpublished versions": {
196
249
  type: "final",
250
+ entry: "showNoUnpublishedVersions",
197
251
  },
198
252
  },
199
253
  initial: "Loading Developer Config",
@@ -2,6 +2,12 @@ import { assign, setup, fromCallback } from "xstate";
2
2
  import { fetchConnections } from "../api/fetch-connections.js";
3
3
  import { emptyConfig } from "../schema.js";
4
4
  import { loadAppConfig, loadDeveloperConfig } from "./actors.js";
5
+ import Table from "cli-table3";
6
+ import { showError, printLogo } from "./actions.js";
7
+ import { printInstallInstructions } from "../util/print-install-instructions.js";
8
+ import boxen from "boxen";
9
+ import chalk from "chalk";
10
+ import Spinner from "tiny-spinner";
5
11
  export const listConnectionsMachine = setup({
6
12
  types: {
7
13
  context: {},
@@ -11,17 +17,61 @@ export const listConnectionsMachine = setup({
11
17
  loadDeveloperConfig,
12
18
  loadAppConfig,
13
19
  loadConnections: fromCallback(({ sendBack, input }) => {
20
+ const spinner = new Spinner();
21
+ spinner.start("Loading connections...");
14
22
  fetchConnections({
15
23
  token: input.developer.token,
16
24
  devSlug: input.developer.slug,
17
25
  appId: input.config.id,
18
26
  major: input.config.major,
19
27
  })
20
- .then((connections) => sendBack({ type: "Connections Loaded", connections }))
21
- .catch((error) => sendBack({ type: "Error", error: error.message }));
28
+ .then((connections) => {
29
+ spinner.success("Connections loaded");
30
+ sendBack({ type: "Connections Loaded", connections });
31
+ })
32
+ .catch((error) => {
33
+ spinner.error("Error loading connections");
34
+ sendBack({ type: "Error", error: error.message });
35
+ });
22
36
  }),
23
37
  },
24
38
  actions: {
39
+ printLogo,
40
+ showError,
41
+ showConfigInstructions: ({ context }) => printInstallInstructions(context.configError),
42
+ showNoConnections: () => {
43
+ process.stdout.write(chalk.red("This app has no connections.\n\n"));
44
+ process.stdout.write("To add one, use:\n");
45
+ process.stdout.write(boxen("attio connection add", {
46
+ padding: 1,
47
+ margin: 1,
48
+ borderStyle: "round",
49
+ }) + "\n");
50
+ },
51
+ showConnections: ({ context: { connections } }) => {
52
+ const maxWidth = process.stdout.columns - 10;
53
+ const table = new Table({
54
+ head: ["Name", "Level", "Type"].map((h) => chalk.bold(h)),
55
+ style: {
56
+ head: [],
57
+ border: [],
58
+ },
59
+ colWidths: [
60
+ Math.floor(maxWidth * 0.5),
61
+ Math.floor(maxWidth * 0.2),
62
+ Math.floor(maxWidth * 0.3),
63
+ ],
64
+ colAligns: ["left", "center", "center"],
65
+ wordWrap: true,
66
+ wrapOnWordBoundary: true,
67
+ });
68
+ table.push(...connections.map((connection) => [
69
+ connection.label,
70
+ connection.global ? "Workspace" : "User",
71
+ connection.connection_type === "secret" ? "Secret" : "OAuth2 Code",
72
+ ]));
73
+ process.stdout.write(table.toString());
74
+ },
25
75
  setError: assign({
26
76
  error: (_, params) => params.error,
27
77
  }),
@@ -66,6 +116,7 @@ export const listConnectionsMachine = setup({
66
116
  },
67
117
  "Show config instructions": {
68
118
  type: "final",
119
+ entry: "showConfigInstructions",
69
120
  },
70
121
  "Loading App Config": {
71
122
  invoke: {
@@ -84,6 +135,7 @@ export const listConnectionsMachine = setup({
84
135
  },
85
136
  "Error": {
86
137
  type: "final",
138
+ entry: { type: "showError", params: ({ context }) => ({ error: context.error }) },
87
139
  },
88
140
  "Loading Connections": {
89
141
  invoke: {
@@ -110,10 +162,13 @@ export const listConnectionsMachine = setup({
110
162
  },
111
163
  "No Connections": {
112
164
  type: "final",
165
+ entry: "showNoConnections",
113
166
  },
114
167
  "Display Connections": {
115
168
  type: "final",
169
+ entry: "showConnections",
116
170
  },
117
171
  },
118
172
  initial: "Loading Developer Config",
173
+ entry: "printLogo",
119
174
  });