assistant-ui 0.0.81 → 0.0.83

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.
@@ -1,88 +1,313 @@
1
1
  import { Command } from "commander";
2
2
  import chalk from "chalk";
3
3
  import { spawn } from "cross-spawn";
4
+ import fs from "node:fs";
4
5
  import path from "node:path";
5
6
  import * as p from "@clack/prompts";
6
7
  import { logger } from "../lib/utils/logger";
7
- import { createFromExample } from "../lib/create-from-example";
8
+ import {
9
+ dlxCommand,
10
+ downloadProject,
11
+ resolveLatestReleaseRef,
12
+ resolvePackageManagerName,
13
+ transformProject,
14
+ type PackageManagerName,
15
+ } from "../lib/create-project";
16
+
17
+ export interface ProjectMetadata {
18
+ name: string;
19
+ label: string;
20
+ description?: string;
21
+ category: "template" | "example";
22
+ path: string;
23
+ hasLocalComponents: boolean;
24
+ }
8
25
 
9
- const templates = {
10
- default: {
11
- url: "https://github.com/assistant-ui/assistant-ui-starter",
26
+ export const PROJECT_METADATA: ProjectMetadata[] = [
27
+ // Templates
28
+ {
29
+ name: "default",
12
30
  label: "Default",
13
- hint: "Default template with Vercel AI SDK",
31
+ description: "Default template with Vercel AI SDK",
32
+ category: "template",
33
+ path: "templates/default",
34
+ hasLocalComponents: true,
14
35
  },
15
- minimal: {
16
- url: "https://github.com/assistant-ui/assistant-ui-starter-minimal",
36
+ {
37
+ name: "minimal",
17
38
  label: "Minimal",
18
- hint: "Bare-bones starting point",
39
+ description: "Bare-bones starting point",
40
+ category: "template",
41
+ path: "templates/minimal",
42
+ hasLocalComponents: true,
19
43
  },
20
- cloud: {
21
- url: "https://github.com/assistant-ui/assistant-ui-starter-cloud",
44
+ {
45
+ name: "cloud",
22
46
  label: "Cloud",
23
- hint: "Cloud-backed persistence starter",
47
+ description: "Cloud-backed persistence starter",
48
+ category: "template",
49
+ path: "templates/cloud",
50
+ hasLocalComponents: true,
24
51
  },
25
- "cloud-clerk": {
26
- url: "https://github.com/assistant-ui/assistant-ui-starter-cloud-clerk",
52
+ {
53
+ name: "cloud-clerk",
27
54
  label: "Cloud + Clerk",
28
- hint: "Cloud-backed starter with Clerk auth",
55
+ description: "Cloud-backed starter with Clerk auth",
56
+ category: "template",
57
+ path: "templates/cloud-clerk",
58
+ hasLocalComponents: true,
29
59
  },
30
- langgraph: {
31
- url: "https://github.com/assistant-ui/assistant-ui-starter-langgraph",
60
+ {
61
+ name: "langgraph",
32
62
  label: "LangGraph",
33
- hint: "LangGraph starter template",
63
+ description: "LangGraph starter template",
64
+ category: "template",
65
+ path: "templates/langgraph",
66
+ hasLocalComponents: true,
34
67
  },
35
- mcp: {
36
- url: "https://github.com/assistant-ui/assistant-ui-starter-mcp",
68
+ {
69
+ name: "mcp",
37
70
  label: "MCP",
38
- hint: "MCP starter template",
71
+ description: "MCP starter template",
72
+ category: "template",
73
+ path: "templates/mcp",
74
+ hasLocalComponents: true,
39
75
  },
40
- } as const;
41
-
42
- type TemplateName = keyof typeof templates;
43
- const templateNames = Object.keys(templates) as TemplateName[];
44
-
45
- const templatePickerOptions: Array<{
46
- value: TemplateName;
47
- label: string;
48
- hint: string;
49
- }> = templateNames.map((name) => ({
50
- value: name,
51
- label: templates[name].label,
52
- hint: templates[name].hint,
53
- }));
54
-
55
- export async function resolveCreateTemplateName(params: {
76
+ // Examples
77
+ {
78
+ name: "with-ag-ui",
79
+ label: "AG-UI",
80
+ description: "AG-UI protocol integration",
81
+ category: "example",
82
+ path: "examples/with-ag-ui",
83
+ hasLocalComponents: false,
84
+ },
85
+ {
86
+ name: "with-ai-sdk-v6",
87
+ label: "AI SDK v6",
88
+ description: "Vercel AI SDK v6",
89
+ category: "example",
90
+ path: "examples/with-ai-sdk-v6",
91
+ hasLocalComponents: false,
92
+ },
93
+ {
94
+ name: "with-artifacts",
95
+ label: "Artifacts",
96
+ description: "Artifact rendering",
97
+ category: "example",
98
+ path: "examples/with-artifacts",
99
+ hasLocalComponents: false,
100
+ },
101
+ {
102
+ name: "with-assistant-transport",
103
+ label: "Assistant Transport",
104
+ description: "Assistant transport protocol",
105
+ category: "example",
106
+ path: "examples/with-assistant-transport",
107
+ hasLocalComponents: false,
108
+ },
109
+ {
110
+ name: "with-chain-of-thought",
111
+ label: "Chain of Thought",
112
+ description: "Chain-of-thought rendering",
113
+ category: "example",
114
+ path: "examples/with-chain-of-thought",
115
+ hasLocalComponents: false,
116
+ },
117
+ {
118
+ name: "with-cloud",
119
+ label: "Cloud Example",
120
+ description: "Cloud integration example",
121
+ category: "example",
122
+ path: "examples/with-cloud",
123
+ hasLocalComponents: false,
124
+ },
125
+ {
126
+ name: "with-custom-thread-list",
127
+ label: "Custom Thread List",
128
+ description: "Custom thread list UI",
129
+ category: "example",
130
+ path: "examples/with-custom-thread-list",
131
+ hasLocalComponents: false,
132
+ },
133
+ {
134
+ name: "with-elevenlabs-scribe",
135
+ label: "ElevenLabs Scribe",
136
+ description: "Audio/speech integration",
137
+ category: "example",
138
+ path: "examples/with-elevenlabs-scribe",
139
+ hasLocalComponents: false,
140
+ },
141
+ {
142
+ name: "with-expo",
143
+ label: "Expo",
144
+ description: "Expo / React Native",
145
+ category: "example",
146
+ path: "examples/with-expo",
147
+ hasLocalComponents: true,
148
+ },
149
+ {
150
+ name: "with-external-store",
151
+ label: "External Store",
152
+ description: "Custom message store",
153
+ category: "example",
154
+ path: "examples/with-external-store",
155
+ hasLocalComponents: false,
156
+ },
157
+ {
158
+ name: "with-ffmpeg",
159
+ label: "FFmpeg",
160
+ description: "File processing",
161
+ category: "example",
162
+ path: "examples/with-ffmpeg",
163
+ hasLocalComponents: false,
164
+ },
165
+ {
166
+ name: "with-langgraph",
167
+ label: "LangGraph Example",
168
+ description: "LangGraph integration",
169
+ category: "example",
170
+ path: "examples/with-langgraph",
171
+ hasLocalComponents: false,
172
+ },
173
+ {
174
+ name: "with-parent-id-grouping",
175
+ label: "Parent ID Grouping",
176
+ description: "Message grouping strategy",
177
+ category: "example",
178
+ path: "examples/with-parent-id-grouping",
179
+ hasLocalComponents: false,
180
+ },
181
+ {
182
+ name: "with-react-hook-form",
183
+ label: "React Hook Form",
184
+ description: "Form integration",
185
+ category: "example",
186
+ path: "examples/with-react-hook-form",
187
+ hasLocalComponents: false,
188
+ },
189
+ {
190
+ name: "with-react-ink",
191
+ label: "React Ink",
192
+ description: "Terminal UI chat",
193
+ category: "example",
194
+ path: "examples/with-react-ink",
195
+ hasLocalComponents: true,
196
+ },
197
+ {
198
+ name: "with-react-router",
199
+ label: "React Router",
200
+ description: "React Router v7 + Vite",
201
+ category: "example",
202
+ path: "examples/with-react-router",
203
+ hasLocalComponents: false,
204
+ },
205
+ {
206
+ name: "with-tanstack",
207
+ label: "TanStack",
208
+ description: "TanStack/React Router + Vite",
209
+ category: "example",
210
+ path: "examples/with-tanstack",
211
+ hasLocalComponents: false,
212
+ },
213
+ ];
214
+
215
+ // Examples that exist in the monorepo but are intentionally excluded from the CLI:
216
+ //
217
+ // - waterfall: Still in development, not ready for production.
218
+ // - with-cloud-standalone: For cloud without assistant-ui — not for the
219
+ // assistant-ui CLI.
220
+ // - with-store: In development, not ready for public use of the tap store.
221
+ // - with-tap-runtime: In development, not ready for public use of the tap
222
+ // store.
223
+
224
+ const templateNames = PROJECT_METADATA.filter(
225
+ (m) => m.category === "template",
226
+ ).map((m) => m.name);
227
+
228
+ const exampleNames = PROJECT_METADATA.filter(
229
+ (m) => m.category === "example",
230
+ ).map((m) => m.name);
231
+
232
+ export async function resolveProject(params: {
56
233
  template?: string;
234
+ example?: string;
57
235
  stdinIsTTY?: boolean;
58
236
  select?: typeof p.select;
59
237
  isCancel?: typeof p.isCancel;
60
- }): Promise<TemplateName | null> {
238
+ }): Promise<ProjectMetadata | null> {
61
239
  const {
62
240
  template,
241
+ example,
63
242
  stdinIsTTY = process.stdin.isTTY,
64
243
  select = p.select,
65
244
  isCancel = p.isCancel,
66
245
  } = params;
67
246
 
68
247
  if (template) {
69
- return template as TemplateName;
248
+ const meta = PROJECT_METADATA.find(
249
+ (m) => m.name === template && m.category === "template",
250
+ );
251
+ if (!meta) {
252
+ logger.error(`Unknown template: ${template}`);
253
+ logger.info(`Available templates: ${templateNames.join(", ")}`);
254
+ process.exit(1);
255
+ }
256
+ return meta;
257
+ }
258
+
259
+ if (example) {
260
+ const meta = PROJECT_METADATA.find(
261
+ (m) => m.name === example && m.category === "example",
262
+ );
263
+ if (!meta) {
264
+ logger.error(`Unknown example: ${example}`);
265
+ logger.info(`Available examples: ${exampleNames.join(", ")}`);
266
+ process.exit(1);
267
+ }
268
+ return meta;
70
269
  }
71
270
 
72
271
  if (!stdinIsTTY) {
73
- return "default";
272
+ return PROJECT_METADATA.find((m) => m.name === "default")!;
74
273
  }
75
274
 
76
275
  const selected = await select({
77
- message: "Select a template:",
78
- options: templatePickerOptions,
276
+ message: "Select a project to scaffold:",
277
+ options: [
278
+ {
279
+ value: "_separator",
280
+ label: "────── Starter Templates ──────",
281
+ disabled: true,
282
+ },
283
+ ...PROJECT_METADATA.filter((m) => m.category === "template").map((m) => ({
284
+ value: m.name,
285
+ label: m.label,
286
+ ...(m.description ? { hint: m.description } : {}),
287
+ })),
288
+ {
289
+ value: "_separator",
290
+ label: "────── Feature Examples ──────",
291
+ disabled: true,
292
+ },
293
+ ...PROJECT_METADATA.filter((m) => m.category === "example").map((m) => ({
294
+ value: m.name,
295
+ label: m.label,
296
+ ...(m.description ? { hint: m.description } : {}),
297
+ })),
298
+ ],
79
299
  });
80
300
 
81
301
  if (isCancel(selected)) {
82
302
  return null;
83
303
  }
84
304
 
85
- return selected as TemplateName;
305
+ const meta = PROJECT_METADATA.find((m) => m.name === selected);
306
+ if (!meta) {
307
+ logger.error(`Unknown selection: ${String(selected)}`);
308
+ process.exit(1);
309
+ }
310
+ return meta;
86
311
  }
87
312
 
88
313
  class SpawnExitError extends Error {
@@ -116,37 +341,6 @@ async function runSpawn(
116
341
  });
117
342
  }
118
343
 
119
- export function buildCreateNextAppArgs(params: {
120
- projectDirectory?: string;
121
- useNpm?: boolean;
122
- usePnpm?: boolean;
123
- useYarn?: boolean;
124
- useBun?: boolean;
125
- skipInstall?: boolean;
126
- templateUrl: string;
127
- }): string[] {
128
- const {
129
- projectDirectory,
130
- useNpm,
131
- usePnpm,
132
- useYarn,
133
- useBun,
134
- skipInstall,
135
- templateUrl,
136
- } = params;
137
-
138
- const args = ["create-next-app@latest"];
139
- if (projectDirectory) args.push(projectDirectory);
140
- if (useNpm) args.push("--use-npm");
141
- if (usePnpm) args.push("--use-pnpm");
142
- if (useYarn) args.push("--use-yarn");
143
- if (useBun) args.push("--use-bun");
144
- if (skipInstall) args.push("--skip-install");
145
-
146
- args.push("-e", templateUrl);
147
- return args;
148
- }
149
-
150
344
  export function resolveCreateProjectDirectory(params: {
151
345
  projectDirectory?: string;
152
346
  stdinIsTTY?: boolean;
@@ -158,8 +352,27 @@ export function resolveCreateProjectDirectory(params: {
158
352
  return undefined;
159
353
  }
160
354
 
161
- function buildPresetAddArgs(presetUrl: string): string[] {
162
- return ["shadcn@latest", "add", "--yes", presetUrl];
355
+ function resolvePackageManager(opts: {
356
+ useNpm?: boolean;
357
+ usePnpm?: boolean;
358
+ useYarn?: boolean;
359
+ useBun?: boolean;
360
+ }): PackageManagerName | undefined {
361
+ if (opts.useNpm) return "npm";
362
+ if (opts.usePnpm) return "pnpm";
363
+ if (opts.useYarn) return "yarn";
364
+ if (opts.useBun) return "bun";
365
+ return undefined;
366
+ }
367
+
368
+ const PLAYGROUND_PRESET_BASE_URL =
369
+ "https://www.assistant-ui.com/playground/init";
370
+
371
+ export function resolvePresetUrl(preset: string): string {
372
+ if (preset.startsWith("http://") || preset.startsWith("https://")) {
373
+ return preset;
374
+ }
375
+ return `${PLAYGROUND_PRESET_BASE_URL}?preset=${encodeURIComponent(preset)}`;
163
376
  }
164
377
 
165
378
  export const create = new Command()
@@ -173,100 +386,210 @@ export const create = new Command()
173
386
  )
174
387
  .option(
175
388
  "-e, --example <example>",
176
- "create from an example (e.g., with-langgraph, with-ai-sdk-v6)",
389
+ `create from an example (${exampleNames.join(", ")})`,
177
390
  )
178
391
  .option(
179
- "-p, --preset <url>",
180
- "preset URL from playground (e.g., https://www.assistant-ui.com/playground/init?preset=chatgpt)",
392
+ "-p, --preset <name-or-url>",
393
+ "preset name or URL (e.g., chatgpt or https://www.assistant-ui.com/playground/init?preset=chatgpt)",
181
394
  )
182
395
  .option("--use-npm", "explicitly use npm")
183
396
  .option("--use-pnpm", "explicitly use pnpm")
184
397
  .option("--use-yarn", "explicitly use yarn")
185
398
  .option("--use-bun", "explicitly use bun")
399
+ .option("--native", "create an Expo / React Native project")
186
400
  .option("--skip-install", "skip installing packages")
187
401
  .action(async (projectDirectory, opts) => {
188
- const resolvedProjectDirectory = resolveCreateProjectDirectory({
189
- projectDirectory,
190
- });
402
+ if (opts.native) {
403
+ opts.example = "with-expo";
404
+ }
191
405
 
192
406
  if (opts.example && opts.preset) {
193
407
  logger.error("Cannot use --preset with --example.");
194
408
  process.exit(1);
195
409
  }
196
410
 
197
- if (opts.preset && !resolvedProjectDirectory) {
198
- logger.error("Project directory is required when using --preset.");
411
+ if (opts.template && opts.example) {
412
+ logger.error("Cannot use both --template and --example.");
199
413
  process.exit(1);
200
414
  }
201
415
 
202
- // Handle --example option
203
- if (opts.example) {
204
- if (!resolvedProjectDirectory) {
205
- logger.error("Project directory is required when using --example");
206
- process.exit(1);
207
- }
416
+ // Start release ref resolution early (runs during user prompts)
417
+ const refPromise = resolveLatestReleaseRef();
418
+
419
+ // 1. Resolve project directory
420
+ let resolvedProjectDirectory = resolveCreateProjectDirectory({
421
+ projectDirectory,
422
+ });
208
423
 
209
- await createFromExample(resolvedProjectDirectory, opts.example, {
210
- skipInstall: opts.skipInstall,
211
- useNpm: opts.useNpm,
212
- usePnpm: opts.usePnpm,
213
- useYarn: opts.useYarn,
214
- useBun: opts.useBun,
424
+ if (!resolvedProjectDirectory) {
425
+ const result = await p.text({
426
+ message: "Project name:",
427
+ placeholder: "my-aui-app",
428
+ defaultValue: "my-aui-app",
429
+ validate: (value?: string) => {
430
+ const name = (value ?? "").trim();
431
+ if (!name) return "Project name cannot be empty";
432
+ if (name === "." || name === "..")
433
+ return "Project name cannot be . or ..";
434
+ if (name.includes("/") || name.includes("\\"))
435
+ return "Project name cannot contain path separators";
436
+ return undefined;
437
+ },
215
438
  });
216
- return;
439
+
440
+ if (p.isCancel(result)) {
441
+ p.cancel("Project creation cancelled.");
442
+ process.exit(0);
443
+ }
444
+
445
+ resolvedProjectDirectory = result;
217
446
  }
218
447
 
219
- // Handle --template option
220
- const templateName = await resolveCreateTemplateName({
448
+ // Check directory
449
+ const absoluteProjectDir = path.resolve(resolvedProjectDirectory);
450
+ try {
451
+ const files = fs.readdirSync(absoluteProjectDir);
452
+ if (files.length > 0) {
453
+ logger.error(
454
+ `Directory ${resolvedProjectDirectory} already exists and is not empty`,
455
+ );
456
+ process.exit(1);
457
+ }
458
+ } catch (err: any) {
459
+ if (err.code === "ENOENT") {
460
+ // Directory doesn't exist — good, proceed
461
+ } else if (err.code === "ENOTDIR") {
462
+ logger.error(
463
+ `${resolvedProjectDirectory} already exists and is not a directory`,
464
+ );
465
+ process.exit(1);
466
+ } else {
467
+ logger.error(
468
+ `Cannot access ${resolvedProjectDirectory}: ${err.message}`,
469
+ );
470
+ process.exit(1);
471
+ }
472
+ }
473
+
474
+ // 2. Resolve scaffold target
475
+ const project = await resolveProject({
221
476
  template: opts.template,
477
+ example: opts.example,
222
478
  });
223
- if (!templateName) {
479
+ if (!project) {
224
480
  p.cancel("Project creation cancelled.");
225
481
  process.exit(0);
226
482
  }
227
483
 
228
- const templateUrl = templates[templateName]?.url;
229
-
230
- if (!templateUrl) {
231
- logger.error(`Unknown template: ${opts.template}`);
232
- logger.info(`Available templates: ${templateNames.join(", ")}`);
233
- process.exit(1);
234
- }
235
-
236
- logger.info(`Creating project with template: ${templateName}`);
484
+ logger.info(`Creating project from ${project.category}: ${project.label}`);
237
485
  logger.break();
238
486
 
239
- const createNextAppArgs = buildCreateNextAppArgs({
240
- ...(resolvedProjectDirectory
241
- ? { projectDirectory: resolvedProjectDirectory }
242
- : {}),
243
- ...(opts.useNpm ? { useNpm: true } : {}),
244
- ...(opts.usePnpm ? { usePnpm: true } : {}),
245
- ...(opts.useYarn ? { useYarn: true } : {}),
246
- ...(opts.useBun ? { useBun: true } : {}),
247
- ...(opts.skipInstall ? { skipInstall: true } : {}),
248
- templateUrl,
249
- });
487
+ const pm = await resolvePackageManagerName(
488
+ absoluteProjectDir,
489
+ resolvePackageManager(opts),
490
+ );
491
+
492
+ // Clean up partial project directory on unexpected exit (e.g. Ctrl+C)
493
+ const cleanupOnExit = () => {
494
+ fs.rmSync(absoluteProjectDir, { recursive: true, force: true });
495
+ };
496
+ process.once("exit", cleanupOnExit);
250
497
 
251
498
  try {
252
- await runSpawn("npx", createNextAppArgs);
499
+ // 3. Resolve latest release ref (started before prompts)
500
+ logger.step("Resolving latest release...");
501
+ const ref = await refPromise;
502
+ if (!ref) {
503
+ logger.warn("Could not resolve latest release, downloading from HEAD");
504
+ }
253
505
 
254
- if (opts.preset) {
255
- if (!resolvedProjectDirectory) {
256
- logger.error("Project directory is required when using --preset.");
257
- process.exit(1);
506
+ // 4. Download project
507
+ logger.step("Downloading project...");
508
+ try {
509
+ await downloadProject(project.path, absoluteProjectDir, ref);
510
+
511
+ // If the template didn't exist at the release tag, retry from HEAD
512
+ if (
513
+ ref &&
514
+ !fs.existsSync(path.join(absoluteProjectDir, "package.json"))
515
+ ) {
516
+ fs.rmSync(absoluteProjectDir, { recursive: true, force: true });
517
+ logger.warn(
518
+ "Template not found at release tag, downloading from HEAD",
519
+ );
520
+ await downloadProject(project.path, absoluteProjectDir);
258
521
  }
522
+
523
+ // 5. Run transform pipeline
524
+ await transformProject(absoluteProjectDir, {
525
+ hasLocalComponents: project.hasLocalComponents,
526
+ skipInstall: opts.skipInstall,
527
+ packageManager: pm,
528
+ });
529
+ } catch (err) {
530
+ // Clean up partially created project directory
531
+ fs.rmSync(absoluteProjectDir, { recursive: true, force: true });
532
+ throw err;
533
+ }
534
+
535
+ // 6. Apply preset if provided
536
+ if (opts.preset) {
537
+ const presetUrl = resolvePresetUrl(opts.preset);
259
538
  logger.info("Applying preset configuration...");
260
539
  logger.break();
261
- await runSpawn(
262
- "npx",
263
- buildPresetAddArgs(opts.preset),
264
- path.resolve(process.cwd(), resolvedProjectDirectory),
265
- );
540
+ const [dlxCmd, dlxArgs] = dlxCommand(pm);
541
+ try {
542
+ await runSpawn(
543
+ dlxCmd,
544
+ [
545
+ ...dlxArgs,
546
+ "shadcn@latest",
547
+ "add",
548
+ "--yes",
549
+ "--overwrite",
550
+ presetUrl,
551
+ ],
552
+ absoluteProjectDir,
553
+ );
554
+ } catch {
555
+ logger.warn(
556
+ `Preset application failed. You can retry manually with:\n ${dlxCmd} ${[...dlxArgs, "shadcn@latest", "add", presetUrl].join(" ")}`,
557
+ );
558
+ }
266
559
  }
267
560
 
561
+ process.removeListener("exit", cleanupOnExit);
562
+
268
563
  logger.break();
269
564
  logger.success("Project created successfully!");
565
+ logger.break();
566
+ const runCmd = pm === "npm" ? "npm run" : pm;
567
+ let devScript = "dev";
568
+ let envFile = ".env.local";
569
+ try {
570
+ const scaffoldedPkg = JSON.parse(
571
+ fs.readFileSync(
572
+ path.join(absoluteProjectDir, "package.json"),
573
+ "utf-8",
574
+ ),
575
+ );
576
+ devScript = scaffoldedPkg.scripts?.dev
577
+ ? "dev"
578
+ : scaffoldedPkg.scripts?.start
579
+ ? "start"
580
+ : "dev";
581
+ envFile = scaffoldedPkg.dependencies?.next ? ".env.local" : ".env";
582
+ } catch {
583
+ // Fall back to defaults if package.json cannot be read
584
+ }
585
+
586
+ logger.info("Next steps:");
587
+ logger.info(` cd ${resolvedProjectDirectory}`);
588
+ if (opts.skipInstall) {
589
+ logger.info(` ${pm} install`);
590
+ }
591
+ logger.info(` # Set up your environment variables in ${envFile}`);
592
+ logger.info(` ${runCmd} ${devScript}`);
270
593
  } catch (error) {
271
594
  if (error instanceof SpawnExitError) {
272
595
  logger.error(`Project creation failed with code ${error.code}`);