@dittowords/cli 2.8.0 → 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 (50) hide show
  1. package/README.md +150 -141
  2. package/bin/add-project.js +5 -7
  3. package/bin/add-project.js.map +1 -1
  4. package/bin/config.js +37 -11
  5. package/bin/config.js.map +1 -1
  6. package/bin/ditto.js +66 -57
  7. package/bin/ditto.js.map +1 -1
  8. package/bin/http/fetchVariants.js +26 -0
  9. package/bin/http/fetchVariants.js.map +1 -0
  10. package/bin/init/init.js +17 -6
  11. package/bin/init/init.js.map +1 -1
  12. package/bin/init/project.js +38 -45
  13. package/bin/init/project.js.map +1 -1
  14. package/bin/init/token.js +22 -20
  15. package/bin/init/token.js.map +1 -1
  16. package/bin/pull.js +136 -193
  17. package/bin/pull.js.map +1 -1
  18. package/bin/remove-project.js +2 -7
  19. package/bin/remove-project.js.map +1 -1
  20. package/bin/utils/cleanFileName.js +11 -0
  21. package/bin/utils/cleanFileName.js.map +1 -0
  22. package/bin/utils/generateJsDriver.js +56 -0
  23. package/bin/utils/generateJsDriver.js.map +1 -0
  24. package/bin/utils/getSelectedProjects.js +3 -18
  25. package/bin/utils/getSelectedProjects.js.map +1 -1
  26. package/bin/utils/projectsToText.js +10 -1
  27. package/bin/utils/projectsToText.js.map +1 -1
  28. package/bin/utils/promptForProject.js +2 -3
  29. package/bin/utils/promptForProject.js.map +1 -1
  30. package/bin/utils/quit.js +10 -0
  31. package/bin/utils/quit.js.map +1 -0
  32. package/lib/add-project.ts +6 -9
  33. package/lib/config.ts +56 -19
  34. package/lib/ditto.ts +74 -58
  35. package/lib/http/fetchVariants.ts +30 -0
  36. package/lib/init/init.ts +38 -6
  37. package/lib/init/project.test.ts +3 -3
  38. package/lib/init/project.ts +47 -58
  39. package/lib/init/token.ts +17 -16
  40. package/lib/pull.ts +199 -279
  41. package/lib/remove-project.ts +2 -8
  42. package/lib/types.ts +22 -3
  43. package/lib/utils/cleanFileName.ts +6 -0
  44. package/lib/utils/generateJsDriver.ts +68 -0
  45. package/lib/utils/getSelectedProjects.ts +5 -24
  46. package/lib/utils/projectsToText.ts +11 -1
  47. package/lib/utils/promptForProject.ts +2 -3
  48. package/lib/utils/quit.ts +5 -0
  49. package/package.json +1 -1
  50. package/tsconfig.json +2 -1
package/lib/init/token.ts CHANGED
@@ -8,6 +8,7 @@ import { create } from "../api";
8
8
  import consts from "../consts";
9
9
  import output from "../output";
10
10
  import config from "../config";
11
+ import { quit } from "../utils/quit";
11
12
 
