@dittowords/cli 2.5.0 → 2.5.2

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 (73) hide show
  1. package/README.md +14 -7
  2. package/babel.config.js +2 -8
  3. package/bin/add-project.js +38 -0
  4. package/bin/add-project.js.map +1 -0
  5. package/bin/api.js +20 -0
  6. package/bin/api.js.map +1 -0
  7. package/bin/config.js +174 -0
  8. package/bin/config.js.map +1 -0
  9. package/bin/consts.js +21 -0
  10. package/bin/consts.js.map +1 -0
  11. package/bin/ditto.js +71 -74
  12. package/bin/ditto.js.map +1 -0
  13. package/bin/init/init.js +39 -0
  14. package/bin/init/init.js.map +1 -0
  15. package/bin/init/project.js +115 -0
  16. package/bin/init/project.js.map +1 -0
  17. package/bin/init/token.js +89 -0
  18. package/bin/init/token.js.map +1 -0
  19. package/bin/output.js +34 -0
  20. package/bin/output.js.map +1 -0
  21. package/bin/pull.js +288 -0
  22. package/bin/pull.js.map +1 -0
  23. package/bin/remove-project.js +40 -0
  24. package/bin/remove-project.js.map +1 -0
  25. package/bin/types.js +3 -0
  26. package/bin/types.js.map +1 -0
  27. package/bin/utils/getSelectedProjects.js +76 -0
  28. package/bin/utils/getSelectedProjects.js.map +1 -0
  29. package/bin/utils/processMetaOption.js +15 -0
  30. package/bin/utils/processMetaOption.js.map +1 -0
  31. package/bin/utils/projectsToText.js +16 -0
  32. package/bin/utils/projectsToText.js.map +1 -0
  33. package/bin/utils/promptForProject.js +44 -0
  34. package/bin/utils/promptForProject.js.map +1 -0
  35. package/bin/utils/sourcesToText.js +25 -0
  36. package/bin/utils/sourcesToText.js.map +1 -0
  37. package/jest.config.js +1 -1
  38. package/lib/add-project.ts +51 -0
  39. package/lib/api.ts +15 -0
  40. package/lib/{config.test.js → config.test.ts} +14 -25
  41. package/lib/config.ts +214 -0
  42. package/lib/consts.ts +20 -0
  43. package/lib/ditto.ts +97 -0
  44. package/lib/init/{init.js → init.ts} +11 -12
  45. package/lib/init/project.test.ts +49 -0
  46. package/lib/init/{project.js → project.ts} +58 -32
  47. package/lib/init/{token.test.js → token.test.ts} +3 -3
  48. package/lib/init/{token.js → token.ts} +23 -19
  49. package/lib/output.ts +21 -0
  50. package/lib/pull.test.ts +142 -0
  51. package/lib/{pull.js → pull.ts} +119 -92
  52. package/lib/remove-project.ts +50 -0
  53. package/lib/types.ts +23 -0
  54. package/lib/utils/getSelectedProjects.ts +55 -0
  55. package/lib/utils/processMetaOption.test.ts +18 -0
  56. package/lib/utils/processMetaOption.ts +16 -0
  57. package/lib/utils/{projectsToText.js → projectsToText.ts} +5 -4
  58. package/lib/utils/{promptForProject.js → promptForProject.ts} +18 -9
  59. package/lib/utils/{sourcesToText.js → sourcesToText.ts} +6 -5
  60. package/package.json +20 -15
  61. package/testing/fixtures/project-config-pull.yml +0 -0
  62. package/tsconfig.json +12 -0
  63. package/lib/add-project.js +0 -30
  64. package/lib/api.js +0 -18
  65. package/lib/config.js +0 -169
  66. package/lib/consts.js +0 -14
  67. package/lib/init/project.test.js +0 -99
  68. package/lib/output.js +0 -21
  69. package/lib/pull.test.js +0 -173
  70. package/lib/remove-project.js +0 -39
  71. package/lib/utils/getSelectedProjects.js +0 -35
  72. package/lib/utils/processMetaOption.js +0 -16
  73. package/lib/utils/processMetaOption.test.js +0 -16
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.getIsUsingComponents = exports.getSelectedProjects = void 0;
30
+ const fs_1 = __importDefault(require("fs"));
31
+ const js_yaml_1 = __importStar(require("js-yaml"));
32
+ const consts_1 = require("../consts");
33
+ function jsonIsConfigYAML(json) {
34
+ return typeof json === "object";
35
+ }
36
+ function yamlToJson(_yaml) {
37
+ try {
38
+ let configYaml = js_yaml_1.default.load(_yaml);
39
+ if (!jsonIsConfigYAML(configYaml)) {
40
+ return {};
41
+ }
42
+ return configYaml;
43
+ }
44
+ catch (e) {
45
+ if (e instanceof js_yaml_1.YAMLException) {
46
+ return null;
47
+ }
48
+ else {
49
+ throw e;
50
+ }
51
+ }
52
+ }
53
+ /**
54
+ * Returns an array containing all valid projects ({ id, name })
55
+ * currently contained in the project config file.
56
+ */
57
+ const getSelectedProjects = (configFile = consts_1.PROJECT_CONFIG_FILE) => {
58
+ if (!fs_1.default.existsSync(configFile))
59
+ return [];
60
+ const contentYaml = fs_1.default.readFileSync(configFile, "utf8");
61
+ const contentJson = yamlToJson(contentYaml);
62
+ if (!(contentJson && contentJson.projects)) {
63
+ return [];
64
+ }
65
+ return contentJson.projects.filter(({ name, id }) => name && id);
66
+ };
67
+ exports.getSelectedProjects = getSelectedProjects;
68
+ const getIsUsingComponents = (configFile = consts_1.PROJECT_CONFIG_FILE) => {
69
+ if (!fs_1.default.existsSync(configFile))
70
+ return false;
71
+ const contentYaml = fs_1.default.readFileSync(configFile, "utf8");
72
+ const contentJson = yamlToJson(contentYaml);
73
+ return !!contentJson && !!contentJson.components;
74
+ };
75
+ exports.getIsUsingComponents = getIsUsingComponents;
76
+ //# sourceMappingURL=getSelectedProjects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getSelectedProjects.js","sourceRoot":"","sources":["../../lib/utils/getSelectedProjects.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,4CAAoB;AACpB,mDAA8C;AAE9C,sCAAgD;AAGhD,SAAS,gBAAgB,CAAC,IAAa;IACrC,OAAO,OAAO,IAAI,KAAK,QAAQ,CAAC;AAClC,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAC/B,IAAI;QACF,IAAI,UAAU,GAAG,iBAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAClC,IAAI,CAAC,gBAAgB,CAAC,UAAU,CAAC,EAAE;YACjC,OAAO,EAAE,CAAC;SACX;QACD,OAAO,UAAU,CAAC;KACnB;IAAC,OAAO,CAAC,EAAE;QACV,IAAI,CAAC,YAAY,uBAAa,EAAE;YAC9B,OAAO,IAAI,CAAC;SACb;aAAM;YACL,MAAM,CAAC,CAAC;SACT;KACF;AACH,CAAC;AAED;;;GAGG;AACI,MAAM,mBAAmB,GAAG,CACjC,UAAU,GAAG,4BAAmB,EACrB,EAAE;IACb,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,EAAE,CAAC;IAE1C,MAAM,WAAW,GAAG,YAAE,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IACxD,MAAM,WAAW,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC;IAE5C,IAAI,CAAC,CAAC,WAAW,IAAI,WAAW,CAAC,QAAQ,CAAC,EAAE;QAC1C,OAAO,EAAE,CAAC;KACX;IAED,OAAO,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;AACnE,CAAC,CAAC;AAbW,QAAA,mBAAmB,uBAa9B;AAEK,MAAM,oBAAoB,GAAG,CAClC,UAAU,GAAG,4BAAmB,EACvB,EAAE;IACX,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,KAAK,CAAC;IAE7C,MAAM,WAAW,GAAG,YAAE,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IACxD,MAAM,WAAW,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC;IAE5C,OAAO,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,WAAW,CAAC,UAAU,CAAC;AACnD,CAAC,CAAC;AATW,QAAA,oBAAoB,wBAS/B"}
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const processMetaOption = (inputArr) => {
4
+ const res = {};
5
+ if (!Array.isArray(inputArr)) {
6
+ return res;
7
+ }
8
+ inputArr.forEach((element) => {
9
+ const [key, value] = element.split(":");
10
+ res[key] = value;
11
+ });
12
+ return res;
13
+ };
14
+ exports.default = processMetaOption;
15
+ //# sourceMappingURL=processMetaOption.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"processMetaOption.js","sourceRoot":"","sources":["../../lib/utils/processMetaOption.ts"],"names":[],"mappings":";;AAAA,MAAM,iBAAiB,GAAG,CAAC,QAAyB,EAAE,EAAE;IACtD,MAAM,GAAG,GAA2B,EAAE,CAAC;IAEvC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE;QAC5B,OAAO,GAAG,CAAC;KACZ;IAED,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC3B,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACxC,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACnB,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,CAAC;AACb,CAAC,CAAC;AAEF,kBAAe,iBAAiB,CAAC"}
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const output_1 = __importDefault(require("../output"));
7
+ const projectsToText = (projects) => {
8
+ return ((projects || []).reduce((outputString, { name, id }) => outputString +
9
+ ("\n" +
10
+ "- " +
11
+ output_1.default.info(name) +
12
+ " " +
13
+ output_1.default.subtle("https://app.dittowords.com/doc/" + id)), "") + "\n");
14
+ };
15
+ exports.default = projectsToText;
16
+ //# sourceMappingURL=projectsToText.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projectsToText.js","sourceRoot":"","sources":["../../lib/utils/projectsToText.ts"],"names":[],"mappings":";;;;;AAAA,uDAA+B;AAG/B,MAAM,cAAc,GAAG,CAAC,QAAmB,EAAE,EAAE;IAC7C,OAAO,CACL,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,MAAM,CACrB,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,CAC7B,YAAY;QACZ,CAAC,IAAI;YACH,IAAI;YACJ,gBAAM,CAAC,IAAI,CAAC,IAAI,CAAC;YACjB,GAAG;YACH,gBAAM,CAAC,MAAM,CAAC,iCAAiC,GAAG,EAAE,CAAC,CAAC,EAC1D,EAAE,CACH,GAAG,IAAI,CACT,CAAC;AACJ,CAAC,CAAC;AAEF,kBAAe,cAAc,CAAC"}
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const { AutoComplete } = require("enquirer");
7
+ const output_1 = __importDefault(require("../output"));
8
+ function formatProjectChoice(project) {
9
+ return (project.name +
10
+ " " +
11
+ output_1.default.subtle(project.url || `https://app.dittowords.com/doc/${project.id}`));
12
+ }
13
+ function parseResponse(response) {
14
+ if (!response) {
15
+ return null;
16
+ }
17
+ const [, name, id] = response.split(/^(.*)\s.*http.*\/(\w+).*$/);
18
+ if (id === "all") {
19
+ return { name, id: "ditto_component_library" };
20
+ }
21
+ return { name, id };
22
+ }
23
+ const promptForProject = async ({ message, projects, limit = 10, }) => {
24
+ output_1.default.nl();
25
+ const choices = projects.map(formatProjectChoice);
26
+ const prompt = new AutoComplete({
27
+ name: "project",
28
+ message,
29
+ limit,
30
+ choices,
31
+ });
32
+ let response;
33
+ try {
34
+ response = await prompt.run();
35
+ }
36
+ catch (e) {
37
+ // this catch handles the case where someone presses
38
+ // Ctrl + C to kill the AutoComplete process
39
+ response = null;
40
+ }
41
+ return parseResponse(response);
42
+ };
43
+ exports.default = promptForProject;
44
+ //# sourceMappingURL=promptForProject.js.map
@@ -0,0 +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"}
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const output_1 = __importDefault(require("../output"));
7
+ const projectsToText_1 = __importDefault(require("./projectsToText"));
8
+ const sourcesToText = (projects, componentLibrary) => {
9
+ let message = "";
10
+ if (componentLibrary) {
11
+ message += `the ${output_1.default.info("Ditto Component Library")}`;
12
+ if ((projects || []).length) {
13
+ message += " and ";
14
+ }
15
+ else {
16
+ message += "..";
17
+ }
18
+ }
19
+ if ((projects || []).length) {
20
+ message += `the following projects: ${(0, projectsToText_1.default)(projects)}\n`;
21
+ }
22
+ return message;
23
+ };
24
+ exports.default = sourcesToText;
25
+ //# sourceMappingURL=sourcesToText.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sourcesToText.js","sourceRoot":"","sources":["../../lib/utils/sourcesToText.ts"],"names":[],"mappings":";;;;;AAAA,uDAA+B;AAC/B,sEAA8C;AAG9C,MAAM,aAAa,GAAG,CAAC,QAAmB,EAAE,gBAAyB,EAAE,EAAE;IACvE,IAAI,OAAO,GAAG,EAAE,CAAC;IAEjB,IAAI,gBAAgB,EAAE;QACpB,OAAO,IAAI,OAAO,gBAAM,CAAC,IAAI,CAAC,yBAAyB,CAAC,EAAE,CAAC;QAE3D,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;YAC3B,OAAO,IAAI,OAAO,CAAC;SACpB;aAAM;YACL,OAAO,IAAI,IAAI,CAAC;SACjB;KACF;IAED,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;QAC3B,OAAO,IAAI,2BAA2B,IAAA,wBAAc,EAAC,QAAQ,CAAC,IAAI,CAAC;KACpE;IAED,OAAO,OAAO,CAAC;AACjB,CAAC,CAAC;AAEF,kBAAe,aAAa,CAAC"}
package/jest.config.js CHANGED
@@ -2,5 +2,5 @@ module.exports = {
2
2
  transformIgnorePatterns: [],
3
3
  maxWorkers: 1,
4
4
  verbose: true,
5
- collectCoverageFrom: ["src/**/*.{js,jsx}"],
5
+ collectCoverageFrom: ["lib/**/*.{js,jsx,ts,tsx}"],
6
6
  };
