create-better-t-stack
Version:
A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations
1,306 lines (1,283 loc) • 230 kB
JavaScript
#!/usr/bin/env node
import { t as __reExport } from "./chunk-CHc3S52W.mjs";
import { createRouterClient, os } from "@orpc/server";
import { Result, Result as Result$1, TaggedError } from "better-result";
import { createCli } from "trpc-cli";
import z from "zod";
import { autocompleteMultiselect, cancel, confirm, group, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
import pc from "picocolors";
import envPaths from "env-paths";
import fs from "fs-extra";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { EMBEDDED_TEMPLATES, EMBEDDED_TEMPLATES as EMBEDDED_TEMPLATES$1, GeneratorError, GeneratorError as GeneratorError$1, TEMPLATE_COUNT, VirtualFileSystem, VirtualFileSystem as VirtualFileSystem$1, dependencyVersionMap, generate, generate as generate$1, generateReproducibleCommand, processAddonTemplates, processAddonsDeps } from "@better-t-stack/template-generator";
import consola, { consola as consola$1 } from "consola";
import gradient from "gradient-string";
import { $, execa } from "execa";
import { writeTree } from "@better-t-stack/template-generator/fs-writer";
import { ConfirmPrompt, GroupMultiSelectPrompt, MultiSelectPrompt, SelectPrompt, isCancel as isCancel$1 } from "@clack/core";
import { AsyncLocalStorage } from "node:async_hooks";
import { applyEdits, modify, parse } from "jsonc-parser";
import os$1 from "node:os";
import { format } from "oxfmt";
//#region src/utils/get-package-manager.ts
const getUserPkgManager = () => {
const userAgent = process.env.npm_config_user_agent;
if (userAgent?.startsWith("pnpm")) return "pnpm";
if (userAgent?.startsWith("bun")) return "bun";
return "npm";
};
//#endregion
//#region src/constants.ts
const __filename = fileURLToPath(import.meta.url);
const distPath = path.dirname(__filename);
const PKG_ROOT = path.join(distPath, "../");
const DEFAULT_CONFIG_BASE = {
projectName: "my-better-t-app",
relativePath: "my-better-t-app",
frontend: ["tanstack-router"],
database: "sqlite",
orm: "drizzle",
auth: "better-auth",
payments: "none",
addons: ["turborepo"],
examples: [],
git: true,
install: true,
dbSetup: "none",
backend: "hono",
runtime: "bun",
api: "trpc",
webDeploy: "none",
serverDeploy: "none"
};
function getDefaultConfig() {
return {
...DEFAULT_CONFIG_BASE,
projectDir: path.resolve(process.cwd(), DEFAULT_CONFIG_BASE.projectName),
packageManager: getUserPkgManager(),
frontend: [...DEFAULT_CONFIG_BASE.frontend],
addons: [...DEFAULT_CONFIG_BASE.addons],
examples: [...DEFAULT_CONFIG_BASE.examples]
};
}
const DEFAULT_CONFIG = getDefaultConfig();
const ADDON_COMPATIBILITY = {
pwa: [
"tanstack-router",
"react-router",
"solid",
"next"
],
tauri: [
"tanstack-router",
"react-router",
"nuxt",
"svelte",
"solid",
"next"
],
biome: [],
husky: [],
lefthook: [],
turborepo: [],
starlight: [],
ultracite: [],
ruler: [],
mcp: [],
oxlint: [],
fumadocs: [],
opentui: [],
wxt: [],
skills: [],
none: []
};
//#endregion
//#region src/utils/errors.ts
/**
* User cancelled the operation (e.g., Ctrl+C in prompts)
*/
var UserCancelledError = class extends TaggedError("UserCancelledError")() {
constructor(args) {
super({ message: args?.message ?? "Operation cancelled" });
}
};
/**
* General CLI error for validation failures, invalid flags, etc.
*/
var CLIError = class extends TaggedError("CLIError")() {};
/**
* Validation error for config/flag validation failures
*/
var ValidationError = class extends TaggedError("ValidationError")() {
constructor(args) {
super(args);
}
};
/**
* Compatibility error for incompatible option combinations
*/
var CompatibilityError = class extends TaggedError("CompatibilityError")() {
constructor(args) {
super(args);
}
};
/**
* Directory conflict error when target directory exists and is not empty
*/
var DirectoryConflictError = class extends TaggedError("DirectoryConflictError")() {
constructor(args) {
super({
directory: args.directory,
message: `Directory "${args.directory}" already exists and is not empty. Use directoryConflict: "overwrite", "merge", or "increment" to handle this.`
});
}
};
/**
* Project creation error for failures during scaffolding
*/
var ProjectCreationError = class extends TaggedError("ProjectCreationError")() {
constructor(args) {
super(args);
}
};
/**
* Database setup error for failures during database configuration
*/
var DatabaseSetupError = class extends TaggedError("DatabaseSetupError")() {
constructor(args) {
super(args);
}
};
/**
* Addon setup error for failures during addon configuration
*/
var AddonSetupError = class extends TaggedError("AddonSetupError")() {
constructor(args) {
super(args);
}
};
/**
* Create a user cancelled error Result
*/
function userCancelled(message) {
return Result.err(new UserCancelledError({ message }));
}
/**
* Create a database setup error Result
*/
function databaseSetupError(provider, message, cause) {
return Result.err(new DatabaseSetupError({
provider,
message,
cause
}));
}
/**
* Display an error to the user (for CLI mode)
*/
function displayError(error) {
if (UserCancelledError.is(error)) cancel(pc.red(error.message));
else consola.error(pc.red(error.message));
}
//#endregion
//#region src/utils/get-latest-cli-version.ts
function getLatestCLIVersionResult() {
const packageJsonPath = path.join(PKG_ROOT, "package.json");
return Result.try({
try: () => {
return fs.readJSONSync(packageJsonPath).version;
},
catch: (e) => new CLIError({
message: `Failed to read CLI version from package.json: ${e instanceof Error ? e.message : String(e)}`,
cause: e
})
});
}
function getLatestCLIVersion() {
return getLatestCLIVersionResult().unwrapOr("1.0.0");
}
//#endregion
//#region src/utils/project-history.ts
const paths = envPaths("better-t-stack", { suffix: "" });
const HISTORY_FILE = "history.json";
var HistoryError = class extends TaggedError("HistoryError")() {};
function getHistoryDir() {
return paths.data;
}
function getHistoryPath() {
return path.join(paths.data, HISTORY_FILE);
}
function generateId() {
return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
}
function emptyHistory() {
return {
version: 1,
entries: []
};
}
async function ensureHistoryDir() {
return Result.tryPromise({
try: async () => {
await fs.ensureDir(getHistoryDir());
},
catch: (e) => new HistoryError({
message: `Failed to create history directory: ${e instanceof Error ? e.message : String(e)}`,
cause: e
})
});
}
async function readHistory() {
const historyPath = getHistoryPath();
const existsResult = await Result.tryPromise({
try: async () => await fs.pathExists(historyPath),
catch: (e) => new HistoryError({
message: `Failed to check history file: ${e instanceof Error ? e.message : String(e)}`,
cause: e
})
});
if (existsResult.isErr()) return existsResult;
if (!existsResult.value) return Result.ok(emptyHistory());
const readResult = await Result.tryPromise({
try: async () => await fs.readJson(historyPath),
catch: (e) => new HistoryError({
message: `Failed to read history file: ${e instanceof Error ? e.message : String(e)}`,
cause: e
})
});
if (readResult.isErr()) return Result.ok(emptyHistory());
return Result.ok(readResult.value);
}
async function writeHistory(history) {
const ensureDirResult = await ensureHistoryDir();
if (ensureDirResult.isErr()) return ensureDirResult;
return Result.tryPromise({
try: async () => {
await fs.writeJson(getHistoryPath(), history, { spaces: 2 });
},
catch: (e) => new HistoryError({
message: `Failed to write history file: ${e instanceof Error ? e.message : String(e)}`,
cause: e
})
});
}
async function addToHistory(config, reproducibleCommand) {
const historyResult = await readHistory();
if (historyResult.isErr()) return historyResult;
const history = historyResult.value;
const entry = {
id: generateId(),
projectName: config.projectName,
projectDir: config.projectDir,
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
stack: {
frontend: config.frontend,
backend: config.backend,
database: config.database,
orm: config.orm,
runtime: config.runtime,
auth: config.auth,
payments: config.payments,
api: config.api,
addons: config.addons,
examples: config.examples,
dbSetup: config.dbSetup,
packageManager: config.packageManager
},
cliVersion: getLatestCLIVersion(),
reproducibleCommand
};
history.entries.unshift(entry);
if (history.entries.length > 100) history.entries = history.entries.slice(0, 100);
return await writeHistory(history);
}
async function getHistory(limit = 10) {
const historyResult = await readHistory();
if (historyResult.isErr()) return historyResult;
return Result.ok(historyResult.value.entries.slice(0, limit));
}
async function clearHistory() {
const historyPath = getHistoryPath();
return Result.tryPromise({
try: async () => {
if (await fs.pathExists(historyPath)) await fs.remove(historyPath);
},
catch: (e) => new HistoryError({
message: `Failed to clear history: ${e instanceof Error ? e.message : String(e)}`,
cause: e
})
});
}
//#endregion
//#region src/utils/render-title.ts
const TITLE_TEXT = `
██████╗ ███████╗████████╗████████╗███████╗██████╗
██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗
██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝
██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗
██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║
╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
████████╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗
╚══██╔══╝ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
██║ ███████╗ ██║ ███████║██║ █████╔╝
██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗
██║ ███████║ ██║ ██║ ██║╚██████╗██║ ██╗
╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
`;
const catppuccinTheme = {
pink: "#F5C2E7",
mauve: "#CBA6F7",
red: "#F38BA8",
maroon: "#E78284",
peach: "#FAB387",
yellow: "#F9E2AF",
green: "#A6E3A1",
teal: "#94E2D5",
sky: "#89DCEB",
sapphire: "#74C7EC",
lavender: "#B4BEFE"
};
const renderTitle = () => {
const terminalWidth = process.stdout.columns || 80;
const titleLines = TITLE_TEXT.split("\n");
if (terminalWidth < Math.max(...titleLines.map((line) => line.length))) console.log(gradient(Object.values(catppuccinTheme)).multiline(`Better T Stack`));
else console.log(gradient(Object.values(catppuccinTheme)).multiline(TITLE_TEXT));
};
//#endregion
//#region src/commands/history.ts
function formatStackSummary(entry) {
const parts = [];
if (entry.stack.frontend.length > 0 && !entry.stack.frontend.includes("none")) parts.push(entry.stack.frontend.join(", "));
if (entry.stack.backend && entry.stack.backend !== "none") parts.push(entry.stack.backend);
if (entry.stack.database && entry.stack.database !== "none") parts.push(entry.stack.database);
if (entry.stack.orm && entry.stack.orm !== "none") parts.push(entry.stack.orm);
return parts.length > 0 ? parts.join(" + ") : "minimal";
}
function formatDate(isoString) {
return new Date(isoString).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit"
});
}
async function historyHandler(input) {
if (input.clear) {
const clearResult = await clearHistory();
if (clearResult.isErr()) {
log.warn(pc.yellow(clearResult.error.message));
return;
}
log.success(pc.green("Project history cleared."));
return;
}
const historyResult = await getHistory(input.limit);
if (historyResult.isErr()) {
log.warn(pc.yellow(historyResult.error.message));
return;
}
const entries = historyResult.value;
if (entries.length === 0) {
log.info(pc.dim("No projects in history yet."));
log.info(pc.dim("Create a project with: create-better-t-stack my-app"));
return;
}
if (input.json) {
console.log(JSON.stringify(entries, null, 2));
return;
}
renderTitle();
intro(pc.magenta(`Project History (${entries.length} entries)`));
for (const [index, entry] of entries.entries()) {
const num = pc.dim(`${index + 1}.`);
const name = pc.cyan(pc.bold(entry.projectName));
const stack = pc.dim(formatStackSummary(entry));
log.message(`${num} ${name}`);
log.message(` ${pc.dim("Created:")} ${formatDate(entry.createdAt)}`);
log.message(` ${pc.dim("Path:")} ${entry.projectDir}`);
log.message(` ${pc.dim("Stack:")} ${stack}`);
log.message(` ${pc.dim("Command:")} ${pc.dim(entry.reproducibleCommand)}`);
log.message("");
}
}
//#endregion
//#region src/utils/open-url.ts
async function openUrl(url) {
const platform = process.platform;
if (platform === "darwin") {
await $({ stdio: "ignore" })`open ${url}`;
return;
}
if (platform === "win32") {
const escapedUrl = url.replace(/&/g, "^&");
await $({ stdio: "ignore" })`cmd /c start "" ${escapedUrl}`;
return;
}
await $({ stdio: "ignore" })`xdg-open ${url}`;
}
//#endregion
//#region src/utils/sponsors.ts
const SPONSORS_JSON_URL = "https://sponsors.better-t-stack.dev/sponsors.json";
async function fetchSponsors(url = SPONSORS_JSON_URL) {
const s = spinner();
s.start("Fetching sponsors…");
const response = await fetch(url);
if (!response.ok) {
s.stop(pc.red(`Failed to fetch sponsors: ${response.statusText}`));
throw new Error(`Failed to fetch sponsors: ${response.statusText}`);
}
const sponsors = await response.json();
s.stop("Sponsors fetched successfully!");
return sponsors;
}
function displaySponsors(sponsors) {
const { total_sponsors } = sponsors.summary;
if (total_sponsors === 0) {
log.info("No sponsors found. You can be the first one! ✨");
outro(pc.cyan("Visit https://github.com/sponsors/AmanVarshney01 to become a sponsor."));
return;
}
displaySponsorsBox(sponsors);
if (total_sponsors - sponsors.specialSponsors.length > 0) log.message(pc.blue(`+${total_sponsors - sponsors.specialSponsors.length} more amazing sponsors.\n`));
outro(pc.magenta("Visit https://github.com/sponsors/AmanVarshney01 to become a sponsor."));
}
function displaySponsorsBox(sponsors) {
if (sponsors.specialSponsors.length === 0) return;
let output = `${pc.bold(pc.cyan("-> Special Sponsors"))}\n\n`;
sponsors.specialSponsors.forEach((sponsor, idx) => {
const displayName = sponsor.name ?? sponsor.githubId;
const tier = sponsor.tierName ? ` ${pc.yellow(`(${sponsor.tierName})`)}` : "";
output += `${pc.green(`• ${displayName}`)}${tier}\n`;
output += ` ${pc.dim("GitHub:")} https://github.com/${sponsor.githubId}\n`;
const website = sponsor.websiteUrl ?? sponsor.githubUrl;
if (website) output += ` ${pc.dim("Website:")} ${website}\n`;
if (idx < sponsors.specialSponsors.length - 1) output += "\n";
});
consola$1.box(output);
}
//#endregion
//#region src/commands/meta.ts
const DOCS_URL = "https://better-t-stack.dev/docs";
const BUILDER_URL = "https://better-t-stack.dev/new";
async function openExternalUrl(url, successMessage) {
if ((await Result.tryPromise({
try: () => openUrl(url),
catch: () => null
})).isOk()) log.success(pc.blue(successMessage));
else log.message(`Please visit ${url}`);
}
async function showSponsorsCommand() {
const result = await Result.tryPromise({
try: async () => {
renderTitle();
intro(pc.magenta("Better-T-Stack Sponsors"));
displaySponsors(await fetchSponsors());
},
catch: (error) => new CLIError({
message: error instanceof Error ? error.message : "Failed to display sponsors",
cause: error
})
});
if (result.isErr()) {
displayError(result.error);
process.exit(1);
}
}
async function openDocsCommand() {
await openExternalUrl(DOCS_URL, "Opened docs in your default browser.");
}
async function openBuilderCommand() {
await openExternalUrl(BUILDER_URL, "Opened builder in your default browser.");
}
//#endregion
//#region src/types.ts
var types_exports = {};
import * as import__better_t_stack_types from "@better-t-stack/types";
__reExport(types_exports, import__better_t_stack_types);
//#endregion
//#region src/utils/compatibility.ts
const WEB_FRAMEWORKS = [
"tanstack-router",
"react-router",
"tanstack-start",
"next",
"nuxt",
"svelte",
"solid",
"astro"
];
//#endregion
//#region src/utils/compatibility-rules.ts
function validationErr$1(message) {
return Result.err(new ValidationError({ message }));
}
function isWebFrontend(value) {
return WEB_FRAMEWORKS.includes(value);
}
function splitFrontends(values = []) {
return {
web: values.filter((f) => isWebFrontend(f)),
native: values.filter((f) => f === "native-bare" || f === "native-uniwind" || f === "native-unistyles")
};
}
function ensureSingleWebAndNative(frontends) {
const { web, native } = splitFrontends(frontends);
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");
if (native.length > 1) return validationErr$1("Cannot select multiple native frameworks. Choose only one of: native-bare, native-uniwind, native-unistyles");
return Result.ok(void 0);
}
const FULLSTACK_FRONTENDS$1 = [
"next",
"tanstack-start",
"nuxt",
"astro"
];
function validateSelfBackendCompatibility(providedFlags, options, config) {
const backend = config.backend || options.backend;
const frontends = config.frontend || options.frontend || [];
if (backend === "self") {
const { web, native } = splitFrontends(frontends);
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.");
if (native.length > 1) return validationErr$1("Cannot select multiple native frameworks. Choose only one of: native-bare, native-uniwind, native-unistyles");
}
const hasFullstackFrontend = frontends.some((f) => FULLSTACK_FRONTENDS$1.includes(f));
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.");
return Result.ok(void 0);
}
function validateWorkersCompatibility(providedFlags, options, config) {
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.`);
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.`);
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.");
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.");
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.");
return Result.ok(void 0);
}
function validateApiFrontendCompatibility(api, frontends = []) {
const includesNuxt = frontends.includes("nuxt");
const includesSvelte = frontends.includes("svelte");
const includesSolid = frontends.includes("solid");
const includesAstro = frontends.includes("astro");
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.`);
return Result.ok(void 0);
}
function isFrontendAllowedWithBackend(frontend, backend, auth) {
if (backend === "convex" && (frontend === "solid" || frontend === "astro")) return false;
if (auth === "clerk" && backend === "convex") {
if ([
"nuxt",
"svelte",
"solid",
"astro"
].includes(frontend)) return false;
}
return true;
}
function allowedApisForFrontends(frontends = []) {
const includesNuxt = frontends.includes("nuxt");
const includesSvelte = frontends.includes("svelte");
const includesSolid = frontends.includes("solid");
const includesAstro = frontends.includes("astro");
const base = [
"trpc",
"orpc",
"none"
];
if (includesNuxt || includesSvelte || includesSolid || includesAstro) return ["orpc", "none"];
return base;
}
function isExampleTodoAllowed(backend, database, api) {
if (backend === "convex") return true;
if (database === "none" || api === "none") return false;
return true;
}
function isExampleAIAllowed(backend, frontends = []) {
const includesSolid = frontends.includes("solid");
const includesAstro = frontends.includes("astro");
if (includesSolid || includesAstro) return false;
if (backend === "convex") {
const includesNuxt = frontends.includes("nuxt");
const includesSvelte = frontends.includes("svelte");
if (includesNuxt || includesSvelte) return false;
}
return true;
}
function validateWebDeployRequiresWebFrontend(webDeploy, hasWebFrontendFlag) {
if (webDeploy && webDeploy !== "none" && !hasWebFrontendFlag) return validationErr$1("'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.");
return Result.ok(void 0);
}
function validateServerDeployRequiresBackend(serverDeploy, backend) {
if (serverDeploy && serverDeploy !== "none" && (!backend || backend === "none")) return validationErr$1("'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.");
return Result.ok(void 0);
}
function validateAddonCompatibility(addon, frontend, _auth) {
const compatibleFrontends = ADDON_COMPATIBILITY[addon];
if (compatibleFrontends.length > 0) {
if (!frontend.some((f) => compatibleFrontends.includes(f))) return {
isCompatible: false,
reason: `${addon} addon requires one of these frontends: ${compatibleFrontends.join(", ")}`
};
}
return { isCompatible: true };
}
function getCompatibleAddons(allAddons, frontend, existingAddons = [], auth) {
return allAddons.filter((addon) => {
if (existingAddons.includes(addon)) return false;
if (addon === "none") return false;
const { isCompatible } = validateAddonCompatibility(addon, frontend, auth);
return isCompatible;
});
}
function validateAddonsAgainstFrontends(addons = [], frontends = [], auth) {
for (const addon of addons) {
if (addon === "none") continue;
const { isCompatible, reason } = validateAddonCompatibility(addon, frontends, auth);
if (!isCompatible) return validationErr$1(`Incompatible addon/frontend combination: ${reason}`);
}
return Result.ok(void 0);
}
function validatePaymentsCompatibility(payments, auth, _backend, frontends = []) {
if (!payments || payments === "none") return Result.ok(void 0);
if (payments === "polar") {
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.");
const { web } = splitFrontends(frontends);
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.");
}
return Result.ok(void 0);
}
function validateExamplesCompatibility(examples, backend, database, frontend, api) {
const examplesArr = examples ?? [];
if (examplesArr.length === 0 || examplesArr.includes("none")) return Result.ok(void 0);
if (examplesArr.includes("todo") && backend !== "convex") {
if (database === "none") return validationErr$1("The 'todo' example requires a database. Cannot use --examples todo when database is 'none'.");
if (api === "none") return validationErr$1("The 'todo' example requires an API layer (tRPC or oRPC). Cannot use --examples todo when api is 'none'.");
}
if (examplesArr.includes("ai") && (frontend ?? []).includes("solid")) return validationErr$1("The 'ai' example is not compatible with the Solid frontend.");
if (examplesArr.includes("ai") && backend === "convex") {
const frontendArr = frontend ?? [];
const includesNuxt = frontendArr.includes("nuxt");
const includesSvelte = frontendArr.includes("svelte");
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.");
}
return Result.ok(void 0);
}
//#endregion
//#region src/utils/context.ts
const cliStorage = new AsyncLocalStorage();
function defaultContext() {
return {
navigation: {
isFirstPrompt: false,
lastPromptShownUI: false
},
silent: false,
verbose: false
};
}
function getContext() {
const ctx = cliStorage.getStore();
if (!ctx) return defaultContext();
return ctx;
}
function tryGetContext() {
return cliStorage.getStore();
}
function isSilent() {
return getContext().silent;
}
function isFirstPrompt() {
return getContext().navigation.isFirstPrompt;
}
function didLastPromptShowUI() {
return getContext().navigation.lastPromptShownUI;
}
function setIsFirstPrompt$1(value) {
const ctx = tryGetContext();
if (ctx) ctx.navigation.isFirstPrompt = value;
}
function setLastPromptShownUI(value) {
const ctx = tryGetContext();
if (ctx) ctx.navigation.lastPromptShownUI = value;
}
async function runWithContextAsync(options, fn) {
const ctx = {
navigation: {
isFirstPrompt: false,
lastPromptShownUI: false
},
silent: options.silent ?? false,
verbose: options.verbose ?? false,
projectDir: options.projectDir,
projectName: options.projectName,
packageManager: options.packageManager
};
return cliStorage.run(ctx, fn);
}
//#endregion
//#region src/utils/navigation.ts
const GO_BACK_SYMBOL = Symbol("clack:goBack");
function isGoBack(value) {
return value === GO_BACK_SYMBOL;
}
//#endregion
//#region src/prompts/navigable.ts
/**
* Navigable prompt wrappers using @clack/core
* These prompts return GO_BACK_SYMBOL when 'b' is pressed (instead of canceling)
*/
const unicode = process.platform !== "win32";
const S_STEP_ACTIVE = unicode ? "◆" : "*";
const S_STEP_CANCEL = unicode ? "■" : "x";
const S_STEP_ERROR = unicode ? "▲" : "x";
const S_STEP_SUBMIT = unicode ? "◇" : "o";
const S_BAR = unicode ? "│" : "|";
const S_BAR_END = unicode ? "└" : "—";
const S_RADIO_ACTIVE = unicode ? "●" : ">";
const S_RADIO_INACTIVE = unicode ? "○" : " ";
const S_CHECKBOX_ACTIVE = unicode ? "◻" : "[•]";
const S_CHECKBOX_SELECTED = unicode ? "◼" : "[+]";
const S_CHECKBOX_INACTIVE = unicode ? "◻" : "[ ]";
function symbol(state) {
switch (state) {
case "initial":
case "active": return pc.cyan(S_STEP_ACTIVE);
case "cancel": return pc.red(S_STEP_CANCEL);
case "error": return pc.yellow(S_STEP_ERROR);
case "submit": return pc.green(S_STEP_SUBMIT);
}
}
const KEYBOARD_HINT = pc.dim(`${pc.gray("↑/↓")} navigate • ${pc.gray("enter")} confirm • ${pc.gray("b")} back • ${pc.gray("ctrl+c")} cancel`);
const KEYBOARD_HINT_FIRST = pc.dim(`${pc.gray("↑/↓")} navigate • ${pc.gray("enter")} confirm • ${pc.gray("ctrl+c")} cancel`);
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`);
const KEYBOARD_HINT_MULTI_FIRST = pc.dim(`${pc.gray("↑/↓")} navigate • ${pc.gray("space")} select • ${pc.gray("enter")} confirm • ${pc.gray("ctrl+c")} cancel`);
const setIsFirstPrompt = setIsFirstPrompt$1;
function getHint() {
return isFirstPrompt() ? KEYBOARD_HINT_FIRST : KEYBOARD_HINT;
}
function getMultiHint() {
return isFirstPrompt() ? KEYBOARD_HINT_MULTI_FIRST : KEYBOARD_HINT_MULTI;
}
async function runWithNavigation(prompt) {
let goBack = false;
prompt.on("key", (char) => {
if (char === "b" && !isFirstPrompt()) {
goBack = true;
prompt.state = "cancel";
}
});
setLastPromptShownUI(true);
const result = await prompt.prompt();
return goBack ? GO_BACK_SYMBOL : result;
}
async function navigableSelect(opts) {
const opt = (option, state) => {
const label = option.label ?? String(option.value);
switch (state) {
case "disabled": return `${pc.gray(S_RADIO_INACTIVE)} ${pc.gray(label)}${option.hint ? ` ${pc.dim(`(${option.hint ?? "disabled"})`)}` : ""}`;
case "selected": return `${pc.dim(label)}`;
case "active": return `${pc.green(S_RADIO_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
case "cancelled": return `${pc.strikethrough(pc.dim(label))}`;
default: return `${pc.dim(S_RADIO_INACTIVE)} ${pc.dim(label)}`;
}
};
return runWithNavigation(new SelectPrompt({
options: opts.options,
initialValue: opts.initialValue,
render() {
const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
switch (this.state) {
case "submit": return `${title}${pc.gray(S_BAR)} ${opt(this.options[this.cursor], "selected")}`;
case "cancel": return `${title}${pc.gray(S_BAR)} ${opt(this.options[this.cursor], "cancelled")}\n${pc.gray(S_BAR)}`;
default: {
const optionsText = this.options.map((option, i) => opt(option, option.disabled ? "disabled" : i === this.cursor ? "active" : "inactive")).join(`\n${pc.cyan(S_BAR)} `);
const hint = `\n${pc.gray(S_BAR)} ${getHint()}`;
return `${title}${pc.cyan(S_BAR)} ${optionsText}\n${pc.cyan(S_BAR_END)}${hint}\n`;
}
}
}
}));
}
async function navigableMultiselect(opts) {
const required = opts.required ?? true;
const opt = (option, state) => {
const label = option.label ?? String(option.value);
if (state === "disabled") return `${pc.gray(S_CHECKBOX_INACTIVE)} ${pc.strikethrough(pc.gray(label))}${option.hint ? ` ${pc.dim(`(${option.hint ?? "disabled"})`)}` : ""}`;
if (state === "active") return `${pc.cyan(S_CHECKBOX_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
if (state === "selected") return `${pc.green(S_CHECKBOX_SELECTED)} ${pc.dim(label)}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
if (state === "cancelled") return `${pc.strikethrough(pc.dim(label))}`;
if (state === "active-selected") return `${pc.green(S_CHECKBOX_SELECTED)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
if (state === "submitted") return `${pc.dim(label)}`;
return `${pc.dim(S_CHECKBOX_INACTIVE)} ${pc.dim(label)}`;
};
return runWithNavigation(new MultiSelectPrompt({
options: opts.options,
initialValues: opts.initialValues,
required,
validate(selected) {
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`))}`;
},
render() {
const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
const value = this.value ?? [];
const styleOption = (option, active) => {
if (option.disabled) return opt(option, "disabled");
const selected = value.includes(option.value);
if (active && selected) return opt(option, "active-selected");
if (selected) return opt(option, "selected");
return opt(option, active ? "active" : "inactive");
};
switch (this.state) {
case "submit": {
const submitText = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "submitted")).join(pc.dim(", ")) || pc.dim("none");
return `${title}${pc.gray(S_BAR)} ${submitText}`;
}
case "cancel": {
const label = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "cancelled")).join(pc.dim(", "));
return `${title}${pc.gray(S_BAR)} ${label}\n${pc.gray(S_BAR)}`;
}
case "error": {
const footer = this.error.split("\n").map((ln, i) => i === 0 ? `${pc.yellow(S_BAR_END)} ${pc.yellow(ln)}` : ` ${ln}`).join("\n");
const optionsText = this.options.map((option, i) => styleOption(option, i === this.cursor)).join(`\n${pc.yellow(S_BAR)} `);
return `${title}${pc.yellow(S_BAR)} ${optionsText}\n${footer}\n`;
}
default: {
const optionsText = this.options.map((option, i) => styleOption(option, i === this.cursor)).join(`\n${pc.cyan(S_BAR)} `);
const hint = `\n${pc.gray(S_BAR)} ${getMultiHint()}`;
return `${title}${pc.cyan(S_BAR)} ${optionsText}\n${pc.cyan(S_BAR_END)}${hint}\n`;
}
}
}
}));
}
async function navigableConfirm(opts) {
const active = opts.active ?? "Yes";
const inactive = opts.inactive ?? "No";
return runWithNavigation(new ConfirmPrompt({
active,
inactive,
initialValue: opts.initialValue ?? true,
render() {
const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
const value = this.value ? active : inactive;
switch (this.state) {
case "submit": return `${title}${pc.gray(S_BAR)} ${pc.dim(value)}`;
case "cancel": return `${title}${pc.gray(S_BAR)} ${pc.strikethrough(pc.dim(value))}\n${pc.gray(S_BAR)}`;
default: {
const hint = `\n${pc.gray(S_BAR)} ${getHint()}`;
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`;
}
}
}
}));
}
async function navigableGroupMultiselect(opts) {
const required = opts.required ?? true;
const opt = (option, state, options = []) => {
const label = option.label ?? String(option.value);
const isItem = typeof option.group === "string";
const next = isItem && (options[options.indexOf(option) + 1] ?? { group: true });
const isLast = isItem && next && next.group === true;
const prefix = isItem ? `${isLast ? S_BAR_END : S_BAR} ` : "";
if (state === "active") return `${pc.dim(prefix)}${pc.cyan(S_CHECKBOX_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
if (state === "group-active") return `${prefix}${pc.cyan(S_CHECKBOX_ACTIVE)} ${pc.dim(label)}`;
if (state === "group-active-selected") return `${prefix}${pc.green(S_CHECKBOX_SELECTED)} ${pc.dim(label)}`;
if (state === "selected") {
const selectedCheckbox = isItem ? pc.green(S_CHECKBOX_SELECTED) : "";
return `${pc.dim(prefix)}${selectedCheckbox} ${pc.dim(label)}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
}
if (state === "cancelled") return `${pc.strikethrough(pc.dim(label))}`;
if (state === "active-selected") return `${pc.dim(prefix)}${pc.green(S_CHECKBOX_SELECTED)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
if (state === "submitted") return `${pc.dim(label)}`;
const unselectedCheckbox = isItem ? pc.dim(S_CHECKBOX_INACTIVE) : "";
return `${pc.dim(prefix)}${unselectedCheckbox} ${pc.dim(label)}`;
};
return runWithNavigation(new GroupMultiSelectPrompt({
options: opts.options,
initialValues: opts.initialValues,
required,
selectableGroups: true,
validate(selected) {
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`))}`;
},
render() {
const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
const value = this.value ?? [];
switch (this.state) {
case "submit": {
const selectedOptions = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "submitted"));
const optionsText = selectedOptions.length === 0 ? "" : ` ${selectedOptions.join(pc.dim(", "))}`;
return `${title}${pc.gray(S_BAR)}${optionsText}`;
}
case "cancel": {
const label = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "cancelled")).join(pc.dim(", "));
return `${title}${pc.gray(S_BAR)} ${label.trim() ? `${label}\n${pc.gray(S_BAR)}` : ""}`;
}
case "error": {
const footer = this.error.split("\n").map((ln, i) => i === 0 ? `${pc.yellow(S_BAR_END)} ${pc.yellow(ln)}` : ` ${ln}`).join("\n");
const optionsText = this.options.map((option, i, options) => {
const selected = value.includes(option.value) || option.group === true && this.isGroupSelected(`${option.value}`);
const active = i === this.cursor;
if (!active && typeof option.group === "string" && this.options[this.cursor].value === option.group) return opt(option, selected ? "group-active-selected" : "group-active", options);
if (active && selected) return opt(option, "active-selected", options);
if (selected) return opt(option, "selected", options);
return opt(option, active ? "active" : "inactive", options);
}).join(`\n${pc.yellow(S_BAR)} `);
return `${title}${pc.yellow(S_BAR)} ${optionsText}\n${footer}\n`;
}
default: {
const optionsText = this.options.map((option, i, options) => {
const selected = value.includes(option.value) || option.group === true && this.isGroupSelected(`${option.value}`);
const active = i === this.cursor;
const groupActive = !active && typeof option.group === "string" && this.options[this.cursor].value === option.group;
let optionText = "";
if (groupActive) optionText = opt(option, selected ? "group-active-selected" : "group-active", options);
else if (active && selected) optionText = opt(option, "active-selected", options);
else if (selected) optionText = opt(option, "selected", options);
else optionText = opt(option, active ? "active" : "inactive", options);
return `${i !== 0 && !optionText.startsWith("\n") ? " " : ""}${optionText}`;
}).join(`\n${pc.cyan(S_BAR)}`);
const optionsPrefix = optionsText.startsWith("\n") ? "" : " ";
const hint = `\n${pc.gray(S_BAR)} ${getMultiHint()}`;
return `${title}${pc.cyan(S_BAR)}${optionsPrefix}${optionsText}\n${pc.cyan(S_BAR_END)}${hint}\n`;
}
}
}
}));
}
//#endregion
//#region src/prompts/addons.ts
function getAddonDisplay(addon) {
let label;
let hint;
switch (addon) {
case "turborepo":
label = "Turborepo";
hint = "High-performance build system";
break;
case "pwa":
label = "PWA";
hint = "Make your app installable and work offline";
break;
case "tauri":
label = "Tauri";
hint = "Build native desktop apps from your web frontend";
break;
case "biome":
label = "Biome";
hint = "Format, lint, and more";
break;
case "oxlint":
label = "Oxlint";
hint = "Oxlint + Oxfmt (linting & formatting)";
break;
case "ultracite":
label = "Ultracite";
hint = "Zero-config Biome preset with AI integration";
break;
case "ruler":
label = "Ruler";
hint = "Centralize your AI rules";
break;
case "lefthook":
label = "Lefthook";
hint = "Fast and powerful Git hooks manager";
break;
case "husky":
label = "Husky";
hint = "Modern native Git hooks made easy";
break;
case "starlight":
label = "Starlight";
hint = "Build stellar docs with astro";
break;
case "fumadocs":
label = "Fumadocs";
hint = "Build excellent documentation site";
break;
case "opentui":
label = "OpenTUI";
hint = "Build terminal user interfaces";
break;
case "wxt":
label = "WXT";
hint = "Build browser extensions";
break;
case "skills":
label = "Skills";
hint = "AI coding agent skills for your stack";
break;
case "mcp":
label = "MCP";
hint = "Install MCP servers (docs, databases, SaaS) via add-mcp";
break;
default:
label = addon;
hint = `Add ${addon}`;
}
return {
label,
hint
};
}
const ADDON_GROUPS = {
Tooling: [
"turborepo",
"biome",
"oxlint",
"ultracite",
"husky",
"lefthook"
],
Documentation: ["starlight", "fumadocs"],
Extensions: [
"pwa",
"tauri",
"opentui",
"wxt"
],
AI: [
"ruler",
"skills",
"mcp"
]
};
async function getAddonsChoice(addons, frontends, auth) {
if (addons !== void 0) return addons;
const allAddons = types_exports.AddonsSchema.options.filter((addon) => addon !== "none");
const groupedOptions = {
Tooling: [],
Documentation: [],
Extensions: [],
AI: []
};
const frontendsArray = frontends || [];
for (const addon of allAddons) {
const { isCompatible } = validateAddonCompatibility(addon, frontendsArray, auth);
if (!isCompatible) continue;
const { label, hint } = getAddonDisplay(addon);
const option = {
value: addon,
label,
hint
};
if (ADDON_GROUPS.Tooling.includes(addon)) groupedOptions.Tooling.push(option);
else if (ADDON_GROUPS.Documentation.includes(addon)) groupedOptions.Documentation.push(option);
else if (ADDON_GROUPS.Extensions.includes(addon)) groupedOptions.Extensions.push(option);
else if (ADDON_GROUPS.AI.includes(addon)) groupedOptions.AI.push(option);
}
Object.keys(groupedOptions).forEach((group) => {
if (groupedOptions[group].length === 0) delete groupedOptions[group];
else {
const groupOrder = ADDON_GROUPS[group] || [];
groupedOptions[group].sort((a, b) => {
return groupOrder.indexOf(a.value) - groupOrder.indexOf(b.value);
});
}
});
const response = await navigableGroupMultiselect({
message: "Select addons",
options: groupedOptions,
initialValues: DEFAULT_CONFIG.addons.filter((addonValue) => Object.values(groupedOptions).some((options) => options.some((opt) => opt.value === addonValue))),
required: false
});
if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
return response;
}
async function getAddonsToAdd(frontend, existingAddons = [], auth) {
const groupedOptions = {
Tooling: [],
Documentation: [],
Extensions: [],
AI: []
};
const frontendArray = frontend || [];
const compatibleAddons = getCompatibleAddons(types_exports.AddonsSchema.options.filter((addon) => addon !== "none"), frontendArray, existingAddons, auth);
for (const addon of compatibleAddons) {
const { label, hint } = getAddonDisplay(addon);
const option = {
value: addon,
label,
hint
};
if (ADDON_GROUPS.Tooling.includes(addon)) groupedOptions.Tooling.push(option);
else if (ADDON_GROUPS.Documentation.includes(addon)) groupedOptions.Documentation.push(option);
else if (ADDON_GROUPS.Extensions.includes(addon)) groupedOptions.Extensions.push(option);
else if (ADDON_GROUPS.AI.includes(addon)) groupedOptions.AI.push(option);
}
Object.keys(groupedOptions).forEach((group) => {
if (groupedOptions[group].length === 0) delete groupedOptions[group];
else {
const groupOrder = ADDON_GROUPS[group] || [];
groupedOptions[group].sort((a, b) => {
return groupOrder.indexOf(a.value) - groupOrder.indexOf(b.value);
});
}
});
if (Object.keys(groupedOptions).length === 0) return [];
const response = await navigableGroupMultiselect({
message: "Select addons to add",
options: groupedOptions,
required: false
});
if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
return response;
}
//#endregion
//#region src/utils/bts-config.ts
const BTS_CONFIG_FILE = "bts.jsonc";
/**
* Reads the BTS configuration file from the project directory.
*/
async function readBtsConfig(projectDir) {
try {
const configPath = path.join(projectDir, BTS_CONFIG_FILE);
if (!await fs.pathExists(configPath)) return null;
return parse(await fs.readFile(configPath, "utf-8"));
} catch {
return null;
}
}
/**
* Updates specific fields in the BTS configuration file.
*/
async function updateBtsConfig(projectDir, updates) {
try {
const configPath = path.join(projectDir, BTS_CONFIG_FILE);
if (!await fs.pathExists(configPath)) return;
let content = await fs.readFile(configPath, "utf-8");
for (const [key, value] of Object.entries(updates)) {
const edits = modify(content, [key], value, { formattingOptions: { tabSize: 2 } });
content = applyEdits(content, edits);
}
await fs.writeFile(configPath, content, "utf-8");
} catch {}
}
//#endregion
//#region src/utils/add-package-deps.ts
const addPackageDependency = async (opts) => {
const { dependencies = [], devDependencies = [], customDependencies = {}, customDevDependencies = {}, projectDir } = opts;
const pkgJsonPath = path.join(projectDir, "package.json");
const pkgJson = await fs.readJson(pkgJsonPath);
if (!pkgJson.dependencies) pkgJson.dependencies = {};
if (!pkgJson.devDependencies) pkgJson.devDependencies = {};
for (const pkgName of dependencies) {
const version = dependencyVersionMap[pkgName];
if (version) pkgJson.dependencies[pkgName] = version;
else console.warn(`Warning: Dependency ${pkgName} not found in version map.`);
}
for (const pkgName of devDependencies) {
const version = dependencyVersionMap[pkgName];
if (version) pkgJson.devDependencies[pkgName] = version;
else console.warn(`Warning: Dev dependency ${pkgName} not found in version map.`);
}
for (const [pkgName, version] of Object.entries(customDependencies)) pkgJson.dependencies[pkgName] = version;
for (const [pkgName, version] of Object.entries(customDevDependencies)) pkgJson.devDependencies[pkgName] = version;
await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
};
//#endregion
//#region src/utils/external-commands.ts
function shouldSkipExternalCommands() {
return process.env.BTS_SKIP_EXTERNAL_COMMANDS === "1" || process.env.BTS_TEST_MODE === "1";
}
//#endregion
//#region src/utils/package-runner.ts
function splitCommandArgs(commandWithArgs) {
const args = [];
let current = "";
let quote = null;
for (let i = 0; i < commandWithArgs.length; i += 1) {
const char = commandWithArgs[i];
if (quote) {
if (char === quote) {
quote = null;
continue;
}
if (char === "\\" && i + 1 < commandWithArgs.length) {
const nextChar = commandWithArgs[i + 1];
if (nextChar === quote || nextChar === "\\") {
current += nextChar;
i += 1;
continue;
}
}
current += char;
continue;
}
if (char === "\"" || char === "'") {
quote = char;
continue;
}
if (/\s/.test(char)) {
if (current.length > 0) {
args.push(current);
current = "";
}
continue;
}
current += char;
}
if (current.length > 0) args.push(current);
return args;
}
/**
* Returns the appropriate command for running a package without installing it globally,
* based on the selected package manager.
*
* @param packageManager - The selected package manager (e.g., 'npm', 'yarn', 'pnpm', 'bun').
* @param commandWithArgs - The command to run, including arguments (e.g., "prisma generate --schema=./prisma/schema.prisma").
* @returns The full command string (e.g., "npx prisma generate --schema=./prisma/schema.prisma").
*/
function getPackageExecutionCommand(packageManager, commandWithArgs) {
switch (packageManager) {
case "pnpm": return `pnpm dlx ${commandWithArgs}`;
case "bun": return `bunx ${commandWithArgs}`;
default: return `npx ${commandWithArgs}`;
}
}
/**
* Returns the command and arguments as an array for use with execa's $ template syntax.
* This avoids the need for shell: true and provides better escaping.
*
* @param packageManager - The selected package manager (e.g., 'npm', 'yarn', 'pnpm', 'bun').
* @param commandWithArgs - The command to run, including arguments (e.g., "prisma generate").
* @returns An array of [command, ...args] (e.g., ["npx", "prisma", "generate"]).
*/
function getPackageExecutionArgs(packageManager, commandWithArgs) {
const args = splitCommandArgs(commandWithArgs);
switch (packageManager) {
case "pnpm": return [
"pnpm",
"dlx",
...args
];
case "bun": return ["bunx", ...args];
default: return ["npx", ...args];
}
}
/**
* Returns just the runner prefix as an array, for when you already have args built.
* Use this when you have complex arguments that shouldn't be split by spaces.
*
* @param packageManager - The selected package manager.
* @returns The runner prefix as an array (e.g., ["npx"] or ["pnpm", "dlx"]).
*
* @example
* const prefix = getPackageRunnerPrefix("bun");
* const args = ["@tauri-apps/cli@latest", "init", "--app-name=foo"];
* await $`${[...prefix, ...args]}`;
*/
function getPackageRunnerPrefix(packageManager) {
switch (packageManager) {
case "pnpm": return ["pnpm", "dlx"];
case "bun": return ["bunx"];
default: return ["npx"];
}
}
//#endregion
//#region src/helpers/addons/fumadocs-setup.ts
const TEMPLATES$2 = {
"next-mdx": {
label: "Next.js: Fumadocs MDX",
hint: "Recommended template with MDX support",
value: "+next+fuma-do