UNPKG

@reliverse/rse

Version:

@reliverse/rse

299 lines (298 loc) 10.5 kB
import { existsSync } from "node:fs"; import { readFile, writeFile } from "node:fs/promises"; import { relative, resolve } from "node:path"; import { defineArgs, defineCommand } from "@reliverse/dler-launcher"; import { logger } from "@reliverse/dler-logger"; import { confirmPrompt, multiselectPrompt } from "@reliverse/dler-prompt"; import { bootstrap, getAvailableFiles } from "@reliverse/rse-rules"; const FILE_TO_AGENTS_MAP = { "AGENTS.md": [ "GitHub Copilot", "OpenAI Codex CLI", "Jules", "Amp", "Aider", "Gemini CLI", "OpenCode", "Qwen Code", "RooCode" ], "CLAUDE.md": ["Claude Code"], "CRUSH.md": ["Crush"], "WARP.md": ["Warp"], ".clinerules": ["Cline"], ".aider.conf.yml": ["Aider"], ".amazonq/rules/ruler_q_rules.md": ["Amazon Q CLI"], ".idx/airules.md": ["Firebase Studio"], ".openhands/microagents/repo.md": ["Open Hands"], ".junie/guidelines.md": ["Junie"], ".augment/rules/ruler_augment_instructions.md": ["AugmentCode"], ".kilocode/rules/ruler_kilocode_instructions.md": ["Kilo Code"], "opencode.json": ["OpenCode"], ".goosehints": ["Goose"], ".trae/rules/project_rules.md": ["Trae AI"], ".kiro/steering/ruler_kiro_instructions.md": ["Kiro"], "firebender.json": ["Firebender"] }; const DIRECTORY_TO_AGENTS_MAP = { ".cursor": ["Cursor"], ".claude": ["Claude Code"], ".codex": ["OpenAI Codex CLI"], ".ruler": ["Ruler"], ".windsurf": ["Windsurf"], ".zed": ["Zed"] }; function buildProviderToFilesMap() { const providerMap = /* @__PURE__ */ new Map(); for (const [file, agents] of Object.entries(FILE_TO_AGENTS_MAP)) { for (const agent of agents) { const existing = providerMap.get(agent) ?? []; if (!existing.includes(file)) { existing.push(file); } providerMap.set(agent, existing); } } for (const [directory, agents] of Object.entries(DIRECTORY_TO_AGENTS_MAP)) { for (const agent of agents) { const existing = providerMap.get(agent) ?? []; if (!existing.includes(directory)) { existing.push(directory); } providerMap.set(agent, existing); } } return providerMap; } function getSelectedProviders(providerMap, providersArg) { if (providersArg) { const csvProviders = providersArg.split(",").map((p) => p.trim()).filter((p) => p.length > 0); const validProviders = []; const allProviders = Array.from(providerMap.keys()); for (const provider of csvProviders) { if (allProviders.includes(provider)) { validProviders.push(provider); } else { logger.warn(`Unknown provider: ${provider}`); } } return validProviders; } return []; } function findExistingBackupFiles(selectedFiles, outputDir) { const backupFiles = []; for (const file of selectedFiles) { const relPath = file.slice(0, -3); const outputFile = resolve(outputDir, relPath); const backupFile = `${outputFile}.bak`; if (existsSync(backupFile)) { backupFiles.push(relative(outputDir, backupFile)); } } return backupFiles; } async function ensureGitignoreEntries(installedFiles, projectRoot) { const gitignorePath = `${projectRoot}/.gitignore`; if (installedFiles.length === 0) { return; } const relativePaths = installedFiles.map((file) => { const relPath = relative(projectRoot, file); return relPath.startsWith("..") ? null : relPath; }).filter((path) => path !== null); if (relativePaths.length === 0) { return; } let gitignoreContent = ""; if (existsSync(gitignorePath)) { gitignoreContent = await readFile(gitignorePath, "utf-8"); } const existingEntries = new Set( gitignoreContent.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")) ); const newEntries = []; for (const path of relativePaths) { if (!existingEntries.has(path)) { newEntries.push(path); existingEntries.add(path); } } if (!existingEntries.has("*.bak")) { newEntries.push("*.bak"); } if (newEntries.length > 0) { const entriesToAdd = newEntries.join("\n"); const updatedContent = gitignoreContent.trim().length > 0 ? `${gitignoreContent.trim()} ${entriesToAdd} ` : `${entriesToAdd} `; await writeFile(gitignorePath, updatedContent, "utf-8"); logger.info(`Added ${newEntries.length} file(s) to .gitignore`); } } export default defineCommand({ meta: { name: "rules", description: "Bootstrap AI rules by converting escaped TypeScript files to their original format", examples: [ "rules", 'rules --providers "Cursor,Claude Code"', 'rules --providers "GitHub Copilot"', 'rules --cwd "./my-project"', 'rules --providers "Cursor" --cwd "./my-project"', 'rules --providers "Cursor" --force', 'rules --providers "Cursor" --backup false', 'rules --providers "Cursor" --verbose' ] }, args: defineArgs({ providers: { type: "string", description: "Comma-separated list of providers to install rules for" }, cwd: { type: "string", description: "Working directory where rules will be installed" }, force: { type: "boolean", description: "Skip confirmation prompt when .bak files exist" }, backup: { type: "boolean", description: "Create backup files when overwriting (default: true)" }, verbose: { type: "boolean", description: "Display detailed information including absolute paths" } }), run: async ({ args }) => { logger.info("Discovering available AI rules..."); try { const availableFiles = await getAvailableFiles(); const availableFilesSet = new Set(availableFiles); if (availableFiles.length === 0) { logger.warn("No rule files found to install."); return; } const providerMap = buildProviderToFilesMap(); const allProviders = Array.from(providerMap.keys()).sort(); let selectedProviders; if (args.providers) { selectedProviders = getSelectedProviders(providerMap, args.providers); if (selectedProviders.length === 0) { logger.warn("No valid providers found in --providers argument."); return; } } else { const options = allProviders.map((provider) => ({ value: provider, label: provider // what the user sees })); const multiselectResult = await multiselectPrompt({ title: "Select providers to install rules for: ", options: options.map(({ value, label }) => ({ value, label })), footerText: "Space: toggle, Enter: confirm" }); if (multiselectResult.error) { logger.error("Selection cancelled or error occurred"); return; } if (multiselectResult.selectedIndices.length === 0) { logger.warn("No providers selected. Exiting."); return; } selectedProviders = multiselectResult.selectedIndices.map((idx) => options[idx]?.value).filter((value) => value !== void 0); } const selectedFilesSet = /* @__PURE__ */ new Set(); for (const provider of selectedProviders) { const allFilesForProvider = providerMap.get(provider) ?? []; for (const file of allFilesForProvider) { const fileWithExtTs = `${file}.ts`; const fileWithExtJs = `${file}.js`; if (availableFilesSet.has(fileWithExtTs)) { selectedFilesSet.add(fileWithExtTs); } if (availableFilesSet.has(fileWithExtJs)) { selectedFilesSet.add(fileWithExtJs); } } for (const [directory, agents] of Object.entries( DIRECTORY_TO_AGENTS_MAP )) { if (agents.includes(provider)) { for (const availableFile of availableFiles) { const normalizedFile = availableFile.replace(/\\/g, "/"); const normalizedDir = directory.replace(/\\/g, "/"); if (normalizedFile === `${normalizedDir}.ts` || normalizedFile === `${normalizedDir}.js` || normalizedFile.startsWith(`${normalizedDir}/`)) { selectedFilesSet.add(availableFile); } } } } } const selectedFiles = Array.from(selectedFilesSet); if (selectedFiles.length === 0) { logger.warn("No available rules selected. Exiting."); return; } const outputDir = args.cwd ? resolve(args.cwd) : process.cwd(); const shouldBackup = args.backup !== false; if (shouldBackup) { const existingBackupFiles = findExistingBackupFiles( selectedFiles, outputDir ); if (existingBackupFiles.length > 0 && !args.force) { logger.warn( `Found ${existingBackupFiles.length} existing .bak file(s) that will be overwritten:` ); for (const backupFile of existingBackupFiles.slice(0, 5)) { logger.warn(` - ${backupFile}`); } if (existingBackupFiles.length > 5) { logger.warn(` ... and ${existingBackupFiles.length - 5} more`); } const confirmResult = await confirmPrompt({ title: "Are you sure you want to overwrite these .bak files?", footerText: "Y: confirm, N: cancel" }); if (confirmResult.error) { logger.error("Confirmation cancelled or error occurred"); return; } if (!confirmResult.confirmed) { logger.warn("Operation cancelled by user."); return; } } } logger.info( `Bootstrapping ${selectedFiles.length} selected rule file(s)...` ); if (args.verbose) { for (const file of selectedFiles) { const fileWithoutExt = file.replace(/\.(ts|js)$/, ""); const absolutePath = resolve(outputDir, fileWithoutExt); logger.info(` - ${absolutePath}`); } } const files = await bootstrap(selectedFiles, outputDir, { backup: shouldBackup }); logger.info(`Successfully bootstrapped ${files.length} rule file(s):`); for (const file of files) { const relPath = relative(outputDir, file); logger.info(` - ${relPath}`); } await ensureGitignoreEntries(files, outputDir); logger.info("AI rules are ready to use!"); } catch (error) { logger.error("Failed to bootstrap AI rules:", error); throw error; } } });