create-better-t-stack 3.17.1 → 3.18.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.
@@ -2,6 +2,7 @@
2
2
  import { t as __reExport } from "./chunk-DPg_XC7m.mjs";
3
3
  import { autocompleteMultiselect, cancel, confirm, group, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
4
4
  import { createRouterClient, os } from "@orpc/server";
5
+ import { Result, Result as Result$1, TaggedError } from "better-result";
5
6
  import pc from "picocolors";
6
7
  import { createCli } from "trpc-cli";
7
8
  import z from "zod";
@@ -9,11 +10,11 @@ import consola, { consola as consola$1 } from "consola";
9
10
  import fs from "fs-extra";
10
11
  import path from "node:path";
11
12
  import { fileURLToPath } from "node:url";
12
- import { EMBEDDED_TEMPLATES, EMBEDDED_TEMPLATES as EMBEDDED_TEMPLATES$1, TEMPLATE_COUNT, VirtualFileSystem, dependencyVersionMap, generateVirtualProject, generateVirtualProject as generateVirtualProject$1 } from "@better-t-stack/template-generator";
13
- import { AsyncLocalStorage } from "node:async_hooks";
13
+ import { EMBEDDED_TEMPLATES, EMBEDDED_TEMPLATES as EMBEDDED_TEMPLATES$1, GeneratorError, GeneratorError as GeneratorError$1, TEMPLATE_COUNT, VirtualFileSystem, dependencyVersionMap, generate, generate as generate$1 } from "@better-t-stack/template-generator";
14
14
  import { ConfirmPrompt, GroupMultiSelectPrompt, MultiSelectPrompt, SelectPrompt, isCancel as isCancel$1 } from "@clack/core";
15
+ import { AsyncLocalStorage } from "node:async_hooks";
15
16
  import gradient from "gradient-string";
16
- import { writeTreeToFilesystem } from "@better-t-stack/template-generator/fs-writer";
17
+ import { writeTree } from "@better-t-stack/template-generator/fs-writer";
17
18
  import { $, execa } from "execa";
18
19
  import * as JSONC from "jsonc-parser";
19
20
  import { format } from "oxfmt";
@@ -91,88 +92,93 @@ const ADDON_COMPATIBILITY = {
91
92
  none: []
92
93
  };
93
94
 
94
- //#endregion
95
- //#region src/utils/context.ts
96
- const cliStorage = new AsyncLocalStorage();
97
- function defaultContext() {
98
- return {
99
- navigation: {
100
- isFirstPrompt: false,
101
- lastPromptShownUI: false
102
- },
103
- silent: false,
104
- verbose: false
105
- };
106
- }
107
- function getContext() {
108
- const ctx = cliStorage.getStore();
109
- if (!ctx) return defaultContext();
110
- return ctx;
111
- }
112
- function tryGetContext() {
113
- return cliStorage.getStore();
114
- }
115
- function isSilent() {
116
- return getContext().silent;
117
- }
118
- function isFirstPrompt() {
119
- return getContext().navigation.isFirstPrompt;
120
- }
121
- function didLastPromptShowUI() {
122
- return getContext().navigation.lastPromptShownUI;
123
- }
124
- function setIsFirstPrompt$1(value) {
125
- const ctx = tryGetContext();
126
- if (ctx) ctx.navigation.isFirstPrompt = value;
127
- }
128
- function setLastPromptShownUI(value) {
129
- const ctx = tryGetContext();
130
- if (ctx) ctx.navigation.lastPromptShownUI = value;
131
- }
132
- async function runWithContextAsync(options, fn) {
133
- const ctx = {
134
- navigation: {
135
- isFirstPrompt: false,
136
- lastPromptShownUI: false
137
- },
138
- silent: options.silent ?? false,
139
- verbose: options.verbose ?? false,
140
- projectDir: options.projectDir,
141
- projectName: options.projectName,
142
- packageManager: options.packageManager
143
- };
144
- return cliStorage.run(ctx, fn);
145
- }
146
-
147
95
  //#endregion
148
96
  //#region src/utils/errors.ts
149
- var UserCancelledError = class extends Error {
150
- constructor(message = "Operation cancelled") {
151
- super(message);
152
- this.name = "UserCancelledError";
97
+ /**
98
+ * User cancelled the operation (e.g., Ctrl+C in prompts)
99
+ */
100
+ var UserCancelledError = class extends TaggedError("UserCancelledError")() {
101
+ constructor(args) {
102
+ super({ message: args?.message ?? "Operation cancelled" });
103
+ }
104
+ };
105
+ /**
106
+ * General CLI error for validation failures, invalid flags, etc.
107
+ */
108
+ var CLIError = class extends TaggedError("CLIError")() {};
109
+ /**
110
+ * Validation error for config/flag validation failures
111
+ */
112
+ var ValidationError = class extends TaggedError("ValidationError")() {
113
+ constructor(args) {
114
+ super(args);
115
+ }
116
+ };
117
+ /**
118
+ * Compatibility error for incompatible option combinations
119
+ */
120
+ var CompatibilityError = class extends TaggedError("CompatibilityError")() {
121
+ constructor(args) {
122
+ super(args);
123
+ }
124
+ };
125
+ /**
126
+ * Directory conflict error when target directory exists and is not empty
127
+ */
128
+ var DirectoryConflictError = class extends TaggedError("DirectoryConflictError")() {
129
+ constructor(args) {
130
+ super({
131
+ directory: args.directory,
132
+ message: `Directory "${args.directory}" already exists and is not empty. Use directoryConflict: "overwrite", "merge", or "increment" to handle this.`
133
+ });
134
+ }
135
+ };
136
+ /**
137
+ * Project creation error for failures during scaffolding
138
+ */
139
+ var ProjectCreationError = class extends TaggedError("ProjectCreationError")() {
140
+ constructor(args) {
141
+ super(args);
142
+ }
143
+ };
144
+ /**
145
+ * Database setup error for failures during database configuration
146
+ */
147
+ var DatabaseSetupError = class extends TaggedError("DatabaseSetupError")() {
148
+ constructor(args) {
149
+ super(args);
153
150
  }
154
151
  };
155
- var CLIError = class extends Error {
156
- constructor(message) {
157
- super(message);
158
- this.name = "CLIError";
152
+ /**
153
+ * Addon setup error for failures during addon configuration
154
+ */
155
+ var AddonSetupError = class extends TaggedError("AddonSetupError")() {
156
+ constructor(args) {
157
+ super(args);
159
158
  }
160
159
  };
161
- function exitWithError(message) {
162
- if (isSilent()) throw new CLIError(message);
163
- consola.error(pc.red(message));
164
- process.exit(1);
165
- }
166
- function exitCancelled(message = "Operation cancelled") {
167
- if (isSilent()) throw new UserCancelledError(message);
168
- cancel(pc.red(message));
169
- process.exit(1);
170
- }
171
- function handleError(error, fallbackMessage) {
172
- const message = error instanceof Error ? error.message : fallbackMessage || String(error);
173
- if (isSilent()) throw error instanceof Error ? error : new Error(message);
174
- consola.error(pc.red(message));
175
- process.exit(1);
160
+ /**
161
+ * Create a user cancelled error Result
162
+ */
163
+ function userCancelled(message) {
164
+ return Result.err(new UserCancelledError({ message }));
165
+ }
166
+ /**
167
+ * Create a database setup error Result
168
+ */
169
+ function databaseSetupError(provider, message, cause) {
170
+ return Result.err(new DatabaseSetupError({
171
+ provider,
172
+ message,
173
+ cause
174
+ }));
175
+ }
176
+ /**
177
+ * Display an error to the user (for CLI mode)
178
+ */
179
+ function displayError(error) {
180
+ if (UserCancelledError.is(error)) cancel(pc.red(error.message));
181
+ else consola.error(pc.red(error.message));
176
182
  }
177
183
 
178
184
  //#endregion
@@ -196,6 +202,9 @@ const WEB_FRAMEWORKS = [
196
202
 
197
203
  //#endregion
198
204
  //#region src/utils/compatibility-rules.ts
205
+ function validationErr$1(message) {
206
+ return Result.err(new ValidationError({ message }));
207
+ }
199
208
  function isWebFrontend(value) {
200
209
  return WEB_FRAMEWORKS.includes(value);
201
210
  }
@@ -207,8 +216,9 @@ function splitFrontends(values = []) {
207
216
  }
208
217
  function ensureSingleWebAndNative(frontends) {
209
218
  const { web, native } = splitFrontends(frontends);
210
- if (web.length > 1) exitWithError("Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid");
211
- if (native.length > 1) exitWithError("Cannot select multiple native frameworks. Choose only one of: native-bare, native-uniwind, native-unistyles");
219
+ if (web.length > 1) return validationErr$1("Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid");
220
+ if (native.length > 1) return validationErr$1("Cannot select multiple native frameworks. Choose only one of: native-bare, native-uniwind, native-unistyles");
221
+ return Result.ok(void 0);
212
222
  }
213
223
  const FULLSTACK_FRONTENDS$1 = [
214
224
  "next",
@@ -221,25 +231,28 @@ function validateSelfBackendCompatibility(providedFlags, options, config) {
221
231
  const frontends = config.frontend || options.frontend || [];
222
232
  if (backend === "self") {
223
233
  const { web, native } = splitFrontends(frontends);
224
- if (!(web.length === 1 && FULLSTACK_FRONTENDS$1.includes(web[0]))) exitWithError("Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, or --frontend astro. Support for SvelteKit will be added in a future update.");
225
- if (native.length > 1) exitWithError("Cannot select multiple native frameworks. Choose only one of: native-bare, native-uniwind, native-unistyles");
234
+ if (!(web.length === 1 && FULLSTACK_FRONTENDS$1.includes(web[0]))) return validationErr$1("Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, or --frontend astro. Support for SvelteKit will be added in a future update.");
235
+ if (native.length > 1) return validationErr$1("Cannot select multiple native frameworks. Choose only one of: native-bare, native-uniwind, native-unistyles");
226
236
  }
227
237
  const hasFullstackFrontend = frontends.some((f) => FULLSTACK_FRONTENDS$1.includes(f));
228
- if (providedFlags.has("backend") && !hasFullstackFrontend && backend === "self") exitWithError("Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, --frontend astro, or choose a different backend. Support for SvelteKit will be added in a future update.");
238
+ if (providedFlags.has("backend") && !hasFullstackFrontend && backend === "self") return validationErr$1("Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, --frontend astro, or choose a different backend. Support for SvelteKit will be added in a future update.");
239
+ return Result.ok(void 0);
229
240
  }
230
241
  function validateWorkersCompatibility(providedFlags, options, config) {
231
- if (providedFlags.has("runtime") && options.runtime === "workers" && config.backend && config.backend !== "hono") exitWithError(`Cloudflare Workers runtime (--runtime workers) is only supported with Hono backend (--backend hono). Current backend: ${config.backend}. Please use '--backend hono' or choose a different runtime.`);
232
- if (providedFlags.has("backend") && config.backend && config.backend !== "hono" && config.runtime === "workers") exitWithError(`Backend '${config.backend}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Hono backend. Please use '--backend hono' or choose a different runtime.`);
233
- if (providedFlags.has("runtime") && options.runtime === "workers" && config.database === "mongodb") exitWithError("Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle or Prisma ORM. Please use a different database or runtime.");
234
- if (providedFlags.has("runtime") && options.runtime === "workers" && config.dbSetup === "docker") exitWithError("Cloudflare Workers runtime (--runtime workers) is not compatible with Docker setup. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
235
- if (providedFlags.has("database") && config.database === "mongodb" && config.runtime === "workers") exitWithError("MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle or Prisma ORM. Please use a different database or runtime.");
242
+ if (providedFlags.has("runtime") && options.runtime === "workers" && config.backend && config.backend !== "hono") return validationErr$1(`Cloudflare Workers runtime (--runtime workers) is only supported with Hono backend (--backend hono). Current backend: ${config.backend}. Please use '--backend hono' or choose a different runtime.`);
243
+ if (providedFlags.has("backend") && config.backend && config.backend !== "hono" && config.runtime === "workers") return validationErr$1(`Backend '${config.backend}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Hono backend. Please use '--backend hono' or choose a different runtime.`);
244
+ if (providedFlags.has("runtime") && options.runtime === "workers" && config.database === "mongodb") return validationErr$1("Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle or Prisma ORM. Please use a different database or runtime.");
245
+ if (providedFlags.has("runtime") && options.runtime === "workers" && config.dbSetup === "docker") return validationErr$1("Cloudflare Workers runtime (--runtime workers) is not compatible with Docker setup. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
246
+ if (providedFlags.has("database") && config.database === "mongodb" && config.runtime === "workers") return validationErr$1("MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle or Prisma ORM. Please use a different database or runtime.");
247
+ return Result.ok(void 0);
236
248
  }
237
249
  function validateApiFrontendCompatibility(api, frontends = []) {
238
250
  const includesNuxt = frontends.includes("nuxt");
239
251
  const includesSvelte = frontends.includes("svelte");
240
252
  const includesSolid = frontends.includes("solid");
241
253
  const includesAstro = frontends.includes("astro");
242
- if ((includesNuxt || includesSvelte || includesSolid || includesAstro) && api === "trpc") exitWithError(`tRPC API is not supported with '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : includesSolid ? "solid" : "astro"}' frontend. Please use --api orpc or --api none or remove '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : includesSolid ? "solid" : "astro"}' from --frontend.`);
254
+ if ((includesNuxt || includesSvelte || includesSolid || includesAstro) && api === "trpc") return validationErr$1(`tRPC API is not supported with '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : includesSolid ? "solid" : "astro"}' frontend. Please use --api orpc or --api none or remove '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : includesSolid ? "solid" : "astro"}' from --frontend.`);
255
+ return Result.ok(void 0);
243
256
  }
244
257
  function isFrontendAllowedWithBackend(frontend, backend, auth) {
245
258
  if (backend === "convex" && (frontend === "solid" || frontend === "astro")) return false;
@@ -283,10 +296,12 @@ function isExampleAIAllowed(backend, frontends = []) {
283
296
  return true;
284
297
  }
285
298
  function validateWebDeployRequiresWebFrontend(webDeploy, hasWebFrontendFlag) {
286
- if (webDeploy && webDeploy !== "none" && !hasWebFrontendFlag) exitWithError("'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.");
299
+ if (webDeploy && webDeploy !== "none" && !hasWebFrontendFlag) return validationErr$1("'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.");
300
+ return Result.ok(void 0);
287
301
  }
288
302
  function validateServerDeployRequiresBackend(serverDeploy, backend) {
289
- if (serverDeploy && serverDeploy !== "none" && (!backend || backend === "none")) exitWithError("'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.");
303
+ if (serverDeploy && serverDeploy !== "none" && (!backend || backend === "none")) return validationErr$1("'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.");
304
+ return Result.ok(void 0);
290
305
  }
291
306
  function validateAddonCompatibility(addon, frontend, _auth) {
292
307
  const compatibleFrontends = ADDON_COMPATIBILITY[addon];
@@ -302,31 +317,87 @@ function validateAddonsAgainstFrontends(addons = [], frontends = [], auth) {
302
317
  for (const addon of addons) {
303
318
  if (addon === "none") continue;
304
319
  const { isCompatible, reason } = validateAddonCompatibility(addon, frontends, auth);
305
- if (!isCompatible) exitWithError(`Incompatible addon/frontend combination: ${reason}`);
320
+ if (!isCompatible) return validationErr$1(`Incompatible addon/frontend combination: ${reason}`);
306
321
  }
322
+ return Result.ok(void 0);
307
323
  }
308
324
  function validatePaymentsCompatibility(payments, auth, _backend, frontends = []) {
309
- if (!payments || payments === "none") return;
325
+ if (!payments || payments === "none") return Result.ok(void 0);
310
326
  if (payments === "polar") {
311
- if (!auth || auth === "none" || auth !== "better-auth") exitWithError("Polar payments requires Better Auth. Please use '--auth better-auth' or choose a different payments provider.");
327
+ if (!auth || auth === "none" || auth !== "better-auth") return validationErr$1("Polar payments requires Better Auth. Please use '--auth better-auth' or choose a different payments provider.");
312
328
  const { web } = splitFrontends(frontends);
313
- if (web.length === 0 && frontends.length > 0) exitWithError("Polar payments requires a web frontend or no frontend. Please select a web frontend or choose a different payments provider.");
329
+ if (web.length === 0 && frontends.length > 0) return validationErr$1("Polar payments requires a web frontend or no frontend. Please select a web frontend or choose a different payments provider.");
314
330
  }
331
+ return Result.ok(void 0);
315
332
  }
316
333
  function validateExamplesCompatibility(examples, backend, database, frontend, api) {
317
334
  const examplesArr = examples ?? [];
318
- if (examplesArr.length === 0 || examplesArr.includes("none")) return;
335
+ if (examplesArr.length === 0 || examplesArr.includes("none")) return Result.ok(void 0);
319
336
  if (examplesArr.includes("todo") && backend !== "convex") {
320
- if (database === "none") exitWithError("The 'todo' example requires a database. Cannot use --examples todo when database is 'none'.");
321
- if (api === "none") exitWithError("The 'todo' example requires an API layer (tRPC or oRPC). Cannot use --examples todo when api is 'none'.");
337
+ if (database === "none") return validationErr$1("The 'todo' example requires a database. Cannot use --examples todo when database is 'none'.");
338
+ if (api === "none") return validationErr$1("The 'todo' example requires an API layer (tRPC or oRPC). Cannot use --examples todo when api is 'none'.");
322
339
  }
323
- if (examplesArr.includes("ai") && (frontend ?? []).includes("solid")) exitWithError("The 'ai' example is not compatible with the Solid frontend.");
340
+ if (examplesArr.includes("ai") && (frontend ?? []).includes("solid")) return validationErr$1("The 'ai' example is not compatible with the Solid frontend.");
324
341
  if (examplesArr.includes("ai") && backend === "convex") {
325
342
  const frontendArr = frontend ?? [];
326
343
  const includesNuxt = frontendArr.includes("nuxt");
327
344
  const includesSvelte = frontendArr.includes("svelte");
328
- if (includesNuxt || includesSvelte) exitWithError("The 'ai' example with Convex backend only supports React-based frontends (Next.js, TanStack Router, TanStack Start, React Router). Svelte and Nuxt are not supported with Convex AI.");
345
+ if (includesNuxt || includesSvelte) return validationErr$1("The 'ai' example with Convex backend only supports React-based frontends (Next.js, TanStack Router, TanStack Start, React Router). Svelte and Nuxt are not supported with Convex AI.");
329
346
  }
347
+ return Result.ok(void 0);
348
+ }
349
+
350
+ //#endregion
351
+ //#region src/utils/context.ts
352
+ const cliStorage = new AsyncLocalStorage();
353
+ function defaultContext() {
354
+ return {
355
+ navigation: {
356
+ isFirstPrompt: false,
357
+ lastPromptShownUI: false
358
+ },
359
+ silent: false,
360
+ verbose: false
361
+ };
362
+ }
363
+ function getContext() {
364
+ const ctx = cliStorage.getStore();
365
+ if (!ctx) return defaultContext();
366
+ return ctx;
367
+ }
368
+ function tryGetContext() {
369
+ return cliStorage.getStore();
370
+ }
371
+ function isSilent() {
372
+ return getContext().silent;
373
+ }
374
+ function isFirstPrompt() {
375
+ return getContext().navigation.isFirstPrompt;
376
+ }
377
+ function didLastPromptShowUI() {
378
+ return getContext().navigation.lastPromptShownUI;
379
+ }
380
+ function setIsFirstPrompt$1(value) {
381
+ const ctx = tryGetContext();
382
+ if (ctx) ctx.navigation.isFirstPrompt = value;
383
+ }
384
+ function setLastPromptShownUI(value) {
385
+ const ctx = tryGetContext();
386
+ if (ctx) ctx.navigation.lastPromptShownUI = value;
387
+ }
388
+ async function runWithContextAsync(options, fn) {
389
+ const ctx = {
390
+ navigation: {
391
+ isFirstPrompt: false,
392
+ lastPromptShownUI: false
393
+ },
394
+ silent: options.silent ?? false,
395
+ verbose: options.verbose ?? false,
396
+ projectDir: options.projectDir,
397
+ projectName: options.projectName,
398
+ packageManager: options.packageManager
399
+ };
400
+ return cliStorage.run(ctx, fn);
330
401
  }
331
402
 
332
403
  //#endregion
@@ -684,7 +755,7 @@ async function getAddonsChoice(addons, frontends, auth) {
684
755
  initialValues: DEFAULT_CONFIG.addons.filter((addonValue) => Object.values(groupedOptions).some((options) => options.some((opt) => opt.value === addonValue))),
685
756
  required: false
686
757
  });
687
- if (isCancel$1(response)) return exitCancelled("Operation cancelled");
758
+ if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
688
759
  return response;
689
760
  }
690
761
 
@@ -712,7 +783,7 @@ async function getApiChoice(Api, frontend, backend) {
712
783
  options: apiOptions,
713
784
  initialValue: apiOptions[0].value
714
785
  });
715
- if (isCancel$1(apiType)) return exitCancelled("Operation cancelled");
786
+ if (isCancel$1(apiType)) throw new UserCancelledError({ message: "Operation cancelled" });
716
787
  return apiType;
717
788
  }
718
789
 
@@ -761,7 +832,7 @@ async function getAuthChoice(auth, backend, frontend) {
761
832
  options,
762
833
  initialValue: "none"
763
834
  });
764
- if (isCancel$1(response$1)) return exitCancelled("Operation cancelled");
835
+ if (isCancel$1(response$1)) throw new UserCancelledError({ message: "Operation cancelled" });
765
836
  return response$1;
766
837
  }
767
838
  const response = await navigableSelect({
@@ -776,7 +847,7 @@ async function getAuthChoice(auth, backend, frontend) {
776
847
  }],
777
848
  initialValue: DEFAULT_CONFIG.auth
778
849
  });
779
- if (isCancel$1(response)) return exitCancelled("Operation cancelled");
850
+ if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
780
851
  return response;
781
852
  }
782
853
 
@@ -830,7 +901,7 @@ async function getBackendFrameworkChoice(backendFramework, frontends) {
830
901
  options: backendOptions,
831
902
  initialValue: hasFullstackFrontend ? "self" : DEFAULT_CONFIG.backend
832
903
  });
833
- if (isCancel$1(response)) return exitCancelled("Operation cancelled");
904
+ if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
834
905
  return response;
835
906
  }
836
907
 
@@ -871,7 +942,7 @@ async function getDatabaseChoice(database, backend, runtime) {
871
942
  options: databaseOptions,
872
943
  initialValue: DEFAULT_CONFIG.database
873
944
  });
874
- if (isCancel$1(response)) return exitCancelled("Operation cancelled");
945
+ if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
875
946
  return response;
876
947
  }
877
948
 
@@ -971,7 +1042,7 @@ async function getDBSetupChoice(databaseType, dbSetup, _orm, backend, runtime) {
971
1042
  options,
972
1043
  initialValue: "none"
973
1044
  });
974
- if (isCancel$1(response)) return exitCancelled("Operation cancelled");
1045
+ if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
975
1046
  return response;
976
1047
  }
977
1048
 
@@ -999,7 +1070,7 @@ async function getExamplesChoice(examples, database, frontends, backend, api) {
999
1070
  required: false,
1000
1071
  initialValues: DEFAULT_CONFIG.examples?.filter((ex) => options.some((o) => o.value === ex))
1001
1072
  });
1002
- if (isCancel$1(response)) return exitCancelled("Operation cancelled");
1073
+ if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1003
1074
  return response;
1004
1075
  }
1005
1076
 
@@ -1024,7 +1095,7 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
1024
1095
  initialValues: ["web"]
1025
1096
  });
1026
1097
  if (isGoBack(frontendTypes)) return GO_BACK_SYMBOL;
1027
- if (isCancel$1(frontendTypes)) return exitCancelled("Operation cancelled");
1098
+ if (isCancel$1(frontendTypes)) throw new UserCancelledError({ message: "Operation cancelled" });
1028
1099
  setIsFirstPrompt(false);
1029
1100
  const result = [];
1030
1101
  let shouldRestart = false;
@@ -1076,7 +1147,7 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
1076
1147
  initialValue: DEFAULT_CONFIG.frontend[0]
1077
1148
  });
1078
1149
  if (isGoBack(webFramework)) shouldRestart = true;
1079
- else if (isCancel$1(webFramework)) return exitCancelled("Operation cancelled");
1150
+ else if (isCancel$1(webFramework)) throw new UserCancelledError({ message: "Operation cancelled" });
1080
1151
  else result.push(webFramework);
1081
1152
  }
1082
1153
  if (shouldRestart) {
@@ -1110,7 +1181,7 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
1110
1181
  setIsFirstPrompt(wasFirstPrompt);
1111
1182
  continue;
1112
1183
  }
1113
- else if (isCancel$1(nativeFramework)) return exitCancelled("Operation cancelled");
1184
+ else if (isCancel$1(nativeFramework)) throw new UserCancelledError({ message: "Operation cancelled" });
1114
1185
  else result.push(nativeFramework);
1115
1186
  }
1116
1187
  if (shouldRestart) {
@@ -1129,7 +1200,7 @@ async function getGitChoice(git) {
1129
1200
  message: "Initialize git repository?",
1130
1201
  initialValue: DEFAULT_CONFIG.git
1131
1202
  });
1132
- if (isCancel$1(response)) return exitCancelled("Operation cancelled");
1203
+ if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1133
1204
  return response;
1134
1205
  }
1135
1206
 
@@ -1141,7 +1212,7 @@ async function getinstallChoice(install) {
1141
1212
  message: "Install dependencies?",
1142
1213
  initialValue: DEFAULT_CONFIG.install
1143
1214
  });
1144
- if (isCancel$1(response)) return exitCancelled("Operation cancelled");
1215
+ if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1145
1216
  return response;
1146
1217
  }
1147
1218
 
@@ -1230,7 +1301,7 @@ async function getORMChoice(orm, hasDatabase, database, backend, runtime) {
1230
1301
  options: database === "mongodb" ? [ormOptions.prisma, ormOptions.mongoose] : [ormOptions.drizzle, ormOptions.prisma],
1231
1302
  initialValue: database === "mongodb" ? "prisma" : runtime === "workers" ? "drizzle" : DEFAULT_CONFIG.orm
1232
1303
  });
1233
- if (isCancel$1(response)) return exitCancelled("Operation cancelled");
1304
+ if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1234
1305
  return response;
1235
1306
  }
1236
1307
 
@@ -1259,7 +1330,7 @@ async function getPackageManagerChoice(packageManager) {
1259
1330
  ],
1260
1331
  initialValue: getUserPkgManager()
1261
1332
  });
