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:
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user