create-bunli 0.5.4 → 0.6.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.
package/dist/cli.js CHANGED
@@ -14,10 +14,13 @@ async function processTemplate(options) {
14
14
  let templateDir;
15
15
  if (source.startsWith("/") || source.startsWith("./") || source.startsWith("../")) {
16
16
  const sourceDir = source.startsWith("/") ? source : join(process.cwd(), source);
17
- await Bun.spawn(["cp", "-r", sourceDir + "/.", dir], {
17
+ const copyExitCode = await Bun.spawn(["cp", "-r", sourceDir + "/.", dir], {
18
18
  stdout: "inherit",
19
19
  stderr: "inherit"
20
20
  }).exited;
21
+ if (copyExitCode !== 0) {
22
+ throw new Error(`Failed to copy local template from ${sourceDir}`);
23
+ }
21
24
  templateDir = dir;
22
25
  } else {
23
26
  const result = await downloadTemplate(source, {
@@ -50,12 +53,13 @@ async function loadTemplateManifest(dir) {
50
53
  const content = await file.text();
51
54
  if (path.endsWith(".json")) {
52
55
  const manifest = JSON.parse(content);
53
- try {
54
- await Bun.spawn(["rm", "-f", path], {
55
- stdout: "ignore",
56
- stderr: "ignore"
57
- }).exited;
58
- } catch {}
56
+ const removeExitCode = await Bun.spawn(["rm", "-f", path], {
57
+ stdout: "ignore",
58
+ stderr: "ignore"
59
+ }).exited;
60
+ if (removeExitCode !== 0) {
61
+ console.warn(`Warning: failed to remove template manifest ${path}`);
62
+ }
59
63
  return manifest;
60
64
  }
61
65
  }
@@ -120,7 +124,10 @@ async function runPostInstallHooks(dir, hooks) {
120
124
  stdout: "inherit",
121
125
  stderr: "inherit"
122
126
  });
123
- await proc.exited;
127
+ const exitCode = await proc.exited;
128
+ if (exitCode !== 0) {
129
+ throw new Error(`Post-install hook failed: ${hook}`);
130
+ }
124
131
  }
125
132
  }
126
133
  function resolveTemplateSource(template) {
@@ -151,85 +158,558 @@ async function isLocalTemplate(template) {
151
158
  return await Bun.file(join(bundledPath, "package.json")).exists();
152
159
  }
153
160
 
161
+ // ../../node_modules/better-result/dist/index.mjs
162
+ function dual(arity, body) {
163
+ if (arity === 2)
164
+ return (...args) => {
165
+ if (args.length >= 2)
166
+ return body(args[0], args[1]);
167
+ return (self) => body(self, args[0]);
168
+ };
169
+ if (arity === 3)
170
+ return (...args) => {
171
+ if (args.length >= 3)
172
+ return body(args[0], args[1], args[2]);
173
+ return (self) => body(self, args[0], args[1]);
174
+ };
175
+ if (arity === 4)
176
+ return (...args) => {
177
+ if (args.length >= 4)
178
+ return body(args[0], args[1], args[2], args[3]);
179
+ return (self) => body(self, args[0], args[1], args[2]);
180
+ };
181
+ return (...args) => {
182
+ if (args.length >= arity)
183
+ return body(...args);
184
+ return (self) => body(self, ...args);
185
+ };
186
+ }
187
+ var serializeCause = (cause) => {
188
+ if (cause instanceof Error)
189
+ return {
190
+ name: cause.name,
191
+ message: cause.message,
192
+ stack: cause.stack
193
+ };
194
+ return cause;
195
+ };
196
+ var isAnyTaggedError = (value) => {
197
+ return value instanceof Error && "_tag" in value && typeof value._tag === "string";
198
+ };
199
+ var TaggedError = Object.assign((tag) => () => {
200
+
201
+ class Base extends Error {
202
+ _tag = tag;
203
+ static is(value) {
204
+ return value instanceof Base;
205
+ }
206
+ constructor(args) {
207
+ const message = args && "message" in args && typeof args.message === "string" ? args.message : undefined;
208
+ const cause = args && "cause" in args ? args.cause : undefined;
209
+ super(message, cause !== undefined ? { cause } : undefined);
210
+ if (args)
211
+ Object.assign(this, args);
212
+ Object.setPrototypeOf(this, new.target.prototype);
213
+ this.name = tag;
214
+ if (cause instanceof Error && cause.stack) {
215
+ const indented = cause.stack.replace(/\n/g, `
216
+ `);
217
+ this.stack = `${this.stack}
218
+ Caused by: ${indented}`;
219
+ }
220
+ }
221
+ toJSON() {
222
+ return {
223
+ ...this,
224
+ _tag: this._tag,
225
+ name: this.name,
226
+ message: this.message,
227
+ cause: serializeCause(this.cause),
228
+ stack: this.stack
229
+ };
230
+ }
231
+ }
232
+ return Base;
233
+ }, { is: isAnyTaggedError });
234
+ var matchError = dual(2, (err$1, handlers) => {
235
+ const handler = handlers[err$1._tag];
236
+ return handler(err$1);
237
+ });
238
+ var matchErrorPartial = dual(3, (err$1, handlers, fallback) => {
239
+ const handler = handlers[err$1._tag];
240
+ if (typeof handler === "function")
241
+ return handler(err$1);
242
+ return fallback(err$1);
243
+ });
244
+ var UnhandledException = class extends TaggedError("UnhandledException")() {
245
+ constructor(args) {
246
+ const message = args.cause instanceof Error ? `Unhandled exception: ${args.cause.message}` : `Unhandled exception: ${String(args.cause)}`;
247
+ super({
248
+ message,
249
+ cause: args.cause
250
+ });
251
+ }
252
+ };
253
+ var Panic = class extends TaggedError("Panic")() {
254
+ };
255
+ var ResultDeserializationError = class extends TaggedError("ResultDeserializationError")() {
256
+ constructor(args) {
257
+ super({
258
+ message: `Failed to deserialize value as Result: expected { status: "ok", value } or { status: "error", error }`,
259
+ value: args.value
260
+ });
261
+ }
262
+ };
263
+ var panic = (message, cause) => {
264
+ throw new Panic({
265
+ message,
266
+ cause
267
+ });
268
+ };
269
+ var tryOrPanic = (fn, message) => {
270
+ try {
271
+ return fn();
272
+ } catch (cause) {
273
+ throw panic(message, cause);
274
+ }
275
+ };
276
+ var tryOrPanicAsync = async (fn, message) => {
277
+ try {
278
+ return await fn();
279
+ } catch (cause) {
280
+ throw panic(message, cause);
281
+ }
282
+ };
283
+ var Ok = class Ok2 {
284
+ status = "ok";
285
+ constructor(value) {
286
+ this.value = value;
287
+ }
288
+ isOk() {
289
+ return true;
290
+ }
291
+ isErr() {
292
+ return false;
293
+ }
294
+ map(fn) {
295
+ return tryOrPanic(() => new Ok2(fn(this.value)), "map callback threw");
296
+ }
297
+ mapError(_fn) {
298
+ return this;
299
+ }
300
+ andThen(fn) {
301
+ return tryOrPanic(() => fn(this.value), "andThen callback threw");
302
+ }
303
+ andThenAsync(fn) {
304
+ return tryOrPanicAsync(() => fn(this.value), "andThenAsync callback threw");
305
+ }
306
+ match(handlers) {
307
+ return tryOrPanic(() => handlers.ok(this.value), "match ok handler threw");
308
+ }
309
+ unwrap(_message) {
310
+ return this.value;
311
+ }
312
+ unwrapOr(_fallback) {
313
+ return this.value;
314
+ }
315
+ tap(fn) {
316
+ return tryOrPanic(() => {
317
+ fn(this.value);
318
+ return this;
319
+ }, "tap callback threw");
320
+ }
321
+ tapAsync(fn) {
322
+ return tryOrPanicAsync(async () => {
323
+ await fn(this.value);
324
+ return this;
325
+ }, "tapAsync callback threw");
326
+ }
327
+ *[Symbol.iterator]() {
328
+ return this.value;
329
+ }
330
+ };
331
+ var Err = class Err2 {
332
+ status = "error";
333
+ constructor(error) {
334
+ this.error = error;
335
+ }
336
+ isOk() {
337
+ return false;
338
+ }
339
+ isErr() {
340
+ return true;
341
+ }
342
+ map(_fn) {
343
+ return this;
344
+ }
345
+ mapError(fn) {
346
+ return tryOrPanic(() => new Err2(fn(this.error)), "mapError callback threw");
347
+ }
348
+ andThen(_fn) {
349
+ return this;
350
+ }
351
+ andThenAsync(_fn) {
352
+ return Promise.resolve(this);
353
+ }
354
+ match(handlers) {
355
+ return tryOrPanic(() => handlers.err(this.error), "match err handler threw");
356
+ }
357
+ unwrap(message) {
358
+ return panic(message ?? `Unwrap called on Err: ${String(this.error)}`, this.error);
359
+ }
360
+ unwrapOr(fallback) {
361
+ return fallback;
362
+ }
363
+ tap(_fn) {
364
+ return this;
365
+ }
366
+ tapAsync(_fn) {
367
+ return Promise.resolve(this);
368
+ }
369
+ *[Symbol.iterator]() {
370
+ yield this;
371
+ return panic("Unreachable: Err yielded in Result.gen but generator continued", this.error);
372
+ }
373
+ };
374
+ function ok(value) {
375
+ return new Ok(value);
376
+ }
377
+ var isOk = (result) => {
378
+ return result.status === "ok";
379
+ };
380
+ var err = (error) => new Err(error);
381
+ var isError = (result) => {
382
+ return result.status === "error";
383
+ };
384
+ var tryFn = (options, config) => {
385
+ const execute = () => {
386
+ if (typeof options === "function")
387
+ try {
388
+ return ok(options());
389
+ } catch (cause) {
390
+ return err(new UnhandledException({ cause }));
391
+ }
392
+ try {
393
+ return ok(options.try());
394
+ } catch (originalCause) {
395
+ try {
396
+ return err(options.catch(originalCause));
397
+ } catch (catchHandlerError) {
398
+ throw panic("Result.try catch handler threw", catchHandlerError);
399
+ }
400
+ }
401
+ };
402
+ const times = config?.retry?.times ?? 0;
403
+ let result = execute();
404
+ for (let retry = 0;retry < times && result.status === "error"; retry++)
405
+ result = execute();
406
+ return result;
407
+ };
408
+ var tryPromise = async (options, config) => {
409
+ const execute = async () => {
410
+ if (typeof options === "function")
411
+ try {
412
+ return ok(await options());
413
+ } catch (cause) {
414
+ return err(new UnhandledException({ cause }));
415
+ }
416
+ try {
417
+ return ok(await options.try());
418
+ } catch (originalCause) {
419
+ try {
420
+ return err(await options.catch(originalCause));
421
+ } catch (catchHandlerError) {
422
+ throw panic("Result.tryPromise catch handler threw", catchHandlerError);
423
+ }
424
+ }
425
+ };
426
+ const retry = config?.retry;
427
+ if (!retry)
428
+ return execute();
429
+ const getDelay = (retryAttempt) => {
430
+ switch (retry.backoff) {
431
+ case "constant":
432
+ return retry.delayMs;
433
+ case "linear":
434
+ return retry.delayMs * (retryAttempt + 1);
435
+ case "exponential":
436
+ return retry.delayMs * 2 ** retryAttempt;
437
+ }
438
+ };
439
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
440
+ let result = await execute();
441
+ const shouldRetryFn = retry.shouldRetry ?? (() => true);
442
+ for (let attempt = 0;attempt < retry.times; attempt++) {
443
+ if (result.status !== "error")
444
+ break;
445
+ const error = result.error;
446
+ if (!tryOrPanic(() => shouldRetryFn(error), "shouldRetry predicate threw"))
447
+ break;
448
+ await sleep(getDelay(attempt));
449
+ result = await execute();
450
+ }
451
+ return result;
452
+ };
453
+ var map = dual(2, (result, fn) => {
454
+ return result.map(fn);
455
+ });
456
+ var mapError = dual(2, (result, fn) => {
457
+ return result.mapError(fn);
458
+ });
459
+ var andThen = dual(2, (result, fn) => {
460
+ return result.andThen(fn);
461
+ });
462
+ var andThenAsync = dual(2, (result, fn) => {
463
+ return result.andThenAsync(fn);
464
+ });
465
+ var match = dual(2, (result, handlers) => {
466
+ return result.match(handlers);
467
+ });
468
+ var tap = dual(2, (result, fn) => {
469
+ return result.tap(fn);
470
+ });
471
+ var tapAsync = dual(2, (result, fn) => {
472
+ return result.tapAsync(fn);
473
+ });
474
+ var unwrap = (result, message) => {
475
+ return result.unwrap(message);
476
+ };
477
+ function assertIsResult(value) {
478
+ if (value !== null && typeof value === "object" && "status" in value && (value.status === "ok" || value.status === "error"))
479
+ return;
480
+ return panic("Result.gen body must return Result.ok() or Result.err(), got: " + (value === null ? "null" : typeof value === "object" ? JSON.stringify(value) : String(value)));
481
+ }
482
+ var unwrapOr = dual(2, (result, fallback) => {
483
+ return result.unwrapOr(fallback);
484
+ });
485
+ var gen = (body, thisArg) => {
486
+ const iterator = body.call(thisArg);
487
+ if (Symbol.asyncIterator in iterator)
488
+ return (async () => {
489
+ const asyncIter = iterator;
490
+ let state$1;
491
+ try {
492
+ state$1 = await asyncIter.next();
493
+ } catch (cause) {
494
+ throw panic("generator body threw", cause);
495
+ }
496
+ assertIsResult(state$1.value);
497
+ if (!state$1.done)
498
+ try {
499
+ await asyncIter.return?.(undefined);
500
+ } catch (cause) {
501
+ throw panic("generator cleanup threw", cause);
502
+ }
503
+ return state$1.value;
504
+ })();
505
+ const syncIter = iterator;
506
+ let state;
507
+ try {
508
+ state = syncIter.next();
509
+ } catch (cause) {
510
+ throw panic("generator body threw", cause);
511
+ }
512
+ assertIsResult(state.value);
513
+ if (!state.done)
514
+ try {
515
+ syncIter.return?.(undefined);
516
+ } catch (cause) {
517
+ throw panic("generator cleanup threw", cause);
518
+ }
519
+ return state.value;
520
+ };
521
+ async function* resultAwait(promise) {
522
+ return yield* await promise;
523
+ }
524
+ function isSerializedResult(obj) {
525
+ return obj !== null && typeof obj === "object" && "status" in obj && (obj.status === "ok" && ("value" in obj) || obj.status === "error" && ("error" in obj));
526
+ }
527
+ var serialize = (result) => {
528
+ return result.status === "ok" ? {
529
+ status: "ok",
530
+ value: result.value
531
+ } : {
532
+ status: "error",
533
+ error: result.error
534
+ };
535
+ };
536
+ var deserialize = (value) => {
537
+ if (isSerializedResult(value))
538
+ return value.status === "ok" ? new Ok(value.value) : new Err(value.error);
539
+ return err(new ResultDeserializationError({ value }));
540
+ };
541
+ var hydrate = (value) => {
542
+ return deserialize(value);
543
+ };
544
+ var partition = (results) => {
545
+ const oks = [];
546
+ const errs = [];
547
+ for (const r of results)
548
+ if (r.status === "ok")
549
+ oks.push(r.value);
550
+ else
551
+ errs.push(r.error);
552
+ return [oks, errs];
553
+ };
554
+ var flatten = (result) => {
555
+ if (result.status === "ok")
556
+ return result.value;
557
+ return result;
558
+ };
559
+ var Result = {
560
+ ok,
561
+ isOk,
562
+ err,
563
+ isError,
564
+ try: tryFn,
565
+ tryPromise,
566
+ map,
567
+ mapError,
568
+ andThen,
569
+ andThenAsync,
570
+ match,
571
+ tap,
572
+ tapAsync,
573
+ unwrap,
574
+ unwrapOr,
575
+ gen,
576
+ await: resultAwait,
577
+ serialize,
578
+ deserialize,
579
+ hydrate,
580
+ partition,
581
+ flatten
582
+ };
583
+
154
584
  // src/create-project.ts
585
+ var toErrorMessage = (error) => error instanceof Error ? error.message : String(error);
586
+ var tryAsync = (fn, mapError2) => Result.tryPromise({ try: fn, catch: mapError2 });
587
+
588
+ class UserCancelledError extends TaggedError("UserCancelledError")() {
589
+ constructor(message) {
590
+ super({ message });
591
+ }
592
+ }
593
+
594
+ class ShellCommandError extends TaggedError("ShellCommandError")() {
595
+ constructor(command, output) {
596
+ super({
597
+ message: `Command failed (${command}): ${output}`,
598
+ command,
599
+ output
600
+ });
601
+ }
602
+ }
603
+
604
+ class TemplateProcessingError extends TaggedError("TemplateProcessingError")() {
605
+ constructor(cause) {
606
+ super({ message: `Failed to process template: ${toErrorMessage(cause)}`, cause });
607
+ }
608
+ }
155
609
  async function createProject(options) {
156
610
  const { name, dir, template, git, install, prompt, spinner, colors, shell, offline } = options;
157
- try {
158
- await shell`test -d ${dir}`.quiet();
611
+ const directoryCheck = await shell`test -d ${dir}`.nothrow();
612
+ if (directoryCheck.exitCode === 0) {
159
613
  const overwrite = await prompt.confirm(`Directory ${dir} already exists. Overwrite?`, { default: false });
160
614
  if (!overwrite) {
161
- console.log(colors.red("Cancelled"));
162
- process.exit(1);
615
+ return Result.err(new UserCancelledError("Cancelled"));
616
+ }
617
+ const removeDirectory = await shell`rm -rf ${dir}`.nothrow();
618
+ if (removeDirectory.exitCode !== 0) {
619
+ return Result.err(new ShellCommandError(`rm -rf ${dir}`, removeDirectory.stderr.toString().trim()));
163
620
  }
164
- await shell`rm -rf ${dir}`;
165
- } catch {}
621
+ }
166
622
  const spin = spinner("Creating project structure...");
167
623
  spin.start();
168
- await shell`mkdir -p ${dir}`;
169
- try {
170
- let templateSource = template;
171
- if (await isLocalTemplate(template)) {
172
- templateSource = getBundledTemplatePath(template);
173
- } else {
174
- templateSource = resolveTemplateSource(template);
624
+ const mkdirResult = await shell`mkdir -p ${dir}`.nothrow();
625
+ if (mkdirResult.exitCode !== 0) {
626
+ spin.fail("Failed to create project directory");
627
+ return Result.err(new ShellCommandError(`mkdir -p ${dir}`, mkdirResult.stderr.toString().trim()));
628
+ }
629
+ let templateSource = template;
630
+ if (await isLocalTemplate(template)) {
631
+ templateSource = getBundledTemplatePath(template);
632
+ } else {
633
+ templateSource = resolveTemplateSource(template);
634
+ }
635
+ const templateResult = await tryAsync(() => processTemplate({
636
+ source: templateSource,
637
+ dir,
638
+ offline,
639
+ variables: {
640
+ name,
641
+ version: "0.1.0",
642
+ description: "A CLI built with Bunli",
643
+ author: "",
644
+ license: "MIT",
645
+ year: new Date().getFullYear().toString()
175
646
  }
176
- const { manifest } = await processTemplate({
177
- source: templateSource,
178
- dir,
179
- offline,
180
- variables: {
181
- name,
182
- version: "0.1.0",
183
- description: `A CLI built with Bunli`,
184
- author: "",
185
- license: "MIT",
186
- year: new Date().getFullYear().toString()
187
- }
188
- });
189
- spin.succeed("Project structure created");
190
- if (git) {
191
- const gitSpin = spinner("Initializing git repository...");
192
- gitSpin.start();
193
- try {
194
- await shell`cd ${dir} && git init`.quiet();
195
- await shell`cd ${dir} && git add .`.quiet();
196
- await shell`cd ${dir} && git commit -m "feat: initialize ${name} CLI project with Bunli
647
+ }), (cause) => new TemplateProcessingError(cause));
648
+ if (Result.isError(templateResult)) {
649
+ spin.fail("Failed to create project");
650
+ console.error(colors.red(templateResult.error.message));
651
+ const cleanup = await shell`rm -rf ${dir}`.nothrow();
652
+ if (cleanup.exitCode !== 0) {
653
+ console.error(colors.yellow(`Warning: cleanup failed: ${cleanup.stderr.toString().trim()}`));
654
+ }
655
+ return templateResult;
656
+ }
657
+ spin.succeed("Project structure created");
658
+ if (git) {
659
+ const gitSpin = spinner("Initializing git repository...");
660
+ gitSpin.start();
661
+ const gitInit = await shell`cd ${dir} && git init`.nothrow();
662
+ const gitAdd = await shell`cd ${dir} && git add .`.nothrow();
663
+ const gitCommit = await shell`cd ${dir} && git commit -m "feat: initialize ${name} CLI project with Bunli
197
664
 
198
665
  - Generated using create-bunli template
199
666
  - Includes basic CLI structure with commands directory
200
667
  - Configured with Bunli build system and TypeScript
201
- - Ready for development with bun run dev"`;
202
- gitSpin.succeed("Git repository initialized");
203
- } catch (error) {
204
- gitSpin.fail("Failed to initialize git repository");
205
- console.error(colors.dim(` ${error}`));
668
+ - Ready for development with bun run dev"`.nothrow();
669
+ if (gitInit.exitCode === 0 && gitAdd.exitCode === 0 && gitCommit.exitCode === 0) {
670
+ gitSpin.succeed("Git repository initialized");
671
+ } else {
672
+ gitSpin.fail("Failed to initialize git repository");
673
+ const output = [gitInit.stderr, gitAdd.stderr, gitCommit.stderr].map((value) => value.toString().trim()).filter(Boolean).join(`
674
+ `);
675
+ if (output) {
676
+ console.error(colors.dim(` ${output}`));
206
677
  }
207
678
  }
208
- if (install) {
209
- const installSpin = spinner(`Installing dependencies...`);
210
- installSpin.start();
211
- try {
212
- await shell`cd ${dir} && bun install`;
213
- installSpin.succeed("Dependencies installed");
214
- } catch (error) {
215
- installSpin.fail("Failed to install dependencies");
216
- console.error(colors.dim(` You can install them manually by running: bun install`));
679
+ }
680
+ if (install) {
681
+ const installSpin = spinner("Installing dependencies...");
682
+ installSpin.start();
683
+ const installResult = await shell`cd ${dir} && bun install`.nothrow();
684
+ if (installResult.exitCode === 0) {
685
+ installSpin.succeed("Dependencies installed");
686
+ } else {
687
+ installSpin.fail("Failed to install dependencies");
688
+ console.error(colors.dim(" You can install them manually by running: bun install"));
689
+ const errorOutput = installResult.stderr.toString().trim();
690
+ if (errorOutput) {
691
+ console.error(colors.dim(` ${errorOutput}`));
217
692
  }
218
693
  }
219
- } catch (error) {
220
- spin.fail("Failed to create project");
221
- console.error(colors.red(`Error: ${error}`));
222
- try {
223
- await shell`rm -rf ${dir}`.quiet();
224
- } catch {}
225
- process.exit(1);
226
694
  }
695
+ return Result.ok(undefined);
227
696
  }
228
697
 
229
698
  // src/create.ts
230
699
  import path from "path";
700
+ class InvalidProjectNameError extends TaggedError("InvalidProjectNameError")() {
701
+ constructor(name) {
702
+ super({ message: `Project name "${name}" must only contain lowercase letters, numbers, and hyphens` });
703
+ }
704
+ }
705
+
706
+ class UserCancelledError2 extends TaggedError("UserCancelledError")() {
707
+ constructor(message) {
708
+ super({ message });
709
+ }
710
+ }
231
711
  async function create(context) {
232
- const { flags, positional, prompt, spinner, colors, shell } = context;
712
+ const { flags, positional, prompt, colors, spinner, shell } = context;
233
713
  let projectName = positional[0] || flags.name;
234
714
  if (!projectName) {
235
715
  projectName = await prompt("Project name:", {
@@ -242,9 +722,11 @@ async function create(context) {
242
722
  return true;
243
723
  }
244
724
  });
245
- } else if (!/^[a-z0-9-]+$/.test(projectName)) {
246
- console.error(colors.red("Project name must only contain lowercase letters, numbers, and hyphens"));
247
- process.exit(1);
725
+ }
726
+ if (!projectName || !/^[a-z0-9-]+$/.test(projectName)) {
727
+ const invalidName = projectName || "";
728
+ console.error(colors.red(`Project name must only contain lowercase letters, numbers, and hyphens`));
729
+ return Result.err(new InvalidProjectNameError(invalidName));
248
730
  }
249
731
  const projectDir = flags.dir || path.join(process.cwd(), projectName);
250
732
  console.log();
@@ -257,11 +739,11 @@ async function create(context) {
257
739
  console.log();
258
740
  const confirmed = await prompt.confirm("Continue?", { default: true });
259
741
  if (!confirmed) {
260
- console.log(colors.red("Cancelled"));
261
- process.exit(1);
742
+ console.log(colors.yellow("Cancelled"));
743
+ return Result.err(new UserCancelledError2("Cancelled"));
262
744
  }
263
745
  console.log();
264
- await createProject({
746
+ const projectResult = await createProject({
265
747
  name: projectName,
266
748
  dir: projectDir,
267
749
  template: flags.template,
@@ -273,16 +755,24 @@ async function create(context) {
273
755
  colors,
274
756
  shell
275
757
  });
758
+ if (Result.isError(projectResult)) {
759
+ if (UserCancelledError.is(projectResult.error)) {
760
+ return Result.err(new UserCancelledError2(projectResult.error.message));
761
+ }
762
+ console.error(colors.red(projectResult.error.message));
763
+ return projectResult;
764
+ }
276
765
  console.log();
277
766
  console.log(colors.green("\u2728 Project created successfully!"));
278
767
  console.log();
279
768
  console.log("Next steps:");
280
769
  console.log(colors.gray(` cd ${path.relative(process.cwd(), projectDir)}`));
281
770
  if (!flags.install) {
282
- console.log(colors.gray(` bun install`));
771
+ console.log(colors.gray(" bun install"));
283
772
  }
284
- console.log(colors.gray(` bun run dev`));
773
+ console.log(colors.gray(" bun run dev"));
285
774
  console.log();
775
+ return Result.ok(undefined);
286
776
  }
287
777
 
288
778
  // src/cli.ts
@@ -309,8 +799,18 @@ async function run() {
309
799
  install: option(z.boolean().default(true), { short: "i", description: "Install dependencies" }),
310
800
  offline: option(z.boolean().default(false), { description: "Use cached templates when available" })
311
801
  },
312
- handler: create
802
+ handler: async (context) => {
803
+ const result = await create(context);
804
+ if (Result.isError(result) && !UserCancelledError2.is(result.error)) {
805
+ process.exitCode = 1;
806
+ }
807
+ }
313
808
  }));
314
809
  await cli.run();
315
810
  }
316
- await run();
811
+ try {
812
+ await run();
813
+ } catch (error) {
814
+ console.error(error instanceof Error ? error.message : String(error));
815
+ process.exit(1);
816
+ }