mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat(cli): migrate vibe-rules to ruler (#503)
This commit is contained in:
5
.changeset/vast-areas-roll.md
Normal file
5
.changeset/vast-areas-roll.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"create-better-t-stack": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
migrate vibe-rules to ruler
|
||||||
@@ -5,7 +5,7 @@ import { PKG_ROOT } from "../../constants";
|
|||||||
import type { ProjectConfig } from "../../types";
|
import type { ProjectConfig } from "../../types";
|
||||||
import { processTemplate } from "../../utils/template-processor";
|
import { processTemplate } from "../../utils/template-processor";
|
||||||
|
|
||||||
async function processAndCopyFiles(
|
export async function processAndCopyFiles(
|
||||||
sourcePattern: string | string[],
|
sourcePattern: string | string[],
|
||||||
baseSourceDir: string,
|
baseSourceDir: string,
|
||||||
destDir: string,
|
destDir: string,
|
||||||
|
|||||||
@@ -7,106 +7,146 @@ import { PKG_ROOT } from "../../constants";
|
|||||||
import type { ProjectConfig } from "../../types";
|
import type { ProjectConfig } from "../../types";
|
||||||
import { exitCancelled } from "../../utils/errors";
|
import { exitCancelled } from "../../utils/errors";
|
||||||
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||||
import { processTemplate } from "../../utils/template-processor";
|
import { processAndCopyFiles } from "../project-generation/template-manager";
|
||||||
|
|
||||||
export async function setupVibeRules(config: ProjectConfig) {
|
export async function setupVibeRules(config: ProjectConfig) {
|
||||||
const { packageManager, projectDir } = config;
|
const { packageManager, projectDir } = config;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log.info("Setting up vibe-rules...");
|
log.info("Setting up Ruler...");
|
||||||
|
|
||||||
const rulesDir = path.join(projectDir, ".bts");
|
const rulerDir = path.join(projectDir, ".ruler");
|
||||||
const ruleFile = path.join(rulesDir, "rules.md");
|
const rulerTemplateDir = path.join(
|
||||||
if (!(await fs.pathExists(ruleFile))) {
|
PKG_ROOT,
|
||||||
const templatePath = path.join(
|
"templates",
|
||||||
PKG_ROOT,
|
"addons",
|
||||||
"templates",
|
"vibe-rules",
|
||||||
"addons",
|
".ruler",
|
||||||
"vibe-rules",
|
);
|
||||||
".bts",
|
|
||||||
"rules.md.hbs",
|
if (!(await fs.pathExists(rulerDir))) {
|
||||||
);
|
if (await fs.pathExists(rulerTemplateDir)) {
|
||||||
if (await fs.pathExists(templatePath)) {
|
await processAndCopyFiles("**/*", rulerTemplateDir, rulerDir, config);
|
||||||
await fs.ensureDir(rulesDir);
|
|
||||||
await processTemplate(templatePath, ruleFile, config);
|
|
||||||
} else {
|
} else {
|
||||||
log.error(pc.red("Rules template not found for vibe-rules addon"));
|
log.error(pc.red("Ruler template directory not found"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const EDITORS = {
|
const EDITORS = {
|
||||||
cursor: { label: "Cursor", hint: ".cursor/rules/*.mdc" },
|
cursor: {
|
||||||
windsurf: { label: "Windsurf", hint: ".windsurfrules" },
|
label: "Cursor",
|
||||||
"claude-code": { label: "Claude Code", hint: "CLAUDE.md" },
|
|
||||||
vscode: {
|
|
||||||
label: "VSCode",
|
|
||||||
hint: ".github/instructions/*.instructions.md",
|
|
||||||
},
|
},
|
||||||
gemini: { label: "Gemini", hint: "GEMINI.md" },
|
windsurf: {
|
||||||
codex: { label: "Codex", hint: "AGENTS.md" },
|
label: "Windsurf",
|
||||||
clinerules: { label: "Cline/Roo", hint: ".clinerules/*.md" },
|
},
|
||||||
roo: { label: "Roo", hint: ".clinerules/*.md" },
|
claude: { label: "Claude Code" },
|
||||||
zed: { label: "Zed", hint: ".rules/*.md" },
|
copilot: {
|
||||||
unified: { label: "Unified", hint: ".rules/*.md" },
|
label: "GitHub Copilot",
|
||||||
|
},
|
||||||
|
"gemini-cli": { label: "Gemini CLI" },
|
||||||
|
codex: { label: "OpenAI Codex CLI" },
|
||||||
|
jules: { label: "Jules" },
|
||||||
|
cline: { label: "Cline" },
|
||||||
|
aider: { label: "Aider" },
|
||||||
|
firebase: { label: "Firebase Studio" },
|
||||||
|
openhands: { label: "Open Hands" },
|
||||||
|
junie: { label: "Junie" },
|
||||||
|
augmentcode: {
|
||||||
|
label: "AugmentCode",
|
||||||
|
},
|
||||||
|
kilocode: {
|
||||||
|
label: "Kilo Code",
|
||||||
|
},
|
||||||
|
opencode: { label: "OpenCode" },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const selectedEditors = await multiselect<keyof typeof EDITORS>({
|
const selectedEditors = await multiselect<keyof typeof EDITORS>({
|
||||||
message: "Choose editors to install BTS rule",
|
message: "Select AI assistants for Ruler",
|
||||||
options: Object.entries(EDITORS).map(([key, v]) => ({
|
options: Object.entries(EDITORS).map(([key, v]) => ({
|
||||||
value: key as keyof typeof EDITORS,
|
value: key as keyof typeof EDITORS,
|
||||||
label: v.label,
|
label: v.label,
|
||||||
hint: v.hint,
|
|
||||||
})),
|
})),
|
||||||
required: false,
|
required: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isCancel(selectedEditors)) return exitCancelled("Operation cancelled");
|
if (isCancel(selectedEditors)) return exitCancelled("Operation cancelled");
|
||||||
|
|
||||||
const editorsArg = selectedEditors.join(", ");
|
if (selectedEditors.length === 0) {
|
||||||
|
log.info("No AI assistants selected. To apply rules later, run:");
|
||||||
|
log.info(
|
||||||
|
pc.cyan(
|
||||||
|
`${getPackageExecutionCommand(packageManager, "@intellectronica/ruler@latest apply --local-only")}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configFile = path.join(rulerDir, "ruler.toml");
|
||||||
|
const currentConfig = await fs.readFile(configFile, "utf-8");
|
||||||
|
|
||||||
|
let updatedConfig = currentConfig;
|
||||||
|
|
||||||
|
const defaultAgentsLine = `default_agents = [${selectedEditors.map((editor) => `"${editor}"`).join(", ")}]`;
|
||||||
|
updatedConfig = updatedConfig.replace(
|
||||||
|
/default_agents = \[\]/,
|
||||||
|
defaultAgentsLine,
|
||||||
|
);
|
||||||
|
|
||||||
|
await fs.writeFile(configFile, updatedConfig);
|
||||||
|
|
||||||
|
await addRulerScriptToPackageJson(projectDir, packageManager);
|
||||||
|
|
||||||
const s = spinner();
|
const s = spinner();
|
||||||
s.start("Saving and applying BTS rules...");
|
s.start("Applying rules with Ruler...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const saveCmd = getPackageExecutionCommand(
|
const rulerApplyCmd = getPackageExecutionCommand(
|
||||||
packageManager,
|
packageManager,
|
||||||
`vibe-rules@latest save bts -f ${JSON.stringify(
|
`@intellectronica/ruler@latest apply --agents ${selectedEditors.join(",")} --local-only`,
|
||||||
path.relative(projectDir, ruleFile),
|
|
||||||
)}`,
|
|
||||||
);
|
);
|
||||||
await execa(saveCmd, {
|
await execa(rulerApplyCmd, {
|
||||||
cwd: projectDir,
|
cwd: projectDir,
|
||||||
env: { CI: "true" },
|
env: { CI: "true" },
|
||||||
shell: true,
|
shell: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const editor of selectedEditors) {
|
s.stop("Applied rules with Ruler");
|
||||||
const loadCmd = getPackageExecutionCommand(
|
} catch (_error) {
|
||||||
packageManager,
|
s.stop(pc.red("Failed to apply rules"));
|
||||||
`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) {
|
} catch (error) {
|
||||||
log.error(pc.red("Failed to set up vibe-rules"));
|
log.error(pc.red("Failed to set up Ruler"));
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
console.error(pc.red(error.message));
|
console.error(pc.red(error.message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addRulerScriptToPackageJson(
|
||||||
|
projectDir: string,
|
||||||
|
packageManager: ProjectConfig["packageManager"],
|
||||||
|
) {
|
||||||
|
const rootPackageJsonPath = path.join(projectDir, "package.json");
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(rootPackageJsonPath))) {
|
||||||
|
log.warn(
|
||||||
|
"Root package.json not found, skipping ruler:apply script addition",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageJson = await fs.readJson(rootPackageJsonPath);
|
||||||
|
|
||||||
|
if (!packageJson.scripts) {
|
||||||
|
packageJson.scripts = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const rulerApplyCommand = getPackageExecutionCommand(
|
||||||
|
packageManager,
|
||||||
|
"@intellectronica/ruler@latest apply --local-only",
|
||||||
|
);
|
||||||
|
packageJson.scripts["ruler:apply"] = rulerApplyCommand;
|
||||||
|
|
||||||
|
await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
|
||||||
|
}
|
||||||
|
|||||||
18
apps/cli/templates/addons/vibe-rules/.ruler/mcp.json.hbs
Normal file
18
apps/cli/templates/addons/vibe-rules/.ruler/mcp.json.hbs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"context7": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@upstash/context7-mcp"]
|
||||||
|
}{{#if (or (eq runtime "workers") (eq webDeploy "workers"))}},
|
||||||
|
"cloudflare": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["mcp-remote", "https://docs.mcp.cloudflare.com/sse"]
|
||||||
|
}{{/if}}{{#if (eq backend "convex")}},
|
||||||
|
"convex": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "convex@latest", "mcp", "start"]
|
||||||
|
}
|
||||||
|
{{/if}}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Ruler Configuration File
|
||||||
|
# See https://okigu.com/ruler for documentation.
|
||||||
|
|
||||||
|
# Default agents to run when --agents is not specified
|
||||||
|
default_agents = []
|
||||||
@@ -2586,6 +2586,31 @@ describe("create-better-t-stack smoke", () => {
|
|||||||
expect(codegenRes.exitCode).toBe(0);
|
expect(codegenRes.exitCode).toBe(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (scripts["check-types"]) {
|
||||||
|
consola.start(`Type checking ${dirName}...`);
|
||||||
|
try {
|
||||||
|
const typeRes = await runScript(
|
||||||
|
pm,
|
||||||
|
projectDir,
|
||||||
|
"check-types",
|
||||||
|
[],
|
||||||
|
120_000,
|
||||||
|
);
|
||||||
|
if (typeRes.exitCode === 0) {
|
||||||
|
consola.success(`${dirName} type check passed`);
|
||||||
|
} else {
|
||||||
|
consola.warn(
|
||||||
|
`${dirName} type check failed (exit code ${typeRes.exitCode}) - likely due to missing generated files`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
consola.warn(
|
||||||
|
`${dirName} type check failed - likely due to missing generated files:`,
|
||||||
|
error.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (scripts.build) {
|
if (scripts.build) {
|
||||||
consola.start(`Building ${dirName}...`);
|
consola.start(`Building ${dirName}...`);
|
||||||
const isTurbo = existsSync(join(projectDir, "turbo.json"));
|
const isTurbo = existsSync(join(projectDir, "turbo.json"));
|
||||||
@@ -2599,18 +2624,9 @@ describe("create-better-t-stack smoke", () => {
|
|||||||
);
|
);
|
||||||
expect(buildRes.exitCode).toBe(0);
|
expect(buildRes.exitCode).toBe(0);
|
||||||
consola.success(`${dirName} built successfully`);
|
consola.success(`${dirName} built successfully`);
|
||||||
} else if (scripts["check-types"]) {
|
}
|
||||||
consola.start(`Type checking ${dirName}...`);
|
|
||||||
const typeRes = await runScript(
|
if (!scripts.build && !scripts["check-types"]) {
|
||||||
pm,
|
|
||||||
projectDir,
|
|
||||||
"check-types",
|
|
||||||
[],
|
|
||||||
120_000,
|
|
||||||
);
|
|
||||||
expect(typeRes.exitCode).toBe(0);
|
|
||||||
consola.success(`${dirName} type check passed`);
|
|
||||||
} else {
|
|
||||||
consola.info(
|
consola.info(
|
||||||
`No build or check-types script for ${dirName}, skipping`,
|
`No build or check-types script for ${dirName}, skipping`,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user