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.
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +132 -95
- package/dist/index.mjs +2 -2
- package/dist/{src-D5L84uxt.mjs → src-R2RWz37j.mjs} +1993 -1486
- package/dist/virtual.d.mts +2 -2
- package/dist/virtual.mjs +2 -2
- package/package.json +12 -11
|
@@ -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,
|
|
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 {
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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)
|
|
211
|
-
if (native.length > 1)
|
|
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])))
|
|
225
|
-
if (native.length > 1)
|
|
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")
|
|
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")
|
|
232
|
-
if (providedFlags.has("backend") && config.backend && config.backend !== "hono" && config.runtime === "workers")
|
|
233
|
-
if (providedFlags.has("runtime") && options.runtime === "workers" && config.database === "mongodb")
|
|
234
|
-
if (providedFlags.has("runtime") && options.runtime === "workers" && config.dbSetup === "docker")
|
|
235
|
-
if (providedFlags.has("database") && config.database === "mongodb" && config.runtime === "workers")
|
|
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")
|
|
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)
|
|
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"))
|
|
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)
|
|
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")
|
|
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)
|
|
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")
|
|
321
|
-
if (api === "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"))
|
|
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)
|
|
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))
|
|
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))
|
|
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))
|
|
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))
|
|
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))
|
|
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))
|
|
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))
|
|
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))
|
|
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))
|
|
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))
|
|
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))
|
|
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))
|
|
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))
|
|
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))
|
|
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))
|
|
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))
|
|
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))
|
|
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))
|
|
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: () =>
|
|
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))
|
|
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
|
-
|
|
1483
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1494
|
-
|
|
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
|
-
|
|
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
|
|
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))
|
|
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":
|
|
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
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
1823
|
-
validateNoneExclusivity(options.
|
|
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")
|
|
1833
|
-
if (has("orm") && has("database") && orm === "drizzle" && db === "mongodb")
|
|
1834
|
-
if (has("database") && has("orm") && db === "mongodb" && orm && orm !== "mongoose" && orm !== "prisma" && orm !== "none")
|
|
1835
|
-
if (has("database") && has("orm") && db && db !== "none" && orm === "none")
|
|
1836
|
-
if (has("orm") && has("database") && orm && orm !== "none" && db === "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")
|
|
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")
|
|
1875
|
-
} else if (validation.database && database !== validation.database)
|
|
1876
|
-
if (validation.runtime && runtime !== validation.runtime)
|
|
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")
|
|
1879
|
-
if (runtime === "workers")
|
|
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")
|
|
1888
|
-
if (has("database") && config.database !== "none")
|
|
1889
|
-
if (has("orm") && config.orm !== "none")
|
|
1890
|
-
if (has("api") && config.api !== "none")
|
|
1891
|
-
if (has("dbSetup") && config.dbSetup !== "none")
|
|
1892
|
-
if (has("serverDeploy") && config.serverDeploy !== "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)))
|
|
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")
|
|
1910
|
-
if (has("database") && config.database !== "none")
|
|
1911
|
-
if (has("orm") && config.orm !== "none")
|
|
1912
|
-
if (has("api") && config.api !== "none")
|
|
1913
|
-
if (has("auth") && config.auth !== "none")
|
|
1914
|
-
if (has("payments") && config.payments !== "none")
|
|
1915
|
-
if (has("dbSetup") && config.dbSetup !== "none")
|
|
1916
|
-
if (has("serverDeploy") && config.serverDeploy !== "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")
|
|
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")
|
|
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)
|
|
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")
|
|
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)
|
|
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 (
|
|
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")
|
|
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
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1987
|
-
|
|
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)
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
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
|
|
2105
|
+
function extractAndValidateProjectName(projectName, projectDirectory) {
|
|
2003
2106
|
const derivedName = projectName || (projectDirectory ? path.basename(path.resolve(process.cwd(), projectDirectory)) : "");
|
|
2004
|
-
if (!derivedName) return "";
|
|
2005
|
-
const
|
|
2006
|
-
if (
|
|
2007
|
-
|
|
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)
|
|
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
|
|
2038
|
-
if (
|
|
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
|
|
2049
|
-
if (
|
|
2050
|
-
|
|
2051
|
-
|
|
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)
|
|
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
|
|
2057
|
-
if (
|
|
2058
|
-
|
|
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
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
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
|
-
|
|
2119
|
-
|
|
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
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
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
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
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
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
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
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
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
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
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
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
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
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
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
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
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
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
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
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
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
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
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
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
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
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
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
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
return
|
|
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
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
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
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
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
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
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
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
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
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
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
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
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
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
}
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
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
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
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
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
return
|
|
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
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
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
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
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
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
}
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
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
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
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
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
}
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
}
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
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
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
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
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
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
|
-
|
|
3589
|
-
}
|
|
3956
|
+
});
|
|
3590
3957
|
}
|
|
3591
3958
|
async function getTursoGroups() {
|
|
3592
3959
|
const s = spinner();
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
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
|
-
|
|
3602
|
-
|
|
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
|
|
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
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
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
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
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
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
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 {
|
|
4084
|
+
const { projectDir, backend } = config;
|
|
3684
4085
|
const manualDb = cliInput?.manualDb ?? false;
|
|
3685
4086
|
const setupSpinner = spinner();
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
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(
|
|
3706
|
-
if (
|
|
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
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
if (
|
|
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
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
if (
|
|
3728
|
-
|
|
3729
|
-
|
|
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
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
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)
|
|
3821
|
-
|
|
3822
|
-
|
|
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
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
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
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
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
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
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
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
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
|
|
4554
|
+
if (options.install) yield* Result.await(installDependencies({
|
|
4084
4555
|
projectDir,
|
|
4085
4556
|
packageManager: options.packageManager
|
|
4086
|
-
});
|
|
4087
|
-
await
|
|
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
|
-
}
|
|
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
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
}
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
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
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
if (
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
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
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
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
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
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.
|
|
4226
|
-
log.message(
|
|
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
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
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
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
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
|
-
|
|
4257
|
-
if (
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
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
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
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
|
-
|
|
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":
|
|
4320
|
-
default:
|
|
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
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
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
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
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
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
}
|
|
4435
|
-
|
|
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
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
}
|
|
4444
|
-
|
|
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
|
|
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
|
-
*
|
|
4473
|
-
* console.log(`Project created at: ${
|
|
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
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
success:
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
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
|
-
*
|
|
4528
|
-
* console.log(`Generated ${
|
|
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
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
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 {
|
|
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 };
|