UNPKG

agent-rules

Version:

Rules and instructions for agentic coding tools like Cursor, Claude CLI, Gemini CLI, Qodo, Cline and more

929 lines (916 loc) 40.4 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/main.ts var main_exports = {}; __export(main_exports, { getAiAppDirectory: () => getAiAppDirectory, resolveCommandsTemplateDirectory: () => resolveCommandsTemplateDirectory, resolveMcpTemplateDirectory: () => resolveMcpTemplateDirectory, resolveTemplateDirectory: () => resolveTemplateDirectory, scaffoldAiAppInstructions: () => scaffoldAiAppInstructions }); module.exports = __toCommonJS(main_exports); var import_node_path6 = __toESM(require("path"), 1); var import_promises6 = __toESM(require("fs/promises"), 1); var import_node_url = require("url"); var import_node_util6 = require("util"); // src/adapters/base-adapter.ts var import_node_path = __toESM(require("path"), 1); var import_promises = __toESM(require("fs/promises"), 1); var import_node_util = require("util"); var debug = (0, import_node_util.debuglog)("agent-rules"); var BaseAdapter = class { static { __name(this, "BaseAdapter"); } config; constructor(config) { this.config = config; } /** * Get the configuration for this AI app */ getConfig() { return this.config; } /** * Process MCP configuration (optional override) * Default implementation handles JSON merging * @param scaffoldInstructions - The instructions containing template choices * @param resolvedMcpTemplateDirectory - The resolved path to the MCP template directory * @param resolvedTargetDirectory - The resolved path to the target directory */ async processMcpConfiguration(scaffoldInstructions, resolvedMcpTemplateDirectory, resolvedTargetDirectory) { const mcpConfig = this.getMcpConfig(); if (!mcpConfig) return; const templateMcpFile = import_node_path.default.join(resolvedMcpTemplateDirectory, "mcp.json"); const targetMcpFile = import_node_path.default.resolve(process.cwd(), mcpConfig.filePath); const mergeKey = mcpConfig.mergeKey || "mcpServers"; debug(`Processing MCP configuration from ${templateMcpFile} to ${targetMcpFile}`); try { const templateContent = await import_promises.default.readFile(templateMcpFile, "utf-8"); const templateMcpConfig = JSON.parse(templateContent); let templateServers = {}; const keysToTry = mergeKey === "mcpServers" ? ["mcpServers"] : [mergeKey, "mcpServers"]; for (const key of keysToTry) { if (Object.hasOwn(templateMcpConfig, key)) { const templateValue = Reflect.get(templateMcpConfig, key); if (templateValue && typeof templateValue === "object" && !Array.isArray(templateValue)) { templateServers = templateValue; break; } } } let existingConfig = {}; try { const existingContent = await import_promises.default.readFile(targetMcpFile, "utf-8"); existingConfig = JSON.parse(existingContent); } catch (error) { debug(`Target MCP file does not exist, creating new one: ${targetMcpFile}`); } let existingServers = {}; if (Object.hasOwn(existingConfig, mergeKey)) { const existingValue = Reflect.get(existingConfig, mergeKey); if (existingValue && typeof existingValue === "object" && !Array.isArray(existingValue)) { existingServers = existingValue; } } const mergedConfig = { ...existingConfig, [mergeKey]: { ...existingServers, ...templateServers } }; await import_promises.default.mkdir(import_node_path.default.dirname(targetMcpFile), { recursive: true }); await import_promises.default.writeFile(targetMcpFile, JSON.stringify(mergedConfig, null, 2), "utf-8"); debug(`MCP configuration merged into: ${targetMcpFile}`); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; console.warn(`Warning: Failed to process MCP configuration: ${errorMessage}`); } } /** * Process commands configuration (optional override) * Default implementation handles copying command files with optional filename transformation * @param scaffoldInstructions - The instructions containing template choices * @param resolvedCommandsTemplateDirectory - The resolved path to the commands template directory * @param resolvedTargetDirectory - The resolved path to the target directory (project root) */ async processCommandsConfiguration(scaffoldInstructions, resolvedCommandsTemplateDirectory, resolvedTargetDirectory) { const commandsConfig = this.getCommandsConfig(); if (!commandsConfig) return; const targetDirectory = import_node_path.default.resolve(process.cwd(), commandsConfig.targetDirectory); debug(`Processing commands from ${resolvedCommandsTemplateDirectory} to ${targetDirectory}`); try { await import_promises.default.mkdir(targetDirectory, { recursive: true }); const files = await import_promises.default.readdir(resolvedCommandsTemplateDirectory); const commandFiles = files.filter((file) => file.endsWith(".command.md")); for (const commandFile of commandFiles) { const sourceFilePath = import_node_path.default.join(resolvedCommandsTemplateDirectory, commandFile); const stat = await import_promises.default.stat(sourceFilePath); if (stat.isFile()) { const targetFileName = commandsConfig.fileNameTransform ? commandsConfig.fileNameTransform(commandFile) : commandFile; const targetFilePath = import_node_path.default.join(targetDirectory, targetFileName); const content = await import_promises.default.readFile(sourceFilePath, "utf-8"); await import_promises.default.writeFile(targetFilePath, content, "utf-8"); debug(`Copied command file: ${commandFile} -> ${targetFileName}`); } } debug(`Commands configuration completed in: ${targetDirectory}`); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; console.warn(`Warning: Failed to process commands configuration: ${errorMessage}`); } } }; // src/adapters/github-copilot-adapter.ts var import_node_path2 = __toESM(require("path"), 1); var import_promises2 = __toESM(require("fs/promises"), 1); var import_node_util2 = require("util"); var debug2 = (0, import_node_util2.debuglog)("agent-rules"); var GitHubCopilotAdapter = class extends BaseAdapter { static { __name(this, "GitHubCopilotAdapter"); } constructor() { const config = { directory: ".github/instructions", filesSuffix: ".instructions.md" }; super(config); } /** * Get MCP configuration for GitHub Copilot */ getMcpConfig() { return { filePath: ".vscode/mcp.json", mergeKey: "servers" }; } /** * Get commands configuration for GitHub Copilot */ getCommandsConfig() { return { targetDirectory: ".github/prompts", fileNameTransform: /* @__PURE__ */ __name((filename) => filename.replace(".command.md", ".prompt.md"), "fileNameTransform") }; } /** * Process instructions by copying template files to the target directory */ async processInstructions(scaffoldInstructions, resolvedTemplateDirectory, resolvedTargetDirectory) { await this.copyTemplateFiles(resolvedTemplateDirectory, resolvedTargetDirectory, this.config.filesSuffix); } /** * Copy all template files from source to target directory */ async copyTemplateFiles(resolvedTemplateDirectory, resolvedTargetDirectory, filesSuffix) { const templateFiles = await import_promises2.default.readdir(resolvedTemplateDirectory); for (const templateFile of templateFiles) { const templateFilePath = import_node_path2.default.join(resolvedTemplateDirectory, templateFile); await this.copyTemplateFile(templateFilePath, resolvedTargetDirectory, resolvedTargetDirectory, filesSuffix); } } /** * Copy a single template file to the target directory */ async copyTemplateFile(templateFilePath, targetFilePath, resolvedTargetDirectory, filesSuffix) { const baseDirectory = resolvedTargetDirectory; const decodedPath = decodeURIComponent(templateFilePath); const normalizedPath = import_node_path2.default.normalize(decodedPath); const sanitizedTemplateFile = import_node_path2.default.basename(normalizedPath); const fullTemplatePath = import_node_path2.default.join(import_node_path2.default.dirname(templateFilePath), sanitizedTemplateFile); debug2("Processing template file:", sanitizedTemplateFile); try { const stat = await import_promises2.default.stat(fullTemplatePath); if (stat.isFile()) { const targetFileName = this.generateTargetFileName(sanitizedTemplateFile, filesSuffix); const targetPath = import_node_path2.default.join(targetFilePath, targetFileName); const resolvedTargetFilePath = this.validateTargetPath(targetPath, baseDirectory); debug2("Writing template file to target path:", resolvedTargetFilePath); const templateContent = await import_promises2.default.readFile(fullTemplatePath, "utf-8"); await import_promises2.default.writeFile(resolvedTargetFilePath, templateContent, "utf-8"); } } catch (error) { console.warn(`Skipping file ${sanitizedTemplateFile}: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * Generate the target filename by applying the file suffix */ generateTargetFileName(templateFileName, filesSuffix) { const parsedFile = import_node_path2.default.parse(templateFileName); let baseName = parsedFile.name; if (baseName.endsWith(".instructions")) { baseName = baseName.replace(/\.instructions$/, ""); } return `${baseName}${filesSuffix}`; } /** * Validate that the target path doesn't escape the base directory */ validateTargetPath(targetFilePath, baseDirectory) { const decodedPath = decodeURIComponent(targetFilePath); const normalizedPath = import_node_path2.default.normalize(decodedPath); const resolvedTargetFilePath = import_node_path2.default.resolve(normalizedPath); if (!resolvedTargetFilePath.startsWith(baseDirectory)) { throw new Error(`Invalid target path: ${targetFilePath}`); } return resolvedTargetFilePath; } }; // src/adapters/cursor-adapter.ts var import_node_path3 = __toESM(require("path"), 1); var import_promises3 = __toESM(require("fs/promises"), 1); var import_node_util3 = require("util"); var import_mdast_util_from_markdown = require("mdast-util-from-markdown"); var import_mdast_util_to_markdown = require("mdast-util-to-markdown"); var import_micromark_extension_frontmatter = require("micromark-extension-frontmatter"); var import_mdast_util_frontmatter = require("mdast-util-frontmatter"); var import_yaml = require("yaml"); var debug3 = (0, import_node_util3.debuglog)("agent-rules"); var CursorAdapter = class extends BaseAdapter { static { __name(this, "CursorAdapter"); } constructor() { const config = { directory: ".cursor/rules", filesSuffix: ".mdc" }; super(config); } /** * Get MCP configuration for Cursor (not supported yet) */ getMcpConfig() { return null; } /** * Get commands configuration for Cursor (not supported yet) */ getCommandsConfig() { return null; } /** * Process instructions by copying template files to the target directory */ async processInstructions(scaffoldInstructions, resolvedTemplateDirectory, resolvedTargetDirectory) { await this.copyTemplateFiles(resolvedTemplateDirectory, resolvedTargetDirectory, this.config.filesSuffix); } /** * Copy all template files from source to target directory */ async copyTemplateFiles(resolvedTemplateDirectory, resolvedTargetDirectory, filesSuffix) { const templateFiles = await import_promises3.default.readdir(resolvedTemplateDirectory); for (const templateFile of templateFiles) { const templateFilePath = import_node_path3.default.join(resolvedTemplateDirectory, templateFile); await this.copyTemplateFile(templateFilePath, resolvedTargetDirectory, resolvedTargetDirectory, filesSuffix); } } /** * Copy a single template file to the target directory */ async copyTemplateFile(templateFilePath, targetFilePath, resolvedTargetDirectory, filesSuffix) { const baseDirectory = resolvedTargetDirectory; const decodedPath = decodeURIComponent(templateFilePath); const normalizedPath = import_node_path3.default.normalize(decodedPath); const sanitizedTemplateFile = import_node_path3.default.basename(normalizedPath); const fullTemplatePath = import_node_path3.default.join(import_node_path3.default.dirname(templateFilePath), sanitizedTemplateFile); debug3("Processing template file:", sanitizedTemplateFile); try { const stat = await import_promises3.default.stat(fullTemplatePath); if (stat.isFile()) { const targetFileName = this.generateTargetFileName(sanitizedTemplateFile, filesSuffix); const targetPath = import_node_path3.default.join(targetFilePath, targetFileName); const resolvedTargetFilePath = this.validateTargetPath(targetPath, baseDirectory); debug3("Writing template file to target path:", resolvedTargetFilePath); const templateContent = await import_promises3.default.readFile(fullTemplatePath, "utf-8"); const processedContent = this.processFrontmatter(templateContent); await import_promises3.default.writeFile(resolvedTargetFilePath, processedContent, "utf-8"); } } catch (error) { console.warn(`Skipping file ${sanitizedTemplateFile}: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * Generate the target filename by applying the file suffix */ generateTargetFileName(templateFileName, filesSuffix) { const parsedFile = import_node_path3.default.parse(templateFileName); let baseName = parsedFile.name; if (baseName.endsWith(".instructions")) { baseName = baseName.replace(/\.instructions$/, ""); } return `${baseName}${filesSuffix}`; } /** * Process markdown content to transform frontmatter from template format to Cursor format */ processFrontmatter(content) { try { const ast = (0, import_mdast_util_from_markdown.fromMarkdown)(content, { extensions: [(0, import_micromark_extension_frontmatter.frontmatter)(["yaml"])], mdastExtensions: [(0, import_mdast_util_frontmatter.frontmatterFromMarkdown)(["yaml"])] }); let hasTransformations = false; for (const node of ast.children) { if (node.type === "yaml") { const transformedValue = this.transformFrontmatterFields(node.value); if (transformedValue !== node.value) { node.value = transformedValue; hasTransformations = true; } } } if (hasTransformations) { return (0, import_mdast_util_to_markdown.toMarkdown)(ast, { extensions: [(0, import_mdast_util_frontmatter.frontmatterToMarkdown)(["yaml"])] }); } return content; } catch (error) { debug3("Error processing frontmatter with AST:", error); return content; } } /** * Transform frontmatter fields from template format to Cursor format using structured YAML parsing */ transformFrontmatterFields(frontmatterValue) { try { const frontmatterData = (0, import_yaml.parse)(frontmatterValue); if (frontmatterData && typeof frontmatterData === "object" && "applyTo" in frontmatterData) { const transformedData = { ...frontmatterData }; transformedData.globs = frontmatterData.applyTo; delete transformedData.applyTo; return (0, import_yaml.stringify)(transformedData, { lineWidth: -1 }).trim(); } return frontmatterValue; } catch (error) { debug3("Error parsing YAML frontmatter:", error); return frontmatterValue.replace(/^applyTo:\s*(.+)$/gm, "globs: $1"); } } /** * Validate that the target path doesn't escape the base directory */ validateTargetPath(targetFilePath, baseDirectory) { const decodedPath = decodeURIComponent(targetFilePath); const normalizedPath = import_node_path3.default.normalize(decodedPath); const resolvedTargetFilePath = import_node_path3.default.resolve(normalizedPath); if (!resolvedTargetFilePath.startsWith(baseDirectory)) { throw new Error(`Invalid target path: ${targetFilePath}`); } return resolvedTargetFilePath; } }; // src/adapters/claude-code-adapter.ts var import_node_path4 = __toESM(require("path"), 1); var import_promises4 = __toESM(require("fs/promises"), 1); var import_node_util4 = require("util"); var import_mdast_util_from_markdown2 = require("mdast-util-from-markdown"); var debug4 = (0, import_node_util4.debuglog)("agent-rules"); var ClaudeCodeAdapter = class _ClaudeCodeAdapter extends BaseAdapter { static { __name(this, "ClaudeCodeAdapter"); } static CLAUDE_MAIN_FILE = "CLAUDE.md"; // Map topic values to their display labels from CLI static TOPIC_LABELS = { "secure-code": "Secure Coding", "security-vulnerabilities": "Security Vulnerabilities", testing: "Testing" }; constructor() { const config = { directory: ".claude/rules", filesSuffix: ".md" }; super(config); } /** * Get MCP configuration for Claude Code (not supported yet) */ getMcpConfig() { return null; } /** * Get commands configuration for Claude Code (not supported yet) */ getCommandsConfig() { return null; } /** * Process instructions by copying template files and updating CLAUDE.md with imports */ async processInstructions(scaffoldInstructions, resolvedTemplateDirectory, resolvedTargetDirectory) { await this.copyTemplateFiles(resolvedTemplateDirectory, resolvedTargetDirectory, this.config.filesSuffix); await this.updateClaudeMainFile(scaffoldInstructions, resolvedTemplateDirectory, resolvedTargetDirectory); } /** * Copy all template files from source to target directory */ async copyTemplateFiles(resolvedTemplateDirectory, resolvedTargetDirectory, filesSuffix) { const templateFiles = await import_promises4.default.readdir(resolvedTemplateDirectory); for (const templateFile of templateFiles) { if (!templateFile.endsWith(".md")) { debug4("Skipping non-markdown file:", templateFile); continue; } const templateFilePath = import_node_path4.default.join(resolvedTemplateDirectory, templateFile); await this.copyTemplateFile(templateFilePath, resolvedTargetDirectory, resolvedTargetDirectory, filesSuffix); } } /** * Copy a single template file to the target directory */ async copyTemplateFile(templateFilePath, targetFilePath, resolvedTargetDirectory, filesSuffix) { const baseDirectory = resolvedTargetDirectory; const decodedPath = decodeURIComponent(templateFilePath); const normalizedPath = import_node_path4.default.normalize(decodedPath); const sanitizedTemplateFile = import_node_path4.default.basename(normalizedPath); const fullTemplatePath = import_node_path4.default.join(import_node_path4.default.dirname(templateFilePath), sanitizedTemplateFile); debug4("Processing template file:", sanitizedTemplateFile); try { const stat = await import_promises4.default.stat(fullTemplatePath); if (stat.isFile()) { const targetFileName = this.generateTargetFileName(sanitizedTemplateFile, filesSuffix); const targetPath = import_node_path4.default.join(targetFilePath, targetFileName); const resolvedTargetFilePath = this.validateTargetPath(targetPath, baseDirectory); debug4("Writing template file to target path:", resolvedTargetFilePath); const templateContent = await import_promises4.default.readFile(fullTemplatePath, "utf-8"); await import_promises4.default.writeFile(resolvedTargetFilePath, templateContent, "utf-8"); } } catch (error) { console.warn(`Skipping file ${sanitizedTemplateFile}: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * Generate the target filename by applying the file suffix */ generateTargetFileName(templateFileName, filesSuffix) { const parsedFile = import_node_path4.default.parse(templateFileName); let baseName = parsedFile.name; if (baseName.endsWith(".instructions")) { baseName = baseName.replace(/\.instructions$/, ""); } return `${baseName}${filesSuffix}`; } /** * Update the main CLAUDE.md file with imports for the processed templates */ async updateClaudeMainFile(scaffoldInstructions, resolvedTemplateDirectory, resolvedTargetDirectory) { const projectRoot = import_node_path4.default.dirname(import_node_path4.default.dirname(resolvedTargetDirectory)); const claudeMainFilePath = import_node_path4.default.join(projectRoot, _ClaudeCodeAdapter.CLAUDE_MAIN_FILE); debug4("Claude main file path:", claudeMainFilePath); let claudeContent = ""; try { claudeContent = await import_promises4.default.readFile(claudeMainFilePath, "utf-8"); } catch (error) { debug4("CLAUDE.md not found, creating new file"); claudeContent = ""; } const templateFiles = await import_promises4.default.readdir(resolvedTemplateDirectory); const imports = templateFiles.filter((file) => file.endsWith(".md")).map((file) => { const targetFileName = this.generateTargetFileName(file, this.config.filesSuffix); return `- @./.claude/rules/${targetFileName}`; }); if (imports.length === 0) { debug4("No template files to import"); return; } const topicLabel = _ClaudeCodeAdapter.TOPIC_LABELS[scaffoldInstructions.codeTopic] || scaffoldInstructions.codeTopic; const updatedContent = this.addImportsToClaudeContent(claudeContent, topicLabel, imports); await import_promises4.default.writeFile(claudeMainFilePath, updatedContent, "utf-8"); debug4("Updated CLAUDE.md with imports for topic:", topicLabel); } /** * Add imports to Claude content, avoiding duplicates and organizing by categories */ addImportsToClaudeContent(content, categoryLabel, imports) { try { (0, import_mdast_util_from_markdown2.fromMarkdown)(content); const existingContent = content.toLowerCase(); const categoryHeaderLower = `# ${categoryLabel}`.toLowerCase(); const categoryExists = existingContent.includes(categoryHeaderLower); if (categoryExists) { const hasExistingImports = imports.some( (importLine) => existingContent.includes(importLine.toLowerCase()) ); if (hasExistingImports) { debug4("Imports already exist in CLAUDE.md, skipping update"); return content; } } let updatedContent = content.trim(); if (updatedContent.length > 0) { updatedContent += "\n\n"; } if (!categoryExists) { updatedContent += `# ${categoryLabel} `; } updatedContent += imports.join("\n") + "\n"; return updatedContent; } catch (error) { debug4("Error processing Claude content with AST:", error); let updatedContent = content.trim(); if (updatedContent.length > 0) { updatedContent += "\n\n"; } updatedContent += `# ${categoryLabel} `; updatedContent += imports.join("\n") + "\n"; return updatedContent; } } /** * Validate that the target path doesn't escape the base directory */ validateTargetPath(targetFilePath, baseDirectory) { const decodedPath = decodeURIComponent(targetFilePath); const normalizedPath = import_node_path4.default.normalize(decodedPath); const resolvedTargetFilePath = import_node_path4.default.resolve(normalizedPath); if (!resolvedTargetFilePath.startsWith(baseDirectory)) { throw new Error(`Invalid target path: ${targetFilePath}`); } return resolvedTargetFilePath; } }; // src/adapters/gemini-adapter.ts var import_node_path5 = __toESM(require("path"), 1); var import_promises5 = __toESM(require("fs/promises"), 1); var import_node_util5 = require("util"); var import_mdast_util_from_markdown3 = require("mdast-util-from-markdown"); var debug5 = (0, import_node_util5.debuglog)("agent-rules"); var GeminiAdapter = class _GeminiAdapter extends BaseAdapter { static { __name(this, "GeminiAdapter"); } static GEMINI_MAIN_FILE = "GEMINI.md"; // Map topic values to their display labels from CLI static TOPIC_LABELS = { "secure-code": "Secure Coding", "security-vulnerabilities": "Security Vulnerabilities", testing: "Testing" }; constructor() { const config = { directory: ".gemini/rules", filesSuffix: ".md" }; super(config); } /** * Get MCP configuration for Gemini */ getMcpConfig() { return { filePath: ".gemini/settings.json", mergeKey: "mcpServers" }; } /** * Get commands configuration for Gemini (not supported yet) */ getCommandsConfig() { return null; } /** * Process instructions by copying template files and updating GEMINI.md with imports */ async processInstructions(scaffoldInstructions, resolvedTemplateDirectory, resolvedTargetDirectory) { await this.copyTemplateFiles(resolvedTemplateDirectory, resolvedTargetDirectory, this.config.filesSuffix); await this.updateGeminiMainFile(scaffoldInstructions, resolvedTemplateDirectory, resolvedTargetDirectory); } /** * Copy all template files from source to target directory */ async copyTemplateFiles(resolvedTemplateDirectory, resolvedTargetDirectory, filesSuffix) { const templateFiles = await import_promises5.default.readdir(resolvedTemplateDirectory); for (const templateFile of templateFiles) { if (!templateFile.endsWith(".md")) { debug5("Skipping non-markdown file:", templateFile); continue; } const templateFilePath = import_node_path5.default.join(resolvedTemplateDirectory, templateFile); await this.copyTemplateFile(templateFilePath, resolvedTargetDirectory, resolvedTargetDirectory, filesSuffix); } } /** * Copy a single template file to the target directory */ async copyTemplateFile(templateFilePath, targetFilePath, resolvedTargetDirectory, filesSuffix) { const baseDirectory = resolvedTargetDirectory; const decodedPath = decodeURIComponent(templateFilePath); const normalizedPath = import_node_path5.default.normalize(decodedPath); const sanitizedTemplateFile = import_node_path5.default.basename(normalizedPath); const fullTemplatePath = import_node_path5.default.join(import_node_path5.default.dirname(templateFilePath), sanitizedTemplateFile); debug5("Processing template file:", sanitizedTemplateFile); try { const stat = await import_promises5.default.stat(fullTemplatePath); if (stat.isFile()) { const targetFileName = this.generateTargetFileName(sanitizedTemplateFile, filesSuffix); const targetPath = import_node_path5.default.join(targetFilePath, targetFileName); const resolvedTargetFilePath = this.validateTargetPath(targetPath, baseDirectory); debug5("Writing template file to target path:", resolvedTargetFilePath); const templateContent = await import_promises5.default.readFile(fullTemplatePath, "utf-8"); const processedContent = this.stripFrontmatter(templateContent); await import_promises5.default.writeFile(resolvedTargetFilePath, processedContent, "utf-8"); } } catch (error) { console.warn(`Skipping file ${sanitizedTemplateFile}: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * Strip YAML frontmatter from markdown content since Gemini doesn't support it */ stripFrontmatter(content) { const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/; const match = content.match(frontmatterRegex); if (match && match[2] !== void 0) { return match[2]; } return content; } /** * Generate the target filename by applying the file suffix */ generateTargetFileName(templateFileName, filesSuffix) { const parsedFile = import_node_path5.default.parse(templateFileName); let baseName = parsedFile.name; if (baseName.endsWith(".instructions")) { baseName = baseName.replace(/\.instructions$/, ""); } return `${baseName}${filesSuffix}`; } /** * Update the main GEMINI.md file with imports for the processed templates */ async updateGeminiMainFile(scaffoldInstructions, resolvedTemplateDirectory, resolvedTargetDirectory) { const projectRoot = import_node_path5.default.dirname(import_node_path5.default.dirname(resolvedTargetDirectory)); const geminiMainFilePath = import_node_path5.default.join(projectRoot, _GeminiAdapter.GEMINI_MAIN_FILE); debug5("Gemini main file path:", geminiMainFilePath); let geminiContent = ""; try { geminiContent = await import_promises5.default.readFile(geminiMainFilePath, "utf-8"); } catch (error) { debug5("GEMINI.md not found, creating new file"); geminiContent = ""; } const templateFiles = await import_promises5.default.readdir(resolvedTemplateDirectory); const imports = templateFiles.filter((file) => file.endsWith(".md")).map((file) => { const targetFileName = this.generateTargetFileName(file, this.config.filesSuffix); return `@./.gemini/rules/${targetFileName}`; }); if (imports.length === 0) { debug5("No template files to import"); return; } const topicLabel = _GeminiAdapter.TOPIC_LABELS[scaffoldInstructions.codeTopic] || scaffoldInstructions.codeTopic; const updatedContent = this.addImportsToGeminiContent(geminiContent, topicLabel, imports); await import_promises5.default.writeFile(geminiMainFilePath, updatedContent, "utf-8"); debug5("Updated GEMINI.md with imports for topic:", topicLabel); } /** * Add imports to Gemini content, avoiding duplicates and organizing by categories */ addImportsToGeminiContent(content, categoryLabel, imports) { try { (0, import_mdast_util_from_markdown3.fromMarkdown)(content); const existingContent = content.toLowerCase(); const categoryHeaderLower = `# ${categoryLabel}`.toLowerCase(); const categoryExists = existingContent.includes(categoryHeaderLower); if (categoryExists) { const hasExistingImports = imports.some( (importLine) => existingContent.includes(importLine.toLowerCase()) ); if (hasExistingImports) { debug5("Imports already exist in GEMINI.md, skipping update"); return content; } } let updatedContent = content.trim(); if (updatedContent.length > 0) { updatedContent += "\n\n"; } if (!categoryExists) { updatedContent += `# ${categoryLabel} `; } updatedContent += imports.join("\n") + "\n"; return updatedContent; } catch (error) { debug5("Error processing Gemini content with AST:", error); let updatedContent = content.trim(); if (updatedContent.length > 0) { updatedContent += "\n\n"; } updatedContent += `# ${categoryLabel} `; updatedContent += imports.join("\n") + "\n"; return updatedContent; } } /** * Validate that the target path doesn't escape the base directory */ validateTargetPath(targetFilePath, baseDirectory) { const decodedPath = decodeURIComponent(targetFilePath); const normalizedPath = import_node_path5.default.normalize(decodedPath); const resolvedTargetFilePath = import_node_path5.default.resolve(normalizedPath); if (!resolvedTargetFilePath.startsWith(baseDirectory)) { throw new Error(`Invalid target path: ${targetFilePath}`); } return resolvedTargetFilePath; } }; // src/adapters/adapter-registry.ts var AdapterRegistry = class { static { __name(this, "AdapterRegistry"); } static adapters = /* @__PURE__ */ new Map([ ["github-copilot", () => new GitHubCopilotAdapter()], ["cursor", () => new CursorAdapter()], ["claude-code", () => new ClaudeCodeAdapter()], ["gemini", () => new GeminiAdapter()] ]); /** * Get an adapter instance for the specified AI app * @param aiApp - The AI app identifier * @returns The adapter instance * @throws Error if the AI app is not supported */ static getAdapter(aiApp) { const adapterFactory = this.adapters.get(aiApp); if (!adapterFactory) { throw new Error(`AI App "${aiApp}" is not supported.`); } return adapterFactory(); } /** * Get the list of supported AI apps * @returns Array of supported AI app identifiers */ static getSupportedAiApps() { return Array.from(this.adapters.keys()); } }; // src/main.ts var import_meta = {}; var debug6 = (0, import_node_util6.debuglog)("agent-rules"); var templateRoot = "__template__"; function resolvePackageRootDirectoryForTemplates() { let guessedDirName = ""; try { if (typeof import_meta !== "undefined" && import_meta.url) { const __filename = (0, import_node_url.fileURLToPath)(import_meta.url); guessedDirName = import_node_path6.default.dirname(__filename); } else { guessedDirName = __dirname; } } catch (error) { guessedDirName = __dirname; } if (guessedDirName.endsWith("src")) { return import_node_path6.default.resolve(guessedDirName, ".."); } else if (guessedDirName.endsWith("dist/bin") || guessedDirName.endsWith("dist\\bin")) { return import_node_path6.default.resolve(guessedDirName, ".."); } else { return guessedDirName; } } __name(resolvePackageRootDirectoryForTemplates, "resolvePackageRootDirectoryForTemplates"); function getAiAppDirectory(aiApp) { const adapter = AdapterRegistry.getAdapter(aiApp); return adapter.getConfig(); } __name(getAiAppDirectory, "getAiAppDirectory"); async function resolveTemplateDirectory(scaffoldInstructions) { const { codeLanguage, codeTopic } = scaffoldInstructions; const currentFileDirectory = resolvePackageRootDirectoryForTemplates(); const templateDirectory = import_node_path6.default.join(currentFileDirectory, templateRoot, codeLanguage, codeTopic); const resolvedTemplateDirectory = import_node_path6.default.resolve(templateDirectory); try { const templateStats = await import_promises6.default.stat(resolvedTemplateDirectory); if (!templateStats.isDirectory()) { throw new Error(`Template directory is not a directory: ${resolvedTemplateDirectory}`); } } catch (error) { throw new Error(`Template directory not found: ${resolvedTemplateDirectory}`); } return resolvedTemplateDirectory; } __name(resolveTemplateDirectory, "resolveTemplateDirectory"); async function resolveMcpTemplateDirectory(scaffoldInstructions) { const { codeLanguage } = scaffoldInstructions; const currentFileDirectory = resolvePackageRootDirectoryForTemplates(); const mcpTemplateDirectory = import_node_path6.default.join(currentFileDirectory, templateRoot, codeLanguage, "_mcp"); const resolvedMcpTemplateDirectory = import_node_path6.default.resolve(mcpTemplateDirectory); try { const templateStats = await import_promises6.default.stat(resolvedMcpTemplateDirectory); if (!templateStats.isDirectory()) { throw new Error(`MCP template directory is not a directory: ${resolvedMcpTemplateDirectory}`); } } catch (error) { throw new Error(`MCP template directory not found: ${resolvedMcpTemplateDirectory}`); } return resolvedMcpTemplateDirectory; } __name(resolveMcpTemplateDirectory, "resolveMcpTemplateDirectory"); async function resolveCommandsTemplateDirectory(scaffoldInstructions) { const { codeLanguage } = scaffoldInstructions; const currentFileDirectory = resolvePackageRootDirectoryForTemplates(); const commandsTemplateDirectory = import_node_path6.default.join(currentFileDirectory, templateRoot, codeLanguage, "_commands"); const resolvedCommandsTemplateDirectory = import_node_path6.default.resolve(commandsTemplateDirectory); try { const templateStats = await import_promises6.default.stat(resolvedCommandsTemplateDirectory); if (!templateStats.isDirectory()) { return null; } return resolvedCommandsTemplateDirectory; } catch (error) { return null; } } __name(resolveCommandsTemplateDirectory, "resolveCommandsTemplateDirectory"); async function createTargetDirectory(directory) { const resolvedTargetDirectory = import_node_path6.default.resolve(directory); await import_promises6.default.mkdir(resolvedTargetDirectory, { recursive: true }); return resolvedTargetDirectory; } __name(createTargetDirectory, "createTargetDirectory"); async function scaffoldAiAppInstructions(scaffoldInstructions) { const { aiApp, codeLanguage, codeTopic, includeMcp } = scaffoldInstructions; if (!aiApp || !codeLanguage || !codeTopic) { throw new Error("Scaffold instructions must include aiApp and all other template choices."); } const adapter = AdapterRegistry.getAdapter(aiApp); const aiAppConfig = adapter.getConfig(); const { directory } = aiAppConfig; debug6(`Scaffolding AI App instructions in directory: ${directory} with adapter: ${aiApp}`); const resolvedTemplateDirectory = await resolveTemplateDirectory(scaffoldInstructions); const resolvedTargetDirectory = await createTargetDirectory(directory); await adapter.processInstructions(scaffoldInstructions, resolvedTemplateDirectory, resolvedTargetDirectory); if (includeMcp) { const mcpConfig = adapter.getMcpConfig(); if (mcpConfig) { debug6(`Processing MCP configuration for ${aiApp}`); const resolvedMcpTemplateDirectory = await resolveMcpTemplateDirectory(scaffoldInstructions); await adapter.processMcpConfiguration(scaffoldInstructions, resolvedMcpTemplateDirectory, resolvedTargetDirectory); } else { console.warn(`MCP configuration not supported for ${aiApp}`); } } if (scaffoldInstructions.includeCommands) { const commandsConfig = adapter.getCommandsConfig(); if (commandsConfig) { debug6(`Processing commands configuration for ${aiApp}`); const resolvedCommandsTemplateDirectory = await resolveCommandsTemplateDirectory(scaffoldInstructions); if (resolvedCommandsTemplateDirectory) { await adapter.processCommandsConfiguration(scaffoldInstructions, resolvedCommandsTemplateDirectory, resolvedTargetDirectory); } } } } __name(scaffoldAiAppInstructions, "scaffoldAiAppInstructions"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { getAiAppDirectory, resolveCommandsTemplateDirectory, resolveMcpTemplateDirectory, resolveTemplateDirectory, scaffoldAiAppInstructions });