@@ -0,0 +1,51 @@
1
+ import { collectAndSaveProject } from "./init/project";
2
+ import projectsToText from "./utils/projectsToText";
3
+ import {
4
+ getSelectedProjects,
5
+ getIsUsingComponents,
6
+ } from "./utils/getSelectedProjects";
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
+ }
14
+
15
+ const addProject = async () => {
16
+ const projects = getSelectedProjects();
17
+ const usingComponents = getIsUsingComponents();
18
+
19
+ try {
20
+ if (usingComponents) {
21
+ if (projects.length) {
22
+ console.log(
23
+ `\nYou're currently syncing text from the ${output.info(
24
+ "Component Library"
25
+ )} and from the following projects: ${projectsToText(projects)}`
26
+ );
27
+ } else {
28
+ console.log(
29
+ `\nYou're currently only syncing text from the ${output.info(
30
+ "Component Library"
31
+ )}`
32
+ );
33
+ }
34
+ } else if (projects.length) {
35
+ console.log(
36
+ `\nYou're currently set up to sync text from the following projects: ${projectsToText(
37
+ projects
38
+ )}`
39
+ );
40
+ }
41
+ await collectAndSaveProject(false);
42
+ } catch (error) {
43
+ console.log(
44
+ `\nSorry, there was an error adding a project to your workspace: `,
45
+ error
46
+ );
47
+ quit();
48
+ }
49
+ };
50
+
51
+ export default addProject;
package/lib/api.ts ADDED
@@ -0,0 +1,15 @@
1
+ import axios from "axios";
2
+
3
+ import config from "./config";
4
+ import consts from "./consts";
5
+
6
+ export const create = (token?: string) => {
7
+ return axios.create({
8
+ baseURL: consts.API_HOST,
9
+ headers: {
10
+ Authorization: `token ${token}`,
11
+ },
12
+ });
13
+ };
14
+
15
+ export default create(config.getToken(consts.CONFIG_FILE, consts.API_HOST));
@@ -1,11 +1,9 @@
1
1
  /* eslint-disable no-underscore-dangle */
