@dittowords/cli 2.7.1 → 3.0.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 (53) hide show
  1. package/README.md +150 -137
  2. package/bin/add-project.js +5 -7
  3. package/bin/add-project.js.map +1 -1
  4. package/bin/api.js +0 -5
  5. package/bin/api.js.map +1 -1
  6. package/bin/config.js +38 -11
  7. package/bin/config.js.map +1 -1
  8. package/bin/ditto.js +66 -57
  9. package/bin/ditto.js.map +1 -1
  10. package/bin/http/fetchVariants.js +26 -0
  11. package/bin/http/fetchVariants.js.map +1 -0
  12. package/bin/init/init.js +17 -6
  13. package/bin/init/init.js.map +1 -1
  14. package/bin/init/project.js +38 -45
  15. package/bin/init/project.js.map +1 -1
  16. package/bin/init/token.js +22 -20
  17. package/bin/init/token.js.map +1 -1
  18. package/bin/pull.js +142 -193
  19. package/bin/pull.js.map +1 -1
  20. package/bin/remove-project.js +2 -7
  21. package/bin/remove-project.js.map +1 -1
  22. package/bin/utils/cleanFileName.js +11 -0
  23. package/bin/utils/cleanFileName.js.map +1 -0
  24. package/bin/utils/generateJsDriver.js +56 -0
  25. package/bin/utils/generateJsDriver.js.map +1 -0
  26. package/bin/utils/getSelectedProjects.js +3 -18
  27. package/bin/utils/getSelectedProjects.js.map +1 -1
  28. package/bin/utils/projectsToText.js +10 -1
  29. package/bin/utils/projectsToText.js.map +1 -1
  30. package/bin/utils/promptForProject.js +2 -3
  31. package/bin/utils/promptForProject.js.map +1 -1
  32. package/bin/utils/quit.js +10 -0
  33. package/bin/utils/quit.js.map +1 -0
  34. package/lib/add-project.ts +6 -9
  35. package/lib/api.ts +0 -5
  36. package/lib/config.ts +57 -19
  37. package/lib/ditto.ts +74 -58
  38. package/lib/http/fetchVariants.ts +30 -0
  39. package/lib/init/init.ts +38 -6
  40. package/lib/init/project.test.ts +3 -3
  41. package/lib/init/project.ts +47 -58
  42. package/lib/init/token.ts +17 -16
  43. package/lib/pull.ts +205 -261
  44. package/lib/remove-project.ts +2 -8
  45. package/lib/types.ts +24 -3
  46. package/lib/utils/cleanFileName.ts +6 -0
  47. package/lib/utils/generateJsDriver.ts +68 -0
  48. package/lib/utils/getSelectedProjects.ts +5 -24
  49. package/lib/utils/projectsToText.ts +11 -1
  50. package/lib/utils/promptForProject.ts +2 -3
  51. package/lib/utils/quit.ts +5 -0
  52. package/package.json +1 -1
  53. package/tsconfig.json +2 -1