12
13
  export const needsToken = (configFile?: string, host = consts.API_HOST) => {
13
14
  if (config.getTokenFromEnv()) {
@@ -30,9 +31,9 @@ async function checkToken(token: string): Promise<any> {
30
31
  const axios = create(token);
31
32
  const endpoint = "/token-check";
32
33
 
33
- const resOrError = await axios
34
- .get(endpoint)
35
- .catch((error: any) => {
34
+ let resOrError;
35
+ try {
36
+ resOrError = await axios.get(endpoint).catch((error: any) => {
36
37
  if (error.code === "ENOTFOUND") {
37
38
  return output.errorText(
38
39
  `Can't connect to API: ${output.url(error.hostname)}`
@@ -44,13 +45,19 @@ async function checkToken(token: string): Promise<any> {
44
45
  );
45
46
  }
46
47
  return output.warnText("We're having trouble reaching the Ditto API.");
47
- })
48
- .catch(() =>
49
- output.errorText("Sorry! We're having trouble reaching the Ditto API.")
50
- );
51
- if (typeof resOrError === "string") return resOrError;
48
+ });
49
+ } catch (e: unknown) {
50
+ output.errorText(e as any);
51
+ output.errorText("Sorry! We're having trouble reaching the Ditto API.");
52
+ }
52
53
 
53
- if (resOrError.status === 200) return true;
54
+ if (typeof resOrError === "string") {
55
+ return resOrError;
56
+ }
57
+
58
+ if (resOrError?.status === 200) {
59
+ return true;
60
+ }
54
61
 
55
62
  return output.errorText("This API key isn't valid. Please try another.");
56
63
  }
@@ -75,12 +82,6 @@ async function collectToken(message: string | null) {
75
82
  return response.token;
76
83
  }
77
84
 
78
- function quit(exitCode = 2) {
79
- console.log("API key was not saved.");
80
- process.exitCode = exitCode;
81
- process.exit();
82
- }
83
-
84
85
  /**
85
86
  *
86
87
  * @param {string | null} message
@@ -99,7 +100,7 @@ export const collectAndSaveToken = async (message: string | null = null) => {
99
100
  config.saveToken(consts.CONFIG_FILE, consts.API_HOST, token);
100
101
  return token;
101
102
  } catch (error) {
102
- quit();
103
+ quit("API token was not saved");
103
104
  }
104
105
  };
105
106
 
package/lib/pull.ts CHANGED
@@ -9,9 +9,48 @@ import consts from "./consts";
9
9
  import output from "./output";
10
10
  import { collectAndSaveToken } from "./init/token";
11
11
  import sourcesToText from "./utils/sourcesToText";
12
+ import { generateJsDriver } from "./utils/generateJsDriver";
13
+ import { cleanFileName } from "./utils/cleanFileName";
12
14
  import { SourceInformation, Token, Project } from "./types";
15
+ import { fetchVariants } from "./http/fetchVariants";
13
16
 
14
- const NON_DEFAULT_FORMATS = ["flat", "structured", "android", "ios-strings"];
17
+ type SupportedFormat = "flat" | "structured" | "android" | "ios-strings";
18
+
19
+ const SUPPORTED_FORMATS: SupportedFormat[] = [
20
+ "flat",
21
+ "structured",
22
+ "android",
23
+ "ios-strings",
24
+ ];
25
+
26
+ const JSON_FORMATS: SupportedFormat[] = ["flat", "structured"];
27
+
28
+ const FORMAT_EXTENSIONS = {
29
+ flat: ".json",
30
+ structured: ".json",
31
+ android: ".xml",
32
+ "ios-strings": ".strings",
33
+ };
34
+
35
+ const getFormatDataIsValid = {
36
+ flat: (data: string) => data !== "{}",
37
+ structured: (data: string) => data !== "{}",
38
+ android: (data: string) => data.includes("<string"),
39
+ "ios-strings": (data: string) => !!data,
40
+ };
41
+
42
+ const getFormat = (formatFromSource: string | undefined): SupportedFormat => {
43
+ const f = formatFromSource as SupportedFormat | undefined;
44
+ if (f && SUPPORTED_FORMATS.includes(f)) {
45
+ return f;
46
+ }
47
+
48
+ return "flat";
49
+ };
50
+
51
+ const getFormatExtension = (format: SupportedFormat) => {
52
+ return FORMAT_EXTENSIONS[format];
53
+ };
15
54
 
16
55
  const DEFAULT_FORMAT_KEYS = ["projects", "exported_at"];
17
56
  const hasVariantData = (data: any) => {
@@ -31,16 +70,6 @@ async function askForAnotherToken() {
31
70
  await collectAndSaveToken(message);
32
71
  }
33
72
 
34
- function getExtension(format: string) {
35
- if (format === "android") {
36
- return ".xml";
37
- }
38
- if (format === "ios-strings") {
39
- return ".strings";
40
- }
41
- return ".json";
42
- }
43
-
44
73
  /**
45
74
  * For a given variant:
46
75
  * - if format is unspecified, fetch data for all projects from `/projects` and
@@ -51,93 +80,60 @@ function getExtension(format: string) {
51
80
  async function downloadAndSaveVariant(
52
81
  variantApiId: string | null,
53
82
  projects: Project[],
54
- format: string | undefined,
83
+ format: SupportedFormat,
55
84
  status: string | undefined,
56
85
  richText: boolean | undefined,
57
86
  token?: Token
58
87
  ) {
59
- const params: Record<string, string | null> = {
60
- variant: variantApiId,
61
- };
62
- if (format) {
63
- params.format = format;
64
- }
65
- if (status) {
66
- params.status = status;
67
- }
68
- if (richText) {
69
- params.includeRichText = richText.toString();
70
- }
71
-
72
- if (format && NON_DEFAULT_FORMATS.includes(format)) {
73
- const savedMessages = await Promise.all(
74
- projects.map(async ({ id, fileName }: Project) => {
75
- const { data } = await api.get(`/projects/${id}`, {
76
- params,
77
- headers: { Authorization: `token ${token}` },
78
- });
79
-
80
- if (!hasVariantData(data)) {
81
- return "";
82
- }
83
-
84
- const extension = getExtension(format);
85
-
86
- const filename =
87
- fileName + ("__" + (variantApiId || "base")) + extension;
88
- const filepath = path.join(consts.TEXT_DIR, filename);
89
-
90
- let dataString = data;
91
- if (extension === ".json") {
92
- dataString = JSON.stringify(data, null, 2);
93
- }
94
-
95
- fs.writeFileSync(filepath, dataString);
96
-
97
- return getSavedMessage(filename);
98
- })
99
- );
88
+ const params: Record<string, string | null> = { variant: variantApiId };
89
+ if (format) params.format = format;
90
+ if (status) params.status = status;
91
+ if (richText) params.includeRichText = richText.toString();
92
+
93
+ const savedMessages = await Promise.all(
94
+ projects.map(async ({ id, fileName }: Project) => {
95
+ const { data } = await api.get(`/projects/${id}`, {
96
+ params,
97
+ headers: { Authorization: `token ${token}` },
98
+ });
99
+
100
+ if (!hasVariantData(data)) {
101
+ return "";
102
+ }
100
103
 
101
- return savedMessages.join("");
102
- } else {
103
- const { data } = await api.get("/projects", {
104
- params: { ...params, projectIds: projects.map(({ id }) => id) },
105
- headers: { Authorization: `token ${token}` },
106
- });
104
+ const extension = getFormatExtension(format);
107
105
 
108
- if (!hasVariantData(data)) {
109
- return "";
110
- }
106
+ const filename = cleanFileName(
107
+ fileName + ("__" + (variantApiId || "base")) + extension
108
+ );
109
+ const filepath = path.join(consts.TEXT_DIR, filename);
111
110
 
112
- const filename = `${variantApiId || "base"}.json`;
113
- const filepath = path.join(consts.TEXT_DIR, filename);
111
+ let dataString = data;
112
+ if (extension === ".json") {
113
+ dataString = JSON.stringify(data, null, 2);
114
+ }
114
115
 
115
- const dataString = JSON.stringify(data, null, 2);
116
+ const dataIsValid = getFormatDataIsValid[format];
117
+ if (!dataIsValid(dataString)) {
118
+ return "";
119
+ }
116
120
 
117
- fs.writeFileSync(filepath, dataString);
121
+ fs.writeFileSync(filepath, dataString);
122
+ return getSavedMessage(filename);
123
+ })
124
+ );
118
125
 
119
- return getSavedMessage(filename);
120
- }
126
+ return savedMessages.join("");
121
127
  }
122
128
 
123
129
  async function downloadAndSaveVariants(
130
+ variants: { apiID: string }[],
124
131
  projects: Project[],
125
- format: string | undefined,
132
+ format: SupportedFormat,
126
133
  status: string | undefined,
127
134
  richText: boolean | undefined,
128
- token?: Token,
129
- options?: PullOptions
135
+ token?: Token
130
136
  ) {
131
- const meta = options ? options.meta : {};
132
-
133
- const { data: variants } = await api.get("/variants", {
134
- params: {
135
- ...meta,
136
- projectIds: projects.map(({ id }) => id),
137
- },
138
- headers: { Authorization: `token ${token}` },
139
- });
140
-
141
137
  const messages = await Promise.all([
142
138
  downloadAndSaveVariant(null, projects, format, status, richText, token),
143
139
  ...variants.map(({ apiID }: { apiID: string }) =>
@@ -150,63 +146,44 @@ async function downloadAndSaveVariants(
150
146
 
151
147
  async function downloadAndSaveBase(
152
148
  projects: Project[],
153
- format: string | undefined,
149
+ format: SupportedFormat,
154
150
  status: string | undefined,
155
- richText: boolean | undefined,
151
+ richText?: boolean | undefined,
156
152
  token?: Token,
157
153
  options?: PullOptions
158
154
  ) {
159
- const meta = options ? options.meta : {};
160
-
161
- const params = {
162
- ...meta,
163
- };
164
- if (format) {
165
- params.format = format;
166
- }
167
- if (status) {
168
- params.status = status;
169
- }
170
- if (richText) {
171
- params.includeRichText = richText.toString();
172
- }
173
-
174
- if (format && NON_DEFAULT_FORMATS.includes(format)) {
175
- const savedMessages = await Promise.all(
176
- projects.map(async ({ id, fileName }: Project) => {
177
- const { data } = await api.get(`/projects/${id}`, {
178
- params,
179
- headers: { Authorization: `token ${token}` },
180
- });
181
-
182
- const extension = getExtension(format);
183
- const filename = `${fileName}${extension}`;
184
- const filepath = path.join(consts.TEXT_DIR, filename);
185
-
186
- let dataString = data;
187
- if (extension === ".json") {
188
- dataString = JSON.stringify(data, null, 2);
189
- }
190
-
191
- fs.writeFileSync(filepath, dataString);
192
-
193
- return getSavedMessage(filename);
194
- })
195
- );
196
-
197
- return savedMessages.join("");
198
- } else {
199
- const { data } = await api.get(`/projects`, {
200
- params: { ...params, projectIds: projects.map(({ id }) => id) },
201
- headers: { Authorization: `token ${token}` },
202
- });
155
+ const params = { ...options?.meta };
156
+ if (format) params.format = format;
157
+ if (status) params.status = status;
158
+ if (richText) params.includeRichText = richText.toString();
159
+
160
+ const savedMessages = await Promise.all(
161
+ projects.map(async ({ id, fileName }: Project) => {
162
+ const { data } = await api.get(`/projects/${id}`, {
163
+ params,
164
+ headers: { Authorization: `token ${token}` },
165
+ });
166
+
167
+ const extension = getFormatExtension(format);
168
+ const filename = cleanFileName(`${fileName}__base${extension}`);
169
+ const filepath = path.join(consts.TEXT_DIR, filename);
170
+
171
+ let dataString = data;
172
+ if (extension === ".json") {
173
+ dataString = JSON.stringify(data, null, 2);
174
+ }
203
175
 
204
- const dataString = JSON.stringify(data, null, 2);
176
+ const dataIsValid = getFormatDataIsValid[format];
177
+ if (!dataIsValid(dataString)) {
178
+ return "";
179
+ }
205
180
 
206
- fs.writeFileSync(consts.TEXT_FILE, dataString);
181
+ fs.writeFileSync(filepath, dataString);
182
+ return getSavedMessage(filename);
183
+ })
184
+ );
207
185
 
208
- return getSavedMessage("text.json");
209
- }
186
+ return savedMessages.join("");
210
187
  }
211
188
 
212
189
  function getSavedMessage(file: string) {
@@ -228,181 +205,124 @@ function cleanOutputFiles() {
228
205
  return "Cleaning old output files..\n";
229
206
  }
230
207
 
231
- // compatability with legacy method of specifying project ids
232
- // that is still used by the default format
233
- const stringifyProjectId = (projectId: string) =>
234
- projectId === "ditto_component_library" ? projectId : `project_${projectId}`;
235
-
236
- /**
237
- * Generates an index.js file that can be consumed
238
- * by an SDK - this is a big DX improvement because
239
- * it provides a single entry point to get all data
240
- * (including variants) instead of having to import
241
- * each generated file individually.
242
- *
243
- * The generated file will have a unified format
244
- * independent of the CLI configuration used to fetch
245
- * data from Ditto.
246
- */
247
- function generateJsDriver(
248
- projects: Project[],
249
- variants: boolean,
250
- format: string | undefined
251
- ) {
252
- const fileNames = fs
253
- .readdirSync(consts.TEXT_DIR)
254
- .filter((fileName) => /\.json$/.test(fileName));
255
-
256
- const projectIdsByName: Record<string, string> = projects.reduce(
257
- (agg, project) => {
258
- if (project.fileName) {
259
- return { ...agg, [project.fileName]: project.id };
260
- }
261
- return agg;
262
- },
263
- {}
264
- );
265
-
266
- const data = fileNames.reduce(
267
- (obj: Record<string, Record<string, string>>, fileName) => {
268
- // filename format: {project-name}__{variant-api-id}.json
269
- // file format: flat or structured
270
- if (variants && format) {
271
- const [projectName, rest] = fileName.split("__");
272
- const [variantApiId] = rest.split(".");
273
-
274
- const projectId = projectIdsByName[projectName];
275
- if (!projectId) {
276
- throw new Error(`Couldn't find id for ${projectName}`);
277
- }
278
-
279
- const projectIdStr = stringifyProjectId(projectId);
280
-
281
- if (!obj[projectIdStr]) {
282
- obj[projectIdStr] = {};
283
- }
284
-
285
- obj[projectIdStr][variantApiId] = `require('./${fileName}')`;
286
- }
287
- // filename format: {variant-api-id}.json
288
- // file format: default
289
- else if (variants) {
290
- const file = require(path.resolve(consts.TEXT_DIR, `./${fileName}`));
291
- const [variantApiId] = fileName.split(".");
292
-
293
- Object.keys(file.projects).forEach((projectId) => {
294
- if (!obj[projectId]) {
295
- obj[projectId] = {};
296
- }
297
-
298
- const project = file.projects[projectId];
299
- obj[projectId][variantApiId] = project.frames || project.components;
300
- });
301
- }
302
- // filename format: {project-name}.json
303
- // file format: flat or structured
304
- else if (format) {
305
- const [projectName] = fileName.split(".");
306
- const projectId = projectIdsByName[projectName];
307
- if (!projectId) {
308
- throw new Error(`Couldn't find id for ${projectName}`);
309
- }
310
-
311
- obj[stringifyProjectId(projectId)] = {
312
- base: `require('./${fileName}')`,
313
- };
314
- }
315
- // filename format: text.json (single file)
316
- // file format: default
317
- else {
318
- const file = require(path.resolve(consts.TEXT_DIR, `./${fileName}`));
319
- Object.keys(file.projects).forEach((projectId) => {
320
- const project = file.projects[projectId];
321
- obj[projectId] = { base: project.frames || project.components };
322
- });
323
- }
324
-
325
- return obj;
326
- },
327
- {}
328
- );
329
-
330
- let dataString = `module.exports = ${JSON.stringify(data, null, 2)}`
331
- // remove quotes around require statements
332
- .replace(/"require\((.*)\)"/g, "require($1)");
333
-
334
- const filePath = path.resolve(consts.TEXT_DIR, "index.js");
335
- fs.writeFileSync(filePath, dataString, { encoding: "utf8" });
336
-
337
- return `Generated .js SDK driver at ${output.info(filePath)}`;
338
- }
339
-
340
208
  async function downloadAndSave(
341
- sourceInformation: SourceInformation,
209
+ source: SourceInformation,
342
210
  token?: Token,
343
211
  options?: PullOptions
344
212
  ) {
345
213
  const {
346
214
  validProjects,
347
- variants,
348
- format,
215
+ format: formatFromSource,
349
216
  shouldFetchComponentLibrary,
350
217
  status,
351
218
  richText,
352
- } = sourceInformation;
219
+ componentFolders,
220
+ } = source;
353
221
 
354
- let msg = `\nFetching the latest text from ${sourcesToText(
355
- validProjects,
356
- shouldFetchComponentLibrary
357
- )}\n`;
222
+ const format = getFormat(formatFromSource);
358
223
 
224
+ let msg = "";
359
225
  const spinner = ora(msg);
360
226
  spinner.start();
361
227
 
362
- // We'll need to move away from this solution if at some
363
- // point down the road we stop allowing the component
364
- // library to be returned from the /projects endpoint
365
- if (shouldFetchComponentLibrary) {
366
- validProjects.push({
367
- id: "ditto_component_library",
368
- name: "Ditto Component Library",
369
- fileName: "ditto-component-library",
370
- });
371
- }
228
+ const variants = await fetchVariants(source);
372
229
 
373
230
  try {
374
231
  msg += cleanOutputFiles();
232
+ msg += `\nFetching the latest text from ${sourcesToText(
233
+ validProjects,
234
+ shouldFetchComponentLibrary
235
+ )}\n`;
375
236
 
376
237
  const meta = options ? options.meta : {};
377
- msg += variants
378
- ? await downloadAndSaveVariants(
379
- validProjects,
380
- format,
381
- status,
382
- richText,
383
- token,
384
- {
385
- meta,
238
+
239
+ if (shouldFetchComponentLibrary) {
240
+ // Always include a variant with an apiID of undefined to ensure that we
241
+ // fetch the base text for the component library.
242
+ const componentVariants = [{ apiID: undefined }, ...(variants || [])];
243
+
244
+ const params = new URLSearchParams();
245
+ if (options?.meta)
246
+ Object.entries(options.meta).forEach(([k, v]) => params.append(k, v));
247
+ if (format) params.append("format", format);
248
+ if (status) params.append("status", status);
249
+ if (richText) params.append("includeRichText", richText.toString());
250
+ if (componentFolders) {
251
+ componentFolders.forEach(({ id }) => params.append("folder_id[]", id));
252
+ }
253
+
254
+ const messages = await Promise.all(
255
+ componentVariants.map(async ({ apiID: variantApiId }) => {
256
+ const p = new URLSearchParams(params);
257
+ if (variantApiId) p.append("variant", variantApiId);
258
+
259
+ const { data } = await api.get(`/components`, { params: p });
260
+
261
+ const nameExt = getFormatExtension(format);
262
+ const nameBase = "ditto-component-library";
263
+ const namePostfix = `__${variantApiId || "base"}`;
264
+
265
+ const fileName = cleanFileName(`${nameBase}${namePostfix}${nameExt}`);
266
+ const filePath = path.join(consts.TEXT_DIR, fileName);
267
+
268
+ let dataString = data;
269
+ if (nameExt === ".json") {
270
+ dataString = JSON.stringify(data, null, 2);
386
271
  }
387
- )
388
- : await downloadAndSaveBase(
389
- validProjects,
390
- format,
391
- status,
392
- richText,
393
- token,
394
- {
395
- meta,
272
+
273
+ const dataIsValid = getFormatDataIsValid[format];
274
+ if (!dataIsValid(dataString)) {
275
+ return "";
396
276
  }
397
- );
398
277
 
399
- msg += generateJsDriver(validProjects, variants, format);
278
+ await new Promise((r) => fs.writeFile(filePath, dataString, r));
279
+ return getSavedMessage(fileName);
280
+ })
281
+ );
282
+
283
+ msg += messages.join("");
284
+ }
285
+
286
+ if (validProjects.length) {
287
+ msg += variants
288
+ ? await downloadAndSaveVariants(
289
+ variants,
290
+ validProjects,
291
+ format,
292
+ status,
293
+ richText,
294
+ token
295
+ )
296
+ : await downloadAndSaveBase(
297
+ validProjects,
298
+ format,
299
+ status,
300
+ richText,
301
+ token,
302
+ {
303
+ meta,
304
+ }
305
+ );
306
+ }
307
+
308
+ const sources = [...validProjects];
309
+ if (shouldFetchComponentLibrary) {
310
+ sources.push({
311
+ id: "ditto_component_library",
312
+ name: "Ditto Component Library",
313
+ fileName: "ditto-component-library",
314
+ });
315
+ }
316
+
317
+ if (JSON_FORMATS.includes(format)) msg += generateJsDriver(sources);
400
318
 
401
319
  msg += `\n${output.success("Done")}!`;
402
320
 
403
321
  spinner.stop();
404
322
  return console.log(msg);
405
323
  } catch (e: any) {
324
+ console.error(e);
325
+
406
326
  spinner.stop();
407
327
  let error = e.message;
408
328
  if (e.response && e.response.status === 404) {
@@ -439,7 +359,7 @@ async function downloadAndSave(
439
359
  }
440
360
  }
441
361
 
442
- interface PullOptions {
362
+ export interface PullOptions {
443
363
  meta?: Record<string, string>;
444
364
  }
445
365
 
@@ -19,15 +19,9 @@ async function removeProject() {
19
19
  return;
20
20
  }
21
21
 
22
- const allProjects = isUsingComponents
23
- ? [{ id: "components", name: "Ditto Component Library" }, ...projects]
24
- : projects;
25
-
26
22
  const projectToRemove = await promptForProject({
27
- projects: allProjects,
28
- message: isUsingComponents
29
- ? "Select a project or library to remove"
30
- : "Select a project to remove",
23
+ projects,
24
+ message: "Select a project to remove",
31
25
  });
32
26
  if (!projectToRemove) return;
33
27