2
- const fs = require("fs");
3
- const path = require("path");
4
-
5
- const tempy = require("tempy");
6
- const yaml = require("js-yaml");
7
-
8
- const config = require("./config");
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import tempy from "tempy";
5
+ import yaml from "js-yaml";
6
+ import config from "./config";
9
7
 
10
8
  const fakeHomedir = fs.mkdtempSync(path.join(__dirname, "../testing/tmp"));
11
9
 
@@ -57,11 +55,15 @@ describe("Tokens in config files", () => {
57
55
  describe("saveToken", () => {
58
56
  it("creates a config file with config data", () => {
59
57
  const fileContents = fs.readFileSync(configFile, "utf8");
60
- const configData = yaml.safeLoad(fileContents);
61
- expect(configData["testing.dittowords.com"]).toBeDefined();
62
- expect(configData["testing.dittowords.com"][0].token).toEqual(
63
- "faketoken"
64
- );
58
+ const configData = yaml.load(fileContents);
59
+ if (configData && typeof configData === "object") {
60
+ expect(configData["testing.dittowords.com"]).toBeDefined();
61
+ expect(configData["testing.dittowords.com"][0].token).toEqual(
62
+ "faketoken"
63
+ );
64
+ } else {
65
+ fail("Config Data should have been an object!");
66
+ }
65
67
  });
66
68
  });
67
69
 
@@ -73,16 +75,3 @@ describe("Tokens in config files", () => {
73
75
  });
74
76
  });