1262
- if (isCancel$1(response)) return exitCancelled("Operation cancelled");
1333
+ if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1263
1334
  return response;
1264
1335
  }
1265
1336
 
@@ -1282,7 +1353,7 @@ async function getPaymentsChoice(payments, auth, backend, frontends) {
1282
1353
  }],
1283
1354
  initialValue: DEFAULT_CONFIG.payments
1284
1355
  });
1285
- if (isCancel$1(response)) return exitCancelled("Operation cancelled");
1356
+ if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1286
1357
  return response;
1287
1358
  }
1288
1359
 
@@ -1310,7 +1381,7 @@ async function getRuntimeChoice(runtime, backend) {
1310
1381
  options: runtimeOptions,
1311
1382
  initialValue: DEFAULT_CONFIG.runtime
1312
1383
  });
1313
- if (isCancel$1(response)) return exitCancelled("Operation cancelled");
1384
+ if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1314
1385
  return response;
1315
1386
  }
1316
1387
 
@@ -1354,7 +1425,7 @@ async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []
1354
1425
  }),
1355
1426
  initialValue: DEFAULT_CONFIG.webDeploy
1356
1427
  });
1357
- if (isCancel$1(response)) return exitCancelled("Operation cancelled");
1428
+ if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1358
1429
  return response;
1359
1430
  }
1360
1431
 
@@ -1378,7 +1449,9 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
1378
1449
  git: () => getGitChoice(flags.git),
1379
1450
  packageManager: () => getPackageManagerChoice(flags.packageManager),
1380
1451
  install: () => getinstallChoice(flags.install)
1381
- }, { onCancel: () => exitCancelled("Operation cancelled") });
1452
+ }, { onCancel: () => {
1453
+ throw new UserCancelledError({ message: "Operation cancelled" });
1454
+ } });
1382
1455
  return {
1383
1456
  projectName,
1384
1457
  projectDir,
@@ -1445,7 +1518,7 @@ async function getProjectName(initialName) {
1445
1518
  }
1446
1519
  }
1447
1520
  });
1448
- if (isCancel(response)) return exitCancelled("Operation cancelled.");
1521
+ if (isCancel(response)) throw new UserCancelledError({ message: "Operation cancelled." });
1449
1522
  projectPath = response || defaultName;
1450
1523
  isValid = true;
1451
1524
  }
