@dittowords/cli 4.3.0 → 4.4.1

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 (85) hide show
  1. package/.github/actions/install-node-dependencies/action.yml +24 -0
  2. package/.github/workflows/required-checks.yml +24 -0
  3. package/.sentryclirc +3 -0
  4. package/__mocks__/fs.js +2 -0
  5. package/bin/__mocks__/api.js +48 -0
  6. package/bin/__mocks__/api.js.map +1 -0
  7. package/bin/config.test.js +4 -3
  8. package/bin/config.test.js.map +1 -1
  9. package/bin/consts.js +19 -29
  10. package/bin/consts.js.map +1 -1
  11. package/bin/ditto.js +12 -4
  12. package/bin/ditto.js.map +1 -1
  13. package/bin/generate-suggestions.js +68 -58
  14. package/bin/generate-suggestions.js.map +1 -1
  15. package/bin/generate-suggestions.test.js +24 -13
  16. package/bin/generate-suggestions.test.js.map +1 -1
  17. package/bin/http/__mocks__/fetchComponentFolders.js +71 -0
  18. package/bin/http/__mocks__/fetchComponentFolders.js.map +1 -0
  19. package/bin/http/__mocks__/fetchComponents.js +73 -0
  20. package/bin/http/__mocks__/fetchComponents.js.map +1 -0
  21. package/bin/http/__mocks__/fetchVariants.js +71 -0
  22. package/bin/http/__mocks__/fetchVariants.js.map +1 -0
  23. package/bin/http/fetchComponentFolders.js +4 -4
  24. package/bin/http/fetchComponentFolders.js.map +1 -1
  25. package/bin/http/fetchComponents.js +4 -4
  26. package/bin/http/fetchComponents.js.map +1 -1
  27. package/bin/http/fetchVariants.js +6 -3
  28. package/bin/http/fetchVariants.js.map +1 -1
  29. package/bin/http/http.test.js +159 -0
  30. package/bin/http/http.test.js.map +1 -0
  31. package/bin/http/importComponents.js +9 -2
  32. package/bin/http/importComponents.js.map +1 -1
  33. package/bin/init/project.test.js +5 -28
  34. package/bin/init/project.test.js.map +1 -1
  35. package/bin/init/token.js +72 -27
  36. package/bin/init/token.js.map +1 -1
  37. package/bin/init/token.test.js +87 -9
  38. package/bin/init/token.test.js.map +1 -1
  39. package/bin/pull-lib.test.js +379 -0
  40. package/bin/pull-lib.test.js.map +1 -0
  41. package/bin/pull.js +15 -4
  42. package/bin/pull.js.map +1 -1
  43. package/bin/pull.test.js +73 -290
  44. package/bin/pull.test.js.map +1 -1
  45. package/bin/replace.js +22 -6
  46. package/bin/replace.js.map +1 -1
  47. package/bin/replace.test.js +53 -11
  48. package/bin/replace.test.js.map +1 -1
  49. package/bin/utils/determineModuleType.js +6 -7
  50. package/bin/utils/determineModuleType.js.map +1 -1
  51. package/bin/utils/determineModuleType.test.js +60 -0
  52. package/bin/utils/determineModuleType.test.js.map +1 -0
  53. package/bin/utils/getSelectedProjects.js +5 -5
  54. package/bin/utils/getSelectedProjects.js.map +1 -1
  55. package/bin/utils/quit.js +3 -3
  56. package/bin/utils/quit.js.map +1 -1
  57. package/jest.config.ts +16 -0
  58. package/lib/__mocks__/api.ts +12 -0
  59. package/lib/config.test.ts +3 -1
  60. package/lib/consts.ts +19 -17
  61. package/lib/ditto.ts +9 -1
  62. package/lib/generate-suggestions.test.ts +23 -11
  63. package/lib/generate-suggestions.ts +89 -79
  64. package/lib/http/__mocks__/fetchComponentFolders.ts +23 -0
  65. package/lib/http/__mocks__/fetchComponents.ts +24 -0
  66. package/lib/http/__mocks__/fetchVariants.ts +21 -0
  67. package/lib/http/fetchComponentFolders.ts +6 -4
  68. package/lib/http/fetchComponents.ts +5 -3
  69. package/lib/http/fetchVariants.ts +15 -4
  70. package/lib/http/http.test.ts +122 -0
  71. package/lib/http/importComponents.ts +8 -0
  72. package/lib/init/project.test.ts +4 -27
  73. package/lib/init/token.test.ts +55 -7
  74. package/lib/init/token.ts +76 -27
  75. package/lib/pull-lib.test.ts +367 -0
  76. package/lib/pull.test.ts +63 -316
  77. package/lib/pull.ts +13 -2
  78. package/lib/replace.test.ts +46 -10
  79. package/lib/replace.ts +20 -3
  80. package/lib/utils/determineModuleType.test.ts +48 -0
  81. package/lib/utils/determineModuleType.ts +4 -6
  82. package/lib/utils/getSelectedProjects.ts +3 -3
  83. package/lib/utils/quit.ts +1 -1
  84. package/package.json +4 -3
  85. package/jest.config.js +0 -6
