mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat(cli): add vibe rules addon (#481)
This commit is contained in:
5
.changeset/smooth-cows-enter.md
Normal file
5
.changeset/smooth-cows-enter.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"create-better-t-stack": minor
|
||||
---
|
||||
|
||||
add vibe-rules addon with a better t stack rules file
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -37,3 +37,5 @@ yarn-error.log*
|
||||
*.pem
|
||||
.vscode
|
||||
.env*.local
|
||||
|
||||
.smoke
|
||||
@@ -135,6 +135,7 @@ export const ADDON_COMPATIBILITY: Record<Addons, readonly Frontend[]> = {
|
||||
turborepo: [],
|
||||
starlight: [],
|
||||
ultracite: [],
|
||||
"vibe-rules": [],
|
||||
oxlint: [],
|
||||
fumadocs: [],
|
||||
none: [],
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import path from "node:path";
|
||||
import { cancel, isCancel, log, select, spinner, text } from "@clack/prompts";
|
||||
import { isCancel, log, select, spinner, text } from "@clack/prompts";
|
||||
import { consola } from "consola";
|
||||
import { execa } from "execa";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { PackageManager, ProjectConfig } from "../../types";
|
||||
import { exitCancelled } from "../../utils/errors";
|
||||
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||
import {
|
||||
addEnvVariablesToFile,
|
||||
@@ -177,10 +178,7 @@ export async function setupNeonPostgres(config: ProjectConfig) {
|
||||
initialValue: "neondb",
|
||||
});
|
||||
|
||||
if (isCancel(setupMethod)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(setupMethod)) return exitCancelled("Operation cancelled");
|
||||
|
||||
if (setupMethod === "neondb") {
|
||||
await setupWithNeonDb(projectDir, packageManager);
|
||||
@@ -198,10 +196,8 @@ export async function setupNeonPostgres(config: ProjectConfig) {
|
||||
initialValue: NEON_REGIONS[0].value,
|
||||
});
|
||||
|
||||
if (isCancel(projectName) || isCancel(regionId)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(projectName) || isCancel(regionId))
|
||||
return exitCancelled("Operation cancelled");
|
||||
|
||||
const neonConfig = await createNeonProject(
|
||||
projectName as string,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import path from "node:path";
|
||||
import { cancel, isCancel, log, select, text } from "@clack/prompts";
|
||||
import { isCancel, log, select, text } from "@clack/prompts";
|
||||
import { consola } from "consola";
|
||||
import { execa } from "execa";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { ORM, PackageManager, ProjectConfig } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
import { exitCancelled } from "../../utils/errors";
|
||||
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||
import {
|
||||
addEnvVariablesToFile,
|
||||
@@ -60,10 +61,7 @@ async function setupWithCreateDb(
|
||||
},
|
||||
});
|
||||
|
||||
if (isCancel(databaseUrl)) {
|
||||
cancel("Database setup cancelled");
|
||||
return null;
|
||||
}
|
||||
if (isCancel(databaseUrl)) return null;
|
||||
|
||||
return {
|
||||
databaseUrl: databaseUrl as string,
|
||||
@@ -115,10 +113,7 @@ async function initPrismaDatabase(
|
||||
},
|
||||
});
|
||||
|
||||
if (isCancel(databaseUrl)) {
|
||||
cancel("Database setup cancelled");
|
||||
return null;
|
||||
}
|
||||
if (isCancel(databaseUrl)) return null;
|
||||
|
||||
return {
|
||||
databaseUrl: databaseUrl as string,
|
||||
@@ -245,10 +240,7 @@ export async function setupPrismaPostgres(config: ProjectConfig) {
|
||||
initialValue: "create-db",
|
||||
});
|
||||
|
||||
if (isCancel(setupMethod)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(setupMethod)) return exitCancelled("Operation cancelled");
|
||||
|
||||
let prismaConfig: PrismaConfig | null = null;
|
||||
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
cancel,
|
||||
confirm,
|
||||
isCancel,
|
||||
log,
|
||||
select,
|
||||
spinner,
|
||||
text,
|
||||
} from "@clack/prompts";
|
||||
import { confirm, isCancel, log, select, spinner, text } from "@clack/prompts";
|
||||
import consola from "consola";
|
||||
import { $ } from "execa";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { commandExists } from "../../utils/command-exists";
|
||||
import { exitCancelled } from "../../utils/errors";
|
||||
import {
|
||||
addEnvVariablesToFile,
|
||||
type EnvVariable,
|
||||
@@ -129,10 +122,7 @@ async function selectTursoGroup(): Promise<string | null> {
|
||||
options: groupOptions,
|
||||
});
|
||||
|
||||
if (isCancel(selectedGroup)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(selectedGroup)) return exitCancelled("Operation cancelled");
|
||||
|
||||
return selectedGroup as string;
|
||||
}
|
||||
@@ -236,10 +226,7 @@ export async function setupTurso(config: ProjectConfig) {
|
||||
initialValue: true,
|
||||
});
|
||||
|
||||
if (isCancel(shouldInstall)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(shouldInstall)) return exitCancelled("Operation cancelled");
|
||||
|
||||
if (!shouldInstall) {
|
||||
await writeEnvFile(projectDir);
|
||||
@@ -269,10 +256,7 @@ export async function setupTurso(config: ProjectConfig) {
|
||||
placeholder: suggestedName,
|
||||
});
|
||||
|
||||
if (isCancel(dbNameResponse)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(dbNameResponse)) return exitCancelled("Operation cancelled");
|
||||
|
||||
dbName = dbNameResponse as string;
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import path from "node:path";
|
||||
import { cancel, log } from "@clack/prompts";
|
||||
import { log } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import type { AddInput, Addons, ProjectConfig } from "../../types";
|
||||
import { validateAddonCompatibility } from "../../utils/addon-compatibility";
|
||||
import { updateBtsConfig } from "../../utils/bts-config";
|
||||
import { exitWithError } from "../../utils/errors";
|
||||
import { setupAddons } from "../setup/addons-setup";
|
||||
import {
|
||||
detectProjectConfig,
|
||||
@@ -12,11 +13,6 @@ import {
|
||||
import { installDependencies } from "./install-dependencies";
|
||||
import { setupAddonsTemplate } from "./template-manager";
|
||||
|
||||
function exitWithError(message: string): never {
|
||||
cancel(pc.red(message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export async function addAddonsToProject(
|
||||
input: AddInput & { addons: Addons[]; suppressInstallMessage?: boolean },
|
||||
) {
|
||||
@@ -71,10 +67,6 @@ export async function addAddonsToProject(
|
||||
}
|
||||
}
|
||||
|
||||
log.info(
|
||||
`Adding ${input.addons.join(", ")} to ${config.frontend.join("/")}`,
|
||||
);
|
||||
|
||||
await setupAddonsTemplate(projectDir, config);
|
||||
await setupAddons(config, true);
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import path from "node:path";
|
||||
import { cancel, log } from "@clack/prompts";
|
||||
import { log } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import type { AddInput, ProjectConfig, WebDeploy } from "../../types";
|
||||
import { updateBtsConfig } from "../../utils/bts-config";
|
||||
import { exitWithError } from "../../utils/errors";
|
||||
import { setupWebDeploy } from "../setup/web-deploy-setup";
|
||||
import {
|
||||
detectProjectConfig,
|
||||
@@ -11,11 +12,6 @@ import {
|
||||
import { installDependencies } from "./install-dependencies";
|
||||
import { setupDeploymentTemplates } from "./template-manager";
|
||||
|
||||
function exitWithError(message: string): never {
|
||||
cancel(pc.red(message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export async function addDeploymentToProject(
|
||||
input: AddInput & { webDeploy: WebDeploy; suppressInstallMessage?: boolean },
|
||||
) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from "node:path";
|
||||
import { cancel, intro, log, outro } from "@clack/prompts";
|
||||
import { intro, log, outro } from "@clack/prompts";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../../constants";
|
||||
@@ -10,6 +10,7 @@ import { getDeploymentToAdd } from "../../prompts/web-deploy";
|
||||
import type { AddInput, CreateInput, ProjectConfig } from "../../types";
|
||||
import { trackProjectCreation } from "../../utils/analytics";
|
||||
import { displayConfig } from "../../utils/display-config";
|
||||
import { exitWithError, handleError } from "../../utils/errors";
|
||||
import { generateReproducibleCommand } from "../../utils/generate-reproducible-command";
|
||||
import {
|
||||
handleDirectoryConflict,
|
||||
@@ -131,8 +132,7 @@ export async function createProjectHandler(
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
handleError(error, "Failed to create project");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,12 +142,9 @@ export async function addAddonsHandler(input: AddInput) {
|
||||
const detectedConfig = await detectProjectConfig(projectDir);
|
||||
|
||||
if (!detectedConfig) {
|
||||
cancel(
|
||||
pc.red(
|
||||
"Could not detect project configuration. Please ensure this is a valid Better-T Stack project.",
|
||||
),
|
||||
exitWithError(
|
||||
"Could not detect project configuration. Please ensure this is a valid Better-T Stack project.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!input.addons || input.addons.length === 0) {
|
||||
@@ -215,7 +212,6 @@ export async function addAddonsHandler(input: AddInput) {
|
||||
|
||||
outro("Add command completed successfully!");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
handleError(error, "Failed to add addons or deployment");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { cancel, log } from "@clack/prompts";
|
||||
import { log } from "@clack/prompts";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { writeBtsConfig } from "../../utils/bts-config";
|
||||
import { exitWithError } from "../../utils/errors";
|
||||
import { setupAddons } from "../setup/addons-setup";
|
||||
import { setupApi } from "../setup/api-setup";
|
||||
import { setupAuth } from "../setup/auth-setup";
|
||||
@@ -104,13 +104,11 @@ export async function createProject(options: ProjectConfig) {
|
||||
return projectDir;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
cancel(pc.red(`Error during project creation: ${error.message}`));
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
exitWithError(`Error during project creation: ${error.message}`);
|
||||
} else {
|
||||
cancel(pc.red(`An unexpected error occurred: ${String(error)}`));
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
exitWithError(`An unexpected error occurred: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,6 +535,8 @@ export async function setupAddonsTemplate(
|
||||
for (const addon of context.addons) {
|
||||
if (addon === "none") continue;
|
||||
|
||||
if (addon === "vibe-rules") continue;
|
||||
|
||||
let addonSrcDir = path.join(PKG_ROOT, `templates/addons/${addon}`);
|
||||
let addonDestDir = projectDir;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { setupFumadocs } from "./fumadocs-setup";
|
||||
import { setupStarlight } from "./starlight-setup";
|
||||
import { setupTauri } from "./tauri-setup";
|
||||
import { setupUltracite } from "./ultracite-setup";
|
||||
import { setupVibeRules } from "./vibe-rules-setup";
|
||||
import { addPwaToViteConfig } from "./vite-pwa-setup";
|
||||
|
||||
export async function setupAddons(config: ProjectConfig, isAddCommand = false) {
|
||||
@@ -85,6 +86,10 @@ ${pc.cyan("Docs:")} ${pc.underline("https://turborepo.com/docs")}
|
||||
if (addons.includes("starlight")) {
|
||||
await setupStarlight(config);
|
||||
}
|
||||
|
||||
if (addons.includes("vibe-rules")) {
|
||||
await setupVibeRules(config);
|
||||
}
|
||||
if (addons.includes("fumadocs")) {
|
||||
await setupFumadocs(config);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import path from "node:path";
|
||||
import { cancel, isCancel, log, select } from "@clack/prompts";
|
||||
import { isCancel, log, select } from "@clack/prompts";
|
||||
import consola from "consola";
|
||||
import { execa } from "execa";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { exitCancelled } from "../../utils/errors";
|
||||
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||
|
||||
type FumadocsTemplate =
|
||||
@@ -52,10 +53,7 @@ export async function setupFumadocs(config: ProjectConfig) {
|
||||
initialValue: "next-mdx",
|
||||
});
|
||||
|
||||
if (isCancel(template)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(template)) return exitCancelled("Operation cancelled");
|
||||
|
||||
const templateArg = TEMPLATES[template].value;
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { cancel, isCancel, log, multiselect } from "@clack/prompts";
|
||||
import { isCancel, log, multiselect } from "@clack/prompts";
|
||||
import { execa } from "execa";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
import { exitCancelled } from "../../utils/errors";
|
||||
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||
import { setupBiome } from "./addons-setup";
|
||||
|
||||
@@ -71,10 +72,7 @@ export async function setupUltracite(config: ProjectConfig, hasHusky: boolean) {
|
||||
required: false,
|
||||
});
|
||||
|
||||
if (isCancel(editors)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(editors)) return exitCancelled("Operation cancelled");
|
||||
|
||||
const rules = await multiselect<UltraciteRule>({
|
||||
message: "Choose rules",
|
||||
@@ -86,10 +84,7 @@ export async function setupUltracite(config: ProjectConfig, hasHusky: boolean) {
|
||||
required: false,
|
||||
});
|
||||
|
||||
if (isCancel(rules)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(rules)) return exitCancelled("Operation cancelled");
|
||||
|
||||
const ultraciteArgs = ["init", "--pm", packageManager];
|
||||
|
||||
|
||||
112
apps/cli/src/helpers/setup/vibe-rules-setup.ts
Normal file
112
apps/cli/src/helpers/setup/vibe-rules-setup.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import path from "node:path";
|
||||
import { isCancel, log, multiselect, spinner } from "@clack/prompts";
|
||||
import { execa } from "execa";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import { PKG_ROOT } from "../../constants";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { exitCancelled } from "../../utils/errors";
|
||||
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||
import { processTemplate } from "../../utils/template-processor";
|
||||
|
||||
export async function setupVibeRules(config: ProjectConfig) {
|
||||
const { packageManager, projectDir } = config;
|
||||
|
||||
try {
|
||||
log.info("Setting up vibe-rules...");
|
||||
|
||||
const rulesDir = path.join(projectDir, ".bts");
|
||||
const ruleFile = path.join(rulesDir, "rules.md");
|
||||
if (!(await fs.pathExists(ruleFile))) {
|
||||
const templatePath = path.join(
|
||||
PKG_ROOT,
|
||||
"templates",
|
||||
"addons",
|
||||
"vibe-rules",
|
||||
".bts",
|
||||
"rules.md.hbs",
|
||||
);
|
||||
if (await fs.pathExists(templatePath)) {
|
||||
await fs.ensureDir(rulesDir);
|
||||
await processTemplate(templatePath, ruleFile, config);
|
||||
} else {
|
||||
log.error(pc.red("Rules template not found for vibe-rules addon"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const EDITORS = {
|
||||
cursor: { label: "Cursor", hint: ".cursor/rules/*.mdc" },
|
||||
windsurf: { label: "Windsurf", hint: ".windsurfrules" },
|
||||
"claude-code": { label: "Claude Code", hint: "CLAUDE.md" },
|
||||
vscode: {
|
||||
label: "VSCode",
|
||||
hint: ".github/instructions/*.instructions.md",
|
||||
},
|
||||
gemini: { label: "Gemini", hint: "GEMINI.md" },
|
||||
codex: { label: "Codex", hint: "AGENTS.md" },
|
||||
clinerules: { label: "Cline/Roo", hint: ".clinerules/*.md" },
|
||||
roo: { label: "Roo", hint: ".clinerules/*.md" },
|
||||
zed: { label: "Zed", hint: ".rules/*.md" },
|
||||
unified: { label: "Unified", hint: ".rules/*.md" },
|
||||
} as const;
|
||||
|
||||
const selectedEditors = await multiselect<keyof typeof EDITORS>({
|
||||
message: "Choose editors to install BTS rule",
|
||||
options: Object.entries(EDITORS).map(([key, v]) => ({
|
||||
value: key as keyof typeof EDITORS,
|
||||
label: v.label,
|
||||
hint: v.hint,
|
||||
})),
|
||||
required: false,
|
||||
});
|
||||
|
||||
if (isCancel(selectedEditors)) return exitCancelled("Operation cancelled");
|
||||
|
||||
const editorsArg = selectedEditors.join(", ");
|
||||
const s = spinner();
|
||||
s.start("Saving and applying BTS rules...");
|
||||
|
||||
try {
|
||||
const saveCmd = getPackageExecutionCommand(
|
||||
packageManager,
|
||||
`vibe-rules@latest save bts -f ${JSON.stringify(
|
||||
path.relative(projectDir, ruleFile),
|
||||
)}`,
|
||||
);
|
||||
await execa(saveCmd, {
|
||||
cwd: projectDir,
|
||||
env: { CI: "true" },
|
||||
shell: true,
|
||||
});
|
||||
|
||||
for (const editor of selectedEditors) {
|
||||
const loadCmd = getPackageExecutionCommand(
|
||||
packageManager,
|
||||
`vibe-rules@latest load bts ${editor}`,
|
||||
);
|
||||
await execa(loadCmd, {
|
||||
cwd: projectDir,
|
||||
env: { CI: "true" },
|
||||
shell: true,
|
||||
});
|
||||
}
|
||||
|
||||
s.stop(`Applied BTS rules to: ${editorsArg}`);
|
||||
} catch (error) {
|
||||
s.stop(pc.red("Failed to apply BTS rules"));
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.remove(rulesDir);
|
||||
} catch (_) {}
|
||||
|
||||
log.success("vibe-rules setup successfully!");
|
||||
} catch (error) {
|
||||
log.error(pc.red("Failed to set up vibe-rules"));
|
||||
if (error instanceof Error) {
|
||||
console.error(pc.red(error.message));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { intro, log } from "@clack/prompts";
|
||||
import { consola } from "consola";
|
||||
import pc from "picocolors";
|
||||
import { createCli, trpcServer } from "trpc-cli";
|
||||
import z from "zod";
|
||||
@@ -21,6 +20,7 @@ import {
|
||||
RuntimeSchema,
|
||||
WebDeploySchema,
|
||||
} from "./types";
|
||||
import { handleError } from "./utils/errors";
|
||||
import { getLatestCLIVersion } from "./utils/get-latest-cli-version";
|
||||
import { openUrl } from "./utils/open-url";
|
||||
import { renderTitle } from "./utils/render-title";
|
||||
@@ -102,8 +102,7 @@ const router = t.router({
|
||||
const sponsors = await fetchSponsors();
|
||||
displaySponsors(sponsors);
|
||||
} catch (error) {
|
||||
consola.error(error);
|
||||
process.exit(1);
|
||||
handleError(error, "Failed to display sponsors");
|
||||
}
|
||||
}),
|
||||
docs: t.procedure
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { cancel, groupMultiselect, isCancel } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { groupMultiselect, isCancel } from "@clack/prompts";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import { type Addons, AddonsSchema, type Frontend } from "../types";
|
||||
import {
|
||||
getCompatibleAddons,
|
||||
validateAddonCompatibility,
|
||||
} from "../utils/addon-compatibility";
|
||||
import { exitCancelled } from "../utils/errors";
|
||||
|
||||
type AddonOption = {
|
||||
value: Addons;
|
||||
@@ -42,6 +42,10 @@ function getAddonDisplay(addon: Addons): { label: string; hint: string } {
|
||||
label = "Ultracite";
|
||||
hint = "Zero-config Biome preset with AI integration";
|
||||
break;
|
||||
case "vibe-rules":
|
||||
label = "vibe-rules";
|
||||
hint = "Install and apply BTS rules to editors";
|
||||
break;
|
||||
case "husky":
|
||||
label = "Husky";
|
||||
hint = "Modern native Git hooks made easy";
|
||||
@@ -65,7 +69,7 @@ function getAddonDisplay(addon: Addons): { label: string; hint: string } {
|
||||
const ADDON_GROUPS = {
|
||||
Documentation: ["starlight", "fumadocs"],
|
||||
Linting: ["biome", "oxlint", "ultracite"],
|
||||
Other: ["turborepo", "pwa", "tauri", "husky"],
|
||||
Other: ["vibe-rules", "turborepo", "pwa", "tauri", "husky"],
|
||||
};
|
||||
|
||||
export async function getAddonsChoice(
|
||||
@@ -119,10 +123,7 @@ export async function getAddonsChoice(
|
||||
selectableGroups: false,
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -175,10 +176,7 @@ export async function getAddonsToAdd(
|
||||
selectableGroups: false,
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { isCancel, select } from "@clack/prompts";
|
||||
import type { API, Backend, Frontend } from "../types";
|
||||
import { allowedApisForFrontends } from "../utils/compatibility-rules";
|
||||
import { exitCancelled } from "../utils/errors";
|
||||
|
||||
export async function getApiChoice(
|
||||
Api?: API | undefined,
|
||||
@@ -43,10 +43,7 @@ export async function getApiChoice(
|
||||
initialValue: apiOptions[0].value,
|
||||
});
|
||||
|
||||
if (isCancel(apiType)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(apiType)) return exitCancelled("Operation cancelled");
|
||||
|
||||
return apiType;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { cancel, confirm, isCancel } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { confirm, isCancel } from "@clack/prompts";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { Backend } from "../types";
|
||||
import { exitCancelled } from "../utils/errors";
|
||||
|
||||
export async function getAuthChoice(
|
||||
auth: boolean | undefined,
|
||||
@@ -21,10 +21,7 @@ export async function getAuthChoice(
|
||||
initialValue: DEFAULT_CONFIG.auth,
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { isCancel, select } from "@clack/prompts";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { Backend, Frontend } from "../types";
|
||||
import { exitCancelled } from "../utils/errors";
|
||||
|
||||
export async function getBackendFrameworkChoice(
|
||||
backendFramework?: Backend,
|
||||
@@ -63,10 +63,7 @@ export async function getBackendFrameworkChoice(
|
||||
initialValue: DEFAULT_CONFIG.backend,
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { cancel, group } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { group } from "@clack/prompts";
|
||||
import type {
|
||||
Addons,
|
||||
API,
|
||||
@@ -14,6 +13,7 @@ import type {
|
||||
Runtime,
|
||||
WebDeploy,
|
||||
} from "../types";
|
||||
import { exitCancelled } from "../utils/errors";
|
||||
import { getAddonsChoice } from "./addons";
|
||||
import { getApiChoice } from "./api";
|
||||
import { getAuthChoice } from "./auth";
|
||||
@@ -102,10 +102,7 @@ export async function gatherConfig(
|
||||
install: () => getinstallChoice(flags.install),
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
},
|
||||
onCancel: () => exitCancelled("Operation cancelled"),
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { isCancel, select } from "@clack/prompts";
|
||||
import type { Backend, DatabaseSetup, ORM, Runtime } from "../types";
|
||||
import { exitCancelled } from "../utils/errors";
|
||||
|
||||
export async function getDBSetupChoice(
|
||||
databaseType: string,
|
||||
@@ -101,10 +101,7 @@ export async function getDBSetupChoice(
|
||||
initialValue: "none",
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { isCancel, select } from "@clack/prompts";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { Backend, Database, Runtime } from "../types";
|
||||
import { exitCancelled } from "../utils/errors";
|
||||
|
||||
export async function getDatabaseChoice(
|
||||
database?: Database,
|
||||
@@ -55,10 +55,7 @@ export async function getDatabaseChoice(
|
||||
initialValue: DEFAULT_CONFIG.database,
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { cancel, isCancel, multiselect } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { isCancel, multiselect } from "@clack/prompts";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { API, Backend, Database, Examples, Frontend } from "../types";
|
||||
import {
|
||||
isExampleAIAllowed,
|
||||
isExampleTodoAllowed,
|
||||
} from "../utils/compatibility-rules";
|
||||
import { exitCancelled } from "../utils/errors";
|
||||
|
||||
export async function getExamplesChoice(
|
||||
examples?: Examples[],
|
||||
@@ -63,10 +63,7 @@ export async function getExamplesChoice(
|
||||
),
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { cancel, isCancel, multiselect, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { isCancel, multiselect, select } from "@clack/prompts";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { Backend, Frontend } from "../types";
|
||||
import { isFrontendAllowedWithBackend } from "../utils/compatibility-rules";
|
||||
import { exitCancelled } from "../utils/errors";
|
||||
|
||||
export async function getFrontendChoice(
|
||||
frontendOptions?: Frontend[],
|
||||
@@ -28,10 +28,7 @@ export async function getFrontendChoice(
|
||||
initialValues: ["web"],
|
||||
});
|
||||
|
||||
if (isCancel(frontendTypes)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(frontendTypes)) return exitCancelled("Operation cancelled");
|
||||
|
||||
const result: Frontend[] = [];
|
||||
|
||||
@@ -69,7 +66,7 @@ export async function getFrontendChoice(
|
||||
},
|
||||
{
|
||||
value: "tanstack-start" as const,
|
||||
label: "TanStack Start (vite)",
|
||||
label: "TanStack Start",
|
||||
hint: "SSR, Server Functions, API Routes and more with TanStack Router",
|
||||
},
|
||||
];
|
||||
@@ -84,10 +81,7 @@ export async function getFrontendChoice(
|
||||
initialValue: DEFAULT_CONFIG.frontend[0],
|
||||
});
|
||||
|
||||
if (isCancel(webFramework)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(webFramework)) return exitCancelled("Operation cancelled");
|
||||
|
||||
result.push(webFramework);
|
||||
}
|
||||
@@ -110,10 +104,7 @@ export async function getFrontendChoice(
|
||||
initialValue: "native-nativewind",
|
||||
});
|
||||
|
||||
if (isCancel(nativeFramework)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(nativeFramework)) return exitCancelled("Operation cancelled");
|
||||
result.push(nativeFramework);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cancel, confirm, isCancel } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { confirm, isCancel } from "@clack/prompts";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import { exitCancelled } from "../utils/errors";
|
||||
|
||||
export async function getGitChoice(git?: boolean) {
|
||||
if (git !== undefined) return git;
|
||||
@@ -10,10 +10,7 @@ export async function getGitChoice(git?: boolean) {
|
||||
initialValue: DEFAULT_CONFIG.git,
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cancel, confirm, isCancel } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { confirm, isCancel } from "@clack/prompts";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import { exitCancelled } from "../utils/errors";
|
||||
|
||||
export async function getinstallChoice(install?: boolean) {
|
||||
if (install !== undefined) return install;
|
||||
@@ -10,10 +10,7 @@ export async function getinstallChoice(install?: boolean) {
|
||||
initialValue: DEFAULT_CONFIG.install,
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { isCancel, select } from "@clack/prompts";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { Backend, Database, ORM, Runtime } from "../types";
|
||||
import { exitCancelled } from "../utils/errors";
|
||||
|
||||
const ormOptions = {
|
||||
prisma: {
|
||||
@@ -51,10 +51,7 @@ export async function getORMChoice(
|
||||
initialValue: database === "mongodb" ? "prisma" : DEFAULT_CONFIG.orm,
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { isCancel, select } from "@clack/prompts";
|
||||
import type { PackageManager } from "../types";
|
||||
import { exitCancelled } from "../utils/errors";
|
||||
import { getUserPkgManager } from "../utils/get-package-manager";
|
||||
|
||||
export async function getPackageManagerChoice(
|
||||
@@ -28,10 +28,7 @@ export async function getPackageManagerChoice(
|
||||
initialValue: detectedPackageManager,
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import path from "node:path";
|
||||
import { cancel, isCancel, text } from "@clack/prompts";
|
||||
import { isCancel, text } from "@clack/prompts";
|
||||
import consola from "consola";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import { ProjectNameSchema } from "../types";
|
||||
import { exitCancelled } from "../utils/errors";
|
||||
|
||||
function isPathWithinCwd(targetPath: string): boolean {
|
||||
const resolved = path.resolve(targetPath);
|
||||
@@ -76,10 +77,7 @@ export async function getProjectName(initialName?: string): Promise<string> {
|
||||
},
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
cancel(pc.red("Operation cancelled."));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(response)) return exitCancelled("Operation cancelled.");
|
||||
|
||||
projectPath = response || defaultName;
|
||||
isValid = true;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { isCancel, select } from "@clack/prompts";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { Backend, Runtime } from "../types";
|
||||
import { exitCancelled } from "../utils/errors";
|
||||
|
||||
export async function getRuntimeChoice(
|
||||
runtime?: Runtime,
|
||||
@@ -48,10 +48,7 @@ export async function getRuntimeChoice(
|
||||
initialValue: DEFAULT_CONFIG.runtime,
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { isCancel, select } from "@clack/prompts";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { Backend, Frontend, Runtime, WebDeploy } from "../types";
|
||||
import { WEB_FRAMEWORKS } from "../utils/compatibility";
|
||||
import { exitCancelled } from "../utils/errors";
|
||||
|
||||
function hasWebFrontend(frontends: Frontend[]): boolean {
|
||||
return frontends.some((f) => WEB_FRAMEWORKS.includes(f));
|
||||
@@ -56,10 +56,7 @@ export async function getDeploymentChoice(
|
||||
initialValue: DEFAULT_CONFIG.webDeploy,
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -105,10 +102,7 @@ export async function getDeploymentToAdd(
|
||||
initialValue: DEFAULT_CONFIG.webDeploy,
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ export const AddonsSchema = z
|
||||
"starlight",
|
||||
"biome",
|
||||
"husky",
|
||||
"vibe-rules",
|
||||
"turborepo",
|
||||
"fumadocs",
|
||||
"ultracite",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { consola } from "consola";
|
||||
import type {
|
||||
Addons,
|
||||
API,
|
||||
@@ -9,6 +8,7 @@ import type {
|
||||
} from "../types";
|
||||
import { validateAddonCompatibility } from "./addon-compatibility";
|
||||
import { WEB_FRAMEWORKS } from "./compatibility";
|
||||
import { exitWithError } from "./errors";
|
||||
|
||||
export function isWebFrontend(value: Frontend): boolean {
|
||||
return WEB_FRAMEWORKS.includes(value);
|
||||
@@ -28,16 +28,14 @@ export function splitFrontends(values: Frontend[] = []): {
|
||||
export function ensureSingleWebAndNative(frontends: Frontend[]) {
|
||||
const { web, native } = splitFrontends(frontends);
|
||||
if (web.length > 1) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (native.length > 1) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"Cannot select multiple native frameworks. Choose only one of: native-nativewind, native-unistyles",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,10 +50,9 @@ export function validateWorkersCompatibility(
|
||||
config.backend &&
|
||||
config.backend !== "hono"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
`Cloudflare Workers runtime (--runtime workers) is only supported with Hono backend (--backend hono). Current backend: ${config.backend}. Please use '--backend hono' or choose a different runtime.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -64,10 +61,9 @@ export function validateWorkersCompatibility(
|
||||
config.backend !== "hono" &&
|
||||
config.runtime === "workers"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
`Backend '${config.backend}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Hono backend. Please use '--backend hono' or choose a different runtime.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -77,10 +73,9 @@ export function validateWorkersCompatibility(
|
||||
config.orm !== "drizzle" &&
|
||||
config.orm !== "none"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
`Cloudflare Workers runtime (--runtime workers) is only supported with Drizzle ORM (--orm drizzle) or no ORM (--orm none). Current ORM: ${config.orm}. Please use '--orm drizzle', '--orm none', or choose a different runtime.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -90,10 +85,9 @@ export function validateWorkersCompatibility(
|
||||
config.orm !== "none" &&
|
||||
config.runtime === "workers"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
`ORM '${config.orm}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Drizzle ORM or no ORM. Please use '--orm drizzle', '--orm none', or choose a different runtime.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -101,10 +95,9 @@ export function validateWorkersCompatibility(
|
||||
options.runtime === "workers" &&
|
||||
config.database === "mongodb"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -112,10 +105,9 @@ export function validateWorkersCompatibility(
|
||||
options.runtime === "workers" &&
|
||||
config.dbSetup === "docker"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"Cloudflare Workers runtime (--runtime workers) is not compatible with Docker setup. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -123,10 +115,9 @@ export function validateWorkersCompatibility(
|
||||
config.database === "mongodb" &&
|
||||
config.runtime === "workers"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -134,10 +125,9 @@ export function validateWorkersCompatibility(
|
||||
options.dbSetup === "docker" &&
|
||||
config.runtime === "workers"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"Docker setup (--db-setup docker) is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,10 +199,9 @@ export function validateApiFrontendCompatibility(
|
||||
const includesSvelte = frontends.includes("svelte");
|
||||
const includesSolid = frontends.includes("solid");
|
||||
if ((includesNuxt || includesSvelte || includesSolid) && api === "trpc") {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
`tRPC API is not supported with '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"}' frontend. Please use --api orpc or --api none or remove '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"}' from --frontend.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,10 +246,9 @@ export function validateWebDeployRequiresWebFrontend(
|
||||
hasWebFrontendFlag: boolean,
|
||||
) {
|
||||
if (webDeploy && webDeploy !== "none" && !hasWebFrontendFlag) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,8 +263,7 @@ export function validateAddonsAgainstFrontends(
|
||||
frontends,
|
||||
);
|
||||
if (!isCompatible) {
|
||||
consola.fatal(`Incompatible addon/frontend combination: ${reason}`);
|
||||
process.exit(1);
|
||||
exitWithError(`Incompatible addon/frontend combination: ${reason}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,21 +282,18 @@ export function validateExamplesCompatibility(
|
||||
backend !== "none" &&
|
||||
database === "none"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"The 'todo' example requires a database if a backend (other than Convex) is present. Cannot use --examples todo when database is 'none' and a backend is selected.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (examplesArr.includes("ai") && backend === "elysia") {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"The 'ai' example is not compatible with the Elysia backend.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (examplesArr.includes("ai") && (frontend ?? []).includes("solid")) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"The 'ai' example is not compatible with the Solid frontend.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
20
apps/cli/src/utils/errors.ts
Normal file
20
apps/cli/src/utils/errors.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { cancel } from "@clack/prompts";
|
||||
import { consola } from "consola";
|
||||
import pc from "picocolors";
|
||||
|
||||
export function exitWithError(message: string): never {
|
||||
consola.error(pc.red(message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export function exitCancelled(message = "Operation cancelled"): never {
|
||||
cancel(pc.red(message));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
export function handleError(error: unknown, fallbackMessage?: string): never {
|
||||
const message =
|
||||
error instanceof Error ? error.message : fallbackMessage || String(error);
|
||||
consola.error(pc.red(message));
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import path from "node:path";
|
||||
import { cancel, isCancel, log, select, spinner } from "@clack/prompts";
|
||||
import { consola } from "consola";
|
||||
import { isCancel, log, select, spinner } from "@clack/prompts";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import { getProjectName } from "../prompts/project-name";
|
||||
import { exitCancelled, handleError } from "./errors";
|
||||
|
||||
export async function handleDirectoryConflict(
|
||||
currentPathInput: string,
|
||||
@@ -49,10 +49,7 @@ export async function handleDirectoryConflict(
|
||||
initialValue: "rename",
|
||||
});
|
||||
|
||||
if (isCancel(action)) {
|
||||
cancel(pc.red("Operation cancelled."));
|
||||
process.exit(0);
|
||||
}
|
||||
if (isCancel(action)) return exitCancelled("Operation cancelled.");
|
||||
|
||||
switch (action) {
|
||||
case "overwrite":
|
||||
@@ -73,8 +70,7 @@ export async function handleDirectoryConflict(
|
||||
return await handleDirectoryConflict(newPathInput);
|
||||
}
|
||||
case "cancel":
|
||||
cancel(pc.red("Operation cancelled."));
|
||||
process.exit(0);
|
||||
return exitCancelled("Operation cancelled.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,8 +98,7 @@ export async function setupProjectDirectory(
|
||||
s.stop(`Directory "${finalResolvedPath}" cleared.`);
|
||||
} catch (error) {
|
||||
s.stop(pc.red(`Failed to clear directory "${finalResolvedPath}".`));
|
||||
consola.error(error);
|
||||
process.exit(1);
|
||||
handleError(error);
|
||||
}
|
||||
} else {
|
||||
await fs.ensureDir(finalResolvedPath);
|
||||
|
||||
@@ -29,6 +29,7 @@ export async function processTemplate(
|
||||
}
|
||||
|
||||
handlebars.registerHelper("eq", (a, b) => a === b);
|
||||
handlebars.registerHelper("ne", (a, b) => a !== b);
|
||||
handlebars.registerHelper("and", (a, b) => a && b);
|
||||
handlebars.registerHelper("or", (a, b) => a || b);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import path from "node:path";
|
||||
import { consola } from "consola";
|
||||
import {
|
||||
type Addons,
|
||||
type API,
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
validateWebDeployRequiresWebFrontend,
|
||||
validateWorkersCompatibility,
|
||||
} from "./utils/compatibility-rules";
|
||||
import { exitWithError } from "./utils/errors";
|
||||
|
||||
export function processAndValidateFlags(
|
||||
options: CLIInput,
|
||||
@@ -43,10 +43,9 @@ export function processAndValidateFlags(
|
||||
!(options.examples.length === 1 && options.examples[0] === "none") &&
|
||||
options.backend !== "convex"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,10 +61,9 @@ export function processAndValidateFlags(
|
||||
config.backend !== "none"
|
||||
) {
|
||||
if (providedFlags.has("runtime") && options.runtime === "none") {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
`'--runtime none' is only supported with '--backend convex' or '--backend none'. Please choose 'bun', 'node', or remove the --runtime flag.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,12 +99,11 @@ export function processAndValidateFlags(
|
||||
if (projectName) {
|
||||
const result = ProjectNameSchema.safeParse(path.basename(projectName));
|
||||
if (!result.success) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
`Invalid project name: ${
|
||||
result.error.issues[0]?.message || "Invalid project name"
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.projectName = projectName;
|
||||
} else if (options.projectDirectory) {
|
||||
@@ -115,12 +112,11 @@ export function processAndValidateFlags(
|
||||
);
|
||||
const result = ProjectNameSchema.safeParse(baseName);
|
||||
if (!result.success) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
`Invalid project name: ${
|
||||
result.error.issues[0]?.message || "Invalid project name"
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.projectName = baseName;
|
||||
}
|
||||
@@ -128,8 +124,7 @@ export function processAndValidateFlags(
|
||||
if (options.frontend && options.frontend.length > 0) {
|
||||
if (options.frontend.includes("none")) {
|
||||
if (options.frontend.length > 1) {
|
||||
consola.fatal(`Cannot combine 'none' with other frontend options.`);
|
||||
process.exit(1);
|
||||
exitWithError(`Cannot combine 'none' with other frontend options.`);
|
||||
}
|
||||
config.frontend = [];
|
||||
} else {
|
||||
@@ -153,8 +148,7 @@ export function processAndValidateFlags(
|
||||
if (options.addons && options.addons.length > 0) {
|
||||
if (options.addons.includes("none")) {
|
||||
if (options.addons.length > 1) {
|
||||
consola.fatal(`Cannot combine 'none' with other addons.`);
|
||||
process.exit(1);
|
||||
exitWithError(`Cannot combine 'none' with other addons.`);
|
||||
}
|
||||
config.addons = [];
|
||||
} else {
|
||||
@@ -166,8 +160,7 @@ export function processAndValidateFlags(
|
||||
if (options.examples && options.examples.length > 0) {
|
||||
if (options.examples.includes("none")) {
|
||||
if (options.examples.length > 1) {
|
||||
consola.fatal("Cannot combine 'none' with other examples.");
|
||||
process.exit(1);
|
||||
exitWithError("Cannot combine 'none' with other examples.");
|
||||
}
|
||||
config.examples = [];
|
||||
} else {
|
||||
@@ -187,12 +180,11 @@ export function processAndValidateFlags(
|
||||
options,
|
||||
);
|
||||
if (incompatibleFlags.length > 0) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
`The following flags are incompatible with '--backend ${config.backend}': ${incompatibleFlags.join(
|
||||
", ",
|
||||
)}. Please remove them.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -204,12 +196,11 @@ export function processAndValidateFlags(
|
||||
(f) => f === "solid",
|
||||
);
|
||||
if (incompatibleFrontends.length > 0) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(
|
||||
", ",
|
||||
)}. Please choose a different frontend or backend.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,10 +213,9 @@ export function processAndValidateFlags(
|
||||
config.orm === "mongoose" &&
|
||||
config.database !== "mongodb"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -236,10 +226,9 @@ export function processAndValidateFlags(
|
||||
config.orm !== "mongoose" &&
|
||||
config.orm !== "prisma"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"MongoDB database requires Mongoose or Prisma ORM. Please use '--orm mongoose' or '--orm prisma' or choose a different database.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -248,10 +237,9 @@ export function processAndValidateFlags(
|
||||
config.orm === "drizzle" &&
|
||||
config.database === "mongodb"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -261,10 +249,9 @@ export function processAndValidateFlags(
|
||||
config.database !== "none" &&
|
||||
config.orm === "none"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -274,10 +261,9 @@ export function processAndValidateFlags(
|
||||
config.orm !== "none" &&
|
||||
config.database === "none"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"ORM selection requires a database. Please choose a database or set '--orm none'.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -286,10 +272,9 @@ export function processAndValidateFlags(
|
||||
config.auth &&
|
||||
config.database === "none"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"Authentication requires a database. Please choose a database or set '--no-auth'.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -299,10 +284,9 @@ export function processAndValidateFlags(
|
||||
config.dbSetup !== "none" &&
|
||||
config.database === "none"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"Database setup requires a database. Please choose a database or set '--db-setup none'.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -311,10 +295,9 @@ export function processAndValidateFlags(
|
||||
config.dbSetup === "turso" &&
|
||||
config.database !== "sqlite"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -323,10 +306,9 @@ export function processAndValidateFlags(
|
||||
config.dbSetup === "neon" &&
|
||||
config.database !== "postgres"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -335,10 +317,9 @@ export function processAndValidateFlags(
|
||||
config.dbSetup === "prisma-postgres" &&
|
||||
config.database !== "postgres"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -347,10 +328,9 @@ export function processAndValidateFlags(
|
||||
config.dbSetup === "mongodb-atlas" &&
|
||||
config.database !== "mongodb"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -359,10 +339,9 @@ export function processAndValidateFlags(
|
||||
config.dbSetup === "supabase" &&
|
||||
config.database !== "postgres"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.dbSetup === "d1") {
|
||||
@@ -371,10 +350,9 @@ export function processAndValidateFlags(
|
||||
(providedFlags.has("dbSetup") && !config.database)
|
||||
) {
|
||||
if (config.database !== "sqlite") {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"Cloudflare D1 setup requires SQLite database. Please use '--database sqlite' or choose a different setup.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,10 +361,9 @@ export function processAndValidateFlags(
|
||||
(providedFlags.has("dbSetup") && !config.runtime)
|
||||
) {
|
||||
if (config.runtime !== "workers") {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"Cloudflare D1 setup requires the Cloudflare Workers runtime. Please use '--runtime workers' or choose a different setup.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -397,10 +374,9 @@ export function processAndValidateFlags(
|
||||
config.dbSetup === "docker" &&
|
||||
config.database === "sqlite"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"Docker setup is not compatible with SQLite database. SQLite is file-based and doesn't require Docker. Please use '--database postgres', '--database mysql', '--database mongodb', or choose a different setup.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -409,10 +385,9 @@ export function processAndValidateFlags(
|
||||
config.dbSetup === "docker" &&
|
||||
config.runtime === "workers"
|
||||
) {
|
||||
consola.fatal(
|
||||
exitWithError(
|
||||
"Docker setup is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
validateWorkersCompatibility(providedFlags, options, config);
|
||||
|
||||
132
apps/cli/templates/addons/vibe-rules/.bts/rules.md.hbs
Normal file
132
apps/cli/templates/addons/vibe-rules/.bts/rules.md.hbs
Normal file
@@ -0,0 +1,132 @@
|
||||
# Better-T-Stack Project Rules
|
||||
|
||||
This is a {{projectName}} project created with Better-T-Stack CLI.
|
||||
|
||||
## Project Structure
|
||||
|
||||
This is a monorepo with the following structure:
|
||||
|
||||
{{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "tanstack-start")
|
||||
(includes frontend "next") (includes frontend "nuxt") (includes frontend "svelte") (includes frontend "solid"))}}
|
||||
- **`apps/web/`** - Frontend application{{#if (includes frontend "tanstack-router")}} (React with TanStack Router){{else
|
||||
if (includes frontend "react-router")}} (React with React Router){{else if (includes frontend "next")}} (Next.js){{else
|
||||
if (includes frontend "nuxt")}} (Nuxt.js){{else if (includes frontend "svelte")}} (SvelteKit){{else if (includes
|
||||
frontend "solid")}} (SolidStart){{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if (ne backend "convex")}}
|
||||
{{#if (ne backend "none")}}
|
||||
- **`apps/server/`** - Backend server{{#if (eq backend "hono")}} (Hono){{else if (eq backend "express")}}
|
||||
(Express){{else if (eq backend "fastify")}} (Fastify){{else if (eq backend "elysia")}} (Elysia){{else if (eq backend
|
||||
"next")}} (Next.js API){{/if}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
- **`packages/backend/`** - Convex backend functions
|
||||
{{/if}}
|
||||
|
||||
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
||||
- **`apps/native/`** - React Native mobile app{{#if (includes frontend "native-nativewind")}} (with NativeWind){{else if
|
||||
(includes frontend "native-unistyles")}} (with Unistyles){{/if}}
|
||||
{{/if}}
|
||||
|
||||
## Available Scripts
|
||||
|
||||
- `{{packageManager}} run dev` - Start all apps in development mode
|
||||
{{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "tanstack-start")
|
||||
(includes frontend "next") (includes frontend "nuxt") (includes frontend "svelte") (includes frontend "solid"))}}
|
||||
- `{{packageManager}} run dev:web` - Start only the web app
|
||||
{{/if}}
|
||||
{{#if (ne backend "none")}}
|
||||
{{#if (ne backend "convex")}}
|
||||
- `{{packageManager}} run dev:server` - Start only the server
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
||||
- `{{packageManager}} run dev:native` - Start only the native app
|
||||
{{/if}}
|
||||
|
||||
{{#if (and (ne database "none") (ne orm "none") (ne backend "convex"))}}
|
||||
## Database Commands
|
||||
|
||||
All database operations should be run from the server workspace:
|
||||
|
||||
- `{{packageManager}} run db:push` - Push schema changes to database
|
||||
- `{{packageManager}} run db:studio` - Open database studio
|
||||
- `{{packageManager}} run db:generate` - Generate {{#if (eq orm "drizzle")}}Drizzle{{else if (eq orm
|
||||
"prisma")}}Prisma{{else}}{{orm}}{{/if}} files
|
||||
- `{{packageManager}} run db:migrate` - Run database migrations
|
||||
|
||||
{{#if (eq orm "drizzle")}}
|
||||
Database schema files are located in `apps/server/src/db/schema/`
|
||||
{{else if (eq orm "prisma")}}
|
||||
Database schema is located in `apps/server/prisma/schema.prisma`
|
||||
{{else if (eq orm "mongoose")}}
|
||||
Database models are located in `apps/server/src/db/models/`
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if (ne api "none")}}
|
||||
## API Structure
|
||||
|
||||
{{#if (eq api "trpc")}}
|
||||
- tRPC routers are in `apps/server/src/routers/`
|
||||
- Client-side tRPC utils are in `apps/web/src/utils/trpc.ts`
|
||||
{{else if (eq api "orpc")}}
|
||||
- oRPC endpoints are in `apps/server/src/api/`
|
||||
- Client-side API utils are in `apps/web/src/utils/api.ts`
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if auth}}
|
||||
## Authentication
|
||||
|
||||
Authentication is enabled in this project:
|
||||
{{#if (ne backend "convex")}}
|
||||
- Server auth logic is in `apps/server/src/lib/auth.ts`
|
||||
{{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "tanstack-start")
|
||||
(includes frontend "next") (includes frontend "nuxt") (includes frontend "svelte") (includes frontend "solid"))}}
|
||||
- Web app auth client is in `apps/web/src/lib/auth-client.ts`
|
||||
{{/if}}
|
||||
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
||||
- Native app auth client is in `apps/native/src/lib/auth-client.ts`
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
## Adding More Features
|
||||
|
||||
You can add additional addons or deployment options to your project using:
|
||||
|
||||
```bash
|
||||
{{#if (eq packageManager "bun")}}bunx{{else if (eq packageManager "pnpm")}}pnpx{{else}}npx{{/if}} create-better-t-stack
|
||||
add
|
||||
```
|
||||
|
||||
Available addons you can add:
|
||||
- **Documentation**: Starlight, Fumadocs
|
||||
- **Linting**: Biome, Oxlint, Ultracite
|
||||
- **Other**: vibe-rules, Turborepo, PWA, Tauri, Husky
|
||||
|
||||
You can also add web deployment configurations like Cloudflare Workers support.
|
||||
|
||||
## Project Configuration
|
||||
|
||||
This project includes a `bts.jsonc` configuration file that stores your Better-T-Stack settings:
|
||||
|
||||
- Contains your selected stack configuration (database, ORM, backend, frontend, etc.)
|
||||
- Used by the CLI to understand your project structure
|
||||
- Safe to delete if not needed
|
||||
- Updated automatically when using the `add` command
|
||||
|
||||
## Key Points
|
||||
|
||||
- This is a {{#if (includes addons "turborepo")}}Turborepo {{/if}}monorepo using {{packageManager}} workspaces
|
||||
- Each app has its own `package.json` and dependencies
|
||||
- Run commands from the root to execute across all workspaces
|
||||
- Run workspace-specific commands with `{{packageManager}} run command-name`
|
||||
{{#if (includes addons "turborepo")}}
|
||||
- Turborepo handles build caching and parallel execution
|
||||
{{/if}}
|
||||
- Use `{{#if (eq packageManager "bun")}}bunx{{else if (eq packageManager "pnpm")}}pnpx{{else}}npx{{/if}}
|
||||
create-better-t-stack add` to add more features later
|
||||
@@ -48,7 +48,7 @@ const showcaseProjects = [
|
||||
"https://screenshothis.com?utm_source=better-t-stack&utm_medium=showcase&utm_campaign=referer",
|
||||
tags: [
|
||||
"oRPC",
|
||||
"TanStack Start (vite)",
|
||||
"TanStack Start",
|
||||
"Hono",
|
||||
"pnpm",
|
||||
"PostgreSQL",
|
||||
|
||||
@@ -57,7 +57,7 @@ export const TECH_OPTIONS: Record<
|
||||
},
|
||||
{
|
||||
id: "tanstack-start",
|
||||
name: "TanStack Start (vite)",
|
||||
name: "TanStack Start",
|
||||
description:
|
||||
"Full-stack React and Solid framework powered by TanStack Router",
|
||||
icon: `${ICON_BASE_URL}/tanstack.svg`,
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
"!**/package.json",
|
||||
"!**/analytics-minimal.json",
|
||||
"!**/schema.json",
|
||||
"!**/_generated/**"
|
||||
"!**/_generated/**",
|
||||
"!**/.smoke/**"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
|
||||
Reference in New Issue
Block a user