feat(cli): add vibe rules addon (#481)

This commit is contained in:
Aman Varshney
2025-08-09 12:06:23 +05:30
committed by GitHub
parent 9005a432cf
commit 6cf476a21e
42 changed files with 429 additions and 302 deletions

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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 },
) {

View File

@@ -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");
}
}

View File

@@ -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)}`);
}
}
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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];

View 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));
}
}
}