@@ -0,0 +1,23 @@
1
+ import type { FetchComponentFoldersResponse } from "../fetchComponentFolders";
2
+
3
+ export const MOCK_COMPONENT_FOLDERS: FetchComponentFoldersResponse = {
4
+ example_folder: "Example Folder",
5
+ };
6
+
7
+ export const MOCK_COMPONENT_FOLDERS_WITH_SAMPLE_DATA: FetchComponentFoldersResponse =
8
+ {
9
+ example_folder: "Example Folder",
10
+ example_folder_sample: "Sample Example Folder",
11
+ };
12
+
13
+ export async function fetchComponentFolders(
14
+ _options: {
15
+ showSampleData?: boolean;
16
+ } = {}
17
+ ): Promise<FetchComponentFoldersResponse> {
18
+ if (_options.showSampleData) {
19
+ return MOCK_COMPONENT_FOLDERS_WITH_SAMPLE_DATA;
20
+ }
21
+
22
+ return MOCK_COMPONENT_FOLDERS;
23
+ }
@@ -0,0 +1,24 @@
1
+ import { FetchComponentResponse } from "../fetchComponents";
2
+
3
+ export const MOCK_COMPONENTS_RESPONSE: FetchComponentResponse = {
4
+ "component-1": {
5
+ name: "Example Component 1",
6
+ text: "This is example component text.",
7
+ status: "NONE",
8
+ folder: null,
9
+ },
10
+ "component-2": {
11
+ name: "Example Component 2",
12
+ text: "This is example component text.",
13
+ status: "NONE",
14
+ folder: null,
15
+ },
16
+ };
17
+
18
+ export async function fetchComponents(
19
+ _options: {
20
+ componentFolder?: string;
21
+ } = {}
22
+ ): Promise<FetchComponentResponse> {
23
+ return MOCK_COMPONENTS_RESPONSE;
24
+ }
@@ -0,0 +1,21 @@
1
+ import { IVariant } from "../fetchVariants";
2
+
3
+ export const MOCK_VARIANTS_RESPONSE: IVariant[] = [
4
+ {
5
+ name: "Example Variant 1",
6
+ description: "This is example variant 1.",
7
+ apiID: "example-variant-1",
8
+ },
9
+ {
10
+ name: "Example Variant 2",
11
+ description: "This is example variant 2.",
12
+ apiID: "example-variant-2",
13
+ },
14
+ ];
15
+
16
+ export async function fetchVariants(
17
+ _source: any,
18
+ _options: any = {}
19
+ ): Promise<IVariant[] | null> {
20
+ return MOCK_VARIANTS_RESPONSE;
21
+ }
@@ -1,12 +1,14 @@
1
1
  import { createApiClient } from "../api";
2
2
 