@@ -1 +1 @@
1
- {"version":3,"file":"promptForProject.js","sourceRoot":"","sources":["../../lib/utils/promptForProject.ts"],"names":[],"mappings":";;;;;AAAA,MAAM,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAE7C,uDAA+B;AAG/B,SAAS,mBAAmB,CAAC,OAAgB;IAC3C,OAAO,CACL,OAAO,CAAC,IAAI;QACZ,GAAG;QACH,gBAAM,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,IAAI,kCAAkC,OAAO,CAAC,EAAE,EAAE,CAAC,CAC7E,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,QAAgB;IACrC,IAAI,CAAC,QAAQ,EAAE;QACb,OAAO,IAAI,CAAC;KACb;IAED,MAAM,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,2BAA2B,CAAC,CAAC;IAEjE,IAAI,EAAE,KAAK,KAAK,EAAE;QAChB,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,yBAAyB,EAAE,CAAC;KAChD;IAED,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;AACtB,CAAC;AAQD,MAAM,gBAAgB,GAAG,KAAK,EAAE,EAC9B,OAAO,EACP,QAAQ,EACR,KAAK,GAAG,EAAE,GACU,EAAE,EAAE;IACxB,gBAAM,CAAC,EAAE,EAAE,CAAC;IAEZ,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAClD,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC;QAC9B,IAAI,EAAE,SAAS;QACf,OAAO;QACP,KAAK;QACL,OAAO;KACR,CAAC,CAAC;IAEH,IAAI,QAAQ,CAAC;IAEb,IAAI;QACF,QAAQ,GAAG,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC;KAC/B;IAAC,OAAO,CAAC,EAAE;QACV,oDAAoD;QACpD,4CAA4C;QAC5C,QAAQ,GAAG,IAAI,CAAC;KACjB;IAED,OAAO,aAAa,CAAC,QAAQ,CAAC,CAAC;AACjC,CAAC,CAAC;AAEF,kBAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"promptForProject.js","sourceRoot":"","sources":["../../lib/utils/promptForProject.ts"],"names":[],"mappings":";;;;;AAAA,MAAM,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAE7C,uDAA+B;AAE/B,qDAAgD;AAEhD,SAAS,mBAAmB,CAAC,OAAgB;IAC3C,OAAO,CACL,OAAO,CAAC,IAAI,GAAG,GAAG,GAAG,gBAAM,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,IAAI,IAAA,6BAAY,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAC5E,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,QAAgB;IACrC,IAAI,CAAC,QAAQ,EAAE;QACb,OAAO,IAAI,CAAC;KACb;IAED,MAAM,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,2BAA2B,CAAC,CAAC;IAEjE,IAAI,EAAE,KAAK,KAAK,EAAE;QAChB,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,yBAAyB,EAAE,CAAC;KAChD;IAED,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;AACtB,CAAC;AAQD,MAAM,gBAAgB,GAAG,KAAK,EAAE,EAC9B,OAAO,EACP,QAAQ,EACR,KAAK,GAAG,EAAE,GACU,EAAE,EAAE;IACxB,gBAAM,CAAC,EAAE,EAAE,CAAC;IAEZ,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAClD,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC;QAC9B,IAAI,EAAE,SAAS;QACf,OAAO;QACP,KAAK;QACL,OAAO;KACR,CAAC,CAAC;IAEH,IAAI,QAAQ,CAAC;IAEb,IAAI;QACF,QAAQ,GAAG,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC;KAC/B;IAAC,OAAO,CAAC,EAAE;QACV,oDAAoD;QACpD,4CAA4C;QAC5C,QAAQ,GAAG,IAAI,CAAC;KACjB;IAED,OAAO,aAAa,CAAC,QAAQ,CAAC,CAAC;AACjC,CAAC,CAAC;AAEF,kBAAe,gBAAgB,CAAC"}
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.quit = void 0;
4
+ function quit(message, exitCode = 2) {
5
+ console.log(`\n${message}\n`);
6
+ process.exitCode = exitCode;
7
+ process.exit();
8
+ }
9
+ exports.quit = quit;
10
+ //# sourceMappingURL=quit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quit.js","sourceRoot":"","sources":["../../lib/utils/quit.ts"],"names":[],"mappings":";;;AAAA,SAAgB,IAAI,CAAC,OAAe,EAAE,QAAQ,GAAG,CAAC;IAChD,OAAO,CAAC,GAAG,CAAC,KAAK,OAAO,IAAI,CAAC,CAAC;IAC9B,OAAO,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC5B,OAAO,CAAC,IAAI,EAAE,CAAC;AACjB,CAAC;AAJD,oBAIC"}
@@ -1,16 +1,11 @@
1
- import { collectAndSaveProject } from "./init/project";
1
+ import { collectAndSaveSource } from "./init/project";
2
2
  import projectsToText from "./utils/projectsToText";
3
3
  import {
4
4
  getSelectedProjects,
5
5
  getIsUsingComponents,
6
6
  } from "./utils/getSelectedProjects";
7
7
  import output from "./output";
8
-
9
- function quit(exitCode = 2) {
10
- console.log("Project selection was not updated.");
11
- process.exitCode = exitCode;
12
- process.exit();
13
- }
8
+ import { quit } from "./utils/quit";
14
9
 
15
10
  const addProject = async () => {
16
11
  const projects = getSelectedProjects();
@@ -38,13 +33,15 @@ const addProject = async () => {
38
33
  )}`
39
34
  );
40
35
  }
41
- await collectAndSaveProject(false);
36
+ await collectAndSaveSource({
37
+ components: false,
38
+ });
42
39
  } catch (error) {
43
40
  console.log(
44
41
  `\nSorry, there was an error adding a project to your workspace: `,
45
42
  error
46
43
  );
47
- quit();
44
+ quit("Project selection was not updated.");
48
45
  }
49
46
  };
50
47
 
package/lib/api.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import axios from "axios";
2
- import https from "https";
3
2
 
4
3
  import config from "./config";
5
4
  import consts from "./consts";
@@ -10,10 +9,6 @@ export const create = (token?: string) => {
10
9
  headers: {
11
10
  Authorization: `token ${token}`,
12
11
  },
13
- httpsAgent: new https.Agent({
14
- requestCert: true,
15
- rejectUnauthorized: false,
16
- }),
17
12
  });
18
13
  };
19
14
 
package/lib/config.ts CHANGED
@@ -3,16 +3,30 @@ import path from "path";
3
3
  import url from "url";
4
4
  import yaml from "js-yaml";
5
5
 
6
+ import output from "./output";
6
7
  import consts from "./consts";
7
8
  import { Project, ConfigYAML } from "./types";
8
9
 
9
- function createFileIfMissing(filename: string) {
10
+ export const DEFAULT_CONFIG_JSON: ConfigYAML = {
11
+ sources: {
12
+ components: { enabled: true },
13
+ },
14
+ variants: true,
15
+ format: "flat",
16
+ };
17
+
18
+ export const DEFAULT_CONFIG = yaml.dump(DEFAULT_CONFIG_JSON);
19
+
20
+ function createFileIfMissing(filename: string, defaultContents?: any) {
10
21
  const dir = path.dirname(filename);
11
22
 
23
+ // create the directory if it doesn't already exist
12
24
  if (!fs.existsSync(dir)) fs.mkdirSync(dir);
13
25
 
26
+ // create the file if it doesn't already exist
14
27
  if (!fs.existsSync(filename)) {
15
- fs.closeSync(fs.openSync(filename, "w"));
28
+ // create the file, writing the `defaultContents` if provided
29
+ fs.writeFileSync(filename, defaultContents || "", "utf-8");
16
30
  }
17
31
  }
18
32
 
@@ -26,11 +40,13 @@ function jsonIsGlobalYAML(
26
40
  return (
27
41
  !!json &&
28
42
  typeof json === "object" &&
29
- Object.values(json).every((arr) =>
30
- arr.every(
31
- (val: any) =>
32
- typeof val === "object" && Object.keys(val).includes("token")
33
- )
43
+ Object.values(json).every(
44
+ (arr) =>
45
+ (arr as any).every &&
46
+ arr.every(
47
+ (val: any) =>
48
+ typeof val === "object" && Object.keys(val).includes("token")
49
+ )
34
50
  )
35
51
  );
36
52
  }
@@ -45,7 +61,7 @@ function readProjectConfigData(
45
61
  file = consts.PROJECT_CONFIG_FILE,
46
62
  defaultData = {}
47
63
  ): ConfigYAML {
48
- createFileIfMissing(file);
64
+ createFileIfMissing(file, DEFAULT_CONFIG);
49
65
  const fileContents = fs.readFileSync(file, "utf8");
50
66
  const yamlData = yaml.load(fileContents);
51
67
  if (jsonIsConfigYAML(yamlData)) {
@@ -73,10 +89,20 @@ function readGlobalConfigData(
73
89
  return defaultData;
74
90
  }
75
91
 
76
- function writeProjectConfigData(file: string, data: object) {
77
- createFileIfMissing(file);
92
+ function writeProjectConfigData(file: string, data: Partial<ConfigYAML>) {
93
+ createFileIfMissing(file, DEFAULT_CONFIG);
78
94
  const existingData = readProjectConfigData(file);
79
- const yamlStr = yaml.dump({ ...existingData, ...data });
95
+
96
+ const configData: ConfigYAML = {
97
+ ...existingData,
98
+ ...data,
99
+ sources: {
100
+ ...existingData.sources,
101
+ ...data.sources,
102
+ },
103
+ };
104
+
105
+ const yamlStr = yaml.dump(configData);
80
106
  fs.writeFileSync(file, yamlStr, "utf8");
81
107
  }
82
108
 
@@ -160,14 +186,22 @@ function dedupeProjectName(projectNames: Set<string>, projectName: string) {
160
186
  * - an array of valid, deduped projects
161
187
  * - the `variants` and `format` config options
162
188
  */
163
- function parseSourceInformation() {
164
- const { projects, components, variants, format, status } =
165
- readProjectConfigData();
189
+ function parseSourceInformation(file?: string) {
190
+ const {
191
+ sources,
192
+ variants,
193
+ format,
194
+ status,
195
+ richText,
196
+ projects: projectsRoot,
197
+ components: componentsRoot,
198
+ } = readProjectConfigData(file);
199
+
200
+ const projects = sources?.projects || [];
166
201
 
167
202
  const projectNames = new Set<string>();
168
203
  const validProjects: Project[] = [];
169
-
170
- let componentLibraryInProjects = false;
204
+ let hasComponentLibraryInProjects = false;
171
205
 
172
206
  (projects || []).forEach((project) => {
173
207
  const isValid = project.id && project.name;
@@ -176,7 +210,7 @@ function parseSourceInformation() {
176
210
  }
177
211
 
178
212
  if (project.id === "ditto_component_library") {
179
- componentLibraryInProjects = true;
213
+ hasComponentLibraryInProjects = true;
180
214
  return;
181
215
  }
182
216
 
@@ -186,8 +220,7 @@ function parseSourceInformation() {
186
220
  validProjects.push(project);
187
221
  });
188
222
 
189
- const shouldFetchComponentLibrary =
190
- !!components || componentLibraryInProjects;
223
+ const shouldFetchComponentLibrary = Boolean(sources?.components?.enabled);
191
224
 
192
225
  const hasSourceData = !!validProjects.length || shouldFetchComponentLibrary;
193
226
 
@@ -198,6 +231,11 @@ function parseSourceInformation() {
198
231
  variants: variants || false,
199
232
  format,
200
233
  status,
234
+ richText,
235
+ hasTopLevelProjectsField: !!projectsRoot,
236
+ hasTopLevelComponentsField: !!componentsRoot,
237
+ hasComponentLibraryInProjects,
238
+ componentFolders: sources?.components?.folders || null,
201
239
  };
202
240
  }
203
241
 
package/lib/ditto.ts CHANGED
@@ -4,93 +4,109 @@ import { program } from "commander";
4
4
  // to use V8's code cache to speed up instantiation time
5
5
  import "v8-compile-cache";
6
6
 
7
- import { init, needsInit } from "./init/init";
7
+ import { init, needsTokenOrSource } from "./init/init";
8
8
  import { pull } from "./pull";
9
-
9
+ import { quit } from "./utils/quit";
10
10
  import addProject from "./add-project";
11
11
  import removeProject from "./remove-project";
12
+
12
13
  import processMetaOption from "./utils/processMetaOption";
13
14
 
14
- /**
15
- * Catch and report unexpected error.
16
- * @param {any} error The thrown error object.
17
- * @returns {void}
18
- */
19
- function quit(exitCode = 2) {
20
- console.log("\nExiting Ditto CLI...\n");
21
- process.exitCode = exitCode;
22
- process.exit();
23
- }
15
+ type Command = "pull" | "project" | "project add" | "project remove";
16
+
17
+ const COMMANDS = [
18
+ {
19
+ name: "pull",
20
+ description: "Sync copy from Ditto into the current working directory",
21
+ },
22
+ {
23
+ name: "project",
24
+ description: "Add a Ditto project to sync copy from",
25
+ commands: [
26
+ {
27
+ name: "add",
28
+ description: "Add a Ditto project to sync copy from",
29
+ },
30
+ {
31
+ name: "remove",
32
+ description: "Stop syncing copy from a Ditto project",
33
+ },
34
+ ],
35
+ },
36
+ ] as const;
24
37
 
25
38
  const setupCommands = () => {
26
39
  program.name("ditto-cli");
27
- program
28
- .command("pull")
29
- .description("Sync copy from Ditto into working directory")
30
- .action(() => checkInit("pull"));
31
-
32
- const projectDescription = "Add a Ditto project to sync copy from";
33
- const projectCommand = program
34
- .command("project")
35
- .description(projectDescription)
36
- .action(() => checkInit("project"));
37
40
 
38
- projectCommand
39
- .command("add")
40
- .description(projectDescription)
41
- .action(() => checkInit("project"));
41
+ COMMANDS.forEach((config) => {
42
+ const cmd = program
43
+ .command(config.name)
44
+ .description(config.description)
45
+ .action(() => executeCommand(config.name));
42
46
 
43
- projectCommand
44
- .command("remove")
45
- .description("Stop syncing copy from a Ditto project")
46
- .action(() => checkInit("project remove"));
47
+ if ("commands" in config) {
48
+ config.commands.forEach((nestedCommand) => {
49
+ cmd
50
+ .command(nestedCommand.name)
51
+ .description(nestedCommand.description)
52
+ .action(() => executeCommand(`${config.name} ${nestedCommand.name}`));
53
+ });
54
+ }
55
+ });
47
56
  };
48
57
 
49
58
  const setupOptions = () => {
50
59
  program.option(
51
60
  "-m, --meta <data...>",
52
- "Optional metadata for this command to send arbitrary data to the backend. Ex: -m githubActionRequest:true trigger:manual"
61
+ "Include arbitrary data in requests to the Ditto API. Ex: -m githubActionRequest:true trigger:manual"
53
62
  );
54
63
  };
55
64
 
56
- const checkInit = async (command: string) => {
57
- if (needsInit() && command !== "project remove") {
65
+ const executeCommand = async (command: Command | "none"): Promise<void> => {
66
+ const needsInitialization = needsTokenOrSource();
67
+ if (needsInitialization) {
58
68
  try {
59
69
  await init();
60
- if (command === "pull") main(); // re-run to actually pull text now that init is finished
61
70
  } catch (error) {
62
- quit();
71
+ quit("Exiting Ditto CLI...");
72
+ return;
63
73
  }
64
- } else {
65
- const { meta } = program.opts();
66
- switch (command) {
67
- case "pull":
68
- pull({ meta: processMetaOption(meta) });
69
- break;
70
- case "project":
71
- case "project add":
72
- addProject();
73
- break;
74
- case "project remove":
75
- removeProject();
76
- break;
77
- case "none":
78
- setupCommands();
79
- program.help();
80
- break;
81
- default:
82
- quit();
74
+ }
75
+
76
+ const { meta } = program.opts();
77
+ switch (command) {
78
+ case "none":
79
+ case "pull": {
80
+ return pull({ meta: processMetaOption(meta) });
81
+ }
82
+ case "project":
83
+ case "project add": {
84
+ // initialization already includes the selection of a source,
85
+ // so if `project add` is called during initialization, don't
86
+ // prompt the user to select a source again
87
+ if (needsInitialization) return;
88
+
89
+ return addProject();
90
+ }
91
+ case "project remove": {
92
+ return removeProject();
93
+ }
94
+ default: {
95
+ quit("Exiting Ditto CLI...");
96
+ return;
83
97
  }
84
98
  }
85
99
  };
86
100
 
87
101
  const main = async () => {
102
+ setupCommands();
103
+ setupOptions();
104
+
88
105
  if (process.argv.length <= 2 && process.argv[1].includes("ditto-cli")) {
89
- await checkInit("none");
90
- } else {
91
- setupCommands();
92
- setupOptions();
106
+ await executeCommand("none");
107
+ return;
93
108
  }
109
+
94
110
  program.parse(process.argv);
95
111
  };
96
112
 
@@ -0,0 +1,30 @@
1
+ import { AxiosRequestConfig } from "axios";
2
+ import api from "../api";
3
+ import { PullOptions } from "../pull";
4
+ import { SourceInformation } from "../types";
5
+
6
+ export async function fetchVariants(
7
+ source: SourceInformation,
8
+ options: PullOptions = {}
9
+ ) {
10
+ if (!source.variants) {
11
+ return null;
12
+ }
13
+
14
+ const { shouldFetchComponentLibrary, validProjects } = source;
15
+
16
+ const config: AxiosRequestConfig = {
17
+ params: { ...options?.meta },
18
+ };
19
+
20
+ // if we're not syncing from the component library, then we pass the project ids
21
+ // to limit the list of returned variants to only those that are relevant for the
22
+ // specified projects
23
+ if (validProjects.length && !shouldFetchComponentLibrary) {
24
+ config.params.projectIds = validProjects.map(({ id }) => id);
25
+ }
26
+
27
+ const { data } = await api.get<{ apiID: string }[]>("/variants", config);
28
+
29
+ return data;
30
+ }
package/lib/init/init.ts CHANGED
@@ -4,13 +4,15 @@ import boxen from "boxen";
4
4
  import chalk from "chalk";
5
5
  import projectsToText from "../utils/projectsToText";
6
6
 
7
- import { needsSource, collectAndSaveProject } from "./project";
7
+ import { needsSource, collectAndSaveSource } from "./project";
8
8
  import { needsToken, collectAndSaveToken } from "./token";
9
9
 
10
10
  import config from "../config";
11
+ import output from "../output";
11
12
  import sourcesToText from "../utils/sourcesToText";
13
+ import { quit } from "../utils/quit";
12
14
 
13
- export const needsInit = () => needsToken() || needsSource();
15
+ export const needsTokenOrSource = () => needsToken() || needsSource();
14
16
 
15
17
  function welcome() {
16
18
  const msg = chalk.white(`${chalk.bold(
@@ -29,11 +31,41 @@ export const init = async () => {
29
31
  await collectAndSaveToken();
30
32
  }
31
33
 
32
- const { hasSourceData, validProjects, shouldFetchComponentLibrary } =
33
- config.parseSourceInformation();
34
+ const {
35
+ hasSourceData,
36
+ validProjects,
37
+ shouldFetchComponentLibrary,
38
+ hasTopLevelComponentsField,
39
+ hasTopLevelProjectsField,
40
+ } = config.parseSourceInformation();
41
+
42
+ if (hasTopLevelProjectsField) {
43
+ return quit(`${output.errorText(
44
+ `Support for ${output.warnText(
45
+ "projects"
46
+ )} as a top-level field has been removed; please configure ${output.warnText(
47
+ "sources.projects"
48
+ )} instead.`
49
+ )}
50
+ See ${output.url("https://github.com/dittowords/cli")} for more information.`);
51
+ }
52
+
53
+ if (hasTopLevelComponentsField) {
54
+ return quit(
55
+ `${output.errorText(
56
+ "Support for `components` as a top-level field has been removed; please configure `sources.components` instead."
57
+ )}
58
+ See ${output.url("https://github.com/dittowords/cli")} for more information.`
59
+ );
60
+ }
34
61
 
35
62
  if (!hasSourceData) {
36
- await collectAndSaveProject(true);
63
+ console.log(
64
+ `Looks like there are no Ditto sources selected for your current directory: ${output.info(
65
+ process.cwd()
66
+ )}.`
67
+ );
68
+ await collectAndSaveSource({ initialize: true, components: true });
37
69
  return;
38
70
  }
39
71
 
@@ -44,4 +76,4 @@ export const init = async () => {
44
76
  console.log(message);
45
77
  };
46
78
 
47
- export default { needsInit, init };
79
+ export default { init };
@@ -42,8 +42,8 @@ describe("saveProject", () => {
42
42
  it("creates a config file with config data", () => {
43
43
  const fileContents = fs.readFileSync(configFile, "utf8");
44
44
  const data = yaml.load(fileContents);
45
- expect(data.projects).toBeDefined();
46
- expect(data.projects[0].name).toEqual(projectName);
47
- expect(data.projects[0].id).toEqual(projectId);
45
+ expect(data.sources.projects).toBeDefined();
46
+ expect(data.sources.projects[0].name).toEqual(projectName);
47
+ expect(data.sources.projects[0].id).toEqual(projectId);
48
48
  });
49
49
  });
@@ -12,25 +12,18 @@ import {
12
12
  import promptForProject from "../utils/promptForProject";
13
13
  import { AxiosResponse } from "axios";
14
14
  import { Project, Token } from "../types";
15
-
16
- function quit(exitCode = 2) {
17
- console.log("\nExiting Ditto CLI...\n");
18
- process.exitCode = exitCode;
19
- process.exit();
20
- }
15
+ import { quit } from "../utils/quit";
21
16
 
22
17
  function saveProject(file: string, name: string, id: string) {
23
- // old functionality included "ditto_component_library" in the `projects`
24
- // array, but we want to always treat the component library as a separate
25
- // entity and use the new notation of a top-level `components` key
26
18
  if (id === "components") {
27
- config.writeProjectConfigData(file, { components: true });
19
+ config.writeProjectConfigData(file, {
20
+ sources: { components: { enabled: true } },
21
+ });
28
22
  return;
29
23
  }
30
24
 
31
- const projects = [...getSelectedProjects(), { name, id }];
32
-
33
- config.writeProjectConfigData(file, { projects });
25
+ const projects = [...getSelectedProjects(file), { name, id }];
26
+ config.writeProjectConfigData(file, { sources: { projects } });
34
27
  }
35
28
 
36
29
  export const needsSource = () => {
@@ -44,12 +37,8 @@ async function askForAnotherToken() {
44
37
  await collectAndSaveToken(message);
45
38
  }
46
39
 
47
- async function listProjects(
48
- token: Token,
49
- projectsAlreadySelected: Project[],
50
- componentsSelected: boolean
51
- ) {
52
- const spinner = ora("Fetching projects in your workspace...");
40
+ async function listProjects(token: Token, projectsAlreadySelected: Project[]) {
41
+ const spinner = ora("Fetching sources in your workspace...");
53
42
  spinner.start();
54
43
 
55
44
  let response: AxiosResponse<{ id: string; name: string }[]>;
@@ -64,35 +53,37 @@ async function listProjects(
64
53
  throw e;
65
54
  }
66
55
 
56
+ const projectsAlreadySelectedSet = projectsAlreadySelected.reduce(
57
+ (set, project) => set.add(project.id.toString()),
58
+ new Set<string>()
59
+ );
60
+
61
+ const result = response.data.filter(
62
+ ({ id }) =>
63
+ // covers an edge case where v0 of the API includes the component library
64
+ // in the response from the `/project-names` endpoint
65
+ id !== "ditto_component_library" &&
66
+ !projectsAlreadySelectedSet.has(id.toString())
67
+ );
68
+
67
69
  spinner.stop();
68
- return response.data.filter(({ id }: Project) => {
69
- if (id === "ditto_component_library") {
70
- return !componentsSelected;
71
- } else {
72
- return !projectsAlreadySelected.some((project) => project.id === id);
73
- }
74
- });
75
- }
76
70
 
77
- async function collectProject(token: Token, initialize: boolean) {
78
- const path = process.cwd();
79
- if (initialize) {
80
- console.log(
81
- `Looks like there are no Ditto projects selected for your current directory: ${output.info(
82
- path
83
- )}.`
84
- );
85
- }
71
+ return result;
72
+ }
86
73
 
74
+ async function collectSource(token: Token, includeComponents: boolean) {
87
75
  const projectsAlreadySelected = getSelectedProjects();
88
- const usingComponents = getIsUsingComponents();
89
- const projects = await listProjects(
90
- token,
91
- projectsAlreadySelected,
92
- usingComponents
93
- );
76
+ const componentSourceSelected = getIsUsingComponents();
77
+
78
+ let sources = await listProjects(token, projectsAlreadySelected);
79
+ if (includeComponents && !componentSourceSelected) {
80
+ sources = [
81
+ { id: "ditto_component_library", name: "Ditto Component Library" },
82
+ ...sources,
83
+ ];
84
+ }
94
85
 
95
- if (!(projects && projects.length)) {
86
+ if (!sources?.length) {
96
87
  console.log("You're currently syncing all projects in your workspace.");
97
88
  console.log(
98
89
  output.warnText(
@@ -102,24 +93,22 @@ async function collectProject(token: Token, initialize: boolean) {
102
93
  return null;
103
94
  }
104
95
 
105
- const nonInitPrompt = usingComponents
106
- ? "Add a project"
107
- : "Add a project or library";
108
-
109
96
  return promptForProject({
110
- projects,
111
- message: initialize
112
- ? "Choose the project or library you'd like to sync text from"
113
- : nonInitPrompt,
97
+ projects: sources,
98
+ message: "Choose the source you'd like to sync text from",
114
99
  });
115
100
  }
116
101
 
117
- export const collectAndSaveProject = async (initialize = false) => {
102
+ export const collectAndSaveSource = async (
103
+ { components = false }: { initialize?: boolean; components?: boolean } = {
104
+ components: false,
105
+ }
106
+ ) => {
118
107
  try {
119
108
  const token = config.getToken(consts.CONFIG_FILE, consts.API_HOST);
120
- const project = await collectProject(token, initialize);
109
+ const project = await collectSource(token, components);
121
110
  if (!project) {
122
- quit(0);
111
+ quit("", 0);
123
112
  return;
124
113
  }
125
114
 
@@ -127,7 +116,7 @@ export const collectAndSaveProject = async (initialize = false) => {
127
116
  "\n" +
128
117
  `Thanks for adding ${output.info(
129
118
  project.name
130
- )} to your selected projects.\n` +
119
+ )} to your selected sources.\n` +
131
120
  `We saved your updated configuration to: ${output.info(
132
121
  consts.PROJECT_CONFIG_FILE
133
122
  )}\n`
@@ -138,13 +127,13 @@ export const collectAndSaveProject = async (initialize = false) => {
138
127
  console.log(e);
139
128
  if (e.response && e.response.status === 404) {
140
129
  await askForAnotherToken();
141
- await collectAndSaveProject();
130
+ await collectAndSaveSource({ components });
142
131
  } else {
143
- quit();
132
+ quit("", 2);
144
133
  }
145
134
  }
146
135
  };
147
136
 
148
137
  export const _testing = { saveProject, needsSource };
149
138
 
150
- export default { needsSource, collectAndSaveProject };
139
+ export default { needsSource, collectAndSaveSource };