create-better-t-stack 3.12.5 → 3.12.7-pr780.f6206a0
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 +13 -53
- package/dist/index.mjs +1 -1
- package/dist/{src-D3EHRs6z.mjs → src-CvDgQFF4.mjs} +711 -304
- package/package.json +3 -2
- package/templates/api/orpc/web/react/base/src/utils/orpc.ts.hbs +3 -3
- package/templates/auth/better-auth/convex/native/uniwind/components/sign-in.tsx.hbs +32 -50
- package/templates/auth/better-auth/convex/native/uniwind/components/sign-up.tsx.hbs +40 -57
- package/templates/auth/better-auth/native/uniwind/components/sign-in.tsx.hbs +32 -35
- package/templates/auth/better-auth/native/uniwind/components/sign-up.tsx.hbs +49 -37
- package/templates/auth/better-auth/web/nuxt/app/components/SignInForm.vue.hbs +40 -35
- package/templates/auth/better-auth/web/nuxt/app/components/SignUpForm.vue.hbs +47 -40
- package/templates/auth/better-auth/web/nuxt/app/middleware/auth.ts.hbs +9 -7
- package/templates/auth/better-auth/web/nuxt/app/pages/dashboard.vue.hbs +61 -29
- package/templates/auth/better-auth/web/nuxt/app/pages/login.vue.hbs +6 -3
- package/templates/backend/server/elysia/src/index.ts.hbs +8 -3
- package/templates/backend/server/express/src/index.ts.hbs +8 -3
- package/templates/backend/server/fastify/src/index.ts.hbs +8 -3
- package/templates/backend/server/hono/src/index.ts.hbs +16 -6
- package/templates/examples/ai/fullstack/next/src/app/api/ai/route.ts.hbs +8 -3
- package/templates/examples/ai/fullstack/tanstack-start/src/routes/api/ai/$.ts.hbs +8 -3
- package/templates/examples/ai/native/bare/polyfills.js +14 -11
- package/templates/examples/ai/native/uniwind/app/(drawer)/ai.tsx.hbs +104 -124
- package/templates/examples/ai/web/nuxt/app/pages/ai.vue.hbs +22 -22
- package/templates/examples/todo/native/uniwind/app/(drawer)/todos.tsx.hbs +67 -80
- package/templates/examples/todo/web/nuxt/app/pages/todos.vue.hbs +115 -90
- package/templates/frontend/native/uniwind/app/(drawer)/index.tsx.hbs +60 -54
- package/templates/frontend/native/uniwind/app/+not-found.tsx.hbs +16 -21
- package/templates/frontend/native/uniwind/app/modal.tsx.hbs +18 -34
- package/templates/frontend/native/uniwind/package.json.hbs +10 -10
- package/templates/frontend/nuxt/app/components/Header.vue.hbs +25 -29
- package/templates/frontend/nuxt/app/layouts/default.vue.hbs +7 -8
- package/templates/frontend/nuxt/app/pages/index.vue.hbs +58 -39
- package/templates/frontend/nuxt/package.json.hbs +1 -1
- package/templates/frontend/react/tanstack-router/package.json.hbs +1 -1
- package/templates/frontend/react/web-base/src/components/ui/skeleton.tsx.hbs +5 -5
- package/templates/frontend/nuxt/app/components/Loader.vue.hbs +0 -5
- package/templates/frontend/nuxt/app/components/ModeToggle.vue.hbs +0 -25
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as __reExport } from "./chunk-Dt3mZKp0.mjs";
|
|
3
|
-
import { autocompleteMultiselect, cancel, confirm, group,
|
|
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
5
|
import pc from "picocolors";
|
|
6
6
|
import { createCli } from "trpc-cli";
|
|
@@ -9,6 +9,8 @@ import consola, { consola as consola$1 } from "consola";
|
|
|
9
9
|
import fs from "fs-extra";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { ConfirmPrompt, GroupMultiSelectPrompt, MultiSelectPrompt, SelectPrompt, isCancel as isCancel$1 } from "@clack/core";
|
|
13
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
12
14
|
import gradient from "gradient-string";
|
|
13
15
|
import * as JSONC from "jsonc-parser";
|
|
14
16
|
import { $, execa } from "execa";
|
|
@@ -100,7 +102,7 @@ const dependencyVersionMap = {
|
|
|
100
102
|
husky: "^9.1.7",
|
|
101
103
|
"lint-staged": "^16.1.2",
|
|
102
104
|
tsx: "^4.19.2",
|
|
103
|
-
"@types/node": "^22.13.
|
|
105
|
+
"@types/node": "^22.13.14",
|
|
104
106
|
"@types/bun": "^1.3.4",
|
|
105
107
|
"@elysiajs/node": "^1.3.1",
|
|
106
108
|
"@elysiajs/cors": "^1.3.3",
|
|
@@ -116,13 +118,14 @@ const dependencyVersionMap = {
|
|
|
116
118
|
fastify: "^5.3.3",
|
|
117
119
|
"@fastify/cors": "^11.0.1",
|
|
118
120
|
turbo: "^2.6.3",
|
|
119
|
-
ai: "^
|
|
120
|
-
"@ai-sdk/google": "^
|
|
121
|
-
"@ai-sdk/vue": "^
|
|
122
|
-
"@ai-sdk/svelte": "^
|
|
123
|
-
"@ai-sdk/react": "^
|
|
121
|
+
ai: "^6.0.3",
|
|
122
|
+
"@ai-sdk/google": "^3.0.1",
|
|
123
|
+
"@ai-sdk/vue": "^3.0.3",
|
|
124
|
+
"@ai-sdk/svelte": "^4.0.3",
|
|
125
|
+
"@ai-sdk/react": "^3.0.3",
|
|
126
|
+
"@ai-sdk/devtools": "^0.0.2",
|
|
124
127
|
streamdown: "^1.6.10",
|
|
125
|
-
shiki: "^3.
|
|
128
|
+
shiki: "^3.20.0",
|
|
126
129
|
"@orpc/server": "^1.12.2",
|
|
127
130
|
"@orpc/client": "^1.12.2",
|
|
128
131
|
"@orpc/openapi": "^1.12.2",
|
|
@@ -214,18 +217,30 @@ const WEB_FRAMEWORKS = [
|
|
|
214
217
|
|
|
215
218
|
//#endregion
|
|
216
219
|
//#region src/utils/errors.ts
|
|
220
|
+
var UserCancelledError = class extends Error {
|
|
221
|
+
constructor(message = "Operation cancelled") {
|
|
222
|
+
super(message);
|
|
223
|
+
this.name = "UserCancelledError";
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
var CLIError = class extends Error {
|
|
227
|
+
constructor(message) {
|
|
228
|
+
super(message);
|
|
229
|
+
this.name = "CLIError";
|
|
230
|
+
}
|
|
231
|
+
};
|
|
217
232
|
function exitWithError(message) {
|
|
218
233
|
consola.error(pc.red(message));
|
|
219
|
-
throw new
|
|
234
|
+
throw new CLIError(message);
|
|
220
235
|
}
|
|
221
236
|
function exitCancelled(message = "Operation cancelled") {
|
|
222
237
|
cancel(pc.red(message));
|
|
223
|
-
throw new
|
|
238
|
+
throw new UserCancelledError(message);
|
|
224
239
|
}
|
|
225
240
|
function handleError(error, fallbackMessage) {
|
|
226
241
|
const message = error instanceof Error ? error.message : fallbackMessage || String(error);
|
|
227
242
|
consola.error(pc.red(message));
|
|
228
|
-
throw new Error(message);
|
|
243
|
+
throw error instanceof Error ? error : new Error(message);
|
|
229
244
|
}
|
|
230
245
|
|
|
231
246
|
//#endregion
|
|
@@ -292,8 +307,10 @@ function allowedApisForFrontends(frontends = []) {
|
|
|
292
307
|
if (includesNuxt || includesSvelte || includesSolid) return ["orpc", "none"];
|
|
293
308
|
return base;
|
|
294
309
|
}
|
|
295
|
-
function isExampleTodoAllowed(backend, database) {
|
|
296
|
-
|
|
310
|
+
function isExampleTodoAllowed(backend, database, api) {
|
|
311
|
+
if (backend === "convex") return true;
|
|
312
|
+
if (database === "none" || api === "none") return false;
|
|
313
|
+
return true;
|
|
297
314
|
}
|
|
298
315
|
function isExampleAIAllowed(backend, frontends = []) {
|
|
299
316
|
if (frontends.includes("solid")) return false;
|
|
@@ -343,10 +360,13 @@ function validatePaymentsCompatibility(payments, auth, _backend, frontends = [])
|
|
|
343
360
|
if (web.length === 0 && frontends.length > 0) exitWithError("Polar payments requires a web frontend or no frontend. Please select a web frontend or choose a different payments provider.");
|
|
344
361
|
}
|
|
345
362
|
}
|
|
346
|
-
function validateExamplesCompatibility(examples, backend, database, frontend) {
|
|
363
|
+
function validateExamplesCompatibility(examples, backend, database, frontend, api) {
|
|
347
364
|
const examplesArr = examples ?? [];
|
|
348
365
|
if (examplesArr.length === 0 || examplesArr.includes("none")) return;
|
|
349
|
-
if (examplesArr.includes("todo") && backend !== "convex"
|
|
366
|
+
if (examplesArr.includes("todo") && backend !== "convex") {
|
|
367
|
+
if (database === "none") exitWithError("The 'todo' example requires a database. Cannot use --examples todo when database is 'none'.");
|
|
368
|
+
if (api === "none") exitWithError("The 'todo' example requires an API layer (tRPC or oRPC). Cannot use --examples todo when api is 'none'.");
|
|
369
|
+
}
|
|
350
370
|
if (examplesArr.includes("ai") && (frontend ?? []).includes("solid")) exitWithError("The 'ai' example is not compatible with the Solid frontend.");
|
|
351
371
|
if (examplesArr.includes("ai") && backend === "convex") {
|
|
352
372
|
const frontendArr = frontend ?? [];
|
|
@@ -356,6 +376,295 @@ function validateExamplesCompatibility(examples, backend, database, frontend) {
|
|
|
356
376
|
}
|
|
357
377
|
}
|
|
358
378
|
|
|
379
|
+
//#endregion
|
|
380
|
+
//#region src/utils/context.ts
|
|
381
|
+
const cliStorage = new AsyncLocalStorage();
|
|
382
|
+
function defaultContext() {
|
|
383
|
+
return {
|
|
384
|
+
navigation: {
|
|
385
|
+
isFirstPrompt: false,
|
|
386
|
+
lastPromptShownUI: false
|
|
387
|
+
},
|
|
388
|
+
silent: false,
|
|
389
|
+
verbose: false
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function getContext() {
|
|
393
|
+
const ctx = cliStorage.getStore();
|
|
394
|
+
if (!ctx) return defaultContext();
|
|
395
|
+
return ctx;
|
|
396
|
+
}
|
|
397
|
+
function tryGetContext() {
|
|
398
|
+
return cliStorage.getStore();
|
|
399
|
+
}
|
|
400
|
+
function isSilent() {
|
|
401
|
+
return getContext().silent;
|
|
402
|
+
}
|
|
403
|
+
function isFirstPrompt() {
|
|
404
|
+
return getContext().navigation.isFirstPrompt;
|
|
405
|
+
}
|
|
406
|
+
function didLastPromptShowUI() {
|
|
407
|
+
return getContext().navigation.lastPromptShownUI;
|
|
408
|
+
}
|
|
409
|
+
function setIsFirstPrompt$1(value) {
|
|
410
|
+
const ctx = tryGetContext();
|
|
411
|
+
if (ctx) ctx.navigation.isFirstPrompt = value;
|
|
412
|
+
}
|
|
413
|
+
function setLastPromptShownUI(value) {
|
|
414
|
+
const ctx = tryGetContext();
|
|
415
|
+
if (ctx) ctx.navigation.lastPromptShownUI = value;
|
|
416
|
+
}
|
|
417
|
+
async function runWithContextAsync(options, fn) {
|
|
418
|
+
const ctx = {
|
|
419
|
+
navigation: {
|
|
420
|
+
isFirstPrompt: false,
|
|
421
|
+
lastPromptShownUI: false
|
|
422
|
+
},
|
|
423
|
+
silent: options.silent ?? false,
|
|
424
|
+
verbose: options.verbose ?? false,
|
|
425
|
+
projectDir: options.projectDir,
|
|
426
|
+
projectName: options.projectName,
|
|
427
|
+
packageManager: options.packageManager
|
|
428
|
+
};
|
|
429
|
+
return cliStorage.run(ctx, fn);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
//#endregion
|
|
433
|
+
//#region src/utils/navigation.ts
|
|
434
|
+
/**
|
|
435
|
+
* Navigation symbols and utilities for prompt navigation
|
|
436
|
+
*/
|
|
437
|
+
const GO_BACK_SYMBOL = Symbol("clack:goBack");
|
|
438
|
+
function isGoBack(value) {
|
|
439
|
+
return value === GO_BACK_SYMBOL;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
//#endregion
|
|
443
|
+
//#region src/prompts/navigable.ts
|
|
444
|
+
/**
|
|
445
|
+
* Navigable prompt wrappers using @clack/core
|
|
446
|
+
* These prompts return GO_BACK_SYMBOL when 'b' is pressed (instead of canceling)
|
|
447
|
+
*/
|
|
448
|
+
const unicode = process.platform !== "win32";
|
|
449
|
+
const S_STEP_ACTIVE = unicode ? "◆" : "*";
|
|
450
|
+
const S_STEP_CANCEL = unicode ? "■" : "x";
|
|
451
|
+
const S_STEP_ERROR = unicode ? "▲" : "x";
|
|
452
|
+
const S_STEP_SUBMIT = unicode ? "◇" : "o";
|
|
453
|
+
const S_BAR = unicode ? "│" : "|";
|
|
454
|
+
const S_BAR_END = unicode ? "└" : "—";
|
|
455
|
+
const S_RADIO_ACTIVE = unicode ? "●" : ">";
|
|
456
|
+
const S_RADIO_INACTIVE = unicode ? "○" : " ";
|
|
457
|
+
const S_CHECKBOX_ACTIVE = unicode ? "◻" : "[•]";
|
|
458
|
+
const S_CHECKBOX_SELECTED = unicode ? "◼" : "[+]";
|
|
459
|
+
const S_CHECKBOX_INACTIVE = unicode ? "◻" : "[ ]";
|
|
460
|
+
function symbol(state) {
|
|
461
|
+
switch (state) {
|
|
462
|
+
case "initial":
|
|
463
|
+
case "active": return pc.cyan(S_STEP_ACTIVE);
|
|
464
|
+
case "cancel": return pc.red(S_STEP_CANCEL);
|
|
465
|
+
case "error": return pc.yellow(S_STEP_ERROR);
|
|
466
|
+
case "submit": return pc.green(S_STEP_SUBMIT);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const KEYBOARD_HINT = pc.dim(`${pc.gray("↑/↓")} navigate • ${pc.gray("enter")} confirm • ${pc.gray("b")} back • ${pc.gray("ctrl+c")} cancel`);
|
|
470
|
+
const KEYBOARD_HINT_FIRST = pc.dim(`${pc.gray("↑/↓")} navigate • ${pc.gray("enter")} confirm • ${pc.gray("ctrl+c")} cancel`);
|
|
471
|
+
const KEYBOARD_HINT_MULTI = pc.dim(`${pc.gray("↑/↓")} navigate • ${pc.gray("space")} select • ${pc.gray("enter")} confirm • ${pc.gray("b")} back • ${pc.gray("ctrl+c")} cancel`);
|
|
472
|
+
const KEYBOARD_HINT_MULTI_FIRST = pc.dim(`${pc.gray("↑/↓")} navigate • ${pc.gray("space")} select • ${pc.gray("enter")} confirm • ${pc.gray("ctrl+c")} cancel`);
|
|
473
|
+
const setIsFirstPrompt = setIsFirstPrompt$1;
|
|
474
|
+
function getHint() {
|
|
475
|
+
return isFirstPrompt() ? KEYBOARD_HINT_FIRST : KEYBOARD_HINT;
|
|
476
|
+
}
|
|
477
|
+
function getMultiHint() {
|
|
478
|
+
return isFirstPrompt() ? KEYBOARD_HINT_MULTI_FIRST : KEYBOARD_HINT_MULTI;
|
|
479
|
+
}
|
|
480
|
+
async function runWithNavigation(prompt) {
|
|
481
|
+
let goBack = false;
|
|
482
|
+
prompt.on("key", (char) => {
|
|
483
|
+
if (char === "b" && !isFirstPrompt()) {
|
|
484
|
+
goBack = true;
|
|
485
|
+
prompt.state = "cancel";
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
setLastPromptShownUI(true);
|
|
489
|
+
const result = await prompt.prompt();
|
|
490
|
+
return goBack ? GO_BACK_SYMBOL : result;
|
|
491
|
+
}
|
|
492
|
+
async function navigableSelect(opts) {
|
|
493
|
+
const opt = (option, state) => {
|
|
494
|
+
const label = option.label ?? String(option.value);
|
|
495
|
+
switch (state) {
|
|
496
|
+
case "disabled": return `${pc.gray(S_RADIO_INACTIVE)} ${pc.gray(label)}${option.hint ? ` ${pc.dim(`(${option.hint ?? "disabled"})`)}` : ""}`;
|
|
497
|
+
case "selected": return `${pc.dim(label)}`;
|
|
498
|
+
case "active": return `${pc.green(S_RADIO_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
|
|
499
|
+
case "cancelled": return `${pc.strikethrough(pc.dim(label))}`;
|
|
500
|
+
default: return `${pc.dim(S_RADIO_INACTIVE)} ${pc.dim(label)}`;
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
return runWithNavigation(new SelectPrompt({
|
|
504
|
+
options: opts.options,
|
|
505
|
+
initialValue: opts.initialValue,
|
|
506
|
+
render() {
|
|
507
|
+
const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
|
|
508
|
+
switch (this.state) {
|
|
509
|
+
case "submit": return `${title}${pc.gray(S_BAR)} ${opt(this.options[this.cursor], "selected")}`;
|
|
510
|
+
case "cancel": return `${title}${pc.gray(S_BAR)} ${opt(this.options[this.cursor], "cancelled")}\n${pc.gray(S_BAR)}`;
|
|
511
|
+
default: {
|
|
512
|
+
const optionsText = this.options.map((option, i) => opt(option, option.disabled ? "disabled" : i === this.cursor ? "active" : "inactive")).join(`\n${pc.cyan(S_BAR)} `);
|
|
513
|
+
const hint = `\n${pc.gray(S_BAR)} ${getHint()}`;
|
|
514
|
+
return `${title}${pc.cyan(S_BAR)} ${optionsText}\n${pc.cyan(S_BAR_END)}${hint}\n`;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}));
|
|
519
|
+
}
|
|
520
|
+
async function navigableMultiselect(opts) {
|
|
521
|
+
const required = opts.required ?? true;
|
|
522
|
+
const opt = (option, state) => {
|
|
523
|
+
const label = option.label ?? String(option.value);
|
|
524
|
+
if (state === "disabled") return `${pc.gray(S_CHECKBOX_INACTIVE)} ${pc.strikethrough(pc.gray(label))}${option.hint ? ` ${pc.dim(`(${option.hint ?? "disabled"})`)}` : ""}`;
|
|
525
|
+
if (state === "active") return `${pc.cyan(S_CHECKBOX_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
|
|
526
|
+
if (state === "selected") return `${pc.green(S_CHECKBOX_SELECTED)} ${pc.dim(label)}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
|
|
527
|
+
if (state === "cancelled") return `${pc.strikethrough(pc.dim(label))}`;
|
|
528
|
+
if (state === "active-selected") return `${pc.green(S_CHECKBOX_SELECTED)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
|
|
529
|
+
if (state === "submitted") return `${pc.dim(label)}`;
|
|
530
|
+
return `${pc.dim(S_CHECKBOX_INACTIVE)} ${pc.dim(label)}`;
|
|
531
|
+
};
|
|
532
|
+
return runWithNavigation(new MultiSelectPrompt({
|
|
533
|
+
options: opts.options,
|
|
534
|
+
initialValues: opts.initialValues,
|
|
535
|
+
required,
|
|
536
|
+
validate(selected) {
|
|
537
|
+
if (required && (selected === void 0 || selected.length === 0)) return `Please select at least one option.\n${pc.reset(pc.dim(`Press ${pc.gray(pc.bgWhite(pc.inverse(" space ")))} to select, ${pc.gray(pc.bgWhite(pc.inverse(" enter ")))} to submit`))}`;
|
|
538
|
+
},
|
|
539
|
+
render() {
|
|
540
|
+
const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
|
|
541
|
+
const value = this.value ?? [];
|
|
542
|
+
const styleOption = (option, active) => {
|
|
543
|
+
if (option.disabled) return opt(option, "disabled");
|
|
544
|
+
const selected = value.includes(option.value);
|
|
545
|
+
if (active && selected) return opt(option, "active-selected");
|
|
546
|
+
if (selected) return opt(option, "selected");
|
|
547
|
+
return opt(option, active ? "active" : "inactive");
|
|
548
|
+
};
|
|
549
|
+
switch (this.state) {
|
|
550
|
+
case "submit": {
|
|
551
|
+
const submitText = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "submitted")).join(pc.dim(", ")) || pc.dim("none");
|
|
552
|
+
return `${title}${pc.gray(S_BAR)} ${submitText}`;
|
|
553
|
+
}
|
|
554
|
+
case "cancel": {
|
|
555
|
+
const label = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "cancelled")).join(pc.dim(", "));
|
|
556
|
+
return `${title}${pc.gray(S_BAR)} ${label}\n${pc.gray(S_BAR)}`;
|
|
557
|
+
}
|
|
558
|
+
case "error": {
|
|
559
|
+
const footer = this.error.split("\n").map((ln, i) => i === 0 ? `${pc.yellow(S_BAR_END)} ${pc.yellow(ln)}` : ` ${ln}`).join("\n");
|
|
560
|
+
const optionsText = this.options.map((option, i) => styleOption(option, i === this.cursor)).join(`\n${pc.yellow(S_BAR)} `);
|
|
561
|
+
return `${title}${pc.yellow(S_BAR)} ${optionsText}\n${footer}\n`;
|
|
562
|
+
}
|
|
563
|
+
default: {
|
|
564
|
+
const optionsText = this.options.map((option, i) => styleOption(option, i === this.cursor)).join(`\n${pc.cyan(S_BAR)} `);
|
|
565
|
+
const hint = `\n${pc.gray(S_BAR)} ${getMultiHint()}`;
|
|
566
|
+
return `${title}${pc.cyan(S_BAR)} ${optionsText}\n${pc.cyan(S_BAR_END)}${hint}\n`;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}));
|
|
571
|
+
}
|
|
572
|
+
async function navigableConfirm(opts) {
|
|
573
|
+
const active = opts.active ?? "Yes";
|
|
574
|
+
const inactive = opts.inactive ?? "No";
|
|
575
|
+
return runWithNavigation(new ConfirmPrompt({
|
|
576
|
+
active,
|
|
577
|
+
inactive,
|
|
578
|
+
initialValue: opts.initialValue ?? true,
|
|
579
|
+
render() {
|
|
580
|
+
const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
|
|
581
|
+
const value = this.value ? active : inactive;
|
|
582
|
+
switch (this.state) {
|
|
583
|
+
case "submit": return `${title}${pc.gray(S_BAR)} ${pc.dim(value)}`;
|
|
584
|
+
case "cancel": return `${title}${pc.gray(S_BAR)} ${pc.strikethrough(pc.dim(value))}\n${pc.gray(S_BAR)}`;
|
|
585
|
+
default: {
|
|
586
|
+
const hint = `\n${pc.gray(S_BAR)} ${getHint()}`;
|
|
587
|
+
return `${title}${pc.cyan(S_BAR)} ${this.value ? `${pc.green(S_RADIO_ACTIVE)} ${active}` : `${pc.dim(S_RADIO_INACTIVE)} ${pc.dim(active)}`} ${pc.dim("/")} ${!this.value ? `${pc.green(S_RADIO_ACTIVE)} ${inactive}` : `${pc.dim(S_RADIO_INACTIVE)} ${pc.dim(inactive)}`}\n${pc.cyan(S_BAR_END)}${hint}\n`;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}));
|
|
592
|
+
}
|
|
593
|
+
async function navigableGroupMultiselect(opts) {
|
|
594
|
+
const required = opts.required ?? true;
|
|
595
|
+
const opt = (option, state, options = []) => {
|
|
596
|
+
const label = option.label ?? String(option.value);
|
|
597
|
+
const isItem = typeof option.group === "string";
|
|
598
|
+
const next = isItem && (options[options.indexOf(option) + 1] ?? { group: true });
|
|
599
|
+
const isLast = isItem && next && next.group === true;
|
|
600
|
+
const prefix = isItem ? `${isLast ? S_BAR_END : S_BAR} ` : "";
|
|
601
|
+
if (state === "active") return `${pc.dim(prefix)}${pc.cyan(S_CHECKBOX_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
|
|
602
|
+
if (state === "group-active") return `${prefix}${pc.cyan(S_CHECKBOX_ACTIVE)} ${pc.dim(label)}`;
|
|
603
|
+
if (state === "group-active-selected") return `${prefix}${pc.green(S_CHECKBOX_SELECTED)} ${pc.dim(label)}`;
|
|
604
|
+
if (state === "selected") {
|
|
605
|
+
const selectedCheckbox = isItem ? pc.green(S_CHECKBOX_SELECTED) : "";
|
|
606
|
+
return `${pc.dim(prefix)}${selectedCheckbox} ${pc.dim(label)}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
|
|
607
|
+
}
|
|
608
|
+
if (state === "cancelled") return `${pc.strikethrough(pc.dim(label))}`;
|
|
609
|
+
if (state === "active-selected") return `${pc.dim(prefix)}${pc.green(S_CHECKBOX_SELECTED)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
|
|
610
|
+
if (state === "submitted") return `${pc.dim(label)}`;
|
|
611
|
+
const unselectedCheckbox = isItem ? pc.dim(S_CHECKBOX_INACTIVE) : "";
|
|
612
|
+
return `${pc.dim(prefix)}${unselectedCheckbox} ${pc.dim(label)}`;
|
|
613
|
+
};
|
|
614
|
+
return runWithNavigation(new GroupMultiSelectPrompt({
|
|
615
|
+
options: opts.options,
|
|
616
|
+
initialValues: opts.initialValues,
|
|
617
|
+
required,
|
|
618
|
+
selectableGroups: true,
|
|
619
|
+
validate(selected) {
|
|
620
|
+
if (required && (selected === void 0 || selected.length === 0)) return `Please select at least one option.\n${pc.reset(pc.dim(`Press ${pc.gray(pc.bgWhite(pc.inverse(" space ")))} to select, ${pc.gray(pc.bgWhite(pc.inverse(" enter ")))} to submit`))}`;
|
|
621
|
+
},
|
|
622
|
+
render() {
|
|
623
|
+
const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
|
|
624
|
+
const value = this.value ?? [];
|
|
625
|
+
switch (this.state) {
|
|
626
|
+
case "submit": {
|
|
627
|
+
const selectedOptions = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "submitted"));
|
|
628
|
+
const optionsText = selectedOptions.length === 0 ? "" : ` ${selectedOptions.join(pc.dim(", "))}`;
|
|
629
|
+
return `${title}${pc.gray(S_BAR)}${optionsText}`;
|
|
630
|
+
}
|
|
631
|
+
case "cancel": {
|
|
632
|
+
const label = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "cancelled")).join(pc.dim(", "));
|
|
633
|
+
return `${title}${pc.gray(S_BAR)} ${label.trim() ? `${label}\n${pc.gray(S_BAR)}` : ""}`;
|
|
634
|
+
}
|
|
635
|
+
case "error": {
|
|
636
|
+
const footer = this.error.split("\n").map((ln, i) => i === 0 ? `${pc.yellow(S_BAR_END)} ${pc.yellow(ln)}` : ` ${ln}`).join("\n");
|
|
637
|
+
const optionsText = this.options.map((option, i, options) => {
|
|
638
|
+
const selected = value.includes(option.value) || option.group === true && this.isGroupSelected(`${option.value}`);
|
|
639
|
+
const active = i === this.cursor;
|
|
640
|
+
if (!active && typeof option.group === "string" && this.options[this.cursor].value === option.group) return opt(option, selected ? "group-active-selected" : "group-active", options);
|
|
641
|
+
if (active && selected) return opt(option, "active-selected", options);
|
|
642
|
+
if (selected) return opt(option, "selected", options);
|
|
643
|
+
return opt(option, active ? "active" : "inactive", options);
|
|
644
|
+
}).join(`\n${pc.yellow(S_BAR)} `);
|
|
645
|
+
return `${title}${pc.yellow(S_BAR)} ${optionsText}\n${footer}\n`;
|
|
646
|
+
}
|
|
647
|
+
default: {
|
|
648
|
+
const optionsText = this.options.map((option, i, options) => {
|
|
649
|
+
const selected = value.includes(option.value) || option.group === true && this.isGroupSelected(`${option.value}`);
|
|
650
|
+
const active = i === this.cursor;
|
|
651
|
+
const groupActive = !active && typeof option.group === "string" && this.options[this.cursor].value === option.group;
|
|
652
|
+
let optionText = "";
|
|
653
|
+
if (groupActive) optionText = opt(option, selected ? "group-active-selected" : "group-active", options);
|
|
654
|
+
else if (active && selected) optionText = opt(option, "active-selected", options);
|
|
655
|
+
else if (selected) optionText = opt(option, "selected", options);
|
|
656
|
+
else optionText = opt(option, active ? "active" : "inactive", options);
|
|
657
|
+
return `${i !== 0 && !optionText.startsWith("\n") ? " " : ""}${optionText}`;
|
|
658
|
+
}).join(`\n${pc.cyan(S_BAR)}`);
|
|
659
|
+
const optionsPrefix = optionsText.startsWith("\n") ? "" : " ";
|
|
660
|
+
const hint = `\n${pc.gray(S_BAR)} ${getMultiHint()}`;
|
|
661
|
+
return `${title}${pc.cyan(S_BAR)}${optionsPrefix}${optionsText}\n${pc.cyan(S_BAR_END)}${hint}\n`;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}));
|
|
666
|
+
}
|
|
667
|
+
|
|
359
668
|
//#endregion
|
|
360
669
|
//#region src/prompts/addons.ts
|
|
361
670
|
function getAddonDisplay(addon) {
|
|
@@ -467,14 +776,13 @@ async function getAddonsChoice(addons, frontends, auth) {
|
|
|
467
776
|
});
|
|
468
777
|
}
|
|
469
778
|
});
|
|
470
|
-
const response = await
|
|
779
|
+
const response = await navigableGroupMultiselect({
|
|
471
780
|
message: "Select addons",
|
|
472
781
|
options: groupedOptions,
|
|
473
782
|
initialValues: DEFAULT_CONFIG.addons.filter((addonValue) => Object.values(groupedOptions).some((options) => options.some((opt) => opt.value === addonValue))),
|
|
474
|
-
required: false
|
|
475
|
-
selectableGroups: false
|
|
783
|
+
required: false
|
|
476
784
|
});
|
|
477
|
-
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
785
|
+
if (isCancel$1(response)) return exitCancelled("Operation cancelled");
|
|
478
786
|
return response;
|
|
479
787
|
}
|
|
480
788
|
async function getAddonsToAdd(frontend, existingAddons = [], auth) {
|
|
@@ -506,13 +814,12 @@ async function getAddonsToAdd(frontend, existingAddons = [], auth) {
|
|
|
506
814
|
}
|
|
507
815
|
});
|
|
508
816
|
if (Object.keys(groupedOptions).length === 0) return [];
|
|
509
|
-
const response = await
|
|
817
|
+
const response = await navigableGroupMultiselect({
|
|
510
818
|
message: "Select addons to add",
|
|
511
819
|
options: groupedOptions,
|
|
512
|
-
required: false
|
|
513
|
-
selectableGroups: false
|
|
820
|
+
required: false
|
|
514
821
|
});
|
|
515
|
-
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
822
|
+
if (isCancel$1(response)) return exitCancelled("Operation cancelled");
|
|
516
823
|
return response;
|
|
517
824
|
}
|
|
518
825
|
|
|
@@ -535,12 +842,12 @@ async function getApiChoice(Api, frontend, backend) {
|
|
|
535
842
|
label: "None",
|
|
536
843
|
hint: "No API layer (e.g. for full-stack frameworks like Next.js with Route Handlers)"
|
|
537
844
|
});
|
|
538
|
-
const apiType = await
|
|
845
|
+
const apiType = await navigableSelect({
|
|
539
846
|
message: "Select API type",
|
|
540
847
|
options: apiOptions,
|
|
541
848
|
initialValue: apiOptions[0].value
|
|
542
849
|
});
|
|
543
|
-
if (isCancel(apiType)) return exitCancelled("Operation cancelled");
|
|
850
|
+
if (isCancel$1(apiType)) return exitCancelled("Operation cancelled");
|
|
544
851
|
return apiType;
|
|
545
852
|
}
|
|
546
853
|
|
|
@@ -578,20 +885,21 @@ async function getAuthChoice(auth, backend, frontend) {
|
|
|
578
885
|
label: "Clerk",
|
|
579
886
|
hint: "More than auth, Complete User Management"
|
|
580
887
|
});
|
|
888
|
+
if (options.length === 0) return "none";
|
|
581
889
|
options.push({
|
|
582
890
|
value: "none",
|
|
583
891
|
label: "None",
|
|
584
892
|
hint: "No auth"
|
|
585
893
|
});
|
|
586
|
-
const response$1 = await
|
|
894
|
+
const response$1 = await navigableSelect({
|
|
587
895
|
message: "Select authentication provider",
|
|
588
896
|
options,
|
|
589
897
|
initialValue: "none"
|
|
590
898
|
});
|
|
591
|
-
if (isCancel(response$1)) return exitCancelled("Operation cancelled");
|
|
899
|
+
if (isCancel$1(response$1)) return exitCancelled("Operation cancelled");
|
|
592
900
|
return response$1;
|
|
593
901
|
}
|
|
594
|
-
const response = await
|
|
902
|
+
const response = await navigableSelect({
|
|
595
903
|
message: "Select authentication provider",
|
|
596
904
|
options: [{
|
|
597
905
|
value: "better-auth",
|
|
@@ -603,7 +911,7 @@ async function getAuthChoice(auth, backend, frontend) {
|
|
|
603
911
|
}],
|
|
604
912
|
initialValue: DEFAULT_CONFIG.auth
|
|
605
913
|
});
|
|
606
|
-
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
914
|
+
if (isCancel$1(response)) return exitCancelled("Operation cancelled");
|
|
607
915
|
return response;
|
|
608
916
|
}
|
|
609
917
|
|
|
@@ -647,12 +955,12 @@ async function getBackendFrameworkChoice(backendFramework, frontends) {
|
|
|
647
955
|
label: "None",
|
|
648
956
|
hint: "No backend server"
|
|
649
957
|
});
|
|
650
|
-
const response = await
|
|
958
|
+
const response = await navigableSelect({
|
|
651
959
|
message: "Select backend",
|
|
652
960
|
options: backendOptions,
|
|
653
961
|
initialValue: hasFullstackFrontend ? "self" : DEFAULT_CONFIG.backend
|
|
654
962
|
});
|
|
655
|
-
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
963
|
+
if (isCancel$1(response)) return exitCancelled("Operation cancelled");
|
|
656
964
|
return response;
|
|
657
965
|
}
|
|
658
966
|
|
|
@@ -688,12 +996,12 @@ async function getDatabaseChoice(database, backend, runtime) {
|
|
|
688
996
|
label: "MongoDB",
|
|
689
997
|
hint: "open-source NoSQL database that stores data in JSON-like documents called BSON"
|
|
690
998
|
});
|
|
691
|
-
const response = await
|
|
999
|
+
const response = await navigableSelect({
|
|
692
1000
|
message: "Select database",
|
|
693
1001
|
options: databaseOptions,
|
|
694
1002
|
initialValue: DEFAULT_CONFIG.database
|
|
695
1003
|
});
|
|
696
|
-
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1004
|
+
if (isCancel$1(response)) return exitCancelled("Operation cancelled");
|
|
697
1005
|
return response;
|
|
698
1006
|
}
|
|
699
1007
|
|
|
@@ -788,12 +1096,12 @@ async function getDBSetupChoice(databaseType, dbSetup, _orm, backend, runtime) {
|
|
|
788
1096
|
}
|
|
789
1097
|
];
|
|
790
1098
|
else return "none";
|
|
791
|
-
const response = await
|
|
1099
|
+
const response = await navigableSelect({
|
|
792
1100
|
message: `Select ${databaseType} setup option`,
|
|
793
1101
|
options,
|
|
794
1102
|
initialValue: "none"
|
|
795
1103
|
});
|
|
796
|
-
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1104
|
+
if (isCancel$1(response)) return exitCancelled("Operation cancelled");
|
|
797
1105
|
return response;
|
|
798
1106
|
}
|
|
799
1107
|
|
|
@@ -802,13 +1110,9 @@ async function getDBSetupChoice(databaseType, dbSetup, _orm, backend, runtime) {
|
|
|
802
1110
|
async function getExamplesChoice(examples, database, frontends, backend, api) {
|
|
803
1111
|
if (examples !== void 0) return examples;
|
|
804
1112
|
if (backend === "none") return [];
|
|
805
|
-
if (backend !== "convex") {
|
|
806
|
-
if (api === "none") return [];
|
|
807
|
-
if (database === "none") return [];
|
|
808
|
-
}
|
|
809
1113
|
let response = [];
|
|
810
1114
|
const options = [];
|
|
811
|
-
if (isExampleTodoAllowed(backend, database)) options.push({
|
|
1115
|
+
if (isExampleTodoAllowed(backend, database, api)) options.push({
|
|
812
1116
|
value: "todo",
|
|
813
1117
|
label: "Todo App",
|
|
814
1118
|
hint: "A simple CRUD example app"
|
|
@@ -819,13 +1123,13 @@ async function getExamplesChoice(examples, database, frontends, backend, api) {
|
|
|
819
1123
|
hint: "A simple AI chat interface using AI SDK"
|
|
820
1124
|
});
|
|
821
1125
|
if (options.length === 0) return [];
|
|
822
|
-
response = await
|
|
1126
|
+
response = await navigableMultiselect({
|
|
823
1127
|
message: "Include examples",
|
|
824
1128
|
options,
|
|
825
1129
|
required: false,
|
|
826
1130
|
initialValues: DEFAULT_CONFIG.examples?.filter((ex) => options.some((o) => o.value === ex))
|
|
827
1131
|
});
|
|
828
|
-
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1132
|
+
if (isCancel$1(response)) return exitCancelled("Operation cancelled");
|
|
829
1133
|
return response;
|
|
830
1134
|
}
|
|
831
1135
|
|
|
@@ -833,104 +1137,124 @@ async function getExamplesChoice(examples, database, frontends, backend, api) {
|
|
|
833
1137
|
//#region src/prompts/frontend.ts
|
|
834
1138
|
async function getFrontendChoice(frontendOptions, backend, auth) {
|
|
835
1139
|
if (frontendOptions !== void 0) return frontendOptions;
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
const result = [];
|
|
852
|
-
if (frontendTypes.includes("web")) {
|
|
853
|
-
const webFramework = await select({
|
|
854
|
-
message: "Choose web",
|
|
855
|
-
options: [
|
|
856
|
-
{
|
|
857
|
-
value: "tanstack-router",
|
|
858
|
-
label: "TanStack Router",
|
|
859
|
-
hint: "Modern and scalable routing for React Applications"
|
|
860
|
-
},
|
|
861
|
-
{
|
|
862
|
-
value: "react-router",
|
|
863
|
-
label: "React Router",
|
|
864
|
-
hint: "A user‑obsessed, standards‑focused, multi‑strategy router"
|
|
865
|
-
},
|
|
866
|
-
{
|
|
867
|
-
value: "next",
|
|
868
|
-
label: "Next.js",
|
|
869
|
-
hint: "The React Framework for the Web"
|
|
870
|
-
},
|
|
871
|
-
{
|
|
872
|
-
value: "nuxt",
|
|
873
|
-
label: "Nuxt",
|
|
874
|
-
hint: "The Progressive Web Framework for Vue.js"
|
|
875
|
-
},
|
|
876
|
-
{
|
|
877
|
-
value: "svelte",
|
|
878
|
-
label: "Svelte",
|
|
879
|
-
hint: "web development for the rest of us"
|
|
880
|
-
},
|
|
881
|
-
{
|
|
882
|
-
value: "solid",
|
|
883
|
-
label: "Solid",
|
|
884
|
-
hint: "Simple and performant reactivity for building user interfaces"
|
|
885
|
-
},
|
|
886
|
-
{
|
|
887
|
-
value: "tanstack-start",
|
|
888
|
-
label: "TanStack Start",
|
|
889
|
-
hint: "SSR, Server Functions, API Routes and more with TanStack Router"
|
|
890
|
-
}
|
|
891
|
-
].filter((option) => isFrontendAllowedWithBackend(option.value, backend, auth)),
|
|
892
|
-
initialValue: DEFAULT_CONFIG.frontend[0]
|
|
893
|
-
});
|
|
894
|
-
if (isCancel(webFramework)) return exitCancelled("Operation cancelled");
|
|
895
|
-
result.push(webFramework);
|
|
896
|
-
}
|
|
897
|
-
if (frontendTypes.includes("native")) {
|
|
898
|
-
const nativeFramework = await select({
|
|
899
|
-
message: "Choose native",
|
|
900
|
-
options: [
|
|
901
|
-
{
|
|
902
|
-
value: "native-bare",
|
|
903
|
-
label: "Bare",
|
|
904
|
-
hint: "Bare Expo without styling library"
|
|
905
|
-
},
|
|
906
|
-
{
|
|
907
|
-
value: "native-uniwind",
|
|
908
|
-
label: "Uniwind",
|
|
909
|
-
hint: "Fastest Tailwind bindings for React Native with HeroUI Native"
|
|
910
|
-
},
|
|
911
|
-
{
|
|
912
|
-
value: "native-unistyles",
|
|
913
|
-
label: "Unistyles",
|
|
914
|
-
hint: "Consistent styling for React Native"
|
|
915
|
-
}
|
|
916
|
-
],
|
|
917
|
-
initialValue: "native-bare"
|
|
1140
|
+
while (true) {
|
|
1141
|
+
const wasFirstPrompt = isFirstPrompt();
|
|
1142
|
+
const frontendTypes = await navigableMultiselect({
|
|
1143
|
+
message: "Select project type",
|
|
1144
|
+
options: [{
|
|
1145
|
+
value: "web",
|
|
1146
|
+
label: "Web",
|
|
1147
|
+
hint: "React, Vue or Svelte Web Application"
|
|
1148
|
+
}, {
|
|
1149
|
+
value: "native",
|
|
1150
|
+
label: "Native",
|
|
1151
|
+
hint: "Create a React Native/Expo app"
|
|
1152
|
+
}],
|
|
1153
|
+
required: false,
|
|
1154
|
+
initialValues: ["web"]
|
|
918
1155
|
});
|
|
919
|
-
if (
|
|
920
|
-
|
|
1156
|
+
if (isGoBack(frontendTypes)) return GO_BACK_SYMBOL;
|
|
1157
|
+
if (isCancel$1(frontendTypes)) return exitCancelled("Operation cancelled");
|
|
1158
|
+
setIsFirstPrompt(false);
|
|
1159
|
+
const result = [];
|
|
1160
|
+
let shouldRestart = false;
|
|
1161
|
+
if (frontendTypes.includes("web")) {
|
|
1162
|
+
const webFramework = await navigableSelect({
|
|
1163
|
+
message: "Choose web",
|
|
1164
|
+
options: [
|
|
1165
|
+
{
|
|
1166
|
+
value: "tanstack-router",
|
|
1167
|
+
label: "TanStack Router",
|
|
1168
|
+
hint: "Modern and scalable routing for React Applications"
|
|
1169
|
+
},
|
|
1170
|
+
{
|
|
1171
|
+
value: "react-router",
|
|
1172
|
+
label: "React Router",
|
|
1173
|
+
hint: "A user‑obsessed, standards‑focused, multi‑strategy router"
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
value: "next",
|
|
1177
|
+
label: "Next.js",
|
|
1178
|
+
hint: "The React Framework for the Web"
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
value: "nuxt",
|
|
1182
|
+
label: "Nuxt",
|
|
1183
|
+
hint: "The Progressive Web Framework for Vue.js"
|
|
1184
|
+
},
|
|
1185
|
+
{
|
|
1186
|
+
value: "svelte",
|
|
1187
|
+
label: "Svelte",
|
|
1188
|
+
hint: "web development for the rest of us"
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
value: "solid",
|
|
1192
|
+
label: "Solid",
|
|
1193
|
+
hint: "Simple and performant reactivity for building user interfaces"
|
|
1194
|
+
},
|
|
1195
|
+
{
|
|
1196
|
+
value: "tanstack-start",
|
|
1197
|
+
label: "TanStack Start",
|
|
1198
|
+
hint: "SSR, Server Functions, API Routes and more with TanStack Router"
|
|
1199
|
+
}
|
|
1200
|
+
].filter((option) => isFrontendAllowedWithBackend(option.value, backend, auth)),
|
|
1201
|
+
initialValue: DEFAULT_CONFIG.frontend[0]
|
|
1202
|
+
});
|
|
1203
|
+
if (isGoBack(webFramework)) shouldRestart = true;
|
|
1204
|
+
else if (isCancel$1(webFramework)) return exitCancelled("Operation cancelled");
|
|
1205
|
+
else result.push(webFramework);
|
|
1206
|
+
}
|
|
1207
|
+
if (shouldRestart) {
|
|
1208
|
+
setIsFirstPrompt(wasFirstPrompt);
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
if (frontendTypes.includes("native")) {
|
|
1212
|
+
const nativeFramework = await navigableSelect({
|
|
1213
|
+
message: "Choose native",
|
|
1214
|
+
options: [
|
|
1215
|
+
{
|
|
1216
|
+
value: "native-bare",
|
|
1217
|
+
label: "Bare",
|
|
1218
|
+
hint: "Bare Expo without styling library"
|
|
1219
|
+
},
|
|
1220
|
+
{
|
|
1221
|
+
value: "native-uniwind",
|
|
1222
|
+
label: "Uniwind",
|
|
1223
|
+
hint: "Fastest Tailwind bindings for React Native with HeroUI Native"
|
|
1224
|
+
},
|
|
1225
|
+
{
|
|
1226
|
+
value: "native-unistyles",
|
|
1227
|
+
label: "Unistyles",
|
|
1228
|
+
hint: "Consistent styling for React Native"
|
|
1229
|
+
}
|
|
1230
|
+
],
|
|
1231
|
+
initialValue: "native-bare"
|
|
1232
|
+
});
|
|
1233
|
+
if (isGoBack(nativeFramework)) if (frontendTypes.includes("web")) shouldRestart = true;
|
|
1234
|
+
else {
|
|
1235
|
+
setIsFirstPrompt(wasFirstPrompt);
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
else if (isCancel$1(nativeFramework)) return exitCancelled("Operation cancelled");
|
|
1239
|
+
else result.push(nativeFramework);
|
|
1240
|
+
}
|
|
1241
|
+
if (shouldRestart) {
|
|
1242
|
+
setIsFirstPrompt(wasFirstPrompt);
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
return result;
|
|
921
1246
|
}
|
|
922
|
-
return result;
|
|
923
1247
|
}
|
|
924
1248
|
|
|
925
1249
|
//#endregion
|
|
926
1250
|
//#region src/prompts/git.ts
|
|
927
1251
|
async function getGitChoice(git) {
|
|
928
1252
|
if (git !== void 0) return git;
|
|
929
|
-
const response = await
|
|
1253
|
+
const response = await navigableConfirm({
|
|
930
1254
|
message: "Initialize git repository?",
|
|
931
1255
|
initialValue: DEFAULT_CONFIG.git
|
|
932
1256
|
});
|
|
933
|
-
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1257
|
+
if (isCancel$1(response)) return exitCancelled("Operation cancelled");
|
|
934
1258
|
return response;
|
|
935
1259
|
}
|
|
936
1260
|
|
|
@@ -938,14 +1262,71 @@ async function getGitChoice(git) {
|
|
|
938
1262
|
//#region src/prompts/install.ts
|
|
939
1263
|
async function getinstallChoice(install) {
|
|
940
1264
|
if (install !== void 0) return install;
|
|
941
|
-
const response = await
|
|
1265
|
+
const response = await navigableConfirm({
|
|
942
1266
|
message: "Install dependencies?",
|
|
943
1267
|
initialValue: DEFAULT_CONFIG.install
|
|
944
1268
|
});
|
|
945
|
-
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1269
|
+
if (isCancel$1(response)) return exitCancelled("Operation cancelled");
|
|
946
1270
|
return response;
|
|
947
1271
|
}
|
|
948
1272
|
|
|
1273
|
+
//#endregion
|
|
1274
|
+
//#region src/prompts/navigable-group.ts
|
|
1275
|
+
/**
|
|
1276
|
+
* Navigable group - a group of prompts that allows going back
|
|
1277
|
+
*/
|
|
1278
|
+
/**
|
|
1279
|
+
* Define a group of prompts that supports going back to previous prompts.
|
|
1280
|
+
* Returns a result object with all the values, or handles cancel/go-back navigation.
|
|
1281
|
+
*/
|
|
1282
|
+
async function navigableGroup(prompts, opts) {
|
|
1283
|
+
const results = {};
|
|
1284
|
+
const promptNames = Object.keys(prompts);
|
|
1285
|
+
let currentIndex = 0;
|
|
1286
|
+
let goingBack = false;
|
|
1287
|
+
while (currentIndex < promptNames.length) {
|
|
1288
|
+
const name = promptNames[currentIndex];
|
|
1289
|
+
const prompt = prompts[name];
|
|
1290
|
+
setIsFirstPrompt$1(currentIndex === 0);
|
|
1291
|
+
setLastPromptShownUI(false);
|
|
1292
|
+
const result = await prompt({ results })?.catch((e) => {
|
|
1293
|
+
throw e;
|
|
1294
|
+
});
|
|
1295
|
+
if (isGoBack(result)) {
|
|
1296
|
+
goingBack = true;
|
|
1297
|
+
if (currentIndex > 0) {
|
|
1298
|
+
const prevName = promptNames[currentIndex - 1];
|
|
1299
|
+
delete results[prevName];
|
|
1300
|
+
currentIndex--;
|
|
1301
|
+
continue;
|
|
1302
|
+
}
|
|
1303
|
+
goingBack = false;
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
if (isCancel$1(result)) {
|
|
1307
|
+
if (typeof opts?.onCancel === "function") {
|
|
1308
|
+
results[name] = "canceled";
|
|
1309
|
+
opts.onCancel({ results });
|
|
1310
|
+
}
|
|
1311
|
+
setIsFirstPrompt$1(false);
|
|
1312
|
+
return results;
|
|
1313
|
+
}
|
|
1314
|
+
if (goingBack && !didLastPromptShowUI()) {
|
|
1315
|
+
if (currentIndex > 0) {
|
|
1316
|
+
const prevName = promptNames[currentIndex - 1];
|
|
1317
|
+
delete results[prevName];
|
|
1318
|
+
currentIndex--;
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
goingBack = false;
|
|
1323
|
+
results[name] = result;
|
|
1324
|
+
currentIndex++;
|
|
1325
|
+
}
|
|
1326
|
+
setIsFirstPrompt$1(false);
|
|
1327
|
+
return results;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
949
1330
|
//#endregion
|
|
950
1331
|
//#region src/prompts/orm.ts
|
|
951
1332
|
const ormOptions = {
|
|
@@ -969,12 +1350,12 @@ async function getORMChoice(orm, hasDatabase, database, backend, runtime) {
|
|
|
969
1350
|
if (backend === "convex") return "none";
|
|
970
1351
|
if (!hasDatabase) return "none";
|
|
971
1352
|
if (orm !== void 0) return orm;
|
|
972
|
-
const response = await
|
|
1353
|
+
const response = await navigableSelect({
|
|
973
1354
|
message: "Select ORM",
|
|
974
1355
|
options: database === "mongodb" ? [ormOptions.prisma, ormOptions.mongoose] : [ormOptions.drizzle, ormOptions.prisma],
|
|
975
1356
|
initialValue: database === "mongodb" ? "prisma" : runtime === "workers" ? "drizzle" : DEFAULT_CONFIG.orm
|
|
976
1357
|
});
|
|
977
|
-
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1358
|
+
if (isCancel$1(response)) return exitCancelled("Operation cancelled");
|
|
978
1359
|
return response;
|
|
979
1360
|
}
|
|
980
1361
|
|
|
@@ -982,13 +1363,13 @@ async function getORMChoice(orm, hasDatabase, database, backend, runtime) {
|
|
|
982
1363
|
//#region src/prompts/package-manager.ts
|
|
983
1364
|
async function getPackageManagerChoice(packageManager) {
|
|
984
1365
|
if (packageManager !== void 0) return packageManager;
|
|
985
|
-
const response = await
|
|
1366
|
+
const response = await navigableSelect({
|
|
986
1367
|
message: "Choose package manager",
|
|
987
1368
|
options: [
|
|
988
1369
|
{
|
|
989
1370
|
value: "npm",
|
|
990
1371
|
label: "npm",
|
|
991
|
-
hint: "
|
|
1372
|
+
hint: "not recommended"
|
|
992
1373
|
},
|
|
993
1374
|
{
|
|
994
1375
|
value: "pnpm",
|
|
@@ -1003,7 +1384,7 @@ async function getPackageManagerChoice(packageManager) {
|
|
|
1003
1384
|
],
|
|
1004
1385
|
initialValue: getUserPkgManager()
|
|
1005
1386
|
});
|
|
1006
|
-
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1387
|
+
if (isCancel$1(response)) return exitCancelled("Operation cancelled");
|
|
1007
1388
|
return response;
|
|
1008
1389
|
}
|
|
1009
1390
|
|
|
@@ -1013,7 +1394,7 @@ async function getPaymentsChoice(payments, auth, backend, frontends) {
|
|
|
1013
1394
|
if (payments !== void 0) return payments;
|
|
1014
1395
|
if (backend === "none") return "none";
|
|
1015
1396
|
if (!(auth === "better-auth" && backend !== "convex" && (frontends?.length === 0 || splitFrontends(frontends).web.length > 0))) return "none";
|
|
1016
|
-
const response = await
|
|
1397
|
+
const response = await navigableSelect({
|
|
1017
1398
|
message: "Select payments provider",
|
|
1018
1399
|
options: [{
|
|
1019
1400
|
value: "polar",
|
|
@@ -1026,7 +1407,7 @@ async function getPaymentsChoice(payments, auth, backend, frontends) {
|
|
|
1026
1407
|
}],
|
|
1027
1408
|
initialValue: DEFAULT_CONFIG.payments
|
|
1028
1409
|
});
|
|
1029
|
-
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1410
|
+
if (isCancel$1(response)) return exitCancelled("Operation cancelled");
|
|
1030
1411
|
return response;
|
|
1031
1412
|
}
|
|
1032
1413
|
|
|
@@ -1049,12 +1430,12 @@ async function getRuntimeChoice(runtime, backend) {
|
|
|
1049
1430
|
label: "Cloudflare Workers",
|
|
1050
1431
|
hint: "Edge runtime on Cloudflare's global network"
|
|
1051
1432
|
});
|
|
1052
|
-
const response = await
|
|
1433
|
+
const response = await navigableSelect({
|
|
1053
1434
|
message: "Select runtime",
|
|
1054
1435
|
options: runtimeOptions,
|
|
1055
1436
|
initialValue: DEFAULT_CONFIG.runtime
|
|
1056
1437
|
});
|
|
1057
|
-
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1438
|
+
if (isCancel$1(response)) return exitCancelled("Operation cancelled");
|
|
1058
1439
|
return response;
|
|
1059
1440
|
}
|
|
1060
1441
|
|
|
@@ -1092,12 +1473,12 @@ async function getServerDeploymentToAdd(runtime, existingDeployment, backend) {
|
|
|
1092
1473
|
}
|
|
1093
1474
|
if (existingDeployment && existingDeployment !== "none") return "none";
|
|
1094
1475
|
if (options.length === 0) return "none";
|
|
1095
|
-
const response = await
|
|
1476
|
+
const response = await navigableSelect({
|
|
1096
1477
|
message: "Select server deployment",
|
|
1097
1478
|
options,
|
|
1098
1479
|
initialValue: DEFAULT_CONFIG.serverDeploy
|
|
1099
1480
|
});
|
|
1100
|
-
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1481
|
+
if (isCancel$1(response)) return exitCancelled("Operation cancelled");
|
|
1101
1482
|
return response;
|
|
1102
1483
|
}
|
|
1103
1484
|
|
|
@@ -1119,7 +1500,7 @@ function getDeploymentDisplay(deployment) {
|
|
|
1119
1500
|
async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []) {
|
|
1120
1501
|
if (deployment !== void 0) return deployment;
|
|
1121
1502
|
if (!hasWebFrontend(frontend)) return "none";
|
|
1122
|
-
const response = await
|
|
1503
|
+
const response = await navigableSelect({
|
|
1123
1504
|
message: "Select web deployment",
|
|
1124
1505
|
options: ["cloudflare", "none"].map((deploy) => {
|
|
1125
1506
|
const { label, hint } = getDeploymentDisplay(deploy);
|
|
@@ -1131,7 +1512,7 @@ async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []
|
|
|
1131
1512
|
}),
|
|
1132
1513
|
initialValue: DEFAULT_CONFIG.webDeploy
|
|
1133
1514
|
});
|
|
1134
|
-
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1515
|
+
if (isCancel$1(response)) return exitCancelled("Operation cancelled");
|
|
1135
1516
|
return response;
|
|
1136
1517
|
}
|
|
1137
1518
|
async function getDeploymentToAdd(frontend, existingDeployment) {
|
|
@@ -1152,19 +1533,19 @@ async function getDeploymentToAdd(frontend, existingDeployment) {
|
|
|
1152
1533
|
hint: "Skip deployment setup"
|
|
1153
1534
|
});
|
|
1154
1535
|
if (options.length === 0) return "none";
|
|
1155
|
-
const response = await
|
|
1536
|
+
const response = await navigableSelect({
|
|
1156
1537
|
message: "Select web deployment",
|
|
1157
1538
|
options,
|
|
1158
1539
|
initialValue: DEFAULT_CONFIG.webDeploy
|
|
1159
1540
|
});
|
|
1160
|
-
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1541
|
+
if (isCancel$1(response)) return exitCancelled("Operation cancelled");
|
|
1161
1542
|
return response;
|
|
1162
1543
|
}
|
|
1163
1544
|
|
|
1164
1545
|
//#endregion
|
|
1165
1546
|
//#region src/prompts/config-prompts.ts
|
|
1166
1547
|
async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
1167
|
-
const result = await
|
|
1548
|
+
const result = await navigableGroup({
|
|
1168
1549
|
frontend: () => getFrontendChoice(flags.frontend, flags.backend, flags.auth),
|
|
1169
1550
|
backend: ({ results }) => getBackendFrameworkChoice(flags.backend, results.frontend),
|
|
1170
1551
|
runtime: ({ results }) => getRuntimeChoice(flags.runtime, results.backend),
|
|
@@ -1272,7 +1653,7 @@ const getLatestCLIVersion = () => {
|
|
|
1272
1653
|
*/
|
|
1273
1654
|
function isTelemetryEnabled() {
|
|
1274
1655
|
const BTS_TELEMETRY_DISABLED = process.env.BTS_TELEMETRY_DISABLED;
|
|
1275
|
-
const BTS_TELEMETRY = "
|
|
1656
|
+
const BTS_TELEMETRY = "0";
|
|
1276
1657
|
if (BTS_TELEMETRY_DISABLED !== void 0) return BTS_TELEMETRY_DISABLED !== "1";
|
|
1277
1658
|
if (BTS_TELEMETRY !== void 0) return BTS_TELEMETRY === "1";
|
|
1278
1659
|
return true;
|
|
@@ -1280,16 +1661,7 @@ function isTelemetryEnabled() {
|
|
|
1280
1661
|
|
|
1281
1662
|
//#endregion
|
|
1282
1663
|
//#region src/utils/analytics.ts
|
|
1283
|
-
|
|
1284
|
-
async function sendConvexEvent(payload) {
|
|
1285
|
-
try {
|
|
1286
|
-
await fetch(CONVEX_INGEST_URL, {
|
|
1287
|
-
method: "POST",
|
|
1288
|
-
headers: { "Content-Type": "application/json" },
|
|
1289
|
-
body: JSON.stringify(payload)
|
|
1290
|
-
});
|
|
1291
|
-
} catch {}
|
|
1292
|
-
}
|
|
1664
|
+
async function sendConvexEvent(payload) {}
|
|
1293
1665
|
async function trackProjectCreation(config, disableAnalytics = false) {
|
|
1294
1666
|
if (!isTelemetryEnabled() || disableAnalytics) return;
|
|
1295
1667
|
const { projectName: _projectName, projectDir: _projectDir, relativePath: _relativePath, ...safeConfig } = config;
|
|
@@ -1380,14 +1752,14 @@ function generateReproducibleCommand(config) {
|
|
|
1380
1752
|
|
|
1381
1753
|
//#endregion
|
|
1382
1754
|
//#region src/utils/project-directory.ts
|
|
1383
|
-
async function handleDirectoryConflict(currentPathInput
|
|
1755
|
+
async function handleDirectoryConflict(currentPathInput) {
|
|
1384
1756
|
while (true) {
|
|
1385
1757
|
const resolvedPath = path.resolve(process.cwd(), currentPathInput);
|
|
1386
1758
|
if (!(await fs.pathExists(resolvedPath) && (await fs.readdir(resolvedPath)).length > 0)) return {
|
|
1387
1759
|
finalPathInput: currentPathInput,
|
|
1388
1760
|
shouldClearDirectory: false
|
|
1389
1761
|
};
|
|
1390
|
-
if (
|
|
1762
|
+
if (isSilent()) throw new Error(`Directory "${currentPathInput}" already exists and is not empty. In silent mode, please provide a different project name or clear the directory manually.`);
|
|
1391
1763
|
log.warn(`Directory "${pc.yellow(currentPathInput)}" already exists and is not empty.`);
|
|
1392
1764
|
const action = await select({
|
|
1393
1765
|
message: "What would you like to do?",
|
|
@@ -1494,11 +1866,7 @@ const catppuccinTheme = {
|
|
|
1494
1866
|
const renderTitle = () => {
|
|
1495
1867
|
const terminalWidth = process.stdout.columns || 80;
|
|
1496
1868
|
const titleLines = TITLE_TEXT.split("\n");
|
|
1497
|
-
if (terminalWidth < Math.max(...titleLines.map((line) => line.length))) console.log(gradient(Object.values(catppuccinTheme)).multiline(`
|
|
1498
|
-
╔══════════════════╗
|
|
1499
|
-
║ Better T Stack ║
|
|
1500
|
-
╚══════════════════╝
|
|
1501
|
-
`));
|
|
1869
|
+
if (terminalWidth < Math.max(...titleLines.map((line) => line.length))) console.log(gradient(Object.values(catppuccinTheme)).multiline(`Better T Stack`));
|
|
1502
1870
|
else console.log(gradient(Object.values(catppuccinTheme)).multiline(TITLE_TEXT));
|
|
1503
1871
|
};
|
|
1504
1872
|
|
|
@@ -1758,7 +2126,7 @@ function validateFrontendConstraints(config, providedFlags) {
|
|
|
1758
2126
|
}
|
|
1759
2127
|
function validateApiConstraints(config, options) {
|
|
1760
2128
|
if (config.api === "none") {
|
|
1761
|
-
if (options.examples
|
|
2129
|
+
if (options.examples?.includes("todo") && options.backend !== "convex" && options.backend !== "none") exitWithError("Cannot use '--examples todo' when '--api' is set to 'none'. The todo example requires an API layer. Please remove 'todo' from --examples or choose an API type.");
|
|
1762
2130
|
}
|
|
1763
2131
|
}
|
|
1764
2132
|
function validateFullConfig(config, providedFlags, options) {
|
|
@@ -1779,7 +2147,7 @@ function validateFullConfig(config, providedFlags, options) {
|
|
|
1779
2147
|
validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth);
|
|
1780
2148
|
config.addons = [...new Set(config.addons)];
|
|
1781
2149
|
}
|
|
1782
|
-
validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? []);
|
|
2150
|
+
validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
|
|
1783
2151
|
validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend ?? []);
|
|
1784
2152
|
}
|
|
1785
2153
|
function validateConfigForProgrammaticUse(config) {
|
|
@@ -1789,7 +2157,7 @@ function validateConfigForProgrammaticUse(config) {
|
|
|
1789
2157
|
validateApiFrontendCompatibility(config.api, config.frontend);
|
|
1790
2158
|
validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend);
|
|
1791
2159
|
if (config.addons && config.addons.length > 0) validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth);
|
|
1792
|
-
validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? []);
|
|
2160
|
+
validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
|
|
1793
2161
|
} catch (error) {
|
|
1794
2162
|
if (error instanceof Error) throw error;
|
|
1795
2163
|
throw new Error(String(error));
|
|
@@ -4007,11 +4375,19 @@ async function setupAIDependencies(config) {
|
|
|
4007
4375
|
projectDir: convexBackendDir
|
|
4008
4376
|
});
|
|
4009
4377
|
else if (backend === "self" && webClientDirExists) await addPackageDependency({
|
|
4010
|
-
dependencies: [
|
|
4378
|
+
dependencies: [
|
|
4379
|
+
"ai",
|
|
4380
|
+
"@ai-sdk/google",
|
|
4381
|
+
"@ai-sdk/devtools"
|
|
4382
|
+
],
|
|
4011
4383
|
projectDir: webClientDir
|
|
4012
4384
|
});
|
|
4013
4385
|
else if (serverDirExists && backend !== "none") await addPackageDependency({
|
|
4014
|
-
dependencies: [
|
|
4386
|
+
dependencies: [
|
|
4387
|
+
"ai",
|
|
4388
|
+
"@ai-sdk/google",
|
|
4389
|
+
"@ai-sdk/devtools"
|
|
4390
|
+
],
|
|
4015
4391
|
projectDir: serverDir
|
|
4016
4392
|
});
|
|
4017
4393
|
if (webClientDirExists) {
|
|
@@ -6788,7 +7164,6 @@ async function updateConvexPackageJson(projectDir, options) {
|
|
|
6788
7164
|
//#endregion
|
|
6789
7165
|
//#region src/helpers/core/create-project.ts
|
|
6790
7166
|
async function createProject(options, cliInput = {}) {
|
|
6791
|
-
const { silent = false } = cliInput;
|
|
6792
7167
|
const projectDir = options.projectDir;
|
|
6793
7168
|
const isConvex = options.backend === "convex";
|
|
6794
7169
|
const isSelfBackend = options.backend === "self";
|
|
@@ -6824,23 +7199,23 @@ async function createProject(options, cliInput = {}) {
|
|
|
6824
7199
|
await setupCatalogs(projectDir, options);
|
|
6825
7200
|
await createReadme(projectDir, options);
|
|
6826
7201
|
await writeBtsConfig(options);
|
|
6827
|
-
if (!
|
|
7202
|
+
if (!isSilent()) log.success("Project template successfully scaffolded!");
|
|
6828
7203
|
if (options.install) await installDependencies({
|
|
6829
7204
|
projectDir,
|
|
6830
7205
|
packageManager: options.packageManager
|
|
6831
7206
|
});
|
|
6832
7207
|
await initializeGit(projectDir, options.git);
|
|
6833
|
-
if (!
|
|
7208
|
+
if (!isSilent()) await displayPostInstallInstructions({
|
|
6834
7209
|
...options,
|
|
6835
7210
|
depsInstalled: options.install
|
|
6836
7211
|
});
|
|
6837
7212
|
return projectDir;
|
|
6838
7213
|
} catch (error) {
|
|
6839
7214
|
if (error instanceof Error) {
|
|
6840
|
-
if (!
|
|
7215
|
+
if (!isSilent()) console.error(error.stack);
|
|
6841
7216
|
exitWithError(`Error during project creation: ${error.message}`);
|
|
6842
7217
|
} else {
|
|
6843
|
-
if (!
|
|
7218
|
+
if (!isSilent()) console.error(error);
|
|
6844
7219
|
exitWithError(`An unexpected error occurred: ${String(error)}`);
|
|
6845
7220
|
}
|
|
6846
7221
|
}
|
|
@@ -6850,139 +7225,169 @@ async function createProject(options, cliInput = {}) {
|
|
|
6850
7225
|
//#region src/helpers/core/command-handlers.ts
|
|
6851
7226
|
async function createProjectHandler(input, options = {}) {
|
|
6852
7227
|
const { silent = false } = options;
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
...input,
|
|
6917
|
-
projectDirectory: input.projectName
|
|
6918
|
-
};
|
|
6919
|
-
const providedFlags = getProvidedFlags(originalInput);
|
|
6920
|
-
let cliInput = originalInput;
|
|
6921
|
-
if (input.template && input.template !== "none") {
|
|
6922
|
-
const templateConfig = getTemplateConfig(input.template);
|
|
6923
|
-
if (templateConfig) {
|
|
6924
|
-
const templateName = input.template.toUpperCase();
|
|
6925
|
-
const templateDescription = getTemplateDescription(input.template);
|
|
6926
|
-
if (!silent) {
|
|
6927
|
-
log.message(pc.bold(pc.cyan(`Using template: ${pc.white(templateName)}`)));
|
|
6928
|
-
log.message(pc.dim(` ${templateDescription}`));
|
|
7228
|
+
return runWithContextAsync({ silent }, async () => {
|
|
7229
|
+
const startTime = Date.now();
|
|
7230
|
+
const timeScaffolded = (/* @__PURE__ */ new Date()).toISOString();
|
|
7231
|
+
try {
|
|
7232
|
+
if (!isSilent() && input.renderTitle !== false) renderTitle();
|
|
7233
|
+
if (!isSilent()) intro(pc.magenta("Creating a new Better-T-Stack project"));
|
|
7234
|
+
if (!isSilent() && input.yolo) consola.fatal("YOLO mode enabled - skipping checks. Things may break!");
|
|
7235
|
+
let currentPathInput;
|
|
7236
|
+
if (input.yes && input.projectName) currentPathInput = input.projectName;
|
|
7237
|
+
else if (input.yes) {
|
|
7238
|
+
const defaultConfig = getDefaultConfig();
|
|
7239
|
+
let defaultName = defaultConfig.relativePath;
|
|
7240
|
+
let counter = 1;
|
|
7241
|
+
while (await fs.pathExists(path.resolve(process.cwd(), defaultName)) && (await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0) {
|
|
7242
|
+
defaultName = `${defaultConfig.projectName}-${counter}`;
|
|
7243
|
+
counter++;
|
|
7244
|
+
}
|
|
7245
|
+
currentPathInput = defaultName;
|
|
7246
|
+
} else currentPathInput = await getProjectName(input.projectName);
|
|
7247
|
+
let finalPathInput;
|
|
7248
|
+
let shouldClearDirectory;
|
|
7249
|
+
try {
|
|
7250
|
+
if (input.directoryConflict) {
|
|
7251
|
+
const result = await handleDirectoryConflictProgrammatically(currentPathInput, input.directoryConflict);
|
|
7252
|
+
finalPathInput = result.finalPathInput;
|
|
7253
|
+
shouldClearDirectory = result.shouldClearDirectory;
|
|
7254
|
+
} else {
|
|
7255
|
+
const result = await handleDirectoryConflict(currentPathInput);
|
|
7256
|
+
finalPathInput = result.finalPathInput;
|
|
7257
|
+
shouldClearDirectory = result.shouldClearDirectory;
|
|
7258
|
+
}
|
|
7259
|
+
} catch (error) {
|
|
7260
|
+
if (error instanceof UserCancelledError || error instanceof CLIError) throw error;
|
|
7261
|
+
return {
|
|
7262
|
+
success: false,
|
|
7263
|
+
projectConfig: {
|
|
7264
|
+
projectName: "",
|
|
7265
|
+
projectDir: "",
|
|
7266
|
+
relativePath: "",
|
|
7267
|
+
database: "none",
|
|
7268
|
+
orm: "none",
|
|
7269
|
+
backend: "none",
|
|
7270
|
+
runtime: "none",
|
|
7271
|
+
frontend: [],
|
|
7272
|
+
addons: [],
|
|
7273
|
+
examples: [],
|
|
7274
|
+
auth: "none",
|
|
7275
|
+
payments: "none",
|
|
7276
|
+
git: false,
|
|
7277
|
+
packageManager: "npm",
|
|
7278
|
+
install: false,
|
|
7279
|
+
dbSetup: "none",
|
|
7280
|
+
api: "none",
|
|
7281
|
+
webDeploy: "none",
|
|
7282
|
+
serverDeploy: "none"
|
|
7283
|
+
},
|
|
7284
|
+
reproducibleCommand: "",
|
|
7285
|
+
timeScaffolded,
|
|
7286
|
+
elapsedTimeMs: Date.now() - startTime,
|
|
7287
|
+
projectDirectory: "",
|
|
7288
|
+
relativePath: "",
|
|
7289
|
+
error: error instanceof Error ? error.message : String(error)
|
|
7290
|
+
};
|
|
6929
7291
|
}
|
|
6930
|
-
const
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
...userOverrides,
|
|
6935
|
-
template: input.template,
|
|
6936
|
-
projectDirectory: originalInput.projectDirectory
|
|
7292
|
+
const { finalResolvedPath, finalBaseName } = await setupProjectDirectory(finalPathInput, shouldClearDirectory);
|
|
7293
|
+
const originalInput = {
|
|
7294
|
+
...input,
|
|
7295
|
+
projectDirectory: input.projectName
|
|
6937
7296
|
};
|
|
7297
|
+
const providedFlags = getProvidedFlags(originalInput);
|
|
7298
|
+
let cliInput = originalInput;
|
|
7299
|
+
if (input.template && input.template !== "none") {
|
|
7300
|
+
const templateConfig = getTemplateConfig(input.template);
|
|
7301
|
+
if (templateConfig) {
|
|
7302
|
+
const templateName = input.template.toUpperCase();
|
|
7303
|
+
const templateDescription = getTemplateDescription(input.template);
|
|
7304
|
+
if (!isSilent()) {
|
|
7305
|
+
log.message(pc.bold(pc.cyan(`Using template: ${pc.white(templateName)}`)));
|
|
7306
|
+
log.message(pc.dim(` ${templateDescription}`));
|
|
7307
|
+
}
|
|
7308
|
+
const userOverrides = {};
|
|
7309
|
+
for (const [key, value] of Object.entries(originalInput)) if (value !== void 0) userOverrides[key] = value;
|
|
7310
|
+
cliInput = {
|
|
7311
|
+
...templateConfig,
|
|
7312
|
+
...userOverrides,
|
|
7313
|
+
template: input.template,
|
|
7314
|
+
projectDirectory: originalInput.projectDirectory
|
|
7315
|
+
};
|
|
7316
|
+
}
|
|
7317
|
+
}
|
|
7318
|
+
let config;
|
|
7319
|
+
if (cliInput.yes) {
|
|
7320
|
+
const flagConfig = processProvidedFlagsWithoutValidation(cliInput, finalBaseName);
|
|
7321
|
+
config = {
|
|
7322
|
+
...getDefaultConfig(),
|
|
7323
|
+
...flagConfig,
|
|
7324
|
+
projectName: finalBaseName,
|
|
7325
|
+
projectDir: finalResolvedPath,
|
|
7326
|
+
relativePath: finalPathInput
|
|
7327
|
+
};
|
|
7328
|
+
validateConfigCompatibility(config, providedFlags, cliInput);
|
|
7329
|
+
if (!isSilent()) {
|
|
7330
|
+
log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
|
|
7331
|
+
log.message(displayConfig(config));
|
|
7332
|
+
}
|
|
7333
|
+
} else {
|
|
7334
|
+
const flagConfig = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
|
|
7335
|
+
const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig;
|
|
7336
|
+
if (!isSilent() && Object.keys(otherFlags).length > 0) {
|
|
7337
|
+
log.info(pc.yellow("Using these pre-selected options:"));
|
|
7338
|
+
log.message(displayConfig(otherFlags));
|
|
7339
|
+
log.message("");
|
|
7340
|
+
}
|
|
7341
|
+
config = await gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput);
|
|
7342
|
+
}
|
|
7343
|
+
await createProject(config, { manualDb: cliInput.manualDb ?? input.manualDb });
|
|
7344
|
+
const reproducibleCommand = generateReproducibleCommand(config);
|
|
7345
|
+
if (!isSilent()) log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
|
|
7346
|
+
await trackProjectCreation(config, input.disableAnalytics);
|
|
7347
|
+
const elapsedTimeMs = Date.now() - startTime;
|
|
7348
|
+
if (!isSilent()) {
|
|
7349
|
+
const elapsedTimeInSeconds = (elapsedTimeMs / 1e3).toFixed(2);
|
|
7350
|
+
outro(pc.magenta(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
|
|
7351
|
+
}
|
|
7352
|
+
return {
|
|
7353
|
+
success: true,
|
|
7354
|
+
projectConfig: config,
|
|
7355
|
+
reproducibleCommand,
|
|
7356
|
+
timeScaffolded,
|
|
7357
|
+
elapsedTimeMs,
|
|
7358
|
+
projectDirectory: config.projectDir,
|
|
7359
|
+
relativePath: config.relativePath
|
|
7360
|
+
};
|
|
7361
|
+
} catch (error) {
|
|
7362
|
+
if (error instanceof UserCancelledError) {
|
|
7363
|
+
if (isSilent()) return {
|
|
7364
|
+
success: false,
|
|
7365
|
+
error: error.message,
|
|
7366
|
+
projectConfig: {},
|
|
7367
|
+
reproducibleCommand: "",
|
|
7368
|
+
timeScaffolded,
|
|
7369
|
+
elapsedTimeMs: Date.now() - startTime,
|
|
7370
|
+
projectDirectory: "",
|
|
7371
|
+
relativePath: ""
|
|
7372
|
+
};
|
|
7373
|
+
return;
|
|
7374
|
+
}
|
|
7375
|
+
if (error instanceof CLIError) {
|
|
7376
|
+
if (isSilent()) return {
|
|
7377
|
+
success: false,
|
|
7378
|
+
error: error.message,
|
|
7379
|
+
projectConfig: {},
|
|
7380
|
+
reproducibleCommand: "",
|
|
7381
|
+
timeScaffolded,
|
|
7382
|
+
elapsedTimeMs: Date.now() - startTime,
|
|
7383
|
+
projectDirectory: "",
|
|
7384
|
+
relativePath: ""
|
|
7385
|
+
};
|
|
7386
|
+
throw error;
|
|
7387
|
+
}
|
|
7388
|
+
throw error;
|
|
6938
7389
|
}
|
|
6939
|
-
}
|
|
6940
|
-
let config;
|
|
6941
|
-
if (cliInput.yes) {
|
|
6942
|
-
const flagConfig = processProvidedFlagsWithoutValidation(cliInput, finalBaseName);
|
|
6943
|
-
config = {
|
|
6944
|
-
...getDefaultConfig(),
|
|
6945
|
-
...flagConfig,
|
|
6946
|
-
projectName: finalBaseName,
|
|
6947
|
-
projectDir: finalResolvedPath,
|
|
6948
|
-
relativePath: finalPathInput
|
|
6949
|
-
};
|
|
6950
|
-
validateConfigCompatibility(config, providedFlags, cliInput);
|
|
6951
|
-
if (!silent) {
|
|
6952
|
-
log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
|
|
6953
|
-
log.message(displayConfig(config));
|
|
6954
|
-
}
|
|
6955
|
-
} else {
|
|
6956
|
-
const flagConfig = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
|
|
6957
|
-
const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig;
|
|
6958
|
-
if (!silent && Object.keys(otherFlags).length > 0) {
|
|
6959
|
-
log.info(pc.yellow("Using these pre-selected options:"));
|
|
6960
|
-
log.message(displayConfig(otherFlags));
|
|
6961
|
-
log.message("");
|
|
6962
|
-
}
|
|
6963
|
-
config = await gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput);
|
|
6964
|
-
}
|
|
6965
|
-
await createProject(config, {
|
|
6966
|
-
manualDb: cliInput.manualDb ?? input.manualDb,
|
|
6967
|
-
silent
|
|
6968
7390
|
});
|
|
6969
|
-
const reproducibleCommand = generateReproducibleCommand(config);
|
|
6970
|
-
if (!silent) log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
|
|
6971
|
-
await trackProjectCreation(config, input.disableAnalytics);
|
|
6972
|
-
const elapsedTimeMs = Date.now() - startTime;
|
|
6973
|
-
if (!silent) {
|
|
6974
|
-
const elapsedTimeInSeconds = (elapsedTimeMs / 1e3).toFixed(2);
|
|
6975
|
-
outro(pc.magenta(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
|
|
6976
|
-
}
|
|
6977
|
-
return {
|
|
6978
|
-
success: true,
|
|
6979
|
-
projectConfig: config,
|
|
6980
|
-
reproducibleCommand,
|
|
6981
|
-
timeScaffolded,
|
|
6982
|
-
elapsedTimeMs,
|
|
6983
|
-
projectDirectory: config.projectDir,
|
|
6984
|
-
relativePath: config.relativePath
|
|
6985
|
-
};
|
|
6986
7391
|
}
|
|
6987
7392
|
async function handleDirectoryConflictProgrammatically(currentPathInput, strategy) {
|
|
6988
7393
|
const currentPath = path.resolve(process.cwd(), currentPathInput);
|
|
@@ -7077,6 +7482,8 @@ async function addAddonsHandler(input) {
|
|
|
7077
7482
|
else log.info(`Run ${pc.bold(`${packageManager} install`)} to install dependencies`);
|
|
7078
7483
|
outro("Add command completed successfully!");
|
|
7079
7484
|
} catch (error) {
|
|
7485
|
+
if (error instanceof UserCancelledError) return;
|
|
7486
|
+
if (error instanceof CLIError) throw error;
|
|
7080
7487
|
handleError(error, "Failed to add addons or deployment");
|
|
7081
7488
|
}
|
|
7082
7489
|
}
|