3
- interface FetchComponentFoldersResponse {
3
+ export interface FetchComponentFoldersResponse {
4
4
  [id: string]: string;
5
5
  }
6
6
 
7
- export async function fetchComponentFolders(options: {
8
- showSampleData?: boolean;
9
- }): Promise<FetchComponentFoldersResponse> {
7
+ export async function fetchComponentFolders(
8
+ options: {
9
+ showSampleData?: boolean;
10
+ } = {}
11
+ ): Promise<FetchComponentFoldersResponse> {
10
12
  const api = createApiClient();
11
13
 
12
14
  let url = "/v1/component-folders";
@@ -11,9 +11,11 @@ export interface FetchComponentResponse {
11
11
  [compApiId: string]: FetchComponentResponseComponent;
12
12
  }
13
13
 
14
- export async function fetchComponents(options: {
15
- componentFolder?: string;
16
- }): Promise<FetchComponentResponse> {
14
+ export async function fetchComponents(
15
+ options: {
16
+ componentFolder?: string;
17
+ } = {}
18
+ ): Promise<FetchComponentResponse> {
17
19
  const api = createApiClient();
18
20
 
19
21
  if (options.componentFolder) {
@@ -3,10 +3,21 @@ import { createApiClient } from "../api";
3
3
  import { PullOptions } from "../pull";
4
4
  import { SourceInformation } from "../types";
5
5
 
6
+ export interface IVariant {
7
+ name: string;
8
+ description: string;
9
+ apiID: string;
10
+ }
11
+
12
+ type SourceArg = Pick<
13
+ SourceInformation,
14
+ "shouldFetchComponentLibrary" | "validProjects" | "variants"
15
+ >;
16
+
6
17
  export async function fetchVariants(
7
- source: SourceInformation,
18
+ source: SourceArg,
8
19
  options: PullOptions = {}
9
- ) {
20
+ ): Promise<IVariant[] | null> {
10
21
  const api = createApiClient();
11
22
  if (!source.variants) {
12
23
  return null;
@@ -15,7 +26,7 @@ export async function fetchVariants(
15
26
  const { shouldFetchComponentLibrary, validProjects } = source;
16
27
 
17
28
  const config: AxiosRequestConfig = {
18
- params: { ...options?.meta },
29
+ params: { ...options?.meta, showSampleData: options.includeSampleData },
19
30
  };
20
31
 
21
32
  // if we're not syncing from the component library, then we pass the project ids
@@ -25,7 +36,7 @@ export async function fetchVariants(
25
36
  config.params.projectIds = validProjects.map(({ id }) => id);
26
37
  }
27
38
 
28
- const { data } = await api.get<{ apiID: string }[]>("/v1/variants", config);
39
+ const { data } = await api.get<IVariant[]>("/v1/variants", config);
29
40
 
30
41
  return data;
31
42
  }
@@ -0,0 +1,122 @@
1
+ import { fetchComponentFolders } from "./fetchComponentFolders";
2
+ import { fetchComponents } from "./fetchComponents";
3
+ import { fetchVariants } from "./fetchVariants";
4
+ import { importComponents } from "./importComponents";
5
+ import { jest } from "@jest/globals";
6
+ import axios from "axios";
7
+ import { vol } from "memfs";
8
+
9
+ jest.mock("../api");
10
+ jest.mock("fs");
11
+
12
+ const axiosMocked = jest.mocked(axios);
13
+
14
+ describe("fetchComponentFolders", () => {
15
+ it("fetches component folders without error", async () => {
16
+ axiosMocked.get.mockResolvedValueOnce({
17
+ data: { "folder-id": "folder-name" },
18
+ });
19
+ const result = await fetchComponentFolders();
20
+ expect(result["folder-id"]).toBe("folder-name");
21
+ });
22
+ it("supports showSampleData option", async () => {
23
+ axiosMocked.get.mockResolvedValueOnce({
24
+ data: { "folder-id": "folder-name" },
25
+ });
26
+ const result = await fetchComponentFolders({ showSampleData: true });
27
+ expect(result["folder-id"]).toBe("folder-name");
28
+ });
29
+ });
30
+
31
+ describe("fetchComponents", () => {
32
+ it("fetches components without error", async () => {
33
+ axiosMocked.get.mockResolvedValueOnce({
34
+ data: {
35
+ "component-id": {
36
+ name: "component-name",
37
+ text: "component-text",
38
+ status: "NONE",
39
+ folder: null,
40
+ },
41
+ },
42
+ });
43
+ const result = await fetchComponents();
44
+ expect(result["component-id"]).toBeDefined();
45
+ expect(result["component-id"].name).toBe("component-name");
46
+ expect(result["component-id"].text).toBe("component-text");
47
+ });
48
+ });
49
+
50
+ describe("fetchVariants", () => {
51
+ it("fetches variants without error", async () => {
52
+ axiosMocked.get.mockResolvedValueOnce({
53
+ data: [
54
+ {
55
+ name: "variant-name",
56
+ description: "variant-description",
57
+ apiID: "variant-api-id",
58
+ },
59
+ ],
60
+ });
61
+ const result = await fetchVariants({
62
+ shouldFetchComponentLibrary: true,
63
+ validProjects: [],
64
+ variants: true,
65
+ });
66
+ expect(result).toBeTruthy();
67
+ expect(result![0]).toBeDefined();
68
+ expect(result![0].name).toBe("variant-name");
69
+ expect(result![0].description).toBe("variant-description");
70
+ expect(result![0].apiID).toBe("variant-api-id");
71
+ });
72
+ it("returns null if `variants` isn't in config", async () => {
73
+ const result = await fetchVariants({
74
+ shouldFetchComponentLibrary: true,
75
+ validProjects: [],
76
+ variants: false,
77
+ });
78
+ expect(result).toBe(null);
79
+ });
80
+ });
81
+
82
+ describe("importComponents", () => {
83
+ beforeEach(() => {
84
+ vol.reset();
85
+ });
86
+ it("imports components from existing file without error", async () => {
87
+ vol.fromJSON({
88
+ "/file.csv": "id,name,text\n1,one1,not empty\n2,two1,empty",
89
+ });
90
+
91
+ axiosMocked.mockResolvedValueOnce({
92
+ data: {
93
+ componentsInserted: 2,
94
+ firstImportedId: "1",
95
+ },
96
+ });
97
+
98
+ const result = await importComponents("/file.csv", {
99
+ csvColumnMapping: {
100
+ name: "name",
101
+ text: 2,
102
+ componentId: 0,
103
+ },
104
+ });
105
+
106
+ expect(result.componentsInserted).toBe(2);
107
+ expect(result.firstImportedId).toBe("1");
108
+ });
109
+
110
+ it("returns null firstImportedId if file doesn't exist", async () => {
111
+ const result = await importComponents("/file.csv", {
112
+ csvColumnMapping: {
113
+ name: "name",
114
+ text: 2,
115
+ componentId: 0,
116
+ },
117
+ });
118
+
119
+ expect(result.componentsInserted).toBe(0);
120
+ expect(result.firstImportedId).toBe("null");
121
+ });
122
+ });
@@ -24,6 +24,14 @@ export async function importComponents(
24
24
  ): Promise<ImportComponentResponse> {
25
25
  const api = createApiClient();
26
26
 
27
+ if (!fs.existsSync(path)) {
28
+ console.error("Failed to import file: couldn't find file at path " + path);
29
+ return {
30
+ componentsInserted: 0,
31
+ firstImportedId: "null",
32
+ };
33
+ }
34
+
27
35
  const form = new FormData();
28
36
  form.append("import", fs.createReadStream(path));
29
37
 
@@ -1,44 +1,21 @@
1
1
  const fs = require("fs");
2
- const path = require("path");
3
-
4
2
  const yaml = require("js-yaml");
5
- const { createFileIfMissing } = require("../config");
6
3
 
7
4
  import { _testing } from "./project";
8
5
 
9
- const fakeProjectDir = path.join(__dirname, "../../testing/tmp");
10
- const badYaml = path.join(__dirname, "../../testing/fixtures/bad-yaml.yml");
11
- const configEmptyProjects = path.join(
12
- __dirname,
13
- "../../testing/fixtures/bad-yaml.yml"
14
- );
15
- const configMissingName = path.join(
16
- __dirname,
17
- "../../testing/fixtures/project-config-no-name.yml"
18
- );
19
- const configMissingId = path.join(
20
- __dirname,
21
- "../../testing/fixtures/project-config-no-id.yml"
22
- );
23
- const configLegit = path.join(
24
- __dirname,
25
- "../../testing/fixtures/project-config-working.yml"
26
- );
6
+ const fakeProjectDir = "/";
7
+
8
+ jest.mock("fs");
27
9
 
28
10
  describe("saveProject", () => {
29
- const configFile = path.join(fakeProjectDir, "ditto/config.yml");
11
+ const configFile = "/ditto/config.yml";
30
12
  const projectName = "My Amazing Project";
31
13
  const projectId = "5f284259ce1d451b2eb2e23c";
32
14
 
33
15
  beforeEach(() => {
34
- if (!fs.existsSync(fakeProjectDir)) fs.mkdirSync(fakeProjectDir);
35
16
  _testing.saveProject(configFile, projectName, projectId);
36
17
  });
37
18
 
38
- afterEach(() => {
39
- fs.rmdirSync(fakeProjectDir, { recursive: true });
40
- });
41
-
42
19
  it("creates a config file with config data", () => {
43
20
  const fileContents = fs.readFileSync(configFile, "utf8");
44
21
  const data = yaml.load(fileContents);
@@ -1,22 +1,51 @@
1
- import tempy from "tempy";
2
-
1
+ import fs from "fs";
3
2
  import config from "../config";
3
+ import { randomUUID } from "crypto";
4
4
  import { needsToken } from "./token";
5
+ import { _test } from "./token";
6
+ import { vol } from "memfs";
7
+ import { jest } from "@jest/globals";
8
+ import axios from "axios";
9
+
10
+ jest.mock("fs");
11
+ jest.mock("../api");
12
+
13
+ const axiosMocked = jest.mocked(axios);
14
+
15
+ const defaultEnv = { ...process.env };
5
16
 
6
- describe("needsToken()", () => {
17
+ beforeEach(() => {
18
+ vol.reset();
19
+ process.env = { ...defaultEnv };
20
+ });
21
+
22
+ describe("needsToken", () => {
7
23
  it("is true if there is no config file", () => {
8
- expect(needsToken(tempy.file())).toBeTruthy();
24
+ expect(needsToken(randomUUID())).toBeTruthy();
25
+ });
26
+
27
+ it("is false if there is a token in the environment", () => {
28
+ process.env.DITTO_API_KEY = "xxx-xxx-xxx";
29
+ expect(needsToken(randomUUID())).toBe(false);
9
30
  });
10
31
 
11
32
  describe("with a config file", () => {
12
33
  let configFile = "";
13
34
 
14
- beforeEach(() => {
15
- configFile = tempy.writeSync("");
35
+ beforeEach(async () => {
36
+ configFile = `/${randomUUID()}`;
37
+ await new Promise((resolve, reject) =>
38
+ fs.writeFile(configFile, "", (err) => {
39
+ if (err) reject(err);
40
+ else {
41
+ resolve(null);
42
+ }
43
+ })
44
+ );
16
45
  });
17
46
 
18
47
  it("returns true if empty", () => {
19
- expect(needsToken(configFile, "testing.dittowrods.com")).toBeTruthy();
48
+ expect(needsToken(configFile, "testing.dittowords.com")).toBeTruthy();
20
49
  });
21
50
 
22
51
  describe("with some data", () => {
@@ -49,3 +78,22 @@ describe("needsToken()", () => {
49
78
  });
50
79
  });
51
80
  });
81
+
82
+ const { verifyTokenUsingTokenCheck } = _test;
83
+ describe("verifyTokenUsingTokenCheck", () => {
84
+ it("returns success: true for api success response", async () => {
85
+ axiosMocked.get.mockResolvedValueOnce({ status: 200 });
86
+ const result = await verifyTokenUsingTokenCheck("xxx-xxx");
87
+ expect(result.success).toBe(true);
88
+ });
89
+ it("returns success: false for api unauthorized response", async () => {
90
+ axiosMocked.get.mockResolvedValueOnce({ status: 401 });
91
+ const result = await verifyTokenUsingTokenCheck("xxx-xxx");
92
+ expect(result.success).toBe(false);
93
+ });
94
+ it("returns success: false for api invalid response", async () => {
95
+ axiosMocked.get.mockResolvedValueOnce("error");
96
+ const result = await verifyTokenUsingTokenCheck("xxx-xxx");
97
+ expect(result.success).toBe(false);
98
+ });
99
+ });
package/lib/init/token.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import fs from "fs";
2
+ import * as Sentry from "@sentry/node";
2
3
 
3
4
  import chalk from "chalk";
4
5
 
@@ -9,6 +10,7 @@ import consts from "../consts";
9
10
  import output from "../output";
10
11
  import config from "../config";
11
12
  import { quit } from "../utils/quit";
13
+ import { AxiosError, AxiosResponse } from "axios";
12
14
 
13
15
  export const needsToken = (configFile?: string, host = consts.API_HOST) => {
14
16
  if (config.getTokenFromEnv()) {
@@ -26,51 +28,82 @@ export const needsToken = (configFile?: string, host = consts.API_HOST) => {
26
28
  return false;
27
29
  };
28
30
 
29
- // Returns true if valid, otherwise an error message.
30
- async function checkToken(token: string): Promise<any> {
31
+ async function verifyTokenUsingTokenCheck(
32
+ token: string
33
+ ): Promise<{ success: true } | { success: false; output: string[] }> {
31
34
  const axios = createApiClient(token);
32
35
  const endpoint = "/token-check";
33
36
 
34
- let resOrError;
37
+ let resOrError: AxiosResponse<any> | undefined;
35
38
  try {
36
- resOrError = await axios.get(endpoint).catch((error: any) => {
37
- if (error.code === "ENOTFOUND") {
38
- return output.errorText(
39
- `Can't connect to API: ${output.url(error.hostname)}`
40
- );
41
- }
42
- if (error.response.status === 401 || error.response.status === 404) {
43
- return output.errorText(
44
- "This API key isn't valid. Please try another."
45
- );
46
- }
47
- return output.warnText("We're having trouble reaching the Ditto API.");
48
- });
39
+ resOrError = await axios.get(endpoint);
49
40
  } catch (e: unknown) {
50
- output.errorText(e as any);
51
- output.errorText("Sorry! We're having trouble reaching the Ditto API.");
41
+ if (!(e instanceof AxiosError)) {
42
+ return {
43
+ success: false,
44
+ output: [
45
+ output.warnText(
46
+ "Sorry! We're having trouble reaching the Ditto API."
47
+ ),
48
+ ],
49
+ };
50
+ }
51
+
52
+ if (e.code === "ENOTFOUND") {
53
+ return {
54
+ success: false,
55
+ output: [
56
+ output.errorText(
57
+ `Can't connect to API: ${output.url(consts.API_HOST)}`
58
+ ),
59
+ ],
60
+ };
61
+ }
62
+
63
+ if (e.response?.status === 401 || e.response?.status === 404) {
64
+ return {
65
+ success: false,
66
+ output: [
67
+ output.errorText("This API key isn't valid. Please try another."),
68
+ ],
69
+ };
70
+ }
52
71
  }
53
72
 
54
73
  if (typeof resOrError === "string") {
55
- return resOrError;
74
+ return {
75
+ success: false,
76
+ output: [resOrError],
77
+ };
56
78
  }
57
79
 
58
80
  if (resOrError?.status === 200) {
59
- return true;
81
+ return { success: true };
60
82
  }
61
83
 
62
- return output.errorText("This API key isn't valid. Please try another.");
84
+ return {
85
+ success: false,
86
+ output: [output.errorText("This API key isn't valid. Please try another.")],
87
+ };
88
+ }
89
+
90
+ // Returns true if valid, otherwise an error message.
91
+ async function checkToken(token: string): Promise<any> {
92
+ const result = await verifyTokenUsingTokenCheck(token);
93
+ if (!result.success) {
94
+ return result.output.join("\n");
95
+ }
96
+
97
+ return true;
63
98
  }
64
99
 
65
100
  async function collectToken(message: string | null) {
66
101
  const blue = output.info;
67
- const apiUrl = output.url("https://app.dittowords.com/account/user");
68
- const breadcrumbs = `${blue("User")}`;
102
+ const apiUrl = output.url("https://app.dittowords.com/account/devtools");
103
+ const breadcrumbs = `${chalk.bold(blue("API Keys"))}`;
69
104
  const tokenDescription =
70
105
  message ||
71
- `To get started, you'll need your Ditto API key. You can find this at: ${apiUrl} > ${breadcrumbs} under "${chalk.bold(
72
- "API Keys"
73
- )}".`;
106
+ `To get started, you'll need your Ditto API key. You can find this at: ${apiUrl} under "${breadcrumbs}".`;
74
107
  console.log(tokenDescription);
75
108
 
76
109
  const response = await prompt<{ token: string }>({
@@ -100,8 +133,24 @@ export const collectAndSaveToken = async (message: string | null = null) => {
100
133
  config.saveToken(consts.CONFIG_FILE, consts.API_HOST, token);
101
134
  return token;
102
135
  } catch (error) {
103
- quit("API token was not saved");
136
+ // https://github.com/enquirer/enquirer/issues/225#issue-516043136
137
+ // Empty string corresponds to the user hitting Ctrl + C
138
+ if (error === "") {
139
+ quit("", 0);
140
+ return;
141
+ }
142
+
143
+ const eventId = Sentry.captureException(error);
144
+ const eventStr = `\n\nError ID: ${output.info(eventId)}`;
145
+
146
+ return quit(
147
+ output.errorText(
148
+ "Something went wrong. Please contact support or try again later."
149
+ ) + eventStr
150
+ );
104
151
  }
105
152
  };
106
153
 
107
154
  export default { needsToken, collectAndSaveToken };
155
+
156
+ export const _test = { verifyTokenUsingTokenCheck };