@@ -1479,25 +1552,27 @@ function isTelemetryEnabled() {
1479
1552
  //#region src/utils/analytics.ts
1480
1553
  const CONVEX_INGEST_URL = "https://striped-seahorse-863.convex.site/api/analytics/ingest";
1481
1554
  async function sendConvexEvent(payload) {
1482
- try {
1483
- await fetch(CONVEX_INGEST_URL, {
1555
+ await Result.tryPromise({
1556
+ try: () => fetch(CONVEX_INGEST_URL, {
1484
1557
  method: "POST",
1485
1558
  headers: { "Content-Type": "application/json" },
1486
1559
  body: JSON.stringify(payload)
1487
- });
1488
- } catch {}
1560
+ }),
1561
+ catch: () => void 0
1562
+ });
1489
1563
  }
1490
1564
  async function trackProjectCreation(config, disableAnalytics = false) {
1491
1565
  if (!isTelemetryEnabled() || disableAnalytics) return;
1492
1566
  const { projectName: _projectName, projectDir: _projectDir, relativePath: _relativePath, ...safeConfig } = config;
1493
- try {
1494
- await sendConvexEvent({
1567
+ await Result.tryPromise({
1568
+ try: () => sendConvexEvent({
1495
1569
  ...safeConfig,
1496
1570
  cli_version: getLatestCLIVersion(),
1497
1571
  node_version: typeof process !== "undefined" ? process.version : "",
1498
1572
  platform: typeof process !== "undefined" ? process.platform : ""
1499
- });
1500
- } catch {}
1573
+ }),
1574
+ catch: () => void 0
1575
+ });
1501
1576
  }
1502
1577
 
1503
1578
  //#endregion
@@ -1584,7 +1659,7 @@ async function handleDirectoryConflict(currentPathInput) {
1584
1659
  finalPathInput: currentPathInput,
1585
1660
  shouldClearDirectory: false
1586
1661
  };
1587
- if (isSilent()) throw new Error(`Directory "${currentPathInput}" already exists and is not empty. In silent mode, please provide a different project name or clear the directory manually.`);
1662
+ if (isSilent()) throw new CLIError({ message: `Directory "${currentPathInput}" already exists and is not empty. In silent mode, please provide a different project name or clear the directory manually.` });
1588
1663
  log.warn(`Directory "${pc.yellow(currentPathInput)}" already exists and is not empty.`);
1589
1664
  const action = await select({
1590
1665
  message: "What would you like to do?",
@@ -1612,7 +1687,7 @@ async function handleDirectoryConflict(currentPathInput) {
1612
1687
  ],
1613
1688
  initialValue: "rename"
1614
1689
  });
1615
- if (isCancel(action)) return exitCancelled("Operation cancelled.");
1690
+ if (isCancel(action)) throw new UserCancelledError({ message: "Operation cancelled." });
1616
1691
  switch (action) {
1617
1692
  case "overwrite": return {
1618
1693
  finalPathInput: currentPathInput,
@@ -1627,7 +1702,7 @@ async function handleDirectoryConflict(currentPathInput) {
1627
1702
  case "rename":
1628
1703
  log.info("Please choose a different project name or path.");
1629
1704
  return await handleDirectoryConflict(await getProjectName(void 0));
1630
- case "cancel": return exitCancelled("Operation cancelled.");
1705
+ case "cancel": throw new UserCancelledError({ message: "Operation cancelled." });
1631
1706
  }
1632
1707
  }
1633
1708
  }
@@ -1644,13 +1719,18 @@ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
1644
1719
  if (shouldClearDirectory) {
1645
1720
  const s = spinner();
1646
1721
  s.start(`Clearing directory "${finalResolvedPath}"...`);
1647
- try {
1648
- await fs.emptyDir(finalResolvedPath);
1649
- s.stop(`Directory "${finalResolvedPath}" cleared.`);
1650
- } catch (error) {
1722
+ const clearResult = await Result.tryPromise({
1723
+ try: () => fs.emptyDir(finalResolvedPath),
1724
+ catch: (error) => new CLIError({
1725
+ message: `Failed to clear directory "${finalResolvedPath}".`,
1726
+ cause: error
1727
+ })
1728
+ });
1729
+ if (clearResult.isErr()) {
1651
1730
  s.stop(pc.red(`Failed to clear directory "${finalResolvedPath}".`));
1652
- handleError(error);
1731
+ throw clearResult.error;
1653
1732
  }
1733
+ s.stop(`Directory "${finalResolvedPath}" cleared.`);
1654
1734
  } else await fs.ensureDir(finalResolvedPath);
1655
1735
  return {
1656
1736
  finalResolvedPath,
@@ -1814,30 +1894,39 @@ function getProvidedFlags(options) {
1814
1894
  return new Set(Object.keys(options).filter((key) => options[key] !== void 0));
1815
1895
  }
1816
1896
  function validateNoneExclusivity(options, optionName) {
1817
- if (!options || options.length === 0) return;
1818
- if (options.includes("none") && options.length > 1) throw new Error(`Cannot combine 'none' with other ${optionName}.`);
1897
+ if (!options || options.length === 0) return Result.ok(void 0);
1898
+ if (options.includes("none") && options.length > 1) return Result.err(new ValidationError({ message: `Cannot combine 'none' with other ${optionName}.` }));
1899
+ return Result.ok(void 0);
1819
1900
  }
1820
1901
  function validateArrayOptions(options) {
1821
- validateNoneExclusivity(options.frontend, "frontend options");
1822
- validateNoneExclusivity(options.addons, "addons");
1823
- validateNoneExclusivity(options.examples, "examples");
1902
+ const frontendResult = validateNoneExclusivity(options.frontend, "frontend options");
1903
+ if (frontendResult.isErr()) return frontendResult;
1904
+ const addonsResult = validateNoneExclusivity(options.addons, "addons");
1905
+ if (addonsResult.isErr()) return addonsResult;
1906
+ const examplesResult = validateNoneExclusivity(options.examples, "examples");
1907
+ if (examplesResult.isErr()) return examplesResult;
1908
+ return Result.ok(void 0);
1824
1909
  }
1825
1910
 
1826
1911
  //#endregion
1827
1912
  //#region src/utils/config-validation.ts
1913
+ function validationErr(message) {
1914
+ return Result.err(new ValidationError({ message }));
1915
+ }
1828
1916
  function validateDatabaseOrmAuth(cfg, flags) {
1829
1917
  const db = cfg.database;
1830
1918
  const orm = cfg.orm;
1831
1919
  const has = (k) => flags ? flags.has(k) : true;
1832
- if (has("orm") && has("database") && orm === "mongoose" && db !== "mongodb") exitWithError("Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.");
1833
- if (has("orm") && has("database") && orm === "drizzle" && db === "mongodb") exitWithError("Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
1834
- if (has("database") && has("orm") && db === "mongodb" && orm && orm !== "mongoose" && orm !== "prisma" && orm !== "none") exitWithError("MongoDB database requires Mongoose or Prisma ORM. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
1835
- if (has("database") && has("orm") && db && db !== "none" && orm === "none") exitWithError("Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.");
1836
- if (has("orm") && has("database") && orm && orm !== "none" && db === "none") exitWithError("ORM selection requires a database. Please choose a database or set '--orm none'.");
1920
+ if (has("orm") && has("database") && orm === "mongoose" && db !== "mongodb") return validationErr("Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.");
1921
+ if (has("orm") && has("database") && orm === "drizzle" && db === "mongodb") return validationErr("Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
1922
+ if (has("database") && has("orm") && db === "mongodb" && orm && orm !== "mongoose" && orm !== "prisma" && orm !== "none") return validationErr("MongoDB database requires Mongoose or Prisma ORM. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
1923
+ if (has("database") && has("orm") && db && db !== "none" && orm === "none") return validationErr("Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.");
1924
+ if (has("orm") && has("database") && orm && orm !== "none" && db === "none") return validationErr("ORM selection requires a database. Please choose a database or set '--orm none'.");
1925
+ return Result.ok(void 0);
1837
1926
  }
1838
1927
  function validateDatabaseSetup(config, providedFlags) {
1839
1928
  const { dbSetup, database, runtime } = config;
1840
- if (providedFlags.has("dbSetup") && providedFlags.has("database") && dbSetup && dbSetup !== "none" && database === "none") exitWithError("Database setup requires a database. Please choose a database or set '--db-setup none'.");
1929
+ if (providedFlags.has("dbSetup") && providedFlags.has("database") && dbSetup && dbSetup !== "none" && database === "none") return validationErr("Database setup requires a database. Please choose a database or set '--db-setup none'.");
1841
1930
  const setupValidations = {
1842
1931
  turso: {
1843
1932
  database: "sqlite",
@@ -1871,25 +1960,26 @@ function validateDatabaseSetup(config, providedFlags) {
1871
1960
  if (dbSetup && dbSetup !== "none") {
1872
1961
  const validation = setupValidations[dbSetup];
1873
1962
  if (dbSetup === "planetscale") {
1874
- if (database !== "postgres" && database !== "mysql") exitWithError(validation.errorMessage);
1875
- } else if (validation.database && database !== validation.database) exitWithError(validation.errorMessage);
1876
- if (validation.runtime && runtime !== validation.runtime) exitWithError(validation.errorMessage);
1963
+ if (database !== "postgres" && database !== "mysql") return validationErr(validation.errorMessage);
1964
+ } else if (validation.database && database !== validation.database) return validationErr(validation.errorMessage);
1965
+ if (validation.runtime && runtime !== validation.runtime) return validationErr(validation.errorMessage);
1877
1966
  if (dbSetup === "docker") {
1878
- if (database === "sqlite") exitWithError("Docker setup is not compatible with SQLite database. SQLite is file-based and doesn't require Docker. Please use '--database postgres', '--database mysql', '--database mongodb', or choose a different setup.");
1879
- if (runtime === "workers") exitWithError("Docker setup is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
1967
+ if (database === "sqlite") return validationErr("Docker setup is not compatible with SQLite database. SQLite is file-based and doesn't require Docker. Please use '--database postgres', '--database mysql', '--database mongodb', or choose a different setup.");
1968
+ if (runtime === "workers") return validationErr("Docker setup is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
1880
1969
  }
1881
1970
  }
1971
+ return Result.ok(void 0);
1882
1972
  }
1883
1973
  function validateConvexConstraints(config, providedFlags) {
1884
1974
  const { backend } = config;
1885
- if (backend !== "convex") return;
1975
+ if (backend !== "convex") return Result.ok(void 0);
1886
1976
  const has = (k) => providedFlags.has(k);
1887
- if (has("runtime") && config.runtime !== "none") exitWithError("Convex backend requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.");
1888
- if (has("database") && config.database !== "none") exitWithError("Convex backend requires '--database none'. Please remove the --database flag or set it to 'none'.");
1889
- if (has("orm") && config.orm !== "none") exitWithError("Convex backend requires '--orm none'. Please remove the --orm flag or set it to 'none'.");
1890
- if (has("api") && config.api !== "none") exitWithError("Convex backend requires '--api none'. Please remove the --api flag or set it to 'none'.");
1891
- if (has("dbSetup") && config.dbSetup !== "none") exitWithError("Convex backend requires '--db-setup none'. Please remove the --db-setup flag or set it to 'none'.");
1892
- if (has("serverDeploy") && config.serverDeploy !== "none") exitWithError("Convex backend requires '--server-deploy none'. Please remove the --server-deploy flag or set it to 'none'.");
1977
+ if (has("runtime") && config.runtime !== "none") return validationErr("Convex backend requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.");
1978
+ if (has("database") && config.database !== "none") return validationErr("Convex backend requires '--database none'. Please remove the --database flag or set it to 'none'.");
1979
+ if (has("orm") && config.orm !== "none") return validationErr("Convex backend requires '--orm none'. Please remove the --orm flag or set it to 'none'.");
1980
+ if (has("api") && config.api !== "none") return validationErr("Convex backend requires '--api none'. Please remove the --api flag or set it to 'none'.");
1981
+ if (has("dbSetup") && config.dbSetup !== "none") return validationErr("Convex backend requires '--db-setup none'. Please remove the --db-setup flag or set it to 'none'.");
1982
+ if (has("serverDeploy") && config.serverDeploy !== "none") return validationErr("Convex backend requires '--server-deploy none'. Please remove the --server-deploy flag or set it to 'none'.");
1893
1983
  if (has("auth") && config.auth === "better-auth") {
1894
1984
  const supportedFrontends = [
1895
1985
  "tanstack-router",
@@ -1899,113 +1989,125 @@ function validateConvexConstraints(config, providedFlags) {
1899
1989
  "native-uniwind",
1900
1990
  "native-unistyles"
1901
1991
  ];
1902
- if (!config.frontend?.some((f) => supportedFrontends.includes(f))) exitWithError("Better-Auth with Convex backend requires a supported frontend (TanStack Router, TanStack Start, Next.js, or Native).");
1992
+ if (!config.frontend?.some((f) => supportedFrontends.includes(f))) return validationErr("Better-Auth with Convex backend requires a supported frontend (TanStack Router, TanStack Start, Next.js, or Native).");
1903
1993
  }
1994
+ return Result.ok(void 0);
1904
1995
  }
1905
1996
  function validateBackendNoneConstraints(config, providedFlags) {
1906
1997
  const { backend } = config;
1907
- if (backend !== "none") return;
1998
+ if (backend !== "none") return Result.ok(void 0);
1908
1999
  const has = (k) => providedFlags.has(k);
1909
- if (has("runtime") && config.runtime !== "none") exitWithError("Backend 'none' requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.");
1910
- if (has("database") && config.database !== "none") exitWithError("Backend 'none' requires '--database none'. Please remove the --database flag or set it to 'none'.");
1911
- if (has("orm") && config.orm !== "none") exitWithError("Backend 'none' requires '--orm none'. Please remove the --orm flag or set it to 'none'.");
1912
- if (has("api") && config.api !== "none") exitWithError("Backend 'none' requires '--api none'. Please remove the --api flag or set it to 'none'.");
1913
- if (has("auth") && config.auth !== "none") exitWithError("Backend 'none' requires '--auth none'. Please remove the --auth flag or set it to 'none'.");
1914
- if (has("payments") && config.payments !== "none") exitWithError("Backend 'none' requires '--payments none'. Please remove the --payments flag or set it to 'none'.");
1915
- if (has("dbSetup") && config.dbSetup !== "none") exitWithError("Backend 'none' requires '--db-setup none'. Please remove the --db-setup flag or set it to 'none'.");
1916
- if (has("serverDeploy") && config.serverDeploy !== "none") exitWithError("Backend 'none' requires '--server-deploy none'. Please remove the --server-deploy flag or set it to 'none'.");
2000
+ if (has("runtime") && config.runtime !== "none") return validationErr("Backend 'none' requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.");
2001
+ if (has("database") && config.database !== "none") return validationErr("Backend 'none' requires '--database none'. Please remove the --database flag or set it to 'none'.");
2002
+ if (has("orm") && config.orm !== "none") return validationErr("Backend 'none' requires '--orm none'. Please remove the --orm flag or set it to 'none'.");
2003
+ if (has("api") && config.api !== "none") return validationErr("Backend 'none' requires '--api none'. Please remove the --api flag or set it to 'none'.");
2004
+ if (has("auth") && config.auth !== "none") return validationErr("Backend 'none' requires '--auth none'. Please remove the --auth flag or set it to 'none'.");
2005
+ if (has("payments") && config.payments !== "none") return validationErr("Backend 'none' requires '--payments none'. Please remove the --payments flag or set it to 'none'.");
2006
+ if (has("dbSetup") && config.dbSetup !== "none") return validationErr("Backend 'none' requires '--db-setup none'. Please remove the --db-setup flag or set it to 'none'.");
2007
+ if (has("serverDeploy") && config.serverDeploy !== "none") return validationErr("Backend 'none' requires '--server-deploy none'. Please remove the --server-deploy flag or set it to 'none'.");
2008
+ return Result.ok(void 0);
1917
2009
  }
1918
2010
  function validateSelfBackendConstraints(config, providedFlags) {
1919
2011
  const { backend } = config;
1920
- if (backend !== "self") return;
2012
+ if (backend !== "self") return Result.ok(void 0);
1921
2013
  const has = (k) => providedFlags.has(k);
1922
- if (has("runtime") && config.runtime !== "none") exitWithError("Backend 'self' (fullstack) requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.");
2014
+ if (has("runtime") && config.runtime !== "none") return validationErr("Backend 'self' (fullstack) requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.");
2015
+ return Result.ok(void 0);
1923
2016
  }
1924
2017
  function validateBackendConstraints(config, providedFlags, options) {
1925
2018
  const { backend } = config;
1926
- if (config.auth === "clerk" && backend !== "convex") exitWithError("Clerk authentication is only supported with the Convex backend. Please use '--backend convex' or choose a different auth provider.");
2019
+ if (config.auth === "clerk" && backend !== "convex") return validationErr("Clerk authentication is only supported with the Convex backend. Please use '--backend convex' or choose a different auth provider.");
1927
2020
  if (backend === "convex" && config.auth === "clerk" && config.frontend) {
1928
2021
  const incompatibleFrontends = config.frontend.filter((f) => [
1929
2022
  "nuxt",
1930
2023
  "svelte",
1931
2024
  "solid"
1932
2025
  ].includes(f));
1933
- if (incompatibleFrontends.length > 0) exitWithError(`Clerk authentication is not compatible with the following frontends: ${incompatibleFrontends.join(", ")}. Please choose a different frontend or auth provider.`);
2026
+ if (incompatibleFrontends.length > 0) return validationErr(`Clerk authentication is not compatible with the following frontends: ${incompatibleFrontends.join(", ")}. Please choose a different frontend or auth provider.`);
1934
2027
  }
1935
2028
  if (providedFlags.has("backend") && backend && backend !== "convex" && backend !== "none" && backend !== "self") {
1936
- if (providedFlags.has("runtime") && options.runtime === "none") exitWithError("'--runtime none' is only supported with '--backend convex', '--backend none', or '--backend self'. Please choose 'bun', 'node', or remove the --runtime flag.");
2029
+ if (providedFlags.has("runtime") && options.runtime === "none") return validationErr("'--runtime none' is only supported with '--backend convex', '--backend none', or '--backend self'. Please choose 'bun', 'node', or remove the --runtime flag.");
1937
2030
  }
1938
2031
  if (backend === "convex" && providedFlags.has("frontend") && options.frontend) {
1939
2032
  const incompatibleFrontends = options.frontend.filter((f) => f === "solid" || f === "astro");
1940
- if (incompatibleFrontends.length > 0) exitWithError(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
2033
+ if (incompatibleFrontends.length > 0) return validationErr(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
1941
2034
  }
2035
+ return Result.ok(void 0);
1942
2036
  }
1943
2037
  function validateFrontendConstraints(config, providedFlags) {
1944
2038
  const { frontend } = config;
1945
2039
  if (frontend && frontend.length > 0) {
1946
- ensureSingleWebAndNative(frontend);
1947
- if (providedFlags.has("api") && providedFlags.has("frontend") && config.api) validateApiFrontendCompatibility(config.api, frontend);
2040
+ const singleWebNativeResult = ensureSingleWebAndNative(frontend);
2041
+ if (singleWebNativeResult.isErr()) return singleWebNativeResult;
2042
+ if (providedFlags.has("api") && providedFlags.has("frontend") && config.api) {
2043
+ const apiResult = validateApiFrontendCompatibility(config.api, frontend);
2044
+ if (apiResult.isErr()) return apiResult;
2045
+ }
1948
2046
  }
1949
2047
  const hasWebFrontendFlag = (frontend ?? []).some((f) => isWebFrontend(f));
1950
- validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
2048
+ const webDeployResult = validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
2049
+ if (webDeployResult.isErr()) return webDeployResult;
2050
+ return Result.ok(void 0);
1951
2051
  }
1952
2052
  function validateApiConstraints(config, options) {
1953
2053
  if (config.api === "none") {
1954
- if (options.examples?.includes("todo") && options.backend !== "convex" && options.backend !== "none") exitWithError("Cannot use '--examples todo' when '--api' is set to 'none'. The todo example requires an API layer. Please remove 'todo' from --examples or choose an API type.");
2054
+ if (options.examples?.includes("todo") && options.backend !== "convex" && options.backend !== "none") return validationErr("Cannot use '--examples todo' when '--api' is set to 'none'. The todo example requires an API layer. Please remove 'todo' from --examples or choose an API type.");
1955
2055
  }
2056
+ return Result.ok(void 0);
1956
2057
  }
1957
2058
  function validateFullConfig(config, providedFlags, options) {
1958
- validateDatabaseOrmAuth(config, providedFlags);
1959
- validateDatabaseSetup(config, providedFlags);
1960
- validateConvexConstraints(config, providedFlags);
1961
- validateBackendNoneConstraints(config, providedFlags);
1962
- validateSelfBackendConstraints(config, providedFlags);
1963
- validateBackendConstraints(config, providedFlags, options);
1964
- validateFrontendConstraints(config, providedFlags);
1965
- validateApiConstraints(config, options);
1966
- validateServerDeployRequiresBackend(config.serverDeploy, config.backend);
1967
- validateSelfBackendCompatibility(providedFlags, options, config);
1968
- validateWorkersCompatibility(providedFlags, options, config);
1969
- if (config.runtime === "workers" && config.serverDeploy === "none") exitWithError("Cloudflare Workers runtime requires a server deployment. Please choose 'alchemy' for --server-deploy.");
1970
- if (providedFlags.has("serverDeploy") && config.serverDeploy === "cloudflare" && config.runtime !== "workers") exitWithError(`Server deployment '${config.serverDeploy}' requires '--runtime workers'. Please use '--runtime workers' or choose a different server deployment.`);
1971
- if (config.addons && config.addons.length > 0) {
1972
- validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth);
1973
- config.addons = [...new Set(config.addons)];
1974
- }
1975
- validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
1976
- validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend ?? []);
2059
+ return Result.gen(function* () {
2060
+ yield* validateDatabaseOrmAuth(config, providedFlags);
2061
+ yield* validateDatabaseSetup(config, providedFlags);
2062
+ yield* validateConvexConstraints(config, providedFlags);
2063
+ yield* validateBackendNoneConstraints(config, providedFlags);
2064
+ yield* validateSelfBackendConstraints(config, providedFlags);
2065
+ yield* validateBackendConstraints(config, providedFlags, options);
2066
+ yield* validateFrontendConstraints(config, providedFlags);
2067
+ yield* validateApiConstraints(config, options);
2068
+ yield* validateServerDeployRequiresBackend(config.serverDeploy, config.backend);
2069
+ yield* validateSelfBackendCompatibility(providedFlags, options, config);
2070
+ yield* validateWorkersCompatibility(providedFlags, options, config);
2071
+ if (config.runtime === "workers" && config.serverDeploy === "none") yield* validationErr("Cloudflare Workers runtime requires a server deployment. Please choose 'alchemy' for --server-deploy.");
2072
+ if (providedFlags.has("serverDeploy") && config.serverDeploy === "cloudflare" && config.runtime !== "workers") yield* validationErr(`Server deployment '${config.serverDeploy}' requires '--runtime workers'. Please use '--runtime workers' or choose a different server deployment.`);
2073
+ if (config.addons && config.addons.length > 0) {
2074
+ yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth);
2075
+ config.addons = [...new Set(config.addons)];
2076
+ }
2077
+ yield* validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
2078
+ yield* validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend ?? []);
2079
+ return Result.ok(void 0);
2080
+ });
1977
2081
  }
1978
2082
  function validateConfigForProgrammaticUse(config) {
1979
- try {
1980
- validateDatabaseOrmAuth(config);
1981
- if (config.frontend && config.frontend.length > 0) ensureSingleWebAndNative(config.frontend);
1982
- validateApiFrontendCompatibility(config.api, config.frontend);
1983
- validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend);
1984
- if (config.addons && config.addons.length > 0) validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth);
1985
- validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
1986
- } catch (error) {
1987
- if (error instanceof Error) throw error;
1988
- throw new Error(String(error));
1989
- }
2083
+ return Result.gen(function* () {
2084
+ yield* validateDatabaseOrmAuth(config);
2085
+ if (config.frontend && config.frontend.length > 0) yield* ensureSingleWebAndNative(config.frontend);
2086
+ yield* validateApiFrontendCompatibility(config.api, config.frontend);
2087
+ yield* validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend);
2088
+ if (config.addons && config.addons.length > 0) yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth);
2089
+ yield* validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
2090
+ return Result.ok(void 0);
2091
+ });
1990
2092
  }
1991
2093
 
1992
2094
  //#endregion
1993
2095
  //#region src/utils/project-name-validation.ts
1994
2096
  function validateProjectName(name) {
1995
2097
  const result = types_exports.ProjectNameSchema.safeParse(name);
1996
- if (!result.success) exitWithError(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
1997
- }
1998
- function validateProjectNameThrow(name) {
1999
- const result = types_exports.ProjectNameSchema.safeParse(name);
2000
- if (!result.success) throw new Error(`Invalid project name: ${result.error.issues[0]?.message}`);
2098
+ if (!result.success) return Result.err(new ValidationError({
2099
+ field: "projectName",
2100
+ value: name,
2101
+ message: `Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`
2102
+ }));
2103
+ return Result.ok(void 0);
2001
2104
  }
2002
- function extractAndValidateProjectName(projectName, projectDirectory, throwOnError = false) {
2105
+ function extractAndValidateProjectName(projectName, projectDirectory) {
2003
2106
  const derivedName = projectName || (projectDirectory ? path.basename(path.resolve(process.cwd(), projectDirectory)) : "");
2004
- if (!derivedName) return "";
2005
- const nameToValidate = projectName ? path.basename(projectName) : derivedName;
2006
- if (throwOnError) validateProjectNameThrow(nameToValidate);
2007
- else validateProjectName(nameToValidate);
2008
- return projectName || derivedName;
2107
+ if (!derivedName) return Result.ok("");
2108
+ const validationResult = validateProjectName(projectName ? path.basename(projectName) : derivedName);
2109
+ if (validationResult.isErr()) return Result.err(validationResult.error);
2110
+ return Result.ok(projectName || derivedName);
2009
2111
  }
2010
2112
 
2011
2113
  //#endregion
@@ -2026,97 +2128,111 @@ const CORE_STACK_FLAGS = new Set([
2026
2128
  "serverDeploy"
2027
2129
  ]);
2028
2130
  function validateYesFlagCombination(options, providedFlags) {
2029
- if (!options.yes) return;
2030
- if (options.template && options.template !== "none") return;
2131
+ if (!options.yes) return Result.ok(void 0);
2132
+ if (options.template && options.template !== "none") return Result.ok(void 0);
2031
2133
  const coreStackFlagsProvided = Array.from(providedFlags).filter((flag) => CORE_STACK_FLAGS.has(flag));
2032
- if (coreStackFlagsProvided.length > 0) exitWithError(`Cannot combine --yes with core stack configuration flags: ${coreStackFlagsProvided.map((f) => `--${f}`).join(", ")}. The --yes flag uses default configuration. Remove these flags or use --yes without them.`);
2134
+ if (coreStackFlagsProvided.length > 0) return Result.err(new ValidationError({ message: `Cannot combine --yes with core stack configuration flags: ${coreStackFlagsProvided.map((f) => `--${f}`).join(", ")}. The --yes flag uses default configuration. Remove these flags or use --yes without them.` }));
2135
+ return Result.ok(void 0);
2033
2136
  }
2034
2137
  function processAndValidateFlags(options, providedFlags, projectName) {
2035
2138
  if (options.yolo) {
2036
2139
  const cfg = processFlags(options, projectName);
2037
- const validatedProjectName$1 = extractAndValidateProjectName(projectName, options.projectDirectory, true);
2038
- if (validatedProjectName$1) cfg.projectName = validatedProjectName$1;
2039
- return cfg;
2040
- }
2041
- validateYesFlagCombination(options, providedFlags);
2042
- try {
2043
- validateArrayOptions(options);
2044
- } catch (error) {
2045
- exitWithError(error instanceof Error ? error.message : String(error));
2140
+ const validatedProjectNameResult$1 = extractAndValidateProjectName(projectName, options.projectDirectory);
2141
+ if (validatedProjectNameResult$1.isOk() && validatedProjectNameResult$1.value) cfg.projectName = validatedProjectNameResult$1.value;
2142
+ return Result.ok(cfg);
2046
2143
  }
2144
+ const yesFlagResult = validateYesFlagCombination(options, providedFlags);
2145
+ if (yesFlagResult.isErr()) return Result.err(yesFlagResult.error);
2146
+ const arrayOptionsResult = validateArrayOptions(options);
2147
+ if (arrayOptionsResult.isErr()) return Result.err(arrayOptionsResult.error);
2047
2148
  const config = processFlags(options, projectName);
2048
- const validatedProjectName = extractAndValidateProjectName(projectName, options.projectDirectory, false);
2049
- if (validatedProjectName) config.projectName = validatedProjectName;
2050
- validateFullConfig(config, providedFlags, options);
2051
- return config;
2149
+ const validatedProjectNameResult = extractAndValidateProjectName(projectName, options.projectDirectory);
2150
+ if (validatedProjectNameResult.isErr()) return Result.err(validatedProjectNameResult.error);
2151
+ if (validatedProjectNameResult.value) config.projectName = validatedProjectNameResult.value;
2152
+ const fullConfigResult = validateFullConfig(config, providedFlags, options);
2153
+ if (fullConfigResult.isErr()) return Result.err(fullConfigResult.error);
2154
+ return Result.ok(config);
2052
2155
  }
2053
2156
  function processProvidedFlagsWithoutValidation(options, projectName) {
2054
- if (!options.yolo) validateYesFlagCombination(options, getProvidedFlags(options));
2157
+ if (!options.yolo) {
2158
+ const yesFlagResult = validateYesFlagCombination(options, getProvidedFlags(options));
2159
+ if (yesFlagResult.isErr()) return Result.err(yesFlagResult.error);
2160
+ }
2055
2161
  const config = processFlags(options, projectName);
2056
- const validatedProjectName = extractAndValidateProjectName(projectName, options.projectDirectory, true);
2057
- if (validatedProjectName) config.projectName = validatedProjectName;
2058
- return config;
2162
+ const validatedProjectNameResult = extractAndValidateProjectName(projectName, options.projectDirectory);
2163
+ if (validatedProjectNameResult.isErr()) return Result.err(validatedProjectNameResult.error);
2164
+ if (validatedProjectNameResult.value) config.projectName = validatedProjectNameResult.value;
2165
+ return Result.ok(config);
2059
2166
  }
2060
2167
  function validateConfigCompatibility(config, providedFlags, options) {
2061
- if (options?.yolo) return;
2062
- if (options && providedFlags) validateFullConfig(config, providedFlags, options);
2063
- else validateConfigForProgrammaticUse(config);
2168
+ if (options?.yolo) return Result.ok(void 0);
2169
+ if (options && providedFlags) return validateFullConfig(config, providedFlags, options);
2170
+ else return validateConfigForProgrammaticUse(config);
2064
2171
  }
2065
2172
 
2066
2173
  //#endregion
2067
2174
  //#region src/utils/bts-config.ts
2068
2175
  const BTS_CONFIG_FILE = "bts.jsonc";
2069
2176
  async function writeBtsConfig(projectConfig) {
2070
- const btsConfig = {
2071
- version: getLatestCLIVersion(),
2072
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2073
- database: projectConfig.database,
2074
- orm: projectConfig.orm,
2075
- backend: projectConfig.backend,
2076
- runtime: projectConfig.runtime,
2077
- frontend: projectConfig.frontend,
2078
- addons: projectConfig.addons,
2079
- examples: projectConfig.examples,
2080
- auth: projectConfig.auth,
2081
- payments: projectConfig.payments,
2082
- packageManager: projectConfig.packageManager,
2083
- dbSetup: projectConfig.dbSetup,
2084
- api: projectConfig.api,
2085
- webDeploy: projectConfig.webDeploy,
2086
- serverDeploy: projectConfig.serverDeploy
2087
- };
2088
- const baseContent = {
2089
- $schema: "https://r2.better-t-stack.dev/schema.json",
2090
- version: btsConfig.version,
2091
- createdAt: btsConfig.createdAt,
2092
- database: btsConfig.database,
2093
- orm: btsConfig.orm,
2094
- backend: btsConfig.backend,
2095
- runtime: btsConfig.runtime,
2096
- frontend: btsConfig.frontend,
2097
- addons: btsConfig.addons,
2098
- examples: btsConfig.examples,
2099
- auth: btsConfig.auth,
2100
- payments: btsConfig.payments,
2101
- packageManager: btsConfig.packageManager,
2102
- dbSetup: btsConfig.dbSetup,
2103
- api: btsConfig.api,
2104
- webDeploy: btsConfig.webDeploy,
2105
- serverDeploy: btsConfig.serverDeploy
2106
- };
2107
- let configContent = JSON.stringify(baseContent);
2108
- const formatResult = JSONC.format(configContent, void 0, {
2109
- tabSize: 2,
2110
- insertSpaces: true,
2111
- eol: "\n"
2112
- });
2113
- configContent = JSONC.applyEdits(configContent, formatResult);
2114
- const finalContent = `// Better-T-Stack configuration file
2177
+ return Result.tryPromise({
2178
+ try: async () => {
2179
+ const btsConfig = {
2180
+ version: getLatestCLIVersion(),
2181
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2182
+ database: projectConfig.database,
2183
+ orm: projectConfig.orm,
2184
+ backend: projectConfig.backend,
2185
+ runtime: projectConfig.runtime,
2186
+ frontend: projectConfig.frontend,
2187
+ addons: projectConfig.addons,
2188
+ examples: projectConfig.examples,
2189
+ auth: projectConfig.auth,
2190
+ payments: projectConfig.payments,
2191
+ packageManager: projectConfig.packageManager,
2192
+ dbSetup: projectConfig.dbSetup,
2193
+ api: projectConfig.api,
2194
+ webDeploy: projectConfig.webDeploy,
2195
+ serverDeploy: projectConfig.serverDeploy
2196
+ };
2197
+ const baseContent = {
2198
+ $schema: "https://r2.better-t-stack.dev/schema.json",
2199
+ version: btsConfig.version,
2200
+ createdAt: btsConfig.createdAt,
2201
+ database: btsConfig.database,
2202
+ orm: btsConfig.orm,
2203
+ backend: btsConfig.backend,
2204
+ runtime: btsConfig.runtime,
2205
+ frontend: btsConfig.frontend,
2206
+ addons: btsConfig.addons,
2207
+ examples: btsConfig.examples,
2208
+ auth: btsConfig.auth,
2209
+ payments: btsConfig.payments,
2210
+ packageManager: btsConfig.packageManager,
2211
+ dbSetup: btsConfig.dbSetup,
2212
+ api: btsConfig.api,
2213
+ webDeploy: btsConfig.webDeploy,
2214
+ serverDeploy: btsConfig.serverDeploy
2215
+ };
2216
+ let configContent = JSON.stringify(baseContent);
2217
+ const formatResult = JSONC.format(configContent, void 0, {
2218
+ tabSize: 2,
2219
+ insertSpaces: true,
2220
+ eol: "\n"
2221
+ });
2222
+ configContent = JSONC.applyEdits(configContent, formatResult);
2223
+ const finalContent = `// Better-T-Stack configuration file
2115
2224
  // safe to delete
2116
2225
 
2117
2226
  ${configContent}`;
2118
- const configPath = path.join(projectConfig.projectDir, BTS_CONFIG_FILE);
2119
- await fs.writeFile(configPath, finalContent, "utf-8");
2227
+ const configPath = path.join(projectConfig.projectDir, BTS_CONFIG_FILE);
2228
+ await fs.writeFile(configPath, finalContent, "utf-8");
2229
+ },
2230
+ catch: (e) => new ProjectCreationError({
2231
+ phase: "config-writing",
2232
+ message: `Failed to write BTS config: ${e instanceof Error ? e.message : String(e)}`,
2233
+ cause: e
2234
+ })
2235
+ });
2120
2236
  }
2121
2237
 
2122
2238
  //#endregion
@@ -2126,28 +2242,42 @@ const formatOptions = {
2126
2242
  experimentalSortImports: { order: "asc" }
2127
2243
  };
2128
2244
  async function formatCode(filePath, content) {
2129
- try {
2130
- const result = await format(path.basename(filePath), content, formatOptions);
2131
- if (result.errors && result.errors.length > 0) return null;
2132
- return result.code;
2133
- } catch {
2134
- return null;
2135
- }
2245
+ const result = await Result.tryPromise({
2246
+ try: async () => {
2247
+ const formatResult = await format(path.basename(filePath), content, formatOptions);
2248
+ if (formatResult.errors && formatResult.errors.length > 0) return null;
2249
+ return formatResult.code;
2250
+ },
2251
+ catch: () => null
2252
+ });
2253
+ return result.isOk() ? result.value : null;
2136
2254
  }
2137
2255
  async function formatProject(projectDir) {
2138
- async function formatDirectory(dir) {
2139
- const entries = await fs.readdir(dir, { withFileTypes: true });
2140
- await Promise.all(entries.map(async (entry) => {
2141
- const fullPath = path.join(dir, entry.name);
2142
- if (entry.isDirectory()) await formatDirectory(fullPath);
2143
- else if (entry.isFile()) try {
2144
- const content = await fs.readFile(fullPath, "utf-8");
2145
- const formatted = await formatCode(fullPath, content);
2146
- if (formatted && formatted !== content) await fs.writeFile(fullPath, formatted, "utf-8");
2147
- } catch {}
2148
- }));
2149
- }
2150
- await formatDirectory(projectDir);
2256
+ return Result.tryPromise({
2257
+ try: async () => {
2258
+ async function formatDirectory(dir) {
2259
+ const entries = await fs.readdir(dir, { withFileTypes: true });
2260
+ await Promise.all(entries.map(async (entry) => {
2261
+ const fullPath = path.join(dir, entry.name);
2262
+ if (entry.isDirectory()) await formatDirectory(fullPath);
2263
+ else if (entry.isFile()) await Result.tryPromise({
2264
+ try: async () => {
2265
+ const content = await fs.readFile(fullPath, "utf-8");
2266
+ const formatted = await formatCode(fullPath, content);
2267
+ if (formatted && formatted !== content) await fs.writeFile(fullPath, formatted, "utf-8");
2268
+ },
2269
+ catch: () => void 0
2270
+ });
2271
+ }));
2272
+ }
2273
+ await formatDirectory(projectDir);
2274
+ },
2275
+ catch: (e) => new ProjectCreationError({
2276
+ phase: "formatting",
2277
+ message: `Failed to format project: ${e instanceof Error ? e.message : String(e)}`,
2278
+ cause: e
2279
+ })
2280
+ });
2151
2281
  }
2152
2282
 
2153
2283
  //#endregion
@@ -2261,40 +2391,49 @@ const TEMPLATES$2 = {
2261
2391
  };
2262
2392
  async function setupFumadocs(config) {
2263
2393
  const { packageManager, projectDir } = config;
2264
- try {
2265
- log.info("Setting up Fumadocs...");
2266
- const template = await select({
2267
- message: "Choose a template",
2268
- options: Object.entries(TEMPLATES$2).map(([key, template$1]) => ({
2269
- value: key,
2270
- label: template$1.label,
2271
- hint: template$1.hint
2272
- })),
2273
- initialValue: "next-mdx"
2274
- });
2275
- if (isCancel(template)) return exitCancelled("Operation cancelled");
2276
- const args = getPackageExecutionArgs(packageManager, `create-fumadocs-app@latest fumadocs --template ${TEMPLATES$2[template].value} --src --pm ${packageManager} --no-git`);
2277
- const appsDir = path.join(projectDir, "apps");
2278
- await fs.ensureDir(appsDir);
2279
- const s = spinner();
2280
- s.start("Running Fumadocs create command...");
2281
- await $({
2282
- cwd: appsDir,
2283
- env: { CI: "true" }
2284
- })`${args}`;
2285
- const fumadocsDir = path.join(projectDir, "apps", "fumadocs");
2286
- const packageJsonPath = path.join(fumadocsDir, "package.json");
2287
- if (await fs.pathExists(packageJsonPath)) {
2288
- const packageJson = await fs.readJson(packageJsonPath);
2289
- packageJson.name = "fumadocs";
2290
- if (packageJson.scripts?.dev) packageJson.scripts.dev = `${packageJson.scripts.dev} --port=4000`;
2291
- await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2292
- }
2293
- s.stop("Fumadocs setup complete!");
2294
- } catch (error) {
2295
- log.error(pc.red("Failed to set up Fumadocs"));
2296
- if (error instanceof Error) consola.error(pc.red(error.message));
2394
+ log.info("Setting up Fumadocs...");
2395
+ const template = await select({
2396
+ message: "Choose a template",
2397
+ options: Object.entries(TEMPLATES$2).map(([key, template$1]) => ({
2398
+ value: key,
2399
+ label: template$1.label,
2400
+ hint: template$1.hint
2401
+ })),
2402
+ initialValue: "next-mdx"
2403
+ });
2404
+ if (isCancel(template)) return userCancelled("Operation cancelled");
2405
+ const args = getPackageExecutionArgs(packageManager, `create-fumadocs-app@latest fumadocs --template ${TEMPLATES$2[template].value} --src --pm ${packageManager} --no-git`);
2406
+ const appsDir = path.join(projectDir, "apps");
2407
+ await fs.ensureDir(appsDir);
2408
+ const s = spinner();
2409
+ s.start("Running Fumadocs create command...");
2410
+ const result = await Result.tryPromise({
2411
+ try: async () => {
2412
+ await $({
2413
+ cwd: appsDir,
2414
+ env: { CI: "true" }
2415
+ })`${args}`;
2416
+ const fumadocsDir = path.join(projectDir, "apps", "fumadocs");
2417
+ const packageJsonPath = path.join(fumadocsDir, "package.json");
2418
+ if (await fs.pathExists(packageJsonPath)) {
2419
+ const packageJson = await fs.readJson(packageJsonPath);
2420
+ packageJson.name = "fumadocs";
2421
+ if (packageJson.scripts?.dev) packageJson.scripts.dev = `${packageJson.scripts.dev} --port=4000`;
2422
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2423
+ }
2424
+ },
2425
+ catch: (e) => new AddonSetupError({
2426
+ addon: "fumadocs",
2427
+ message: `Failed to set up Fumadocs: ${e instanceof Error ? e.message : String(e)}`,
2428
+ cause: e
2429
+ })
2430
+ });
2431
+ if (result.isErr()) {
2432
+ s.stop("Failed to set up Fumadocs");
2433
+ return result;
2297
2434
  }
2435
+ s.stop("Fumadocs setup complete!");
2436
+ return Result.ok(void 0);
2298
2437
  }
2299
2438
 
2300
2439
  //#endregion
@@ -2332,79 +2471,84 @@ async function setupOxlint(projectDir, packageManager) {
2332
2471
  //#region src/helpers/addons/ruler-setup.ts
2333
2472
  async function setupRuler(config) {
2334
2473
  const { packageManager, projectDir } = config;
2335
- try {
2336
- log.info("Setting up Ruler...");
2337
- const rulerDir = path.join(projectDir, ".ruler");
2338
- if (!await fs.pathExists(rulerDir)) {
2339
- log.error(pc.red("Ruler template directory not found. Please ensure ruler addon is properly installed."));
2340
- return;
2341
- }
2342
- const selectedEditors = await autocompleteMultiselect({
2343
- message: "Select AI assistants for Ruler",
2344
- options: Object.entries({
2345
- agentsmd: { label: "Agents.md" },
2346
- aider: { label: "Aider" },
2347
- amazonqcli: { label: "Amazon Q CLI" },
2348
- amp: { label: "AMP" },
2349
- antigravity: { label: "Antigravity" },
2350
- augmentcode: { label: "AugmentCode" },
2351
- claude: { label: "Claude Code" },
2352
- cline: { label: "Cline" },
2353
- codex: { label: "OpenAI Codex CLI" },
2354
- copilot: { label: "GitHub Copilot" },
2355
- crush: { label: "Crush" },
2356
- cursor: { label: "Cursor" },
2357
- firebase: { label: "Firebase Studio" },
2358
- firebender: { label: "Firebender" },
2359
- "gemini-cli": { label: "Gemini CLI" },
2360
- goose: { label: "Goose" },
2361
- jules: { label: "Jules" },
2362
- junie: { label: "Junie" },
2363
- kilocode: { label: "Kilo Code" },
2364
- kiro: { label: "Kiro" },
2365
- mistral: { label: "Mistral" },
2366
- opencode: { label: "OpenCode" },
2367
- openhands: { label: "Open Hands" },
2368
- qwen: { label: "Qwen" },
2369
- roo: { label: "RooCode" },
2370
- trae: { label: "Trae AI" },
2371
- warp: { label: "Warp" },
2372
- windsurf: { label: "Windsurf" },
2373
- zed: { label: "Zed" }
2374
- }).map(([key, v]) => ({
2375
- value: key,
2376
- label: v.label
2377
- })),
2378
- required: false
2379
- });
2380
- if (isCancel(selectedEditors)) return exitCancelled("Operation cancelled");
2381
- if (selectedEditors.length === 0) {
2382
- log.info("No AI assistants selected. To apply rules later, run:");
2383
- log.info(pc.cyan(`${getPackageExecutionCommand(packageManager, "@intellectronica/ruler@latest apply --local-only")}`));
2384
- return;
2385
- }
2386
- const configFile = path.join(rulerDir, "ruler.toml");
2387
- let updatedConfig = await fs.readFile(configFile, "utf-8");
2388
- const defaultAgentsLine = `default_agents = [${selectedEditors.map((editor) => `"${editor}"`).join(", ")}]`;
2389
- updatedConfig = updatedConfig.replace(/default_agents = \[\]/, defaultAgentsLine);
2390
- await fs.writeFile(configFile, updatedConfig);
2391
- await addRulerScriptToPackageJson(projectDir, packageManager);
2392
- const s = spinner();
2393
- s.start("Applying rules with Ruler...");
2394
- try {
2474
+ log.info("Setting up Ruler...");
2475
+ const rulerDir = path.join(projectDir, ".ruler");
2476
+ if (!await fs.pathExists(rulerDir)) {
2477
+ log.error(pc.red("Ruler template directory not found. Please ensure ruler addon is properly installed."));
2478
+ return Result.ok(void 0);
2479
+ }
2480
+ const selectedEditors = await autocompleteMultiselect({
2481
+ message: "Select AI assistants for Ruler",
2482
+ options: Object.entries({
2483
+ agentsmd: { label: "Agents.md" },
2484
+ aider: { label: "Aider" },
2485
+ amazonqcli: { label: "Amazon Q CLI" },
2486
+ amp: { label: "AMP" },
2487
+ antigravity: { label: "Antigravity" },
2488
+ augmentcode: { label: "AugmentCode" },
2489
+ claude: { label: "Claude Code" },
2490
+ cline: { label: "Cline" },
2491
+ codex: { label: "OpenAI Codex CLI" },
2492
+ copilot: { label: "GitHub Copilot" },
2493
+ crush: { label: "Crush" },
2494
+ cursor: { label: "Cursor" },
2495
+ firebase: { label: "Firebase Studio" },
2496
+ firebender: { label: "Firebender" },
2497
+ "gemini-cli": { label: "Gemini CLI" },
2498
+ goose: { label: "Goose" },
2499
+ jules: { label: "Jules" },
2500
+ junie: { label: "Junie" },
2501
+ kilocode: { label: "Kilo Code" },
2502
+ kiro: { label: "Kiro" },
2503
+ mistral: { label: "Mistral" },
2504
+ opencode: { label: "OpenCode" },
2505
+ openhands: { label: "Open Hands" },
2506
+ qwen: { label: "Qwen" },
2507
+ roo: { label: "RooCode" },
2508
+ trae: { label: "Trae AI" },
2509
+ warp: { label: "Warp" },
2510
+ windsurf: { label: "Windsurf" },
2511
+ zed: { label: "Zed" }
2512
+ }).map(([key, v]) => ({
2513
+ value: key,
2514
+ label: v.label
2515
+ })),
2516
+ required: false
2517
+ });
2518
+ if (isCancel(selectedEditors)) return userCancelled("Operation cancelled");
2519
+ if (selectedEditors.length === 0) {
2520
+ log.info("No AI assistants selected. To apply rules later, run:");
2521
+ log.info(pc.cyan(`${getPackageExecutionCommand(packageManager, "@intellectronica/ruler@latest apply --local-only")}`));
2522
+ return Result.ok(void 0);
2523
+ }
2524
+ const configFile = path.join(rulerDir, "ruler.toml");
2525
+ let updatedConfig = await fs.readFile(configFile, "utf-8");
2526
+ const defaultAgentsLine = `default_agents = [${selectedEditors.map((editor) => `"${editor}"`).join(", ")}]`;
2527
+ updatedConfig = updatedConfig.replace(/default_agents = \[\]/, defaultAgentsLine);
2528
+ await fs.writeFile(configFile, updatedConfig);
2529
+ await addRulerScriptToPackageJson(projectDir, packageManager);
2530
+ const s = spinner();
2531
+ s.start("Applying rules with Ruler...");
2532
+ const applyResult = await Result.tryPromise({
2533
+ try: async () => {
2395
2534
  const rulerApplyArgs = getPackageExecutionArgs(packageManager, `@intellectronica/ruler@latest apply --agents ${selectedEditors.join(",")} --local-only`);
2396
2535
  await $({
2397
2536
  cwd: projectDir,
2398
2537
  env: { CI: "true" }
2399
2538
  })`${rulerApplyArgs}`;
2400
- s.stop("Applied rules with Ruler");
2401
- } catch {
2402
- s.stop(pc.red("Failed to apply rules"));
2403
- }
2404
- } catch (error) {
2405
- log.error(pc.red("Failed to set up Ruler"));
2406
- if (error instanceof Error) console.error(pc.red(error.message));
2539
+ },
2540
+ catch: (e) => new AddonSetupError({
2541
+ addon: "ruler",
2542
+ message: `Failed to apply rules: ${e instanceof Error ? e.message : String(e)}`,
2543
+ cause: e
2544
+ })
2545
+ });
2546
+ if (applyResult.isErr()) {
2547
+ s.stop(pc.red("Failed to apply rules"));
2548
+ return applyResult;
2407
2549
  }
2550
+ s.stop("Applied rules with Ruler");
2551
+ return Result.ok(void 0);
2408
2552
  }
2409
2553
  async function addRulerScriptToPackageJson(projectDir, packageManager) {
2410
2554
  const rootPackageJsonPath = path.join(projectDir, "package.json");
@@ -2424,29 +2568,38 @@ async function addRulerScriptToPackageJson(projectDir, packageManager) {
2424
2568
  async function setupStarlight(config) {
2425
2569
  const { packageManager, projectDir } = config;
2426
2570
  const s = spinner();
2427
- try {
2428
- s.start("Setting up Starlight docs...");
2429
- const args = getPackageExecutionArgs(packageManager, `create-astro@latest ${[
2430
- "docs",
2431
- "--template",
2432
- "starlight",
2433
- "--no-install",
2434
- "--add",
2435
- "tailwind",
2436
- "--no-git",
2437
- "--skip-houston"
2438
- ].join(" ")}`);
2439
- const appsDir = path.join(projectDir, "apps");
2440
- await fs.ensureDir(appsDir);
2441
- await $({
2442
- cwd: appsDir,
2443
- env: { CI: "true" }
2444
- })`${args}`;
2445
- s.stop("Starlight docs setup successfully!");
2446
- } catch (error) {
2447
- s.stop(pc.red("Failed to set up Starlight docs"));
2448
- if (error instanceof Error) consola.error(pc.red(error.message));
2571
+ s.start("Setting up Starlight docs...");
2572
+ const args = getPackageExecutionArgs(packageManager, `create-astro@latest ${[
2573
+ "docs",
2574
+ "--template",
2575
+ "starlight",
2576
+ "--no-install",
2577
+ "--add",
2578
+ "tailwind",
2579
+ "--no-git",
2580
+ "--skip-houston"
2581
+ ].join(" ")}`);
2582
+ const appsDir = path.join(projectDir, "apps");
2583
+ await fs.ensureDir(appsDir);
2584
+ const result = await Result.tryPromise({
2585
+ try: async () => {
2586
+ await $({
2587
+ cwd: appsDir,
2588
+ env: { CI: "true" }
2589
+ })`${args}`;
2590
+ },
2591
+ catch: (e) => new AddonSetupError({
2592
+ addon: "starlight",
2593
+ message: `Failed to set up Starlight docs: ${e instanceof Error ? e.message : String(e)}`,
2594
+ cause: e
2595
+ })
2596
+ });
2597
+ if (result.isErr()) {
2598
+ s.stop("Failed to set up Starlight docs");
2599
+ return result;
2449
2600
  }
2601
+ s.stop("Starlight docs setup successfully!");
2602
+ return Result.ok(void 0);
2450
2603
  }
2451
2604
 
2452
2605
  //#endregion
@@ -2455,35 +2608,44 @@ async function setupTauri(config) {
2455
2608
  const { packageManager, frontend, projectDir } = config;
2456
2609
  const s = spinner();
2457
2610
  const clientPackageDir = path.join(projectDir, "apps/web");
2458
- if (!await fs.pathExists(clientPackageDir)) return;
2459
- try {
2460
- s.start("Setting up Tauri desktop app support...");
2461
- const hasReactRouter = frontend.includes("react-router");
2462
- const hasNuxt = frontend.includes("nuxt");
2463
- const hasSvelte = frontend.includes("svelte");
2464
- const hasNext = frontend.includes("next");
2465
- const devUrl = hasReactRouter || hasSvelte ? "http://localhost:5173" : hasNext ? "http://localhost:3001" : "http://localhost:3001";
2466
- const frontendDist = hasNuxt ? "../.output/public" : hasSvelte ? "../build" : hasNext ? "../.next" : hasReactRouter ? "../build/client" : "../dist";
2467
- const tauriArgs = [
2468
- "@tauri-apps/cli@latest",
2469
- "init",
2470
- `--app-name=${path.basename(projectDir)}`,
2471
- `--window-title=${path.basename(projectDir)}`,
2472
- `--frontend-dist=${frontendDist}`,
2473
- `--dev-url=${devUrl}`,
2474
- `--before-dev-command=${packageManager} run dev`,
2475
- `--before-build-command=${packageManager} run build`
2476
- ];
2477
- const prefix = getPackageRunnerPrefix(packageManager);
2478
- await $({
2479
- cwd: clientPackageDir,
2480
- env: { CI: "true" }
2481
- })`${[...prefix, ...tauriArgs]}`;
2482
- s.stop("Tauri desktop app support configured successfully!");
2483
- } catch (error) {
2484
- s.stop(pc.red("Failed to set up Tauri"));
2485
- if (error instanceof Error) consola$1.error(pc.red(error.message));
2611
+ if (!await fs.pathExists(clientPackageDir)) return Result.ok(void 0);
2612
+ s.start("Setting up Tauri desktop app support...");
2613
+ const hasReactRouter = frontend.includes("react-router");
2614
+ const hasNuxt = frontend.includes("nuxt");
2615
+ const hasSvelte = frontend.includes("svelte");
2616
+ const hasNext = frontend.includes("next");
2617
+ const devUrl = hasReactRouter || hasSvelte ? "http://localhost:5173" : hasNext ? "http://localhost:3001" : "http://localhost:3001";
2618
+ const frontendDist = hasNuxt ? "../.output/public" : hasSvelte ? "../build" : hasNext ? "../.next" : hasReactRouter ? "../build/client" : "../dist";
2619
+ const tauriArgs = [
2620
+ "@tauri-apps/cli@latest",
2621
+ "init",
2622
+ `--app-name=${path.basename(projectDir)}`,
2623
+ `--window-title=${path.basename(projectDir)}`,
2624
+ `--frontend-dist=${frontendDist}`,
2625
+ `--dev-url=${devUrl}`,
2626
+ `--before-dev-command=${packageManager} run dev`,
2627
+ `--before-build-command=${packageManager} run build`
2628
+ ];
2629
+ const prefix = getPackageRunnerPrefix(packageManager);
2630
+ const result = await Result.tryPromise({
2631
+ try: async () => {
2632
+ await $({
2633
+ cwd: clientPackageDir,
2634
+ env: { CI: "true" }
2635
+ })`${[...prefix, ...tauriArgs]}`;
2636
+ },
2637
+ catch: (e) => new AddonSetupError({
2638
+ addon: "tauri",
2639
+ message: `Failed to set up Tauri: ${e instanceof Error ? e.message : String(e)}`,
2640
+ cause: e
2641
+ })
2642
+ });
2643
+ if (result.isErr()) {
2644
+ s.stop("Failed to set up Tauri");
2645
+ return result;
2486
2646
  }
2647
+ s.stop("Tauri desktop app support configured successfully!");
2648
+ return Result.ok(void 0);
2487
2649
  }
2488
2650
 
2489
2651
  //#endregion
@@ -2504,32 +2666,52 @@ const TEMPLATES$1 = {
2504
2666
  };
2505
2667
  async function setupTui(config) {
2506
2668
  const { packageManager, projectDir } = config;
2507
- try {
2508
- log.info("Setting up OpenTUI...");
2509
- const template = await select({
2510
- message: "Choose a template",
2511
- options: Object.entries(TEMPLATES$1).map(([key, template$1]) => ({
2512
- value: key,
2513
- label: template$1.label,
2514
- hint: template$1.hint
2515
- })),
2516
- initialValue: "core"
2517
- });
2518
- if (isCancel(template)) return exitCancelled("Operation cancelled");
2519
- const args = getPackageExecutionArgs(packageManager, `create-tui@latest --template ${template} --no-git --no-install tui`);
2520
- const appsDir = path.join(projectDir, "apps");
2521
- await fs.ensureDir(appsDir);
2522
- const s = spinner();
2523
- s.start("Running OpenTUI create command...");
2524
- await $({
2525
- cwd: appsDir,
2526
- env: { CI: "true" }
2527
- })`${args}`;
2528
- s.stop("OpenTUI setup complete!");
2529
- } catch (error) {
2669
+ log.info("Setting up OpenTUI...");
2670
+ const template = await select({
2671
+ message: "Choose a template",
2672
+ options: Object.entries(TEMPLATES$1).map(([key, template$1]) => ({
2673
+ value: key,
2674
+ label: template$1.label,
2675
+ hint: template$1.hint
2676
+ })),
2677
+ initialValue: "core"
2678
+ });
2679
+ if (isCancel(template)) return userCancelled("Operation cancelled");
2680
+ const args = getPackageExecutionArgs(packageManager, `create-tui@latest --template ${template} --no-git --no-install tui`);
2681
+ const appsDir = path.join(projectDir, "apps");
2682
+ const ensureDirResult = await Result.tryPromise({
2683
+ try: () => fs.ensureDir(appsDir),
2684
+ catch: (e) => new AddonSetupError({
2685
+ addon: "tui",
2686
+ message: `Failed to create directory: ${e instanceof Error ? e.message : String(e)}`,
2687
+ cause: e
2688
+ })
2689
+ });
2690
+ if (ensureDirResult.isErr()) return ensureDirResult;
2691
+ const s = spinner();
2692
+ s.start("Running OpenTUI create command...");
2693
+ const initResult = await Result.tryPromise({
2694
+ try: async () => {
2695
+ await $({
2696
+ cwd: appsDir,
2697
+ env: { CI: "true" }
2698
+ })`${args}`;
2699
+ },
2700
+ catch: (e) => {
2701
+ s.stop(pc.red("Failed to run OpenTUI create command"));
2702
+ return new AddonSetupError({
2703
+ addon: "tui",
2704
+ message: `Failed to set up OpenTUI: ${e instanceof Error ? e.message : String(e)}`,
2705
+ cause: e
2706
+ });
2707
+ }
2708
+ });
2709
+ if (initResult.isErr()) {
2530
2710
  log.error(pc.red("Failed to set up OpenTUI"));
2531
- if (error instanceof Error) console.error(pc.red(error.message));
2711
+ return initResult;
2532
2712
  }
2713
+ s.stop("OpenTUI setup complete!");
2714
+ return Result.ok(void 0);
2533
2715
  }
2534
2716
 
2535
2717
  //#endregion
@@ -2609,77 +2791,108 @@ function getFrameworksFromFrontend(frontend) {
2609
2791
  }
2610
2792
  async function setupUltracite(config, gitHooks) {
2611
2793
  const { packageManager, projectDir, frontend } = config;
2612
- try {
2613
- log.info("Setting up Ultracite...");
2614
- const result = await group({
2615
- linter: () => select({
2616
- message: "Choose linter/formatter",
2617
- options: Object.entries(LINTERS).map(([key, linter$1]) => ({
2618
- value: key,
2619
- label: linter$1.label,
2620
- hint: linter$1.hint
2621
- })),
2622
- initialValue: "biome"
2623
- }),
2624
- editors: () => multiselect({
2625
- message: "Choose editors",
2626
- options: Object.entries(EDITORS).map(([key, editor]) => ({
2627
- value: key,
2628
- label: editor.label
2629
- })),
2630
- required: true
2631
- }),
2632
- agents: () => multiselect({
2633
- message: "Choose agents",
2634
- options: Object.entries(AGENTS).map(([key, agent]) => ({
2635
- value: key,
2636
- label: agent.label
2637
- })),
2638
- required: true
2639
- }),
2640
- hooks: () => multiselect({
2641
- message: "Choose hooks",
2642
- options: Object.entries(HOOKS).map(([key, hook]) => ({
2643
- value: key,
2644
- label: hook.label
2645
- }))
2646
- })
2647
- }, { onCancel: () => {
2648
- exitCancelled("Operation cancelled");
2649
- } });
2650
- const linter = result.linter;
2651
- const editors = result.editors;
2652
- const agents = result.agents;
2653
- const hooks = result.hooks;
2654
- const frameworks = getFrameworksFromFrontend(frontend);
2655
- const ultraciteArgs = [
2656
- "init",
2657
- "--pm",
2658
- packageManager,
2659
- "--linter",
2660
- linter
2661
- ];
2662
- if (frameworks.length > 0) ultraciteArgs.push("--frameworks", ...frameworks);
2663
- if (editors.length > 0) ultraciteArgs.push("--editors", ...editors);
2664
- if (agents.length > 0) ultraciteArgs.push("--agents", ...agents);
2665
- if (hooks.length > 0) ultraciteArgs.push("--hooks", ...hooks);
2666
- if (gitHooks.length > 0) {
2667
- const integrations = [...gitHooks];
2668
- if (gitHooks.includes("husky")) integrations.push("lint-staged");
2669
- ultraciteArgs.push("--integrations", ...integrations);
2794
+ log.info("Setting up Ultracite...");
2795
+ let result;
2796
+ const groupResult = await Result.tryPromise({
2797
+ try: async () => {
2798
+ return await group({
2799
+ linter: () => select({
2800
+ message: "Choose linter/formatter",
2801
+ options: Object.entries(LINTERS).map(([key, linter$1]) => ({
2802
+ value: key,
2803
+ label: linter$1.label,
2804
+ hint: linter$1.hint
2805
+ })),
2806
+ initialValue: "biome"
2807
+ }),
2808
+ editors: () => multiselect({
2809
+ message: "Choose editors",
2810
+ options: Object.entries(EDITORS).map(([key, editor]) => ({
2811
+ value: key,
2812
+ label: editor.label
2813
+ })),
2814
+ required: true
2815
+ }),
2816
+ agents: () => multiselect({
2817
+ message: "Choose agents",
2818
+ options: Object.entries(AGENTS).map(([key, agent]) => ({
2819
+ value: key,
2820
+ label: agent.label
2821
+ })),
2822
+ required: true
2823
+ }),
2824
+ hooks: () => multiselect({
2825
+ message: "Choose hooks",
2826
+ options: Object.entries(HOOKS).map(([key, hook]) => ({
2827
+ value: key,
2828
+ label: hook.label
2829
+ }))
2830
+ })
2831
+ }, { onCancel: () => {
2832
+ throw new UserCancelledError({ message: "Operation cancelled" });
2833
+ } });
2834
+ },
2835
+ catch: (e) => {
2836
+ if (e instanceof UserCancelledError) return e;
2837
+ return new AddonSetupError({
2838
+ addon: "ultracite",
2839
+ message: `Failed to get user preferences: ${e instanceof Error ? e.message : String(e)}`,
2840
+ cause: e
2841
+ });
2670
2842
  }
2671
- const args = getPackageExecutionArgs(packageManager, `ultracite@latest ${ultraciteArgs.join(" ")} --skip-install`);
2672
- const s = spinner();
2673
- s.start("Running Ultracite init command...");
2674
- await $({
2675
- cwd: projectDir,
2676
- env: { CI: "true" }
2677
- })`${args}`;
2678
- s.stop("Ultracite setup successfully!");
2679
- } catch (error) {
2843
+ });
2844
+ if (groupResult.isErr()) {
2845
+ if (UserCancelledError.is(groupResult.error)) return userCancelled(groupResult.error.message);
2846
+ log.error(pc.red("Failed to set up Ultracite"));
2847
+ return groupResult;
2848
+ }
2849
+ result = groupResult.value;
2850
+ const linter = result.linter;
2851
+ const editors = result.editors;
2852
+ const agents = result.agents;
2853
+ const hooks = result.hooks;
2854
+ const frameworks = getFrameworksFromFrontend(frontend);
2855
+ const ultraciteArgs = [
2856
+ "init",
2857
+ "--pm",
2858
+ packageManager,
2859
+ "--linter",
2860
+ linter
2861
+ ];
2862
+ if (frameworks.length > 0) ultraciteArgs.push("--frameworks", ...frameworks);
2863
+ if (editors.length > 0) ultraciteArgs.push("--editors", ...editors);
2864
+ if (agents.length > 0) ultraciteArgs.push("--agents", ...agents);
2865
+ if (hooks.length > 0) ultraciteArgs.push("--hooks", ...hooks);
2866
+ if (gitHooks.length > 0) {
2867
+ const integrations = [...gitHooks];
2868
+ if (gitHooks.includes("husky")) integrations.push("lint-staged");
2869
+ ultraciteArgs.push("--integrations", ...integrations);
2870
+ }
2871
+ const args = getPackageExecutionArgs(packageManager, `ultracite@latest ${ultraciteArgs.join(" ")} --skip-install`);
2872
+ const s = spinner();
2873
+ s.start("Running Ultracite init command...");
2874
+ const initResult = await Result.tryPromise({
2875
+ try: async () => {
2876
+ await $({
2877
+ cwd: projectDir,
2878
+ env: { CI: "true" }
2879
+ })`${args}`;
2880
+ },
2881
+ catch: (e) => {
2882
+ s.stop(pc.red("Failed to run Ultracite init command"));
2883
+ return new AddonSetupError({
2884
+ addon: "ultracite",
2885
+ message: `Failed to set up Ultracite: ${e instanceof Error ? e.message : String(e)}`,
2886
+ cause: e
2887
+ });
2888
+ }
2889
+ });
2890
+ if (initResult.isErr()) {
2680
2891
  log.error(pc.red("Failed to set up Ultracite"));
2681
- if (error instanceof Error) console.error(pc.red(error.message));
2892
+ return initResult;
2682
2893
  }
2894
+ s.stop("Ultracite setup successfully!");
2895
+ return Result.ok(void 0);
2683
2896
  }
2684
2897
 
2685
2898
  //#endregion
@@ -2708,44 +2921,80 @@ const TEMPLATES = {
2708
2921
  };
2709
2922
  async function setupWxt(config) {
2710
2923
  const { packageManager, projectDir } = config;
2711
- try {
2712
- log.info("Setting up WXT...");
2713
- const template = await select({
2714
- message: "Choose a template",
2715
- options: Object.entries(TEMPLATES).map(([key, template$1]) => ({
2716
- value: key,
2717
- label: template$1.label,
2718
- hint: template$1.hint
2719
- })),
2720
- initialValue: "react"
2721
- });
2722
- if (isCancel(template)) return exitCancelled("Operation cancelled");
2723
- const args = getPackageExecutionArgs(packageManager, `wxt@latest init extension --template ${template} --pm ${packageManager}`);
2724
- const appsDir = path.join(projectDir, "apps");
2725
- await fs.ensureDir(appsDir);
2726
- const s = spinner();
2727
- s.start("Running WXT init command...");
2728
- await $({
2729
- cwd: appsDir,
2730
- env: { CI: "true" }
2731
- })`${args}`;
2732
- const extensionDir = path.join(projectDir, "apps", "extension");
2733
- const packageJsonPath = path.join(extensionDir, "package.json");
2734
- if (await fs.pathExists(packageJsonPath)) {
2735
- const packageJson = await fs.readJson(packageJsonPath);
2736
- packageJson.name = "extension";
2737
- if (packageJson.scripts?.dev) packageJson.scripts.dev = `${packageJson.scripts.dev} --port 5555`;
2738
- await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2739
- }
2740
- s.stop("WXT setup complete!");
2741
- } catch (error) {
2742
- log.error(pc.red("Failed to set up WXT"));
2743
- if (error instanceof Error) console.error(pc.red(error.message));
2744
- }
2745
- }
2746
-
2924
+ log.info("Setting up WXT...");
2925
+ const template = await select({
2926
+ message: "Choose a template",
2927
+ options: Object.entries(TEMPLATES).map(([key, template$1]) => ({
2928
+ value: key,
2929
+ label: template$1.label,
2930
+ hint: template$1.hint
2931
+ })),
2932
+ initialValue: "react"
2933
+ });
2934
+ if (isCancel(template)) return userCancelled("Operation cancelled");
2935
+ const args = getPackageExecutionArgs(packageManager, `wxt@latest init extension --template ${template} --pm ${packageManager}`);
2936
+ const appsDir = path.join(projectDir, "apps");
2937
+ const ensureDirResult = await Result.tryPromise({
2938
+ try: () => fs.ensureDir(appsDir),
2939
+ catch: (e) => new AddonSetupError({
2940
+ addon: "wxt",
2941
+ message: `Failed to create directory: ${e instanceof Error ? e.message : String(e)}`,
2942
+ cause: e
2943
+ })
2944
+ });
2945
+ if (ensureDirResult.isErr()) return ensureDirResult;
2946
+ const s = spinner();
2947
+ s.start("Running WXT init command...");
2948
+ const initResult = await Result.tryPromise({
2949
+ try: async () => {
2950
+ await $({
2951
+ cwd: appsDir,
2952
+ env: { CI: "true" }
2953
+ })`${args}`;
2954
+ },
2955
+ catch: (e) => {
2956
+ s.stop(pc.red("Failed to run WXT init command"));
2957
+ return new AddonSetupError({
2958
+ addon: "wxt",
2959
+ message: `Failed to set up WXT: ${e instanceof Error ? e.message : String(e)}`,
2960
+ cause: e
2961
+ });
2962
+ }
2963
+ });
2964
+ if (initResult.isErr()) {
2965
+ log.error(pc.red("Failed to set up WXT"));
2966
+ return initResult;
2967
+ }
2968
+ const extensionDir = path.join(projectDir, "apps", "extension");
2969
+ const packageJsonPath = path.join(extensionDir, "package.json");
2970
+ if ((await Result.tryPromise({
2971
+ try: async () => {
2972
+ if (await fs.pathExists(packageJsonPath)) {
2973
+ const packageJson = await fs.readJson(packageJsonPath);
2974
+ packageJson.name = "extension";
2975
+ if (packageJson.scripts?.dev) packageJson.scripts.dev = `${packageJson.scripts.dev} --port 5555`;
2976
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2977
+ }
2978
+ },
2979
+ catch: (e) => new AddonSetupError({
2980
+ addon: "wxt",
2981
+ message: `Failed to update package.json: ${e instanceof Error ? e.message : String(e)}`,
2982
+ cause: e
2983
+ })
2984
+ })).isErr()) log.warn(pc.yellow("WXT setup completed but failed to update package.json"));
2985
+ s.stop("WXT setup complete!");
2986
+ return Result.ok(void 0);
2987
+ }
2988
+
2747
2989
  //#endregion
2748
2990
  //#region src/helpers/addons/addons-setup.ts
2991
+ async function runSetup(setupFn) {
2992
+ const result = await setupFn();
2993
+ if (result.isErr()) {
2994
+ if (UserCancelledError.is(result.error)) throw result.error;
2995
+ consola.error(pc.red(result.error.message));
2996
+ }
2997
+ }
2749
2998
  async function setupAddons(config) {
2750
2999
  const { addons, frontend, projectDir } = config;
2751
3000
  const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next");
@@ -2753,7 +3002,7 @@ async function setupAddons(config) {
2753
3002
  const hasSvelteFrontend = frontend.includes("svelte");
2754
3003
  const hasSolidFrontend = frontend.includes("solid");
2755
3004
  const hasNextFrontend = frontend.includes("next");
2756
- if (addons.includes("tauri") && (hasReactWebFrontend || hasNuxtFrontend || hasSvelteFrontend || hasSolidFrontend || hasNextFrontend)) await setupTauri(config);
3005
+ if (addons.includes("tauri") && (hasReactWebFrontend || hasNuxtFrontend || hasSvelteFrontend || hasSolidFrontend || hasNextFrontend)) await runSetup(() => setupTauri(config));
2757
3006
  const hasUltracite = addons.includes("ultracite");
2758
3007
  const hasBiome = addons.includes("biome");
2759
3008
  const hasHusky = addons.includes("husky");
@@ -2763,7 +3012,7 @@ async function setupAddons(config) {
2763
3012
  const gitHooks = [];
2764
3013
  if (hasHusky) gitHooks.push("husky");
2765
3014
  if (hasLefthook) gitHooks.push("lefthook");
2766
- await setupUltracite(config, gitHooks);
3015
+ await runSetup(() => setupUltracite(config, gitHooks));
2767
3016
  } else {
2768
3017
  if (hasBiome) await setupBiome(projectDir);
2769
3018
  if (hasOxlint) await setupOxlint(projectDir, config.packageManager);
@@ -2775,11 +3024,11 @@ async function setupAddons(config) {
2775
3024
  if (hasLefthook) await setupLefthook(projectDir);
2776
3025
  }
2777
3026
  }
2778
- if (addons.includes("starlight")) await setupStarlight(config);
2779
- if (addons.includes("fumadocs")) await setupFumadocs(config);
2780
- if (addons.includes("opentui")) await setupTui(config);
2781
- if (addons.includes("wxt")) await setupWxt(config);
2782
- if (addons.includes("ruler")) await setupRuler(config);
3027
+ if (addons.includes("starlight")) await runSetup(() => setupStarlight(config));
3028
+ if (addons.includes("fumadocs")) await runSetup(() => setupFumadocs(config));
3029
+ if (addons.includes("opentui")) await runSetup(() => setupTui(config));
3030
+ if (addons.includes("wxt")) await runSetup(() => setupWxt(config));
3031
+ if (addons.includes("ruler")) await runSetup(() => setupRuler(config));
2783
3032
  }
2784
3033
  async function setupBiome(projectDir) {
2785
3034
  await addPackageDependency({
@@ -2873,12 +3122,18 @@ async function setupCloudflareD1(config) {
2873
3122
  //#region src/helpers/database-providers/docker-compose-setup.ts
2874
3123
  async function setupDockerCompose(config) {
2875
3124
  const { database, projectDir, projectName, backend } = config;
2876
- if (database === "none" || database === "sqlite") return;
2877
- try {
2878
- await writeEnvFile$4(projectDir, database, projectName, backend);
2879
- } catch (error) {
2880
- if (error instanceof Error) console.error(`Error: ${error.message}`);
2881
- }
3125
+ if (database === "none" || database === "sqlite") return Result.ok(void 0);
3126
+ const result = await Result.tryPromise({
3127
+ try: async () => {
3128
+ await writeEnvFile$4(projectDir, database, projectName, backend);
3129
+ },
3130
+ catch: (e) => new DatabaseSetupError({
3131
+ provider: "docker-compose",
3132
+ message: `Failed to setup docker compose env: ${e instanceof Error ? e.message : String(e)}`,
3133
+ cause: e
3134
+ })
3135
+ });
3136
+ return result.isErr() ? result : Result.ok(void 0);
2882
3137
  }
2883
3138
  async function writeEnvFile$4(projectDir, database, projectName, backend) {
2884
3139
  const targetApp = backend === "self" ? "apps/web" : "apps/server";
@@ -2900,69 +3155,75 @@ function getDatabaseUrl(database, projectName) {
2900
3155
  //#endregion
2901
3156
  //#region src/utils/command-exists.ts
2902
3157
  async function commandExists(command) {
2903
- try {
2904
- if (process.platform === "win32") return (await $({ reject: false })`where ${command}`).exitCode === 0;
2905
- return (await $({ reject: false })`which ${command}`).exitCode === 0;
2906
- } catch {
2907
- return false;
2908
- }
3158
+ const result = await Result.tryPromise({
3159
+ try: async () => {
3160
+ if (process.platform === "win32") return (await $({ reject: false })`where ${command}`).exitCode === 0;
3161
+ return (await $({ reject: false })`which ${command}`).exitCode === 0;
3162
+ },
3163
+ catch: () => false
3164
+ });
3165
+ return result.isOk() ? result.value : false;
2909
3166
  }
2910
3167
 
2911
3168
  //#endregion
2912
3169
  //#region src/helpers/database-providers/mongodb-atlas-setup.ts
2913
3170
  async function checkAtlasCLI() {
2914
- try {
2915
- const exists = await commandExists("atlas");
2916
- if (exists) log.info("MongoDB Atlas CLI found");
2917
- else log.warn(pc.yellow("MongoDB Atlas CLI not found"));
2918
- return exists;
2919
- } catch {
2920
- log.error(pc.red("Error checking MongoDB Atlas CLI"));
2921
- return false;
2922
- }
3171
+ const exists = await commandExists("atlas");
3172
+ if (exists) log.info("MongoDB Atlas CLI found");
3173
+ else log.warn(pc.yellow("MongoDB Atlas CLI not found"));
3174
+ return exists;
2923
3175
  }
2924
3176
  async function initMongoDBAtlas(serverDir) {
2925
- try {
2926
- if (!await checkAtlasCLI()) {
2927
- consola.error(pc.red("MongoDB Atlas CLI not found."));
2928
- log.info(pc.yellow("Please install it from: https://www.mongodb.com/docs/atlas/cli/current/install-atlas-cli/"));
2929
- return null;
2930
- }
2931
- log.info("Running MongoDB Atlas setup...");
2932
- await $({
2933
- cwd: serverDir,
2934
- stdio: "inherit"
2935
- })`atlas deployments setup`;
2936
- log.success("MongoDB Atlas deployment ready");
2937
- const connectionString = await text({
2938
- message: "Enter your MongoDB connection string:",
2939
- placeholder: "mongodb+srv://username:password@cluster.mongodb.net/database",
2940
- validate(value) {
2941
- if (!value) return "Please enter a connection string";
2942
- if (!value.startsWith("mongodb")) return "URL should start with mongodb:// or mongodb+srv://";
2943
- }
2944
- });
2945
- if (isCancel(connectionString)) {
2946
- cancel("MongoDB setup cancelled");
2947
- return null;
3177
+ if (!await checkAtlasCLI()) {
3178
+ log.info(pc.yellow("Please install it from: https://www.mongodb.com/docs/atlas/cli/current/install-atlas-cli/"));
3179
+ return databaseSetupError("mongodb-atlas", "MongoDB Atlas CLI not found");
3180
+ }
3181
+ log.info("Running MongoDB Atlas setup...");
3182
+ const deployResult = await Result.tryPromise({
3183
+ try: async () => {
3184
+ await $({
3185
+ cwd: serverDir,
3186
+ stdio: "inherit"
3187
+ })`atlas deployments setup`;
3188
+ log.success("MongoDB Atlas deployment ready");
3189
+ },
3190
+ catch: (e) => new DatabaseSetupError({
3191
+ provider: "mongodb-atlas",
3192
+ message: `Failed to setup MongoDB Atlas deployment: ${e instanceof Error ? e.message : String(e)}`,
3193
+ cause: e
3194
+ })
3195
+ });
3196
+ if (deployResult.isErr()) return deployResult;
3197
+ const connectionString = await text({
3198
+ message: "Enter your MongoDB connection string:",
3199
+ placeholder: "mongodb+srv://username:password@cluster.mongodb.net/database",
3200
+ validate(value) {
3201
+ if (!value) return "Please enter a connection string";
3202
+ if (!value.startsWith("mongodb")) return "URL should start with mongodb:// or mongodb+srv://";
2948
3203
  }
2949
- return { connectionString };
2950
- } catch (error) {
2951
- if (error instanceof Error) consola.error(pc.red(error.message));
2952
- return null;
3204
+ });
3205
+ if (isCancel(connectionString)) {
3206
+ cancel("MongoDB setup cancelled");
3207
+ return userCancelled("MongoDB setup cancelled");
2953
3208
  }
3209
+ return Result.ok({ connectionString });
2954
3210
  }
2955
3211
  async function writeEnvFile$3(projectDir, backend, config) {
2956
- try {
2957
- const targetApp = backend === "self" ? "apps/web" : "apps/server";
2958
- await addEnvVariablesToFile(path.join(projectDir, targetApp, ".env"), [{
2959
- key: "DATABASE_URL",
2960
- value: config?.connectionString ?? "mongodb://localhost:27017/mydb",
2961
- condition: true
2962
- }]);
2963
- } catch {
2964
- consola.error("Failed to update environment configuration");
2965
- }
3212
+ return Result.tryPromise({
3213
+ try: async () => {
3214
+ const targetApp = backend === "self" ? "apps/web" : "apps/server";
3215
+ await addEnvVariablesToFile(path.join(projectDir, targetApp, ".env"), [{
3216
+ key: "DATABASE_URL",
3217
+ value: config?.connectionString ?? "mongodb://localhost:27017/mydb",
3218
+ condition: true
3219
+ }]);
3220
+ },
3221
+ catch: (e) => new DatabaseSetupError({
3222
+ provider: "mongodb-atlas",
3223
+ message: `Failed to update environment configuration: ${e instanceof Error ? e.message : String(e)}`,
3224
+ cause: e
3225
+ })
3226
+ });
2966
3227
  }
2967
3228
  function displayManualSetupInstructions$3() {
2968
3229
  log.info(`
@@ -2985,50 +3246,56 @@ async function setupMongoDBAtlas(config, cliInput) {
2985
3246
  const { projectDir, backend } = config;
2986
3247
  const manualDb = cliInput?.manualDb ?? false;
2987
3248
  const serverDir = path.join(projectDir, "packages/db");
2988
- try {
2989
- await fs.ensureDir(serverDir);
2990
- if (manualDb) {
2991
- log.info("MongoDB Atlas manual setup selected");
2992
- await writeEnvFile$3(projectDir, backend);
2993
- displayManualSetupInstructions$3();
2994
- return;
2995
- }
2996
- const mode = await select({
2997
- message: "MongoDB Atlas setup: choose mode",
2998
- options: [{
2999
- label: "Automatic",
3000
- value: "auto",
3001
- hint: "Automated setup with provider CLI, sets .env"
3002
- }, {
3003
- label: "Manual",
3004
- value: "manual",
3005
- hint: "Manual setup, add env vars yourself"
3006
- }],
3007
- initialValue: "auto"
3008
- });
3009
- if (isCancel(mode)) return exitCancelled("Operation cancelled");
3010
- if (mode === "manual") {
3011
- log.info("MongoDB Atlas manual setup selected");
3012
- await writeEnvFile$3(projectDir, backend);
3013
- displayManualSetupInstructions$3();
3014
- return;
3015
- }
3016
- const config$1 = await initMongoDBAtlas(serverDir);
3017
- if (config$1) {
3018
- await writeEnvFile$3(projectDir, backend, config$1);
3019
- log.success(pc.green("MongoDB Atlas setup complete! Connection saved to .env file."));
3020
- } else {
3021
- log.warn(pc.yellow("Falling back to local MongoDB configuration"));
3022
- await writeEnvFile$3(projectDir, backend);
3023
- displayManualSetupInstructions$3();
3024
- }
3025
- } catch (error) {
3026
- consola.error(pc.red(`Error during MongoDB Atlas setup: ${error instanceof Error ? error.message : String(error)}`));
3027
- try {
3028
- await writeEnvFile$3(projectDir, backend);
3029
- displayManualSetupInstructions$3();
3030
- } catch {}
3249
+ const ensureDirResult = await Result.tryPromise({
3250
+ try: () => fs.ensureDir(serverDir),
3251
+ catch: (e) => new DatabaseSetupError({
3252
+ provider: "mongodb-atlas",
3253
+ message: `Failed to create directory: ${e instanceof Error ? e.message : String(e)}`,
3254
+ cause: e
3255
+ })
3256
+ });
3257
+ if (ensureDirResult.isErr()) return ensureDirResult;
3258
+ if (manualDb) {
3259
+ log.info("MongoDB Atlas manual setup selected");
3260
+ const envResult$1 = await writeEnvFile$3(projectDir, backend);
3261
+ if (envResult$1.isErr()) return envResult$1;
3262
+ displayManualSetupInstructions$3();
3263
+ return Result.ok(void 0);
3031
3264
  }
3265
+ const mode = await select({
3266
+ message: "MongoDB Atlas setup: choose mode",
3267
+ options: [{
3268
+ label: "Automatic",
3269
+ value: "auto",
3270
+ hint: "Automated setup with provider CLI, sets .env"
3271
+ }, {
3272
+ label: "Manual",
3273
+ value: "manual",
3274
+ hint: "Manual setup, add env vars yourself"
3275
+ }],
3276
+ initialValue: "auto"
3277
+ });
3278
+ if (isCancel(mode)) return userCancelled("Operation cancelled");
3279
+ if (mode === "manual") {
3280
+ log.info("MongoDB Atlas manual setup selected");
3281
+ const envResult$1 = await writeEnvFile$3(projectDir, backend);
3282
+ if (envResult$1.isErr()) return envResult$1;
3283
+ displayManualSetupInstructions$3();
3284
+ return Result.ok(void 0);
3285
+ }
3286
+ const mongoConfigResult = await initMongoDBAtlas(serverDir);
3287
+ if (mongoConfigResult.isOk()) {
3288
+ const envResult$1 = await writeEnvFile$3(projectDir, backend, mongoConfigResult.value);
3289
+ if (envResult$1.isErr()) return envResult$1;
3290
+ log.success(pc.green("MongoDB Atlas setup complete! Connection saved to .env file."));
3291
+ return Result.ok(void 0);
3292
+ }
3293
+ if (UserCancelledError.is(mongoConfigResult.error)) return mongoConfigResult;
3294
+ log.warn(pc.yellow("Falling back to local MongoDB configuration"));
3295
+ const envResult = await writeEnvFile$3(projectDir, backend);
3296
+ if (envResult.isErr()) return envResult;
3297
+ displayManualSetupInstructions$3();
3298
+ return Result.ok(void 0);
3032
3299
  }
3033
3300
 
3034
3301
  //#endregion
@@ -3069,62 +3336,99 @@ const NEON_REGIONS = [
3069
3336
  ];
3070
3337
  async function executeNeonCommand(packageManager, commandArgsString, spinnerText) {
3071
3338
  const s = spinner();
3072
- try {
3073
- const args = getPackageExecutionArgs(packageManager, commandArgsString);
3074
- if (spinnerText) s.start(spinnerText);
3075
- const result = await $`${args}`;
3076
- if (spinnerText) s.stop(pc.green(spinnerText.replace("...", "").replace("ing ", "ed ").trim()));
3077
- return result;
3078
- } catch (error) {
3079
- if (s) s.stop(pc.red(`Failed: ${spinnerText || "Command execution"}`));
3080
- throw error;
3081
- }
3339
+ const args = getPackageExecutionArgs(packageManager, commandArgsString);
3340
+ if (spinnerText) s.start(spinnerText);
3341
+ return Result.tryPromise({
3342
+ try: async () => {
3343
+ const result = await $`${args}`;
3344
+ if (spinnerText) s.stop(pc.green(spinnerText.replace("...", "").replace("ing ", "ed ").trim()));
3345
+ return result;
3346
+ },
3347
+ catch: (e) => {
3348
+ if (s) s.stop(pc.red(`Failed: ${spinnerText || "Command execution"}`));
3349
+ return new DatabaseSetupError({
3350
+ provider: "neon",
3351
+ message: `Command failed: ${e instanceof Error ? e.message : String(e)}`,
3352
+ cause: e
3353
+ });
3354
+ }
3355
+ });
3082
3356
  }
3083
3357
  async function createNeonProject(projectName, regionId, packageManager) {
3084
- try {
3085
- const { stdout } = await executeNeonCommand(packageManager, `neonctl@latest projects create --name ${projectName} --region-id ${regionId} --output json`, `Creating Neon project "${projectName}"...`);
3086
- const response = JSON.parse(stdout);
3087
- if (response.project && response.connection_uris && response.connection_uris.length > 0) {
3088
- const projectId = response.project.id;
3089
- const connectionUri = response.connection_uris[0].connection_uri;
3090
- const params = response.connection_uris[0].connection_parameters;
3091
- return {
3092
- connectionString: connectionUri,
3093
- projectId,
3094
- dbName: params.database,
3095
- roleName: params.role
3096
- };
3097
- }
3098
- consola$1.error(pc.red("Failed to extract connection information from response"));
3099
- return null;
3100
- } catch {
3101
- consola$1.error(pc.red("Failed to create Neon project"));
3358
+ const execResult = await executeNeonCommand(packageManager, `neonctl@latest projects create --name ${projectName} --region-id ${regionId} --output json`, `Creating Neon project "${projectName}"...`);
3359
+ if (execResult.isErr()) return execResult;
3360
+ const parseResult = Result.try({
3361
+ try: () => JSON.parse(execResult.value.stdout),
3362
+ catch: (e) => new DatabaseSetupError({
3363
+ provider: "neon",
3364
+ message: `Failed to parse Neon response: ${e instanceof Error ? e.message : String(e)}`,
3365
+ cause: e
3366
+ })
3367
+ });
3368
+ if (parseResult.isErr()) return parseResult;
3369
+ const response = parseResult.value;
3370
+ if (response.project && response.connection_uris && response.connection_uris.length > 0) {
3371
+ const projectId = response.project.id;
3372
+ const connectionUri = response.connection_uris[0].connection_uri;
3373
+ const params = response.connection_uris[0].connection_parameters;
3374
+ return Result.ok({
3375
+ connectionString: connectionUri,
3376
+ projectId,
3377
+ dbName: params.database,
3378
+ roleName: params.role
3379
+ });
3102
3380
  }
3381
+ return databaseSetupError("neon", "Failed to extract connection information from Neon response");
3103
3382
  }
3104
3383
  async function writeEnvFile$2(projectDir, backend, config) {
3105
- const targetApp = backend === "self" ? "apps/web" : "apps/server";
3106
- await addEnvVariablesToFile(path.join(projectDir, targetApp, ".env"), [{
3107
- key: "DATABASE_URL",
3108
- value: config?.connectionString ?? "postgresql://postgres:postgres@localhost:5432/mydb?schema=public",
3109
- condition: true
3110
- }]);
3111
- return true;
3384
+ return Result.tryPromise({
3385
+ try: async () => {
3386
+ const targetApp = backend === "self" ? "apps/web" : "apps/server";
3387
+ await addEnvVariablesToFile(path.join(projectDir, targetApp, ".env"), [{
3388
+ key: "DATABASE_URL",
3389
+ value: config?.connectionString ?? "postgresql://postgres:postgres@localhost:5432/mydb?schema=public",
3390
+ condition: true
3391
+ }]);
3392
+ },
3393
+ catch: (e) => new DatabaseSetupError({
3394
+ provider: "neon",
3395
+ message: `Failed to update .env file: ${e instanceof Error ? e.message : String(e)}`,
3396
+ cause: e
3397
+ })
3398
+ });
3112
3399
  }
3113
3400
  async function setupWithNeonDb(projectDir, packageManager, backend) {
3114
- try {
3115
- const s = spinner();
3116
- s.start("Creating Neon database using get-db...");
3117
- const targetApp = backend === "self" ? "apps/web" : "apps/server";
3118
- const targetDir = path.join(projectDir, targetApp);
3119
- await fs.ensureDir(targetDir);
3120
- const packageArgs = getPackageExecutionArgs(packageManager, "get-db@latest --yes");
3121
- await $({ cwd: targetDir })`${packageArgs}`;
3122
- s.stop(pc.green("Neon database created successfully!"));
3123
- return true;
3124
- } catch (error) {
3125
- consola$1.error(pc.red("Failed to create database with get-db"));
3126
- throw error;
3401
+ const s = spinner();
3402
+ s.start("Creating Neon database using get-db...");
3403
+ const targetApp = backend === "self" ? "apps/web" : "apps/server";
3404
+ const targetDir = path.join(projectDir, targetApp);
3405
+ const ensureDirResult = await Result.tryPromise({
3406
+ try: () => fs.ensureDir(targetDir),
3407
+ catch: (e) => new DatabaseSetupError({
3408
+ provider: "neon",
3409
+ message: `Failed to create directory: ${e instanceof Error ? e.message : String(e)}`,
3410
+ cause: e
3411
+ })
3412
+ });
3413
+ if (ensureDirResult.isErr()) {
3414
+ s.stop(pc.red("Failed to create directory"));
3415
+ return ensureDirResult;
3127
3416
  }
3417
+ const packageArgs = getPackageExecutionArgs(packageManager, "get-db@latest --yes");
3418
+ return Result.tryPromise({
3419
+ try: async () => {
3420
+ await $({ cwd: targetDir })`${packageArgs}`;
3421
+ s.stop(pc.green("Neon database created successfully!"));
3422
+ },
3423
+ catch: (e) => {
3424
+ s.stop(pc.red("Failed to create database with get-db"));
3425
+ return new DatabaseSetupError({
3426
+ provider: "neon",
3427
+ message: `Failed to create database with get-db: ${e instanceof Error ? e.message : String(e)}`,
3428
+ cause: e
3429
+ });
3430
+ }
3431
+ });
3128
3432
  }
3129
3433
  function displayManualSetupInstructions$2(target) {
3130
3434
  log.info(`Manual Neon PostgreSQL Setup Instructions:
@@ -3139,71 +3443,87 @@ DATABASE_URL="your_connection_string"`);
3139
3443
  async function setupNeonPostgres(config, cliInput) {
3140
3444
  const { packageManager, projectDir, backend } = config;
3141
3445
  const manualDb = cliInput?.manualDb ?? false;
3142
- try {
3143
- if (manualDb) {
3144
- await writeEnvFile$2(projectDir, backend);
3145
- displayManualSetupInstructions$2(backend === "self" ? "apps/web" : "apps/server");
3146
- return;
3147
- }
3148
- const mode = await select({
3149
- message: "Neon setup: choose mode",
3150
- options: [{
3151
- label: "Automatic",
3152
- value: "auto",
3153
- hint: "Automated setup with provider CLI, sets .env"
3154
- }, {
3155
- label: "Manual",
3156
- value: "manual",
3157
- hint: "Manual setup, add env vars yourself"
3158
- }],
3159
- initialValue: "auto"
3160
- });
3161
- if (isCancel(mode)) return exitCancelled("Operation cancelled");
3162
- if (mode === "manual") {
3163
- await writeEnvFile$2(projectDir, backend);
3164
- displayManualSetupInstructions$2(backend === "self" ? "apps/web" : "apps/server");
3165
- return;
3166
- }
3167
- const setupMethod = await select({
3168
- message: "Choose your Neon setup method:",
3169
- options: [{
3170
- label: "Quick setup with get-db",
3171
- value: "neondb",
3172
- hint: "fastest, no auth required"
3173
- }, {
3174
- label: "Custom setup with neonctl",
3175
- value: "neonctl",
3176
- hint: "More control - choose project name and region"
3177
- }],
3178
- initialValue: "neondb"
3179
- });
3180
- if (isCancel(setupMethod)) return exitCancelled("Operation cancelled");
3181
- if (setupMethod === "neondb") await setupWithNeonDb(projectDir, packageManager, backend);
3182
- else {
3183
- const suggestedProjectName = path.basename(projectDir);
3184
- const projectName = await text({
3185
- message: "Enter a name for your Neon project:",
3186
- defaultValue: suggestedProjectName,
3187
- initialValue: suggestedProjectName
3188
- });
3189
- const regionId = await select({
3190
- message: "Select a region for your Neon project:",
3191
- options: NEON_REGIONS,
3192
- initialValue: NEON_REGIONS[0].value
3193
- });
3194
- if (isCancel(projectName) || isCancel(regionId)) return exitCancelled("Operation cancelled");
3195
- const neonConfig = await createNeonProject(projectName, regionId, packageManager);
3196
- if (!neonConfig) throw new Error("Failed to create project - couldn't get connection information");
3197
- const finalSpinner = spinner();
3198
- finalSpinner.start("Configuring database connection");
3199
- await writeEnvFile$2(projectDir, backend, neonConfig);
3200
- finalSpinner.stop("Neon database configured!");
3446
+ const target = backend === "self" ? "apps/web" : "apps/server";
3447
+ if (manualDb) {
3448
+ const envResult$1 = await writeEnvFile$2(projectDir, backend);
3449
+ if (envResult$1.isErr()) return envResult$1;
3450
+ displayManualSetupInstructions$2(target);
3451
+ return Result.ok(void 0);
3452
+ }
3453
+ const mode = await select({
3454
+ message: "Neon setup: choose mode",
3455
+ options: [{
3456
+ label: "Automatic",
3457
+ value: "auto",
3458
+ hint: "Automated setup with provider CLI, sets .env"
3459
+ }, {
3460
+ label: "Manual",
3461
+ value: "manual",
3462
+ hint: "Manual setup, add env vars yourself"
3463
+ }],
3464
+ initialValue: "auto"
3465
+ });
3466
+ if (isCancel(mode)) return userCancelled("Operation cancelled");
3467
+ if (mode === "manual") {
3468
+ const envResult$1 = await writeEnvFile$2(projectDir, backend);
3469
+ if (envResult$1.isErr()) return envResult$1;
3470
+ displayManualSetupInstructions$2(target);
3471
+ return Result.ok(void 0);
3472
+ }
3473
+ const setupMethod = await select({
3474
+ message: "Choose your Neon setup method:",
3475
+ options: [{
3476
+ label: "Quick setup with get-db",
3477
+ value: "neondb",
3478
+ hint: "fastest, no auth required"
3479
+ }, {
3480
+ label: "Custom setup with neonctl",
3481
+ value: "neonctl",
3482
+ hint: "More control - choose project name and region"
3483
+ }],
3484
+ initialValue: "neondb"
3485
+ });
3486
+ if (isCancel(setupMethod)) return userCancelled("Operation cancelled");
3487
+ if (setupMethod === "neondb") {
3488
+ const neonDbResult = await setupWithNeonDb(projectDir, packageManager, backend);
3489
+ if (neonDbResult.isErr()) {
3490
+ log.error(pc.red(neonDbResult.error.message));
3491
+ const envResult$1 = await writeEnvFile$2(projectDir, backend);
3492
+ if (envResult$1.isErr()) return envResult$1;
3493
+ displayManualSetupInstructions$2(target);
3201
3494
  }
3202
- } catch (error) {
3203
- if (error instanceof Error) consola$1.error(pc.red(error.message));
3204
- await writeEnvFile$2(projectDir, backend);
3205
- displayManualSetupInstructions$2(backend === "self" ? "apps/web" : "apps/server");
3495
+ return neonDbResult;
3496
+ }
3497
+ const suggestedProjectName = path.basename(projectDir);
3498
+ const projectName = await text({
3499
+ message: "Enter a name for your Neon project:",
3500
+ defaultValue: suggestedProjectName,
3501
+ initialValue: suggestedProjectName
3502
+ });
3503
+ if (isCancel(projectName)) return userCancelled("Operation cancelled");
3504
+ const regionId = await select({
3505
+ message: "Select a region for your Neon project:",
3506
+ options: NEON_REGIONS,
3507
+ initialValue: NEON_REGIONS[0].value
3508
+ });
3509
+ if (isCancel(regionId)) return userCancelled("Operation cancelled");
3510
+ const neonConfigResult = await createNeonProject(projectName, regionId, packageManager);
3511
+ if (neonConfigResult.isErr()) {
3512
+ log.error(pc.red(neonConfigResult.error.message));
3513
+ const envResult$1 = await writeEnvFile$2(projectDir, backend);
3514
+ if (envResult$1.isErr()) return envResult$1;
3515
+ displayManualSetupInstructions$2(target);
3516
+ return Result.ok(void 0);
3517
+ }
3518
+ const finalSpinner = spinner();
3519
+ finalSpinner.start("Configuring database connection");
3520
+ const envResult = await writeEnvFile$2(projectDir, backend, neonConfigResult.value);
3521
+ if (envResult.isErr()) {
3522
+ finalSpinner.stop(pc.red("Failed to configure database connection"));
3523
+ return envResult;
3206
3524
  }
3525
+ finalSpinner.stop("Neon database configured!");
3526
+ return Result.ok(void 0);
3207
3527
  }
3208
3528
 
3209
3529
  //#endregion
@@ -3296,53 +3616,70 @@ const AVAILABLE_REGIONS = [
3296
3616
  }
3297
3617
  ];
3298
3618
  async function setupWithCreateDb(serverDir, packageManager) {
3299
- try {
3300
- log.info("Starting Prisma Postgres setup with create-db.");
3301
- const selectedRegion = await select({
3302
- message: "Select your preferred region:",
3303
- options: AVAILABLE_REGIONS,
3304
- initialValue: "ap-southeast-1"
3305
- });
3306
- if (isCancel(selectedRegion)) return null;
3307
- const createDbArgs = getPackageExecutionArgs(packageManager, `create-db@latest --json --region ${selectedRegion}`);
3308
- const s = spinner();
3309
- s.start("Creating Prisma Postgres database...");
3310
- const { stdout } = await $({ cwd: serverDir })`${createDbArgs}`;
3311
- s.stop("Database created successfully!");
3312
- let createDbResponse;
3313
- try {
3314
- createDbResponse = JSON.parse(stdout);
3315
- } catch {
3316
- consola$1.error("Failed to parse create-db response");
3317
- return null;
3619
+ log.info("Starting Prisma Postgres setup with create-db.");
3620
+ const selectedRegion = await select({
3621
+ message: "Select your preferred region:",
3622
+ options: AVAILABLE_REGIONS,
3623
+ initialValue: "ap-southeast-1"
3624
+ });
3625
+ if (isCancel(selectedRegion)) return userCancelled("Operation cancelled");
3626
+ const createDbArgs = getPackageExecutionArgs(packageManager, `create-db@latest --json --region ${selectedRegion}`);
3627
+ const s = spinner();
3628
+ s.start("Creating Prisma Postgres database...");
3629
+ const execResult = await Result.tryPromise({
3630
+ try: async () => {
3631
+ const { stdout } = await $({ cwd: serverDir })`${createDbArgs}`;
3632
+ s.stop("Database created successfully!");
3633
+ return stdout;
3634
+ },
3635
+ catch: (e) => {
3636
+ s.stop(pc.red("Failed to create database"));
3637
+ return new DatabaseSetupError({
3638
+ provider: "prisma-postgres",
3639
+ message: `Failed to create database: ${e instanceof Error ? e.message : String(e)}`,
3640
+ cause: e
3641
+ });
3318
3642
  }
3319
- return {
3320
- databaseUrl: createDbResponse.connectionString,
3321
- claimUrl: createDbResponse.claimUrl
3322
- };
3323
- } catch (error) {
3324
- if (error instanceof Error) consola$1.error(error.message);
3325
- return null;
3326
- }
3643
+ });
3644
+ if (execResult.isErr()) return execResult;
3645
+ const parseResult = Result.try({
3646
+ try: () => JSON.parse(execResult.value),
3647
+ catch: (e) => new DatabaseSetupError({
3648
+ provider: "prisma-postgres",
3649
+ message: `Failed to parse create-db response: ${e instanceof Error ? e.message : String(e)}`,
3650
+ cause: e
3651
+ })
3652
+ });
3653
+ if (parseResult.isErr()) return parseResult;
3654
+ const createDbResponse = parseResult.value;
3655
+ return Result.ok({
3656
+ databaseUrl: createDbResponse.connectionString,
3657
+ claimUrl: createDbResponse.claimUrl
3658
+ });
3327
3659
  }
3328
3660
  async function writeEnvFile$1(projectDir, backend, config) {
3329
- try {
3330
- const targetApp = backend === "self" ? "apps/web" : "apps/server";
3331
- const envPath = path.join(projectDir, targetApp, ".env");
3332
- const variables = [{
3333
- key: "DATABASE_URL",
3334
- value: config?.databaseUrl ?? "postgresql://postgres:postgres@localhost:5432/mydb?schema=public",
3335
- condition: true
3336
- }];
3337
- if (config?.claimUrl) variables.push({
3338
- key: "CLAIM_URL",
3339
- value: config.claimUrl,
3340
- condition: true
3341
- });
3342
- await addEnvVariablesToFile(envPath, variables);
3343
- } catch {
3344
- consola$1.error("Failed to update environment configuration");
3345
- }
3661
+ return Result.tryPromise({
3662
+ try: async () => {
3663
+ const targetApp = backend === "self" ? "apps/web" : "apps/server";
3664
+ const envPath = path.join(projectDir, targetApp, ".env");
3665
+ const variables = [{
3666
+ key: "DATABASE_URL",
3667
+ value: config?.databaseUrl ?? "postgresql://postgres:postgres@localhost:5432/mydb?schema=public",
3668
+ condition: true
3669
+ }];
3670
+ if (config?.claimUrl) variables.push({
3671
+ key: "CLAIM_URL",
3672
+ value: config.claimUrl,
3673
+ condition: true
3674
+ });
3675
+ await addEnvVariablesToFile(envPath, variables);
3676
+ },
3677
+ catch: (e) => new DatabaseSetupError({
3678
+ provider: "prisma-postgres",
3679
+ message: `Failed to update environment configuration: ${e instanceof Error ? e.message : String(e)}`,
3680
+ cause: e
3681
+ })
3682
+ });
3346
3683
  }
3347
3684
  function displayManualSetupInstructions$1(target) {
3348
3685
  log.info(`Manual Prisma PostgreSQL Setup Instructions:
@@ -3358,124 +3695,134 @@ async function setupPrismaPostgres(config, cliInput) {
3358
3695
  const { packageManager, projectDir, backend } = config;
3359
3696
  const manualDb = cliInput?.manualDb ?? false;
3360
3697
  const dbDir = path.join(projectDir, "packages/db");
3361
- try {
3362
- await fs.ensureDir(dbDir);
3363
- if (manualDb) {
3364
- await writeEnvFile$1(projectDir, backend);
3365
- displayManualSetupInstructions$1(backend === "self" ? "apps/web" : "apps/server");
3366
- return;
3367
- }
3368
- const setupMode = await select({
3369
- message: "Prisma Postgres setup: choose mode",
3370
- options: [{
3371
- label: "Automatic (create-db)",
3372
- value: "auto",
3373
- hint: "Provision a database via Prisma's create-db CLI"
3374
- }, {
3375
- label: "Manual",
3376
- value: "manual",
3377
- hint: "Add your own DATABASE_URL later"
3378
- }],
3379
- initialValue: "auto"
3380
- });
3381
- if (isCancel(setupMode)) return;
3382
- if (setupMode === "manual") {
3383
- await writeEnvFile$1(projectDir, backend);
3384
- displayManualSetupInstructions$1(backend === "self" ? "apps/web" : "apps/server");
3385
- return;
3386
- }
3387
- const prismaConfig = await setupWithCreateDb(dbDir, packageManager);
3388
- if (prismaConfig) {
3389
- await writeEnvFile$1(projectDir, backend, prismaConfig);
3390
- log.success(pc.green("Prisma Postgres database configured successfully!"));
3391
- if (prismaConfig.claimUrl) log.info(pc.blue(`Claim URL saved to .env: ${prismaConfig.claimUrl}`));
3392
- } else {
3393
- await writeEnvFile$1(projectDir, backend);
3394
- displayManualSetupInstructions$1(backend === "self" ? "apps/web" : "apps/server");
3395
- }
3396
- } catch (error) {
3397
- consola$1.error(pc.red(`Error during Prisma Postgres setup: ${error instanceof Error ? error.message : String(error)}`));
3398
- try {
3399
- await writeEnvFile$1(projectDir, backend);
3400
- displayManualSetupInstructions$1(backend === "self" ? "apps/web" : "apps/server");
3401
- } catch {}
3698
+ const target = backend === "self" ? "apps/web" : "apps/server";
3699
+ const ensureDirResult = await Result.tryPromise({
3700
+ try: () => fs.ensureDir(dbDir),
3701
+ catch: (e) => new DatabaseSetupError({
3702
+ provider: "prisma-postgres",
3703
+ message: `Failed to create directory: ${e instanceof Error ? e.message : String(e)}`,
3704
+ cause: e
3705
+ })
3706
+ });
3707
+ if (ensureDirResult.isErr()) return ensureDirResult;
3708
+ if (manualDb) {
3709
+ const envResult$1 = await writeEnvFile$1(projectDir, backend);
3710
+ if (envResult$1.isErr()) return envResult$1;
3711
+ displayManualSetupInstructions$1(target);
3712
+ return Result.ok(void 0);
3713
+ }
3714
+ const setupMode = await select({
3715
+ message: "Prisma Postgres setup: choose mode",
3716
+ options: [{
3717
+ label: "Automatic (create-db)",
3718
+ value: "auto",
3719
+ hint: "Provision a database via Prisma's create-db CLI"
3720
+ }, {
3721
+ label: "Manual",
3722
+ value: "manual",
3723
+ hint: "Add your own DATABASE_URL later"
3724
+ }],
3725
+ initialValue: "auto"
3726
+ });
3727
+ if (isCancel(setupMode)) return userCancelled("Operation cancelled");
3728
+ if (setupMode === "manual") {
3729
+ const envResult$1 = await writeEnvFile$1(projectDir, backend);
3730
+ if (envResult$1.isErr()) return envResult$1;
3731
+ displayManualSetupInstructions$1(target);
3732
+ return Result.ok(void 0);
3733
+ }
3734
+ const prismaConfigResult = await setupWithCreateDb(dbDir, packageManager);
3735
+ if (prismaConfigResult.isErr()) {
3736
+ if (UserCancelledError.is(prismaConfigResult.error)) return prismaConfigResult;
3737
+ log.error(pc.red(prismaConfigResult.error.message));
3738
+ const envResult$1 = await writeEnvFile$1(projectDir, backend);
3739
+ if (envResult$1.isErr()) return envResult$1;
3740
+ displayManualSetupInstructions$1(target);
3402
3741
  log.info("Setup completed with manual configuration required.");
3742
+ return Result.ok(void 0);
3403
3743
  }
3744
+ const envResult = await writeEnvFile$1(projectDir, backend, prismaConfigResult.value);
3745
+ if (envResult.isErr()) return envResult;
3746
+ log.success(pc.green("Prisma Postgres database configured successfully!"));
3747
+ if (prismaConfigResult.value.claimUrl) log.info(pc.blue(`Claim URL saved to .env: ${prismaConfigResult.value.claimUrl}`));
3748
+ return Result.ok(void 0);
3404
3749
  }
3405
3750
 
3406
3751
  //#endregion
3407
3752
  //#region src/helpers/database-providers/supabase-setup.ts
3408
3753
  async function writeSupabaseEnvFile(projectDir, backend, databaseUrl) {
3409
- try {
3410
- const targetApp = backend === "self" ? "apps/web" : "apps/server";
3411
- const envPath = path.join(projectDir, targetApp, ".env");
3412
- const dbUrlToUse = databaseUrl || "postgresql://postgres:postgres@127.0.0.1:54322/postgres";
3413
- await addEnvVariablesToFile(envPath, [{
3414
- key: "DATABASE_URL",
3415
- value: dbUrlToUse,
3416
- condition: true
3417
- }, {
3418
- key: "DIRECT_URL",
3419
- value: dbUrlToUse,
3420
- condition: true
3421
- }]);
3422
- return true;
3423
- } catch (error) {
3424
- consola$1.error(pc.red("Failed to update .env file for Supabase."));
3425
- if (error instanceof Error) consola$1.error(error.message);
3426
- return false;
3427
- }
3754
+ return Result.tryPromise({
3755
+ try: async () => {
3756
+ const targetApp = backend === "self" ? "apps/web" : "apps/server";
3757
+ const envPath = path.join(projectDir, targetApp, ".env");
3758
+ const dbUrlToUse = databaseUrl || "postgresql://postgres:postgres@127.0.0.1:54322/postgres";
3759
+ await addEnvVariablesToFile(envPath, [{
3760
+ key: "DATABASE_URL",
3761
+ value: dbUrlToUse,
3762
+ condition: true
3763
+ }, {
3764
+ key: "DIRECT_URL",
3765
+ value: dbUrlToUse,
3766
+ condition: true
3767
+ }]);
3768
+ },
3769
+ catch: (e) => new DatabaseSetupError({
3770
+ provider: "supabase",
3771
+ message: `Failed to update .env file: ${e instanceof Error ? e.message : String(e)}`,
3772
+ cause: e
3773
+ })
3774
+ });
3428
3775
  }
3429
3776
  function extractDbUrl(output) {
3430
- const url = output.match(/DB URL:\s*(postgresql:\/\/[^\s]+)/)?.[1];
3431
- if (url) return url;
3432
- return null;
3777
+ return output.match(/DB URL:\s*(postgresql:\/\/[^\s]+)/)?.[1] ?? null;
3433
3778
  }
3434
3779
  async function initializeSupabase(serverDir, packageManager) {
3435
3780
  log.info("Initializing Supabase project...");
3436
- try {
3437
- const supabaseInitArgs = getPackageExecutionArgs(packageManager, "supabase init");
3438
- await $({
3439
- cwd: serverDir,
3440
- stdio: "inherit"
3441
- })`${supabaseInitArgs}`;
3442
- log.success("Supabase project initialized");
3443
- return true;
3444
- } catch (error) {
3445
- consola$1.error(pc.red("Failed to initialize Supabase project."));
3446
- if (error instanceof Error) consola$1.error(error.message);
3447
- else consola$1.error(String(error));
3448
- if (error instanceof Error && error.message.includes("ENOENT")) {
3449
- log.error(pc.red("Supabase CLI not found. Please install it globally or ensure it's in your PATH."));
3450
- log.info("You can install it using: npm install -g supabase");
3781
+ return Result.tryPromise({
3782
+ try: async () => {
3783
+ const supabaseInitArgs = getPackageExecutionArgs(packageManager, "supabase init");
3784
+ await execa(supabaseInitArgs[0], supabaseInitArgs.slice(1), {
3785
+ cwd: serverDir,
3786
+ stdio: "inherit"
3787
+ });
3788
+ log.success("Supabase project initialized");
3789
+ },
3790
+ catch: (e) => {
3791
+ const error = e;
3792
+ return new DatabaseSetupError({
3793
+ provider: "supabase",
3794
+ message: error.message?.includes("ENOENT") ? "Supabase CLI not found. Please install it globally (npm install -g supabase) or ensure it's in your PATH." : `Failed to initialize Supabase project: ${error.message ?? String(e)}`,
3795
+ cause: e
3796
+ });
3451
3797
  }
3452
- return false;
3453
- }
3798
+ });
3454
3799
  }
3455
3800
  async function startSupabase(serverDir, packageManager) {
3456
3801
  log.info("Starting Supabase services (this may take a moment)...");
3457
3802
  const supabaseStartArgs = getPackageExecutionArgs(packageManager, "supabase start");
3458
- try {
3459
- const subprocess = execa(supabaseStartArgs[0], supabaseStartArgs.slice(1), { cwd: serverDir });
3460
- let stdoutData = "";
3461
- if (subprocess.stdout) subprocess.stdout.on("data", (data) => {
3462
- const text$1 = data.toString();
3463
- process.stdout.write(text$1);
3464
- stdoutData += text$1;
3465
- });
3466
- if (subprocess.stderr) subprocess.stderr.pipe(process.stderr);
3467
- await subprocess;
3468
- await new Promise((resolve) => setTimeout(resolve, 100));
3469
- return stdoutData;
3470
- } catch (error) {
3471
- consola$1.error(pc.red("Failed to start Supabase services."));
3472
- const execaError = error;
3473
- if (execaError?.message) {
3474
- consola$1.error(`Error details: ${execaError.message}`);
3475
- if (execaError.message.includes("Docker is not running")) log.error(pc.red("Docker is not running. Please start Docker and try again."));
3476
- } else consola$1.error(String(error));
3477
- return null;
3478
- }
3803
+ return Result.tryPromise({
3804
+ try: async () => {
3805
+ const subprocess = execa(supabaseStartArgs[0], supabaseStartArgs.slice(1), { cwd: serverDir });
3806
+ let stdoutData = "";
3807
+ if (subprocess.stdout) subprocess.stdout.on("data", (data) => {
3808
+ const text$1 = data.toString();
3809
+ process.stdout.write(text$1);
3810
+ stdoutData += text$1;
3811
+ });
3812
+ if (subprocess.stderr) subprocess.stderr.pipe(process.stderr);
3813
+ await subprocess;
3814
+ await new Promise((resolve) => setTimeout(resolve, 100));
3815
+ return stdoutData;
3816
+ },
3817
+ catch: (e) => {
3818
+ const execaError = e;
3819
+ return new DatabaseSetupError({
3820
+ provider: "supabase",
3821
+ message: execaError?.message?.includes("Docker is not running") ? "Docker is not running. Please start Docker and try again." : `Failed to start Supabase services: ${execaError?.message ?? String(e)}`,
3822
+ cause: e
3823
+ });
3824
+ }
3825
+ });
3479
3826
  }
3480
3827
  function displayManualSupabaseInstructions(output) {
3481
3828
  log.info(`"Manual Supabase Setup Instructions:"
@@ -3493,56 +3840,63 @@ async function setupSupabase(config, cliInput) {
3493
3840
  const { projectDir, packageManager, backend } = config;
3494
3841
  const manualDb = cliInput?.manualDb ?? false;
3495
3842
  const serverDir = path.join(projectDir, "packages", "db");
3496
- try {
3497
- await fs.ensureDir(serverDir);
3498
- if (manualDb) {
3499
- displayManualSupabaseInstructions();
3500
- await writeSupabaseEnvFile(projectDir, backend, "");
3501
- return;
3502
- }
3503
- const mode = await select({
3504
- message: "Supabase setup: choose mode",
3505
- options: [{
3506
- label: "Automatic",
3507
- value: "auto",
3508
- hint: "Automated setup with provider CLI, sets .env"
3509
- }, {
3510
- label: "Manual",
3511
- value: "manual",
3512
- hint: "Manual setup, add env vars yourself"
3513
- }],
3514
- initialValue: "auto"
3515
- });
3516
- if (isCancel(mode)) return exitCancelled("Operation cancelled");
3517
- if (mode === "manual") {
3518
- displayManualSupabaseInstructions();
3519
- await writeSupabaseEnvFile(projectDir, backend, "");
3520
- return;
3521
- }
3522
- if (!await initializeSupabase(serverDir, packageManager)) {
3523
- displayManualSupabaseInstructions();
3524
- return;
3525
- }
3526
- const supabaseOutput = await startSupabase(serverDir, packageManager);
3527
- if (!supabaseOutput) {
3528
- displayManualSupabaseInstructions();
3529
- return;
3530
- }
3531
- const dbUrl = extractDbUrl(supabaseOutput);
3532
- if (dbUrl) if (await writeSupabaseEnvFile(projectDir, backend, dbUrl)) log.success(pc.green("Supabase local development setup ready!"));
3843
+ const ensureDirResult = await Result.tryPromise({
3844
+ try: () => fs.ensureDir(serverDir),
3845
+ catch: (e) => new DatabaseSetupError({
3846
+ provider: "supabase",
3847
+ message: `Failed to create directory: ${e instanceof Error ? e.message : String(e)}`,
3848
+ cause: e
3849
+ })
3850
+ });
3851
+ if (ensureDirResult.isErr()) return ensureDirResult;
3852
+ if (manualDb) {
3853
+ displayManualSupabaseInstructions();
3854
+ return writeSupabaseEnvFile(projectDir, backend, "");
3855
+ }
3856
+ const mode = await select({
3857
+ message: "Supabase setup: choose mode",
3858
+ options: [{
3859
+ label: "Automatic",
3860
+ value: "auto",
3861
+ hint: "Automated setup with provider CLI, sets .env"
3862
+ }, {
3863
+ label: "Manual",
3864
+ value: "manual",
3865
+ hint: "Manual setup, add env vars yourself"
3866
+ }],
3867
+ initialValue: "auto"
3868
+ });
3869
+ if (isCancel(mode)) return userCancelled("Operation cancelled");
3870
+ if (mode === "manual") {
3871
+ displayManualSupabaseInstructions();
3872
+ return writeSupabaseEnvFile(projectDir, backend, "");
3873
+ }
3874
+ const initResult = await initializeSupabase(serverDir, packageManager);
3875
+ if (initResult.isErr()) {
3876
+ log.error(pc.red(initResult.error.message));
3877
+ displayManualSupabaseInstructions();
3878
+ return writeSupabaseEnvFile(projectDir, backend, "");
3879
+ }
3880
+ const startResult = await startSupabase(serverDir, packageManager);
3881
+ if (startResult.isErr()) {
3882
+ log.error(pc.red(startResult.error.message));
3883
+ displayManualSupabaseInstructions();
3884
+ return writeSupabaseEnvFile(projectDir, backend, "");
3885
+ }
3886
+ const supabaseOutput = startResult.value;
3887
+ const dbUrl = extractDbUrl(supabaseOutput);
3888
+ if (dbUrl) {
3889
+ const envResult = await writeSupabaseEnvFile(projectDir, backend, dbUrl);
3890
+ if (envResult.isOk()) log.success(pc.green("Supabase local development setup ready!"));
3533
3891
  else {
3534
3892
  log.error(pc.red("Supabase setup completed, but failed to update .env automatically."));
3535
3893
  displayManualSupabaseInstructions(supabaseOutput);
3536
3894
  }
3537
- else {
3538
- log.error(pc.yellow("Supabase started, but could not extract DB URL automatically."));
3539
- displayManualSupabaseInstructions(supabaseOutput);
3540
- }
3541
- } catch (error) {
3542
- if (error instanceof Error) consola$1.error(pc.red(`Error during Supabase setup: ${error.message}`));
3543
- else consola$1.error(pc.red(`An unknown error occurred during Supabase setup: ${String(error)}`));
3544
- displayManualSupabaseInstructions();
3895
+ return envResult;
3545
3896
  }
3897
+ log.error(pc.yellow("Supabase started, but could not extract DB URL automatically."));
3898
+ displayManualSupabaseInstructions(supabaseOutput);
3899
+ return databaseSetupError("supabase", "Could not extract database URL from Supabase output. Please configure manually.");
3546
3900
  }
3547
3901
 
3548
3902
  //#endregion
@@ -3551,76 +3905,92 @@ async function isTursoInstalled() {
3551
3905
  return commandExists("turso");
3552
3906
  }
3553
3907
  async function isTursoLoggedIn() {
3554
- try {
3555
- return !(await $`turso auth whoami`).stdout.includes("You are not logged in");
3556
- } catch {
3557
- return false;
3558
- }
3908
+ const result = await Result.tryPromise({
3909
+ try: async () => {
3910
+ return !(await $`turso auth whoami`).stdout.includes("You are not logged in");
3911
+ },
3912
+ catch: () => false
3913
+ });
3914
+ return result.isOk() ? result.value : false;
3559
3915
  }
3560
3916
  async function loginToTurso() {
3561
3917
  const s = spinner();
3562
- try {
3563
- s.start("Logging in to Turso...");
3564
- await $`turso auth login`;
3565
- s.stop("Logged into Turso");
3566
- return true;
3567
- } catch {
3568
- s.stop(pc.red("Failed to log in to Turso"));
3569
- }
3918
+ s.start("Logging in to Turso...");
3919
+ return Result.tryPromise({
3920
+ try: async () => {
3921
+ await $`turso auth login`;
3922
+ s.stop("Logged into Turso");
3923
+ },
3924
+ catch: (e) => {
3925
+ s.stop(pc.red("Failed to log in to Turso"));
3926
+ return new DatabaseSetupError({
3927
+ provider: "turso",
3928
+ message: `Failed to log in to Turso: ${e instanceof Error ? e.message : String(e)}`,
3929
+ cause: e
3930
+ });
3931
+ }
3932
+ });
3570
3933
  }
3571
3934
  async function installTursoCLI(isMac) {
3572
3935
  const s = spinner();
3573
- try {
3574
- s.start("Installing Turso CLI...");
3575
- if (isMac) await $`brew install tursodatabase/tap/turso`;
3576
- else {
3577
- const { stdout: installScript } = await $`curl -sSfL https://get.tur.so/install.sh`;
3578
- await $`bash -c '${installScript}'`;
3579
- }
3580
- s.stop("Turso CLI installed");
3581
- return true;
3582
- } catch (error) {
3583
- if (error instanceof Error && error.message.includes("User force closed")) {
3584
- s.stop("Turso CLI installation cancelled");
3585
- log.warn(pc.yellow("Turso CLI installation cancelled by user"));
3586
- throw new Error("Installation cancelled");
3936
+ s.start("Installing Turso CLI...");
3937
+ return Result.tryPromise({
3938
+ try: async () => {
3939
+ if (isMac) await $`brew install tursodatabase/tap/turso`;
3940
+ else {
3941
+ const { stdout: installScript } = await $`curl -sSfL https://get.tur.so/install.sh`;
3942
+ await $`bash -c '${installScript}'`;
3943
+ }
3944
+ s.stop("Turso CLI installed");
3945
+ },
3946
+ catch: (e) => {
3947
+ const error = e;
3948
+ const isCancelled = error.message?.includes("User force closed");
3949
+ s.stop(isCancelled ? "Turso CLI installation cancelled" : pc.red("Failed to install Turso CLI"));
3950
+ return new DatabaseSetupError({
3951
+ provider: "turso",
3952
+ message: isCancelled ? "Installation cancelled by user" : `Failed to install Turso CLI: ${error.message ?? String(e)}`,
3953
+ cause: e
3954
+ });
3587
3955
  }
3588
- s.stop(pc.red("Failed to install Turso CLI"));
3589
- }
3956
+ });
3590
3957
  }
3591
3958
  async function getTursoGroups() {
3592
3959
  const s = spinner();
3593
- try {
3594
- s.start("Fetching Turso groups...");
3595
- const { stdout } = await $`turso group list`;
3596
- const lines = stdout.trim().split("\n");
3597
- if (lines.length <= 1) {
3598
- s.stop("No Turso groups found");
3960
+ s.start("Fetching Turso groups...");
3961
+ const result = await Result.tryPromise({
3962
+ try: async () => {
3963
+ const { stdout } = await $`turso group list`;
3964
+ const lines = stdout.trim().split("\n");
3965
+ if (lines.length <= 1) {
3966
+ s.stop("No Turso groups found");
3967
+ return [];
3968
+ }
3969
+ const groups = lines.slice(1).map((line) => {
3970
+ const [name, locations, version, status] = line.trim().split(/\s{2,}/);
3971
+ return {
3972
+ name,
3973
+ locations,
3974
+ version,
3975
+ status
3976
+ };
3977
+ });
3978
+ s.stop(`Found ${groups.length} Turso groups`);
3979
+ return groups;
3980
+ },
3981
+ catch: () => {
3982
+ s.stop(pc.red("Error fetching Turso groups"));
3599
3983
  return [];
3600
3984
  }
3601
- const groups = lines.slice(1).map((line) => {
3602
- const [name, locations, version, status] = line.trim().split(/\s{2,}/);
3603
- return {
3604
- name,
3605
- locations,
3606
- version,
3607
- status
3608
- };
3609
- });
3610
- s.stop(`Found ${groups.length} Turso groups`);
3611
- return groups;
3612
- } catch (error) {
3613
- s.stop(pc.red("Error fetching Turso groups"));
3614
- console.error("Error fetching Turso groups:", error);
3615
- return [];
3616
- }
3985
+ });
3986
+ return result.isOk() ? result.value : [];
3617
3987
  }
3618
3988
  async function selectTursoGroup() {
3619
3989
  const groups = await getTursoGroups();
3620
- if (groups.length === 0) return null;
3990
+ if (groups.length === 0) return Result.ok(null);
3621
3991
  if (groups.length === 1) {
3622
3992
  log.info(`Using the only available group: ${pc.blue(groups[0].name)}`);
3623
- return groups[0].name;
3993
+ return Result.ok(groups[0].name);
3624
3994
  }
3625
3995
  const selectedGroup = await select({
3626
3996
  message: "Select a Turso database group:",
@@ -3629,44 +3999,75 @@ async function selectTursoGroup() {
3629
3999
  label: `${group$1.name} (${group$1.locations})`
3630
4000
  }))
3631
4001
  });
3632
- if (isCancel(selectedGroup)) return exitCancelled("Operation cancelled");
3633
- return selectedGroup;
4002
+ if (isCancel(selectedGroup)) return userCancelled("Operation cancelled");
4003
+ return Result.ok(selectedGroup);
3634
4004
  }
3635
4005
  async function createTursoDatabase(dbName, groupName) {
3636
4006
  const s = spinner();
3637
- try {
3638
- s.start(`Creating Turso database "${dbName}"${groupName ? ` in group "${groupName}"` : ""}...`);
3639
- if (groupName) await $`turso db create ${dbName} --group ${groupName}`;
3640
- else await $`turso db create ${dbName}`;
3641
- s.stop(`Turso database "${dbName}" created`);
3642
- } catch (error) {
3643
- s.stop(pc.red(`Failed to create database "${dbName}"`));
3644
- if (error instanceof Error && error.message.includes("already exists")) throw new Error("DATABASE_EXISTS");
3645
- }
4007
+ s.start(`Creating Turso database "${dbName}"${groupName ? ` in group "${groupName}"` : ""}...`);
4008
+ const createResult = await Result.tryPromise({
4009
+ try: async () => {
4010
+ if (groupName) await $`turso db create ${dbName} --group ${groupName}`;
4011
+ else await $`turso db create ${dbName}`;
4012
+ s.stop(`Turso database "${dbName}" created`);
4013
+ },
4014
+ catch: (e) => {
4015
+ const error = e;
4016
+ s.stop(pc.red(`Failed to create database "${dbName}"`));
4017
+ if (error.message?.includes("already exists")) return new DatabaseSetupError({
4018
+ provider: "turso",
4019
+ message: "DATABASE_EXISTS",
4020
+ cause: e
4021
+ });
4022
+ return new DatabaseSetupError({
4023
+ provider: "turso",
4024
+ message: `Failed to create database: ${error.message ?? String(e)}`,
4025
+ cause: e
4026
+ });
4027
+ }
4028
+ });
4029
+ if (createResult.isErr()) return createResult;
3646
4030
  s.start("Retrieving database connection details...");
3647
- try {
3648
- const { stdout: dbUrl } = await $`turso db show ${dbName} --url`;
3649
- const { stdout: authToken } = await $`turso db tokens create ${dbName}`;
3650
- s.stop("Database connection details retrieved");
3651
- return {
3652
- dbUrl: dbUrl.trim(),
3653
- authToken: authToken.trim()
3654
- };
3655
- } catch {
3656
- s.stop(pc.red("Failed to retrieve database connection details"));
3657
- }
4031
+ return Result.tryPromise({
4032
+ try: async () => {
4033
+ const { stdout: dbUrl } = await $`turso db show ${dbName} --url`;
4034
+ const { stdout: authToken } = await $`turso db tokens create ${dbName}`;
4035
+ s.stop("Database connection details retrieved");
4036
+ return {
4037
+ dbUrl: dbUrl.trim(),
4038
+ authToken: authToken.trim()
4039
+ };
4040
+ },
4041
+ catch: (e) => {
4042
+ s.stop(pc.red("Failed to retrieve database connection details"));
4043
+ return new DatabaseSetupError({
4044
+ provider: "turso",
4045
+ message: `Failed to retrieve connection details: ${e instanceof Error ? e.message : String(e)}`,
4046
+ cause: e
4047
+ });
4048
+ }
4049
+ });
3658
4050
  }
3659
4051
  async function writeEnvFile(projectDir, backend, config) {
3660
- const targetApp = backend === "self" ? "apps/web" : "apps/server";
3661
- await addEnvVariablesToFile(path.join(projectDir, targetApp, ".env"), [{
3662
- key: "DATABASE_URL",
3663
- value: config?.dbUrl ?? "",
3664
- condition: true
3665
- }, {
3666
- key: "DATABASE_AUTH_TOKEN",
3667
- value: config?.authToken ?? "",
3668
- condition: true
3669
- }]);
4052
+ return Result.tryPromise({
4053
+ try: async () => {
4054
+ const targetApp = backend === "self" ? "apps/web" : "apps/server";
4055
+ await addEnvVariablesToFile(path.join(projectDir, targetApp, ".env"), [{
4056
+ key: "DATABASE_URL",
4057
+ value: config?.dbUrl ?? "",
4058
+ condition: true
4059
+ }, {
4060
+ key: "DATABASE_AUTH_TOKEN",
4061
+ value: config?.authToken ?? "",
4062
+ condition: true
4063
+ }]);
4064
+ },
4065
+ catch: (e) => new DatabaseSetupError({
4066
+ provider: "turso",
4067
+ message: `Failed to update .env file: ${e instanceof Error ? e.message : String(e)}`,
4068
+ cause: e
4069
+ })
4070
+ });
3670
4071
  }
3671
4072
  function displayManualSetupInstructions() {
3672
4073
  log.info(`Manual Turso Setup Instructions:
@@ -3680,99 +4081,114 @@ DATABASE_URL=your_database_url
3680
4081
  DATABASE_AUTH_TOKEN=your_auth_token`);
3681
4082
  }
3682
4083
  async function setupTurso(config, cliInput) {
3683
- const { orm, projectDir, backend } = config;
4084
+ const { projectDir, backend } = config;
3684
4085
  const manualDb = cliInput?.manualDb ?? false;
3685
4086
  const setupSpinner = spinner();
3686
- try {
3687
- if (manualDb) {
3688
- await writeEnvFile(projectDir, backend);
3689
- displayManualSetupInstructions();
3690
- return;
3691
- }
3692
- const mode = await select({
3693
- message: "Turso setup: choose mode",
3694
- options: [{
3695
- label: "Automatic",
3696
- value: "auto",
3697
- hint: "Automated setup with provider CLI, sets .env"
3698
- }, {
3699
- label: "Manual",
3700
- value: "manual",
3701
- hint: "Manual setup, add env vars yourself"
3702
- }],
3703
- initialValue: "auto"
4087
+ if (manualDb) {
4088
+ const envResult = await writeEnvFile(projectDir, backend);
4089
+ if (envResult.isErr()) return envResult;
4090
+ displayManualSetupInstructions();
4091
+ return Result.ok(void 0);
4092
+ }
4093
+ const mode = await select({
4094
+ message: "Turso setup: choose mode",
4095
+ options: [{
4096
+ label: "Automatic",
4097
+ value: "auto",
4098
+ hint: "Automated setup with provider CLI, sets .env"
4099
+ }, {
4100
+ label: "Manual",
4101
+ value: "manual",
4102
+ hint: "Manual setup, add env vars yourself"
4103
+ }],
4104
+ initialValue: "auto"
4105
+ });
4106
+ if (isCancel(mode)) return userCancelled("Operation cancelled");
4107
+ if (mode === "manual") {
4108
+ const envResult = await writeEnvFile(projectDir, backend);
4109
+ if (envResult.isErr()) return envResult;
4110
+ displayManualSetupInstructions();
4111
+ return Result.ok(void 0);
4112
+ }
4113
+ setupSpinner.start("Checking Turso CLI availability...");
4114
+ const platform = os$1.platform();
4115
+ const isMac = platform === "darwin";
4116
+ if (platform === "win32") {
4117
+ setupSpinner.stop(pc.yellow("Turso setup not supported on Windows"));
4118
+ log.warn(pc.yellow("Automatic Turso setup is not supported on Windows."));
4119
+ const envResult = await writeEnvFile(projectDir, backend);
4120
+ if (envResult.isErr()) return envResult;
4121
+ displayManualSetupInstructions();
4122
+ return Result.ok(void 0);
4123
+ }
4124
+ setupSpinner.stop("Turso CLI availability checked");
4125
+ if (!await isTursoInstalled()) {
4126
+ const shouldInstall = await confirm({
4127
+ message: "Would you like to install Turso CLI?",
4128
+ initialValue: true
3704
4129
  });
3705
- if (isCancel(mode)) return exitCancelled("Operation cancelled");
3706
- if (mode === "manual") {
3707
- await writeEnvFile(projectDir, backend);
4130
+ if (isCancel(shouldInstall)) return userCancelled("Operation cancelled");
4131
+ if (!shouldInstall) {
4132
+ const envResult = await writeEnvFile(projectDir, backend);
4133
+ if (envResult.isErr()) return envResult;
3708
4134
  displayManualSetupInstructions();
3709
- return;
4135
+ return Result.ok(void 0);
3710
4136
  }
3711
- setupSpinner.start("Checking Turso CLI availability...");
3712
- const platform = os$1.platform();
3713
- const isMac = platform === "darwin";
3714
- if (platform === "win32") {
3715
- if (setupSpinner) setupSpinner.stop(pc.yellow("Turso setup not supported on Windows"));
3716
- log.warn(pc.yellow("Automatic Turso setup is not supported on Windows."));
3717
- await writeEnvFile(projectDir, backend);
4137
+ const installResult = await installTursoCLI(isMac);
4138
+ if (installResult.isErr()) {
4139
+ log.error(pc.red(installResult.error.message));
4140
+ const envResult = await writeEnvFile(projectDir, backend);
4141
+ if (envResult.isErr()) return envResult;
3718
4142
  displayManualSetupInstructions();
3719
- return;
4143
+ return Result.ok(void 0);
3720
4144
  }
3721
- if (setupSpinner) setupSpinner.stop("Turso CLI availability checked");
3722
- if (!await isTursoInstalled()) {
3723
- const shouldInstall = await confirm({
3724
- message: "Would you like to install Turso CLI?",
3725
- initialValue: true
3726
- });
3727
- if (isCancel(shouldInstall)) return exitCancelled("Operation cancelled");
3728
- if (!shouldInstall) {
3729
- await writeEnvFile(projectDir, backend);
3730
- displayManualSetupInstructions();
3731
- return;
3732
- }
3733
- await installTursoCLI(isMac);
4145
+ }
4146
+ if (!await isTursoLoggedIn()) {
4147
+ const loginResult = await loginToTurso();
4148
+ if (loginResult.isErr()) {
4149
+ log.error(pc.red(loginResult.error.message));
4150
+ const envResult = await writeEnvFile(projectDir, backend);
4151
+ if (envResult.isErr()) return envResult;
4152
+ displayManualSetupInstructions();
4153
+ return Result.ok(void 0);
3734
4154
  }
3735
- if (!await isTursoLoggedIn()) await loginToTurso();
3736
- const selectedGroup = await selectTursoGroup();
3737
- let success = false;
3738
- let dbName = "";
3739
- let suggestedName = path.basename(projectDir);
3740
- while (!success) {
3741
- const dbNameResponse = await text({
3742
- message: "Enter a name for your database:",
3743
- defaultValue: suggestedName,
3744
- initialValue: suggestedName,
3745
- placeholder: suggestedName
3746
- });
3747
- if (isCancel(dbNameResponse)) return exitCancelled("Operation cancelled");
3748
- dbName = dbNameResponse;
3749
- try {
3750
- await writeEnvFile(projectDir, backend, await createTursoDatabase(dbName, selectedGroup));
3751
- success = true;
3752
- } catch (error) {
3753
- if (error instanceof Error && error.message === "DATABASE_EXISTS") {
3754
- log.warn(pc.yellow(`Database "${pc.red(dbName)}" already exists`));
3755
- suggestedName = `${dbName}-${Math.floor(Math.random() * 1e3)}`;
3756
- } else throw error;
4155
+ }
4156
+ const groupResult = await selectTursoGroup();
4157
+ if (groupResult.isErr()) return groupResult;
4158
+ const selectedGroup = groupResult.value;
4159
+ let suggestedName = path.basename(projectDir);
4160
+ while (true) {
4161
+ const dbNameResponse = await text({
4162
+ message: "Enter a name for your database:",
4163
+ defaultValue: suggestedName,
4164
+ initialValue: suggestedName,
4165
+ placeholder: suggestedName
4166
+ });
4167
+ if (isCancel(dbNameResponse)) return userCancelled("Operation cancelled");
4168
+ const dbName = dbNameResponse;
4169
+ const createResult = await createTursoDatabase(dbName, selectedGroup);
4170
+ if (createResult.isErr()) {
4171
+ if (createResult.error.message === "DATABASE_EXISTS") {
4172
+ log.warn(pc.yellow(`Database "${pc.red(dbName)}" already exists`));
4173
+ suggestedName = `${dbName}-${Math.floor(Math.random() * 1e3)}`;
4174
+ continue;
3757
4175
  }
4176
+ log.error(pc.red(createResult.error.message));
4177
+ const envResult$1 = await writeEnvFile(projectDir, backend);
4178
+ if (envResult$1.isErr()) return envResult$1;
4179
+ displayManualSetupInstructions();
4180
+ log.success("Setup completed with manual configuration required.");
4181
+ return Result.ok(void 0);
3758
4182
  }
4183
+ const envResult = await writeEnvFile(projectDir, backend, createResult.value);
4184
+ if (envResult.isErr()) return envResult;
3759
4185
  log.success("Turso database setup completed successfully!");
3760
- } catch (error) {
3761
- if (setupSpinner) setupSpinner.stop(pc.red("Turso CLI availability check failed"));
3762
- consola.error(pc.red(`Error during Turso setup: ${error instanceof Error ? error.message : String(error)}`));
3763
- await writeEnvFile(projectDir, backend);
3764
- displayManualSetupInstructions();
3765
- log.success("Setup completed with manual configuration required.");
4186
+ return Result.ok(void 0);
3766
4187
  }
3767
4188
  }
3768
4189
 
3769
4190
  //#endregion
3770
4191
  //#region src/helpers/core/db-setup.ts
3771
- /**
3772
- * Database setup - CLI-only operations
3773
- * Calls external database provider CLIs (turso, neon, prisma-postgres, etc.)
3774
- * Dependencies are handled by the generator's db-deps processor
3775
- */
3776
4192
  async function setupDatabase(config, cliInput) {
3777
4193
  const { database, dbSetup, backend, projectDir } = config;
3778
4194
  if (backend === "convex" || database === "none") {
@@ -3784,59 +4200,80 @@ async function setupDatabase(config, cliInput) {
3784
4200
  }
3785
4201
  const dbPackageDir = path.join(projectDir, "packages/db");
3786
4202
  if (!await fs.pathExists(dbPackageDir)) return;
3787
- try {
3788
- if (dbSetup === "docker") await setupDockerCompose(config);
3789
- else if (database === "sqlite" && dbSetup === "turso") await setupTurso(config, cliInput);
3790
- else if (database === "sqlite" && dbSetup === "d1") await setupCloudflareD1(config);
3791
- else if (database === "postgres") {
3792
- if (dbSetup === "prisma-postgres") await setupPrismaPostgres(config, cliInput);
3793
- else if (dbSetup === "neon") await setupNeonPostgres(config, cliInput);
3794
- else if (dbSetup === "planetscale") await setupPlanetScale(config);
3795
- else if (dbSetup === "supabase") await setupSupabase(config, cliInput);
3796
- } else if (database === "mysql" && dbSetup === "planetscale") await setupPlanetScale(config);
3797
- else if (database === "mongodb" && dbSetup === "mongodb-atlas") await setupMongoDBAtlas(config, cliInput);
3798
- } catch (error) {
3799
- if (error instanceof Error) consola.error(pc.red(error.message));
4203
+ async function runSetup$1(setupFn) {
4204
+ const result = await setupFn();
4205
+ if (result.isErr()) {
4206
+ if (UserCancelledError.is(result.error)) throw result.error;
4207
+ consola.error(pc.red(result.error.message));
4208
+ }
3800
4209
  }
4210
+ if (dbSetup === "docker") await runSetup$1(() => setupDockerCompose(config));
4211
+ else if (database === "sqlite" && dbSetup === "turso") await runSetup$1(() => setupTurso(config, cliInput));
4212
+ else if (database === "sqlite" && dbSetup === "d1") await setupCloudflareD1(config);
4213
+ else if (database === "postgres") {
4214
+ if (dbSetup === "prisma-postgres") await runSetup$1(() => setupPrismaPostgres(config, cliInput));
4215
+ else if (dbSetup === "neon") await runSetup$1(() => setupNeonPostgres(config, cliInput));
4216
+ else if (dbSetup === "planetscale") await setupPlanetScale(config);
4217
+ else if (dbSetup === "supabase") await runSetup$1(() => setupSupabase(config, cliInput));
4218
+ } else if (database === "mysql" && dbSetup === "planetscale") await setupPlanetScale(config);
4219
+ else if (database === "mongodb" && dbSetup === "mongodb-atlas") await runSetup$1(() => setupMongoDBAtlas(config, cliInput));
3801
4220
  }
3802
4221
 
3803
4222
  //#endregion
3804
4223
  //#region src/helpers/core/git.ts
3805
4224
  async function initializeGit(projectDir, useGit) {
3806
- if (!useGit) return;
4225
+ if (!useGit) return Result.ok(void 0);
3807
4226
  if ((await $({
3808
4227
  cwd: projectDir,
3809
4228
  reject: false,
3810
4229
  stderr: "pipe"
3811
4230
  })`git --version`).exitCode !== 0) {
3812
4231
  log.warn(pc.yellow("Git is not installed"));
3813
- return;
4232
+ return Result.ok(void 0);
3814
4233
  }
3815
4234
  const result = await $({
3816
4235
  cwd: projectDir,
3817
4236
  reject: false,
3818
4237
  stderr: "pipe"
3819
4238
  })`git init`;
3820
- if (result.exitCode !== 0) throw new Error(`Git initialization failed: ${result.stderr}`);
3821
- await $({ cwd: projectDir })`git add -A`;
3822
- await $({ cwd: projectDir })`git commit -m ${"initial commit"}`;
4239
+ if (result.exitCode !== 0) return Result.err(new ProjectCreationError({
4240
+ phase: "git-initialization",
4241
+ message: `Git initialization failed: ${result.stderr}`
4242
+ }));
4243
+ return Result.tryPromise({
4244
+ try: async () => {
4245
+ await $({ cwd: projectDir })`git add -A`;
4246
+ await $({ cwd: projectDir })`git commit -m ${"initial commit"}`;
4247
+ },
4248
+ catch: (e) => new ProjectCreationError({
4249
+ phase: "git-initialization",
4250
+ message: `Git commit failed: ${e instanceof Error ? e.message : String(e)}`,
4251
+ cause: e
4252
+ })
4253
+ });
3823
4254
  }
3824
4255
 
3825
4256
  //#endregion
3826
4257
  //#region src/helpers/core/install-dependencies.ts
3827
4258
  async function installDependencies({ projectDir, packageManager }) {
3828
4259
  const s = spinner();
3829
- try {
3830
- s.start(`Running ${packageManager} install...`);
3831
- await $({
3832
- cwd: projectDir,
3833
- stderr: "inherit"
3834
- })`${packageManager} install`;
3835
- s.stop("Dependencies installed successfully");
3836
- } catch (error) {
3837
- s.stop(pc.red("Failed to install dependencies"));
3838
- if (error instanceof Error) consola.error(pc.red(`Installation error: ${error.message}`));
3839
- }
4260
+ s.start(`Running ${packageManager} install...`);
4261
+ const result = await Result.tryPromise({
4262
+ try: async () => {
4263
+ await $({
4264
+ cwd: projectDir,
4265
+ stderr: "inherit"
4266
+ })`${packageManager} install`;
4267
+ },
4268
+ catch: (e) => new ProjectCreationError({
4269
+ phase: "dependency-installation",
4270
+ message: `Installation error: ${e instanceof Error ? e.message : String(e)}`,
4271
+ cause: e
4272
+ })
4273
+ });
4274
+ if (result.isOk()) s.stop("Dependencies installed successfully");
4275
+ else s.stop(pc.red("Failed to install dependencies"));
4276
+ return result;
3840
4277
  }
3841
4278
 
3842
4279
  //#endregion
@@ -3845,12 +4282,14 @@ async function isDockerInstalled() {
3845
4282
  return commandExists("docker");
3846
4283
  }
3847
4284
  async function isDockerRunning() {
3848
- try {
3849
- await $`docker info`;
3850
- return true;
3851
- } catch {
3852
- return false;
3853
- }
4285
+ const result = await Result.tryPromise({
4286
+ try: async () => {
4287
+ await $`docker info`;
4288
+ return true;
4289
+ },
4290
+ catch: () => false
4291
+ });
4292
+ return result.isOk() ? result.value : false;
3854
4293
  }
3855
4294
  function getDockerInstallInstructions(platform, database) {
3856
4295
  const isMac = platform === "darwin";
@@ -4063,246 +4502,314 @@ function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend)
4063
4502
 
4064
4503
  //#endregion
4065
4504
  //#region src/helpers/core/create-project.ts
4505
+ /**
4506
+ * Creates a new project with the given configuration.
4507
+ * Returns a Result with the project directory path on success, or an error on failure.
4508
+ */
4066
4509
  async function createProject(options, cliInput = {}) {
4067
- const projectDir = options.projectDir;
4068
- const isConvex = options.backend === "convex";
4069
- try {
4070
- await fs.ensureDir(projectDir);
4071
- const result = await generateVirtualProject({
4510
+ return Result.gen(async function* () {
4511
+ const projectDir = options.projectDir;
4512
+ const isConvex = options.backend === "convex";
4513
+ yield* Result.await(Result.tryPromise({
4514
+ try: () => fs.ensureDir(projectDir),
4515
+ catch: (e) => new ProjectCreationError({
4516
+ phase: "directory-setup",
4517
+ message: `Failed to create project directory: ${e instanceof Error ? e.message : String(e)}`,
4518
+ cause: e
4519
+ })
4520
+ }));
4521
+ const tree = yield* Result.await(generate({
4072
4522
  config: options,
4073
4523
  templates: EMBEDDED_TEMPLATES
4074
- });
4075
- if (!result.success || !result.tree) throw new Error(result.error || "Failed to generate project templates");
4076
- await writeTreeToFilesystem(result.tree, projectDir);
4077
- await setPackageManagerVersion(projectDir, options.packageManager);
4078
- if (!isConvex && options.database !== "none") await setupDatabase(options, cliInput);
4079
- if (options.addons.length > 0 && options.addons[0] !== "none") await setupAddons(options);
4080
- await writeBtsConfig(options);
4081
- await formatProject(projectDir);
4524
+ }).then((result) => result.mapError((e) => new ProjectCreationError({
4525
+ phase: e.phase || "template-generation",
4526
+ message: e.message,
4527
+ cause: e
4528
+ }))));
4529
+ yield* Result.await(writeTree(tree, projectDir).then((result) => result.mapError((e) => new ProjectCreationError({
4530
+ phase: "file-writing",
4531
+ message: e.message,
4532
+ cause: e
4533
+ }))));
4534
+ yield* Result.await(setPackageManagerVersion(projectDir, options.packageManager));
4535
+ if (!isConvex && options.database !== "none") yield* Result.await(Result.tryPromise({
4536
+ try: () => setupDatabase(options, cliInput),
4537
+ catch: (e) => new ProjectCreationError({
4538
+ phase: "database-setup",
4539
+ message: `Failed to setup database: ${e instanceof Error ? e.message : String(e)}`,
4540
+ cause: e
4541
+ })
4542
+ }));
4543
+ if (options.addons.length > 0 && options.addons[0] !== "none") yield* Result.await(Result.tryPromise({
4544
+ try: () => setupAddons(options),
4545
+ catch: (e) => new ProjectCreationError({
4546
+ phase: "addons-setup",
4547
+ message: `Failed to setup addons: ${e instanceof Error ? e.message : String(e)}`,
4548
+ cause: e
4549
+ })
4550
+ }));
4551
+ yield* Result.await(writeBtsConfig(options));
4552
+ yield* Result.await(formatProject(projectDir));
4082
4553
  if (!isSilent()) log.success("Project template successfully scaffolded!");
4083
- if (options.install) await installDependencies({
4554
+ if (options.install) yield* Result.await(installDependencies({
4084
4555
  projectDir,
4085
4556
  packageManager: options.packageManager
4086
- });
4087
- await initializeGit(projectDir, options.git);
4557
+ }));
4558
+ yield* Result.await(initializeGit(projectDir, options.git));
4088
4559
  if (!isSilent()) await displayPostInstallInstructions({
4089
4560
  ...options,
4090
4561
  depsInstalled: options.install
4091
4562
  });
4092
- return projectDir;
4093
- } catch (error) {
4094
- if (error instanceof Error) {
4095
- if (!isSilent()) console.error(error.stack);
4096
- exitWithError(`Error during project creation: ${error.message}`);
4097
- } else {
4098
- if (!isSilent()) console.error(error);
4099
- exitWithError(`An unexpected error occurred: ${String(error)}`);
4100
- }
4101
- }
4563
+ return Result.ok(projectDir);
4564
+ });
4102
4565
  }
4103
4566
  async function setPackageManagerVersion(projectDir, packageManager) {
4104
4567
  const pkgJsonPath = path.join(projectDir, "package.json");
4105
- if (!await fs.pathExists(pkgJsonPath)) return;
4106
- try {
4107
- const { stdout } = await $`${packageManager} -v`;
4108
- const version = stdout.trim();
4109
- const pkgJson = await fs.readJson(pkgJsonPath);
4110
- pkgJson.packageManager = `${packageManager}@${version}`;
4111
- await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
4112
- } catch {
4113
- const pkgJson = await fs.readJson(pkgJsonPath);
4114
- delete pkgJson.packageManager;
4115
- await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
4116
- }
4568
+ if (!await fs.pathExists(pkgJsonPath)) return Result.ok(void 0);
4569
+ const versionResult = await Result.tryPromise({
4570
+ try: async () => {
4571
+ const { stdout } = await $`${packageManager} -v`;
4572
+ return stdout.trim();
4573
+ },
4574
+ catch: () => null
4575
+ });
4576
+ return Result.tryPromise({
4577
+ try: async () => {
4578
+ const pkgJson = await fs.readJson(pkgJsonPath);
4579
+ if (versionResult.isOk() && versionResult.value) pkgJson.packageManager = `${packageManager}@${versionResult.value}`;
4580
+ else delete pkgJson.packageManager;
4581
+ await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
4582
+ },
4583
+ catch: (e) => new ProjectCreationError({
4584
+ phase: "package-manager-version",
4585
+ message: `Failed to set package manager version: ${e instanceof Error ? e.message : String(e)}`,
4586
+ cause: e
4587
+ })
4588
+ });
4117
4589
  }
4118
4590
 
4119
4591
  //#endregion
4120
4592
  //#region src/helpers/core/command-handlers.ts
4593
+ /**
4594
+ * Create an empty/failed result
4595
+ */
4596
+ function createEmptyResult(timeScaffolded, elapsedTimeMs, error) {
4597
+ return {
4598
+ success: false,
4599
+ projectConfig: {
4600
+ projectName: "",
4601
+ projectDir: "",
4602
+ relativePath: "",
4603
+ database: "none",
4604
+ orm: "none",
4605
+ backend: "none",
4606
+ runtime: "none",
4607
+ frontend: [],
4608
+ addons: [],
4609
+ examples: [],
4610
+ auth: "none",
4611
+ payments: "none",
4612
+ git: false,
4613
+ packageManager: "npm",
4614
+ install: false,
4615
+ dbSetup: "none",
4616
+ api: "none",
4617
+ webDeploy: "none",
4618
+ serverDeploy: "none"
4619
+ },
4620
+ reproducibleCommand: "",
4621
+ timeScaffolded,
4622
+ elapsedTimeMs,
4623
+ projectDirectory: "",
4624
+ relativePath: "",
4625
+ error
4626
+ };
4627
+ }
4121
4628
  async function createProjectHandler(input, options = {}) {
4122
4629
  const { silent = false } = options;
4123
4630
  return runWithContextAsync({ silent }, async () => {
4124
4631
  const startTime = Date.now();
4125
4632
  const timeScaffolded = (/* @__PURE__ */ new Date()).toISOString();
4126
- try {
4127
- if (!isSilent() && input.renderTitle !== false) renderTitle();
4128
- if (!isSilent()) intro(pc.magenta("Creating a new Better-T-Stack project"));
4129
- if (!isSilent() && input.yolo) consola.fatal("YOLO mode enabled - skipping checks. Things may break!");
4130
- let currentPathInput;
4131
- if (input.yes && input.projectName) currentPathInput = input.projectName;
4132
- else if (input.yes) {
4133
- const defaultConfig = getDefaultConfig();
4134
- let defaultName = defaultConfig.relativePath;
4135
- let counter = 1;
4136
- while (await fs.pathExists(path.resolve(process.cwd(), defaultName)) && (await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0) {
4137
- defaultName = `${defaultConfig.projectName}-${counter}`;
4138
- counter++;
4139
- }
4140
- currentPathInput = defaultName;
4141
- } else currentPathInput = await getProjectName(input.projectName);
4142
- let finalPathInput;
4143
- let shouldClearDirectory;
4144
- try {
4145
- if (input.directoryConflict) {
4146
- const result = await handleDirectoryConflictProgrammatically(currentPathInput, input.directoryConflict);
4147
- finalPathInput = result.finalPathInput;
4148
- shouldClearDirectory = result.shouldClearDirectory;
4149
- } else {
4150
- const result = await handleDirectoryConflict(currentPathInput);
4151
- finalPathInput = result.finalPathInput;
4152
- shouldClearDirectory = result.shouldClearDirectory;
4153
- }
4154
- } catch (error) {
4155
- if (error instanceof UserCancelledError || error instanceof CLIError) throw error;
4156
- return {
4157
- success: false,
4158
- projectConfig: {
4159
- projectName: "",
4160
- projectDir: "",
4161
- relativePath: "",
4162
- database: "none",
4163
- orm: "none",
4164
- backend: "none",
4165
- runtime: "none",
4166
- frontend: [],
4167
- addons: [],
4168
- examples: [],
4169
- auth: "none",
4170
- payments: "none",
4171
- git: false,
4172
- packageManager: "npm",
4173
- install: false,
4174
- dbSetup: "none",
4175
- api: "none",
4176
- webDeploy: "none",
4177
- serverDeploy: "none"
4178
- },
4179
- reproducibleCommand: "",
4180
- timeScaffolded,
4181
- elapsedTimeMs: Date.now() - startTime,
4182
- projectDirectory: "",
4183
- relativePath: "",
4184
- error: error instanceof Error ? error.message : String(error)
4185
- };
4633
+ const result = await createProjectHandlerInternal(input, startTime, timeScaffolded);
4634
+ if (result.isOk()) return result.value;
4635
+ const error = result.error;
4636
+ const elapsedTimeMs = Date.now() - startTime;
4637
+ if (UserCancelledError.is(error)) {
4638
+ if (isSilent()) return createEmptyResult(timeScaffolded, elapsedTimeMs, error.message);
4639
+ return;
4640
+ }
4641
+ if (isSilent()) return createEmptyResult(timeScaffolded, elapsedTimeMs, error.message);
4642
+ displayError(error);
4643
+ process.exit(1);
4644
+ });
4645
+ }
4646
+ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
4647
+ return Result.gen(async function* () {
4648
+ if (!isSilent() && input.renderTitle !== false) renderTitle();
4649
+ if (!isSilent()) intro(pc.magenta("Creating a new Better-T-Stack project"));
4650
+ if (!isSilent() && input.yolo) consola.fatal("YOLO mode enabled - skipping checks. Things may break!");
4651
+ let currentPathInput;
4652
+ if (input.yes && input.projectName) currentPathInput = input.projectName;
4653
+ else if (input.yes) {
4654
+ const defaultConfig = getDefaultConfig();
4655
+ let defaultName = defaultConfig.relativePath;
4656
+ let counter = 1;
4657
+ while (await fs.pathExists(path.resolve(process.cwd(), defaultName)) && (await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0) {
4658
+ defaultName = `${defaultConfig.projectName}-${counter}`;
4659
+ counter++;
4186
4660
  }
4187
- const { finalResolvedPath, finalBaseName } = await setupProjectDirectory(finalPathInput, shouldClearDirectory);
4188
- const originalInput = {
4189
- ...input,
4190
- projectDirectory: input.projectName
4191
- };
4192
- const providedFlags = getProvidedFlags(originalInput);
4193
- let cliInput = originalInput;
4194
- if (input.template && input.template !== "none") {
4195
- const templateConfig = getTemplateConfig(input.template);
4196
- if (templateConfig) {
4197
- const templateName = input.template.toUpperCase();
4198
- const templateDescription = getTemplateDescription(input.template);
4199
- if (!isSilent()) {
4200
- log.message(pc.bold(pc.cyan(`Using template: ${pc.white(templateName)}`)));
4201
- log.message(pc.dim(` ${templateDescription}`));
4202
- }
4203
- const userOverrides = {};
4204
- for (const [key, value] of Object.entries(originalInput)) if (value !== void 0) userOverrides[key] = value;
4205
- cliInput = {
4206
- ...templateConfig,
4207
- ...userOverrides,
4208
- template: input.template,
4209
- projectDirectory: originalInput.projectDirectory
4210
- };
4211
- }
4661
+ currentPathInput = defaultName;
4662
+ } else currentPathInput = yield* Result.await(Result.tryPromise({
4663
+ try: async () => getProjectName(input.projectName),
4664
+ catch: (e) => {
4665
+ if (e instanceof UserCancelledError) return e;
4666
+ return new CLIError({
4667
+ message: e instanceof Error ? e.message : String(e),
4668
+ cause: e
4669
+ });
4212
4670
  }
4213
- let config;
4214
- if (cliInput.yes) {
4215
- const flagConfig = processProvidedFlagsWithoutValidation(cliInput, finalBaseName);
4216
- config = {
4217
- ...getDefaultConfig(),
4218
- ...flagConfig,
4219
- projectName: finalBaseName,
4220
- projectDir: finalResolvedPath,
4221
- relativePath: finalPathInput
4222
- };
4223
- validateConfigCompatibility(config, providedFlags, cliInput);
4671
+ }));
4672
+ let finalPathInput;
4673
+ let shouldClearDirectory;
4674
+ const conflictResult = yield* Result.await(handleDirectoryConflictResult(currentPathInput, input.directoryConflict));
4675
+ finalPathInput = conflictResult.finalPathInput;
4676
+ shouldClearDirectory = conflictResult.shouldClearDirectory;
4677
+ const { finalResolvedPath, finalBaseName } = yield* Result.await(Result.tryPromise({
4678
+ try: async () => setupProjectDirectory(finalPathInput, shouldClearDirectory),
4679
+ catch: (e) => {
4680
+ if (e instanceof UserCancelledError) return e;
4681
+ return new CLIError({
4682
+ message: e instanceof Error ? e.message : String(e),
4683
+ cause: e
4684
+ });
4685
+ }
4686
+ }));
4687
+ const originalInput = {
4688
+ ...input,
4689
+ projectDirectory: input.projectName
4690
+ };
4691
+ const providedFlags = getProvidedFlags(originalInput);
4692
+ let cliInput = originalInput;
4693
+ if (input.template && input.template !== "none") {
4694
+ const templateConfig = getTemplateConfig(input.template);
4695
+ if (templateConfig) {
4696
+ const templateName = input.template.toUpperCase();
4697
+ const templateDescription = getTemplateDescription(input.template);
4224
4698
  if (!isSilent()) {
4225
- log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
4226
- log.message(displayConfig(config));
4227
- }
4228
- } else {
4229
- const flagConfig = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
4230
- const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig;
4231
- if (!isSilent() && Object.keys(otherFlags).length > 0) {
4232
- log.info(pc.yellow("Using these pre-selected options:"));
4233
- log.message(displayConfig(otherFlags));
4234
- log.message("");
4699
+ log.message(pc.bold(pc.cyan(`Using template: ${pc.white(templateName)}`)));
4700
+ log.message(pc.dim(` ${templateDescription}`));
4235
4701
  }
4236
- config = await gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput);
4237
- }
4238
- await createProject(config, { manualDb: cliInput.manualDb ?? input.manualDb });
4239
- const reproducibleCommand = generateReproducibleCommand(config);
4240
- if (!isSilent()) log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
4241
- await trackProjectCreation(config, input.disableAnalytics);
4242
- const elapsedTimeMs = Date.now() - startTime;
4243
- if (!isSilent()) {
4244
- const elapsedTimeInSeconds = (elapsedTimeMs / 1e3).toFixed(2);
4245
- outro(pc.magenta(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
4702
+ const userOverrides = {};
4703
+ for (const [key, value] of Object.entries(originalInput)) if (value !== void 0) userOverrides[key] = value;
4704
+ cliInput = {
4705
+ ...templateConfig,
4706
+ ...userOverrides,
4707
+ template: input.template,
4708
+ projectDirectory: originalInput.projectDirectory
4709
+ };
4246
4710
  }
4247
- return {
4248
- success: true,
4249
- projectConfig: config,
4250
- reproducibleCommand,
4251
- timeScaffolded,
4252
- elapsedTimeMs,
4253
- projectDirectory: config.projectDir,
4254
- relativePath: config.relativePath
4711
+ }
4712
+ let config;
4713
+ if (cliInput.yes) {
4714
+ const flagConfigResult = processProvidedFlagsWithoutValidation(cliInput, finalBaseName);
4715
+ if (flagConfigResult.isErr()) return Result.err(new CLIError({
4716
+ message: flagConfigResult.error.message,
4717
+ cause: flagConfigResult.error
4718
+ }));
4719
+ const flagConfig = flagConfigResult.value;
4720
+ config = {
4721
+ ...getDefaultConfig(),
4722
+ ...flagConfig,
4723
+ projectName: finalBaseName,
4724
+ projectDir: finalResolvedPath,
4725
+ relativePath: finalPathInput
4255
4726
  };
4256
- } catch (error) {
4257
- if (error instanceof UserCancelledError) {
4258
- if (isSilent()) return {
4259
- success: false,
4260
- error: error.message,
4261
- projectConfig: {},
4262
- reproducibleCommand: "",
4263
- timeScaffolded,
4264
- elapsedTimeMs: Date.now() - startTime,
4265
- projectDirectory: "",
4266
- relativePath: ""
4267
- };
4268
- return;
4727
+ const validationResult = validateConfigCompatibility(config, providedFlags, cliInput);
4728
+ if (validationResult.isErr()) return Result.err(new CLIError({
4729
+ message: validationResult.error.message,
4730
+ cause: validationResult.error
4731
+ }));
4732
+ if (!isSilent()) {
4733
+ log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
4734
+ log.message(displayConfig(config));
4269
4735
  }
4270
- if (error instanceof CLIError) {
4271
- if (isSilent()) return {
4272
- success: false,
4273
- error: error.message,
4274
- projectConfig: {},
4275
- reproducibleCommand: "",
4276
- timeScaffolded,
4277
- elapsedTimeMs: Date.now() - startTime,
4278
- projectDirectory: "",
4279
- relativePath: ""
4280
- };
4281
- throw error;
4736
+ } else {
4737
+ const flagConfigResult = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
4738
+ if (flagConfigResult.isErr()) return Result.err(new CLIError({
4739
+ message: flagConfigResult.error.message,
4740
+ cause: flagConfigResult.error
4741
+ }));
4742
+ const flagConfig = flagConfigResult.value;
4743
+ const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig;
4744
+ if (!isSilent() && Object.keys(otherFlags).length > 0) {
4745
+ log.info(pc.yellow("Using these pre-selected options:"));
4746
+ log.message(displayConfig(otherFlags));
4747
+ log.message("");
4282
4748
  }
4283
- throw error;
4749
+ config = yield* Result.await(Result.tryPromise({
4750
+ try: async () => gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput),
4751
+ catch: (e) => {
4752
+ if (e instanceof UserCancelledError) return e;
4753
+ return new CLIError({
4754
+ message: e instanceof Error ? e.message : String(e),
4755
+ cause: e
4756
+ });
4757
+ }
4758
+ }));
4759
+ }
4760
+ yield* Result.await(createProject(config, { manualDb: cliInput.manualDb ?? input.manualDb }));
4761
+ const reproducibleCommand = generateReproducibleCommand(config);
4762
+ if (!isSilent()) log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
4763
+ await trackProjectCreation(config, input.disableAnalytics);
4764
+ const elapsedTimeMs = Date.now() - startTime;
4765
+ if (!isSilent()) {
4766
+ const elapsedTimeInSeconds = (elapsedTimeMs / 1e3).toFixed(2);
4767
+ outro(pc.magenta(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
4768
+ }
4769
+ return Result.ok({
4770
+ success: true,
4771
+ projectConfig: config,
4772
+ reproducibleCommand,
4773
+ timeScaffolded,
4774
+ elapsedTimeMs,
4775
+ projectDirectory: config.projectDir,
4776
+ relativePath: config.relativePath
4777
+ });
4778
+ });
4779
+ }
4780
+ async function handleDirectoryConflictResult(currentPathInput, strategy) {
4781
+ if (strategy) return handleDirectoryConflictProgrammatically(currentPathInput, strategy);
4782
+ return Result.tryPromise({
4783
+ try: async () => handleDirectoryConflict(currentPathInput),
4784
+ catch: (e) => {
4785
+ if (e instanceof UserCancelledError) return e;
4786
+ if (e instanceof CLIError) return e;
4787
+ return new CLIError({
4788
+ message: e instanceof Error ? e.message : String(e),
4789
+ cause: e
4790
+ });
4284
4791
  }
4285
4792
  });
4286
4793
  }
4287
4794
  async function handleDirectoryConflictProgrammatically(currentPathInput, strategy) {
4288
4795
  const currentPath = path.resolve(process.cwd(), currentPathInput);
4289
- if (!await fs.pathExists(currentPath)) return {
4796
+ if (!await fs.pathExists(currentPath)) return Result.ok({
4290
4797
  finalPathInput: currentPathInput,
4291
4798
  shouldClearDirectory: false
4292
- };
4293
- if (!((await fs.readdir(currentPath)).length > 0)) return {
4799
+ });
4800
+ if (!((await fs.readdir(currentPath)).length > 0)) return Result.ok({
4294
4801
  finalPathInput: currentPathInput,
4295
4802
  shouldClearDirectory: false
4296
- };
4803
+ });
4297
4804
  switch (strategy) {
4298
- case "overwrite": return {
4805
+ case "overwrite": return Result.ok({
4299
4806
  finalPathInput: currentPathInput,
4300
4807
  shouldClearDirectory: true
4301
- };
4302
- case "merge": return {
4808
+ });
4809
+ case "merge": return Result.ok({
4303
4810
  finalPathInput: currentPathInput,
4304
4811
  shouldClearDirectory: false
4305
- };
4812
+ });
4306
4813
  case "increment": {
4307
4814
  let counter = 1;
4308
4815
  const baseName = currentPathInput;
@@ -4311,13 +4818,13 @@ async function handleDirectoryConflictProgrammatically(currentPathInput, strateg
4311
4818
  counter++;
4312
4819
  finalPathInput = `${baseName}-${counter}`;
4313
4820
  }
4314
- return {
4821
+ return Result.ok({
4315
4822
  finalPathInput,
4316
4823
  shouldClearDirectory: false
4317
- };
4824
+ });
4318
4825
  }
4319
- case "error": throw new Error(`Directory "${currentPathInput}" already exists and is not empty. Use directoryConflict: "overwrite", "merge", or "increment" to handle this.`);
4320
- default: throw new Error(`Unknown directory conflict strategy: ${strategy}`);
4826
+ case "error": return Result.err(new DirectoryConflictError({ directory: currentPathInput }));
4827
+ default: return Result.err(new DirectoryConflictError({ directory: currentPathInput }));
4321
4828
  }
4322
4829
  }
4323
4830
 
@@ -4325,15 +4832,16 @@ async function handleDirectoryConflictProgrammatically(currentPathInput, strateg
4325
4832
  //#region src/utils/open-url.ts
4326
4833
  async function openUrl(url) {
4327
4834
  const platform = process.platform;
4328
- try {
4329
- if (platform === "darwin") await $({ stdio: "ignore" })`open ${url}`;
4330
- else if (platform === "win32") {
4331
- const escapedUrl = url.replace(/&/g, "^&");
4332
- await $({ stdio: "ignore" })`cmd /c start "" ${escapedUrl}`;
4333
- } else await $({ stdio: "ignore" })`xdg-open ${url}`;
4334
- } catch {
4335
- log.message(`Please open ${url} in your browser.`);
4336
- }
4835
+ if ((await Result.tryPromise({
4836
+ try: async () => {
4837
+ if (platform === "darwin") await $({ stdio: "ignore" })`open ${url}`;
4838
+ else if (platform === "win32") {
4839
+ const escapedUrl = url.replace(/&/g, "^&");
4840
+ await $({ stdio: "ignore" })`cmd /c start "" ${escapedUrl}`;
4841
+ } else await $({ stdio: "ignore" })`xdg-open ${url}`;
4842
+ },
4843
+ catch: () => void 0
4844
+ })).isErr()) log.message(`Please open ${url} in your browser.`);
4337
4845
  }
4338
4846
 
4339
4847
  //#endregion
@@ -4418,31 +4926,37 @@ const router = os.router({
4418
4926
  if (options.verbose) return result;
4419
4927
  }),
4420
4928
  sponsors: os.meta({ description: "Show Better-T-Stack sponsors" }).handler(async () => {
4421
- try {
4422
- renderTitle();
4423
- intro(pc.magenta("Better-T-Stack Sponsors"));
4424
- displaySponsors(await fetchSponsors());
4425
- } catch (error) {
4426
- handleError(error, "Failed to display sponsors");
4929
+ const result = await Result.tryPromise({
4930
+ try: async () => {
4931
+ renderTitle();
4932
+ intro(pc.magenta("Better-T-Stack Sponsors"));
4933
+ displaySponsors(await fetchSponsors());
4934
+ },
4935
+ catch: (e) => new CLIError({
4936
+ message: e instanceof Error ? e.message : "Failed to display sponsors",
4937
+ cause: e
4938
+ })
4939
+ });
4940
+ if (result.isErr()) {
4941
+ displayError(result.error);
4942
+ process.exit(1);
4427
4943
  }
4428
4944
  }),
4429
4945
  docs: os.meta({ description: "Open Better-T-Stack documentation" }).handler(async () => {
4430
4946
  const DOCS_URL = "https://better-t-stack.dev/docs";
4431
- try {
4432
- await openUrl(DOCS_URL);
4433
- log.success(pc.blue("Opened docs in your default browser."));
4434
- } catch {
4435
- log.message(`Please visit ${DOCS_URL}`);
4436
- }
4947
+ if ((await Result.tryPromise({
4948
+ try: () => openUrl(DOCS_URL),
4949
+ catch: () => null
4950
+ })).isOk()) log.success(pc.blue("Opened docs in your default browser."));
4951
+ else log.message(`Please visit ${DOCS_URL}`);
4437
4952
  }),
4438
4953
  builder: os.meta({ description: "Open the web-based stack builder" }).handler(async () => {
4439
4954
  const BUILDER_URL = "https://better-t-stack.dev/new";
4440
- try {
4441
- await openUrl(BUILDER_URL);
4442
- log.success(pc.blue("Opened builder in your default browser."));
4443
- } catch {
4444
- log.message(`Please visit ${BUILDER_URL}`);
4445
- }
4955
+ if ((await Result.tryPromise({
4956
+ try: () => openUrl(BUILDER_URL),
4957
+ catch: () => null
4958
+ })).isOk()) log.success(pc.blue("Opened builder in your default browser."));
4959
+ else log.message(`Please visit ${BUILDER_URL}`);
4446
4960
  })
4447
4961
  });
4448
4962
  const caller = createRouterClient(router, { context: {} });
@@ -4455,11 +4969,11 @@ function createBtsCli() {
4455
4969
  }
4456
4970
  /**
4457
4971
  * Programmatic API to create a new Better-T-Stack project.
4458
- * Returns pure JSON - no console output, no interactive prompts.
4972
+ * Returns a Result type - no console output, no interactive prompts.
4459
4973
  *
4460
4974
  * @example
4461
4975
  * ```typescript
4462
- * import { create } from "create-better-t-stack";
4976
+ * import { create, Result } from "create-better-t-stack";
4463
4977
  *
4464
4978
  * const result = await create("my-app", {
4465
4979
  * frontend: ["tanstack-router"],
@@ -4469,9 +4983,13 @@ function createBtsCli() {
4469
4983
  * orm: "drizzle",
4470
4984
  * });
4471
4985
  *
4472
- * if (result.success) {
4473
- * console.log(`Project created at: ${result.projectDirectory}`);
4474
- * }
4986
+ * result.match({
4987
+ * ok: (data) => console.log(`Project created at: ${data.projectDirectory}`),
4988
+ * err: (error) => console.error(`Failed: ${error.message}`),
4989
+ * });
4990
+ *
4991
+ * // Or use unwrapOr for a default value
4992
+ * const data = result.unwrapOr(null);
4475
4993
  * ```
4476
4994
  */
4477
4995
  async function create(projectName, options) {
@@ -4483,20 +5001,23 @@ async function create(projectName, options) {
4483
5001
  disableAnalytics: options?.disableAnalytics ?? true,
4484
5002
  directoryConflict: options?.directoryConflict ?? "error"
4485
5003
  };
4486
- try {
4487
- return await createProjectHandler(input, { silent: true });
4488
- } catch (error) {
4489
- return {
4490
- success: false,
4491
- error: error instanceof Error ? error.message : String(error),
4492
- projectConfig: {},
4493
- reproducibleCommand: "",
4494
- timeScaffolded: (/* @__PURE__ */ new Date()).toISOString(),
4495
- elapsedTimeMs: 0,
4496
- projectDirectory: "",
4497
- relativePath: ""
4498
- };
4499
- }
5004
+ return Result.tryPromise({
5005
+ try: async () => {
5006
+ const result = await createProjectHandler(input, { silent: true });
5007
+ if (!result) throw new UserCancelledError({ message: "Operation cancelled" });
5008
+ if (!result.success) throw new CLIError({ message: result.error || "Unknown error occurred" });
5009
+ return result;
5010
+ },
5011
+ catch: (e) => {
5012
+ if (e instanceof UserCancelledError) return e;
5013
+ if (e instanceof CLIError) return e;
5014
+ if (e instanceof ProjectCreationError) return e;
5015
+ return new CLIError({
5016
+ message: e instanceof Error ? e.message : String(e),
5017
+ cause: e
5018
+ });
5019
+ }
5020
+ });
4500
5021
  }
4501
5022
  async function sponsors() {
4502
5023
  return caller.sponsors();
@@ -4509,12 +5030,12 @@ async function builder() {
4509
5030
  }
4510
5031
  /**
4511
5032
  * Programmatic API to generate a project in-memory (virtual filesystem).
4512
- * Returns a VirtualFileTree without writing to disk.
5033
+ * Returns a Result with a VirtualFileTree without writing to disk.
4513
5034
  * Useful for web previews and testing.
4514
5035
  *
4515
5036
  * @example
4516
5037
  * ```typescript
4517
- * import { createVirtual, EMBEDDED_TEMPLATES } from "create-better-t-stack";
5038
+ * import { createVirtual, EMBEDDED_TEMPLATES, Result } from "create-better-t-stack";
4518
5039
  *
4519
5040
  * const result = await createVirtual({
4520
5041
  * frontend: ["tanstack-router"],
@@ -4524,52 +5045,38 @@ async function builder() {
4524
5045
  * orm: "drizzle",
4525
5046
  * });
4526
5047
  *
4527
- * if (result.success) {
4528
- * console.log(`Generated ${result.tree.fileCount} files`);
4529
- * }
5048
+ * result.match({
5049
+ * ok: (tree) => console.log(`Generated ${tree.fileCount} files`),
5050
+ * err: (error) => console.error(`Failed: ${error.message}`),
5051
+ * });
4530
5052
  * ```
4531
5053
  */
4532
5054
  async function createVirtual(options) {
4533
- try {
4534
- const result = await generateVirtualProject({
4535
- config: {
4536
- projectName: options.projectName || "my-project",
4537
- projectDir: "/virtual",
4538
- relativePath: "./virtual",
4539
- database: options.database || "none",
4540
- orm: options.orm || "none",
4541
- backend: options.backend || "hono",
4542
- runtime: options.runtime || "bun",
4543
- frontend: options.frontend || ["tanstack-router"],
4544
- addons: options.addons || [],
4545
- examples: options.examples || [],
4546
- auth: options.auth || "none",
4547
- payments: options.payments || "none",
4548
- git: options.git ?? false,
4549
- packageManager: options.packageManager || "bun",
4550
- install: false,
4551
- dbSetup: options.dbSetup || "none",
4552
- api: options.api || "trpc",
4553
- webDeploy: options.webDeploy || "none",
4554
- serverDeploy: options.serverDeploy || "none"
4555
- },
4556
- templates: EMBEDDED_TEMPLATES
4557
- });
4558
- if (result.success && result.tree) return {
4559
- success: true,
4560
- tree: result.tree
4561
- };
4562
- return {
4563
- success: false,
4564
- error: result.error || "Unknown error"
4565
- };
4566
- } catch (error) {
4567
- return {
4568
- success: false,
4569
- error: error instanceof Error ? error.message : String(error)
4570
- };
4571
- }
5055
+ return generate({
5056
+ config: {
5057
+ projectName: options.projectName || "my-project",
5058
+ projectDir: "/virtual",
5059
+ relativePath: "./virtual",
5060
+ database: options.database || "none",
5061
+ orm: options.orm || "none",
5062
+ backend: options.backend || "hono",
5063
+ runtime: options.runtime || "bun",
5064
+ frontend: options.frontend || ["tanstack-router"],
5065
+ addons: options.addons || [],
5066
+ examples: options.examples || [],
5067
+ auth: options.auth || "none",
5068
+ payments: options.payments || "none",
5069
+ git: options.git ?? false,
5070
+ packageManager: options.packageManager || "bun",
5071
+ install: false,
5072
+ dbSetup: options.dbSetup || "none",
5073
+ api: options.api || "trpc",
5074
+ webDeploy: options.webDeploy || "none",
5075
+ serverDeploy: options.serverDeploy || "none"
5076
+ },
5077
+ templates: EMBEDDED_TEMPLATES
5078
+ });
4572
5079
  }
4573
5080
 
4574
5081
  //#endregion
4575
- export { create as a, docs as c, sponsors as d, builder as i, generateVirtualProject$1 as l, TEMPLATE_COUNT as n, createBtsCli as o, VirtualFileSystem as r, createVirtual as s, EMBEDDED_TEMPLATES$1 as t, router as u };
5082
+ export { DirectoryConflictError as _, VirtualFileSystem as a, ValidationError as b, createBtsCli as c, generate$1 as d, router as f, DatabaseSetupError as g, CompatibilityError as h, TEMPLATE_COUNT as i, createVirtual as l, CLIError as m, GeneratorError$1 as n, builder as o, sponsors as p, Result$1 as r, create as s, EMBEDDED_TEMPLATES$1 as t, docs as u, ProjectCreationError as v, UserCancelledError as y };