75
77
  });
76
-
77
- describe("save", () => {
78
- let data;
79
- beforeEach(() => {
80
- const file = tempy.writeSync("");
81
- config.save(file, "test.setting", true);
82
- data = config.readData(file);
83
- });
84
-
85
- it("writes a setting correctly", () => {
86
- expect(data.test.setting).toEqual(true);
87
- });
88
- });
package/lib/config.ts ADDED
@@ -0,0 +1,214 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import url from "url";
4
+ import yaml from "js-yaml";
5
+
6
+ import consts from "./consts";
7
+ import { Project, ConfigYAML } from "./types";
8
+
9
+ function createFileIfMissing(filename: string) {
10
+ const dir = path.dirname(filename);
11
+
12
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir);
13
+
14
+ if (!fs.existsSync(filename)) {
15
+ fs.closeSync(fs.openSync(filename, "w"));
16
+ }
17
+ }
18
+
19
+ function jsonIsConfigYAML(json: unknown): json is ConfigYAML {
20
+ return typeof json === "object";
21
+ }
22
+
23
+ function jsonIsGlobalYAML(
24
+ json: unknown
25
+ ): json is Record<string, { token: string }[]> {
26
+ return (
27
+ !!json &&
28
+ 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
+ )
34
+ )
35
+ );
36
+ }
37
+
38
+ /**
39
+ * Read data from a project config file
40
+ * @param {string} file defaults to `PROJECT_CONFIG_FILE` defined in `constants.js`
41
+ * @param {*} defaultData defaults to `{}`
42
+ * @returns { ConfigYAML }
43
+ */
44
+ function readProjectConfigData(
45
+ file = consts.PROJECT_CONFIG_FILE,
46
+ defaultData = {}
47
+ ): ConfigYAML {
48
+ createFileIfMissing(file);
49
+ const fileContents = fs.readFileSync(file, "utf8");
50
+ const yamlData = yaml.load(fileContents);
51
+ if (jsonIsConfigYAML(yamlData)) {
52
+ return yamlData;
53
+ }
54
+ return defaultData;
55
+ }
56
+
57
+ /**
58
+ * Read data from a global config file
59
+ * @param {string} file defaults to `CONFIG_FILE` defined in `constants.js`
60
+ * @param {*} defaultData defaults to `{}`
61
+ * @returns { Record<string, { token: string }[]> }
62
+ */
63
+ function readGlobalConfigData(
64
+ file = consts.CONFIG_FILE,
65
+ defaultData = {}
66
+ ): Record<string, { token: string }[]> {
67
+ createFileIfMissing(file);
68
+ const fileContents = fs.readFileSync(file, "utf8");
69
+ const yamlData = yaml.load(fileContents);
70
+ if (jsonIsGlobalYAML(yamlData)) {
71
+ return yamlData;
72
+ }
73
+ return defaultData;
74
+ }
75
+
76
+ function writeProjectConfigData(file: string, data: object) {
77
+ createFileIfMissing(file);
78
+ const existingData = readProjectConfigData(file);
79
+ const yamlStr = yaml.dump({ ...existingData, ...data });
80
+ fs.writeFileSync(file, yamlStr, "utf8");
81
+ }
82
+
83
+ function writeGlobalConfigData(file: string, data: object) {
84
+ createFileIfMissing(file);
85
+ const existingData = readGlobalConfigData(file);
86
+ const yamlStr = yaml.dump({ ...existingData, ...data });
87
+ fs.writeFileSync(file, yamlStr, "utf8");
88
+ }
89
+
90
+ function justTheHost(host: string) {
91
+ if (!host.includes("://")) return host;
92
+ return url.parse(host).hostname || "";
93
+ }
94
+
95
+ function deleteToken(file: string, host: string) {
96
+ const data = readGlobalConfigData(file);
97
+ const hostParsed = justTheHost(host);
98
+ data[hostParsed] = [];
99
+ data[hostParsed][0] = { token: "" };
100
+ writeGlobalConfigData(file, data);
101
+ }
102
+
103
+ function saveToken(file: string, host: string, token: string) {
104
+ const data = readGlobalConfigData(file);
105
+ const hostParsed = justTheHost(host);
106
+ data[hostParsed] = []; // only allow one token per host
107
+ data[hostParsed][0] = { token };
108
+ writeGlobalConfigData(file, data);
109
+ }
110
+
111
+ function getTokenFromEnv() {
112
+ return process.env.DITTO_API_KEY;
113
+ }
114
+
115
+ /**
116
+ *
117
+ * @param {string} file
118
+ * @param {string} host
119
+ * @returns {Token}
120
+ */
121
+ function getToken(file: string, host: string) {
122
+ const tokenFromEnv = getTokenFromEnv();
123
+ if (tokenFromEnv) {
124
+ return tokenFromEnv;
125
+ }
126
+
127
+ const data = readGlobalConfigData(file);
128
+ const hostEntry = data[justTheHost(host)];
129
+ if (!hostEntry) return undefined;
130
+ const { length } = hostEntry;
131
+ return hostEntry[length - 1].token;
132
+ }
133
+
134
+ const IS_DUPLICATE = /-(\d+$)/;
135
+
136
+ function dedupeProjectName(projectNames: Set<string>, projectName: string) {
137
+ let dedupedName = projectName;
138
+
139
+ if (projectNames.has(dedupedName)) {
140
+ while (projectNames.has(dedupedName)) {
141
+ const [_, numberStr] = dedupedName.match(IS_DUPLICATE) || [];
142
+ if (numberStr && !isNaN(parseInt(numberStr))) {
143
+ dedupedName = `${dedupedName.replace(IS_DUPLICATE, "")}-${
144
+ parseInt(numberStr) + 1
145
+ }`;
146
+ } else {
147
+ dedupedName = `${dedupedName}-1`;
148
+ }
149
+ }
150
+ }
151
+
152
+ return dedupedName;
153
+ }
154
+
155
+ /**
156
+ * Reads from the config file, filters out
157
+ * invalid projects, dedupes those remaining, and returns:
158
+ * - whether or not the data required to `pull` is present
159
+ * - whether or not the component library should be fetched
160
+ * - an array of valid, deduped projects
161
+ * - the `variants` and `format` config options
162
+ */
163
+ function parseSourceInformation() {
164
+ const { projects, components, variants, format } = readProjectConfigData();
165
+
166
+ const projectNames = new Set<string>();
167
+ const validProjects: Project[] = [];
168
+
169
+ let componentLibraryInProjects = false;
170
+
171
+ (projects || []).forEach((project) => {
172
+ const isValid = project.id && project.name;
173
+ if (!isValid) {
174
+ return;
175
+ }
176
+
177
+ if (project.id === "ditto_component_library") {
178
+ componentLibraryInProjects = true;
179
+ return;
180
+ }
181
+
182
+ project.fileName = dedupeProjectName(projectNames, project.name);
183
+ projectNames.add(project.fileName);
184
+
185
+ validProjects.push(project);
186
+ });
187
+
188
+ const shouldFetchComponentLibrary =
189
+ !!components || componentLibraryInProjects;
190
+
191
+ const hasSourceData = !!validProjects.length || shouldFetchComponentLibrary;
192
+
193
+ return {
194
+ hasSourceData,
195
+ validProjects,
196
+ shouldFetchComponentLibrary,
197
+ variants: variants || false,
198
+ format,
199
+ };
200
+ }
201
+
202
+ export default {
203
+ createFileIfMissing,
204
+ readProjectConfigData,
205
+ readGlobalConfigData,
206
+ writeGlobalConfigData,
207
+ writeProjectConfigData,
208
+ justTheHost,
209
+ saveToken,
210
+ deleteToken,
211
+ getToken,
212
+ getTokenFromEnv,
213
+ parseSourceInformation,
214
+ };
package/lib/consts.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { homedir } from "os";
2
+ import path from "path";
3
+
4
+ export const API_HOST =
5
+ process.env.DITTO_API_HOST || "https://api.dittowords.com";
6
+ export const CONFIG_FILE =
7
+ process.env.DITTO_CONFIG_FILE || path.join(homedir(), ".config", "ditto");
8
+ export const PROJECT_CONFIG_FILE = path.normalize(
9
+ path.join("ditto", "config.yml")
10
+ );
11
+ export const TEXT_DIR = process.env.DITTO_TEXT_DIR || "ditto";
12
+ export const TEXT_FILE = path.normalize(path.join(TEXT_DIR, "text.json"));
13
+
14
+ export default {
15
+ API_HOST,
16
+ CONFIG_FILE,
17
+ PROJECT_CONFIG_FILE,
18
+ TEXT_DIR,
19
+ TEXT_FILE,
20
+ };
package/lib/ditto.ts ADDED
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ // This is the main entry point for the ditto-cli command.
3
+ import { program } from "commander";
4
+ // to use V8's code cache to speed up instantiation time
5
+ import "v8-compile-cache";
6
+
7
+ import { init, needsInit } from "./init/init";
8
+ import { pull } from "./pull";
9
+
10
+ import addProject from "./add-project";
11
+ import removeProject from "./remove-project";
12
+ import processMetaOption from "./utils/processMetaOption";
13
+
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
+ }
24
+
25
+ const setupCommands = () => {
26
+ 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
+
38
+ projectCommand
39
+ .command("add")
40
+ .description(projectDescription)
41
+ .action(() => checkInit("project"));
42
+
43
+ projectCommand
44
+ .command("remove")
45
+ .description("Stop syncing copy from a Ditto project")
46
+ .action(() => checkInit("project remove"));
47
+ };
48
+
49
+ const setupOptions = () => {
50
+ program.option(
51
+ "-m, --meta <data...>",
52
+ "Optional metadata for this command to send arbitrary data to the backend. Ex: -m githubActionRequest:true trigger:manual"
53
+ );
54
+ };
55
+
56
+ const checkInit = async (command: string) => {
57
+ if (needsInit() && command !== "project remove") {
58
+ try {
59
+ await init();
60
+ if (command === "pull") main(); // re-run to actually pull text now that init is finished
61
+ } catch (error) {
62
+ quit();
63
+ }
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();
83
+ }
84
+ }
85
+ };
86
+
87
+ const main = async () => {
88
+ if (process.argv.length <= 2 && process.argv[1].includes("ditto-cli")) {
89
+ await checkInit("none");
90
+ } else {
91
+ setupCommands();
92
+ setupOptions();
93
+ }
94
+ program.parse(process.argv);
95
+ };
96
+
97
+ main();