agent-rules
Version:
Rules and instructions for agentic coding tools like Cursor, Claude CLI, Gemini CLI, Qodo, Cline and more
891 lines (878 loc) • 35.9 kB
JavaScript
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
// src/main.ts
import path6 from "path";
import fs6 from "fs/promises";
import { fileURLToPath } from "url";
import { debuglog as debuglog6 } from "util";
// src/adapters/base-adapter.ts
import path from "path";
import fs from "fs/promises";
import { debuglog } from "util";
var debug = 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 = path.join(resolvedMcpTemplateDirectory, "mcp.json");
const targetMcpFile = path.resolve(process.cwd(), mcpConfig.filePath);
const mergeKey = mcpConfig.mergeKey || "mcpServers";
debug(`Processing MCP configuration from ${templateMcpFile} to ${targetMcpFile}`);
try {
const templateContent = await fs.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 fs.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 fs.mkdir(path.dirname(targetMcpFile), { recursive: true });
await fs.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 = path.resolve(process.cwd(), commandsConfig.targetDirectory);
debug(`Processing commands from ${resolvedCommandsTemplateDirectory} to ${targetDirectory}`);
try {
await fs.mkdir(targetDirectory, { recursive: true });
const files = await fs.readdir(resolvedCommandsTemplateDirectory);
const commandFiles = files.filter((file) => file.endsWith(".command.md"));
for (const commandFile of commandFiles) {
const sourceFilePath = path.join(resolvedCommandsTemplateDirectory, commandFile);
const stat = await fs.stat(sourceFilePath);
if (stat.isFile()) {
const targetFileName = commandsConfig.fileNameTransform ? commandsConfig.fileNameTransform(commandFile) : commandFile;
const targetFilePath = path.join(targetDirectory, targetFileName);
const content = await fs.readFile(sourceFilePath, "utf-8");
await fs.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
import path2 from "path";
import fs2 from "fs/promises";
import { debuglog as debuglog2 } from "util";
var debug2 = debuglog2("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 fs2.readdir(resolvedTemplateDirectory);
for (const templateFile of templateFiles) {
const templateFilePath = path2.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 = path2.normalize(decodedPath);
const sanitizedTemplateFile = path2.basename(normalizedPath);
const fullTemplatePath = path2.join(path2.dirname(templateFilePath), sanitizedTemplateFile);
debug2("Processing template file:", sanitizedTemplateFile);
try {
const stat = await fs2.stat(fullTemplatePath);
if (stat.isFile()) {
const targetFileName = this.generateTargetFileName(sanitizedTemplateFile, filesSuffix);
const targetPath = path2.join(targetFilePath, targetFileName);
const resolvedTargetFilePath = this.validateTargetPath(targetPath, baseDirectory);
debug2("Writing template file to target path:", resolvedTargetFilePath);
const templateContent = await fs2.readFile(fullTemplatePath, "utf-8");
await fs2.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 = path2.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 = path2.normalize(decodedPath);
const resolvedTargetFilePath = path2.resolve(normalizedPath);
if (!resolvedTargetFilePath.startsWith(baseDirectory)) {
throw new Error(`Invalid target path: ${targetFilePath}`);
}
return resolvedTargetFilePath;
}
};
// src/adapters/cursor-adapter.ts
import path3 from "path";
import fs3 from "fs/promises";
import { debuglog as debuglog3 } from "util";
import { fromMarkdown } from "mdast-util-from-markdown";
import { toMarkdown } from "mdast-util-to-markdown";
import { frontmatter } from "micromark-extension-frontmatter";
import { frontmatterFromMarkdown, frontmatterToMarkdown } from "mdast-util-frontmatter";
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
var debug3 = debuglog3("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 fs3.readdir(resolvedTemplateDirectory);
for (const templateFile of templateFiles) {
const templateFilePath = path3.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 = path3.normalize(decodedPath);
const sanitizedTemplateFile = path3.basename(normalizedPath);
const fullTemplatePath = path3.join(path3.dirname(templateFilePath), sanitizedTemplateFile);
debug3("Processing template file:", sanitizedTemplateFile);
try {
const stat = await fs3.stat(fullTemplatePath);
if (stat.isFile()) {
const targetFileName = this.generateTargetFileName(sanitizedTemplateFile, filesSuffix);
const targetPath = path3.join(targetFilePath, targetFileName);
const resolvedTargetFilePath = this.validateTargetPath(targetPath, baseDirectory);
debug3("Writing template file to target path:", resolvedTargetFilePath);
const templateContent = await fs3.readFile(fullTemplatePath, "utf-8");
const processedContent = this.processFrontmatter(templateContent);
await fs3.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 = path3.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 = fromMarkdown(content, {
extensions: [frontmatter(["yaml"])],
mdastExtensions: [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 toMarkdown(ast, {
extensions: [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 = parseYaml(frontmatterValue);
if (frontmatterData && typeof frontmatterData === "object" && "applyTo" in frontmatterData) {
const transformedData = { ...frontmatterData };
transformedData.globs = frontmatterData.applyTo;
delete transformedData.applyTo;
return stringifyYaml(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 = path3.normalize(decodedPath);
const resolvedTargetFilePath = path3.resolve(normalizedPath);
if (!resolvedTargetFilePath.startsWith(baseDirectory)) {
throw new Error(`Invalid target path: ${targetFilePath}`);
}
return resolvedTargetFilePath;
}
};
// src/adapters/claude-code-adapter.ts
import path4 from "path";
import fs4 from "fs/promises";
import { debuglog as debuglog4 } from "util";
import { fromMarkdown as fromMarkdown2 } from "mdast-util-from-markdown";
var debug4 = debuglog4("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 fs4.readdir(resolvedTemplateDirectory);
for (const templateFile of templateFiles) {
if (!templateFile.endsWith(".md")) {
debug4("Skipping non-markdown file:", templateFile);
continue;
}
const templateFilePath = path4.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 = path4.normalize(decodedPath);
const sanitizedTemplateFile = path4.basename(normalizedPath);
const fullTemplatePath = path4.join(path4.dirname(templateFilePath), sanitizedTemplateFile);
debug4("Processing template file:", sanitizedTemplateFile);
try {
const stat = await fs4.stat(fullTemplatePath);
if (stat.isFile()) {
const targetFileName = this.generateTargetFileName(sanitizedTemplateFile, filesSuffix);
const targetPath = path4.join(targetFilePath, targetFileName);
const resolvedTargetFilePath = this.validateTargetPath(targetPath, baseDirectory);
debug4("Writing template file to target path:", resolvedTargetFilePath);
const templateContent = await fs4.readFile(fullTemplatePath, "utf-8");
await fs4.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 = path4.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 = path4.dirname(path4.dirname(resolvedTargetDirectory));
const claudeMainFilePath = path4.join(projectRoot, _ClaudeCodeAdapter.CLAUDE_MAIN_FILE);
debug4("Claude main file path:", claudeMainFilePath);
let claudeContent = "";
try {
claudeContent = await fs4.readFile(claudeMainFilePath, "utf-8");
} catch (error) {
debug4("CLAUDE.md not found, creating new file");
claudeContent = "";
}
const templateFiles = await fs4.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 fs4.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 {
fromMarkdown2(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 = path4.normalize(decodedPath);
const resolvedTargetFilePath = path4.resolve(normalizedPath);
if (!resolvedTargetFilePath.startsWith(baseDirectory)) {
throw new Error(`Invalid target path: ${targetFilePath}`);
}
return resolvedTargetFilePath;
}
};
// src/adapters/gemini-adapter.ts
import path5 from "path";
import fs5 from "fs/promises";
import { debuglog as debuglog5 } from "util";
import { fromMarkdown as fromMarkdown3 } from "mdast-util-from-markdown";
var debug5 = debuglog5("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 fs5.readdir(resolvedTemplateDirectory);
for (const templateFile of templateFiles) {
if (!templateFile.endsWith(".md")) {
debug5("Skipping non-markdown file:", templateFile);
continue;
}
const templateFilePath = path5.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 = path5.normalize(decodedPath);
const sanitizedTemplateFile = path5.basename(normalizedPath);
const fullTemplatePath = path5.join(path5.dirname(templateFilePath), sanitizedTemplateFile);
debug5("Processing template file:", sanitizedTemplateFile);
try {
const stat = await fs5.stat(fullTemplatePath);
if (stat.isFile()) {
const targetFileName = this.generateTargetFileName(sanitizedTemplateFile, filesSuffix);
const targetPath = path5.join(targetFilePath, targetFileName);
const resolvedTargetFilePath = this.validateTargetPath(targetPath, baseDirectory);
debug5("Writing template file to target path:", resolvedTargetFilePath);
const templateContent = await fs5.readFile(fullTemplatePath, "utf-8");
const processedContent = this.stripFrontmatter(templateContent);
await fs5.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 = path5.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 = path5.dirname(path5.dirname(resolvedTargetDirectory));
const geminiMainFilePath = path5.join(projectRoot, _GeminiAdapter.GEMINI_MAIN_FILE);
debug5("Gemini main file path:", geminiMainFilePath);
let geminiContent = "";
try {
geminiContent = await fs5.readFile(geminiMainFilePath, "utf-8");
} catch (error) {
debug5("GEMINI.md not found, creating new file");
geminiContent = "";
}
const templateFiles = await fs5.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 fs5.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 {
fromMarkdown3(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 = path5.normalize(decodedPath);
const resolvedTargetFilePath = path5.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 debug6 = debuglog6("agent-rules");
var templateRoot = "__template__";
function resolvePackageRootDirectoryForTemplates() {
let guessedDirName = "";
try {
if (typeof import.meta !== "undefined" && import.meta.url) {
const __filename = fileURLToPath(import.meta.url);
guessedDirName = path6.dirname(__filename);
} else {
guessedDirName = __dirname;
}
} catch (error) {
guessedDirName = __dirname;
}
if (guessedDirName.endsWith("src")) {
return path6.resolve(guessedDirName, "..");
} else if (guessedDirName.endsWith("dist/bin") || guessedDirName.endsWith("dist\\bin")) {
return path6.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 = path6.join(currentFileDirectory, templateRoot, codeLanguage, codeTopic);
const resolvedTemplateDirectory = path6.resolve(templateDirectory);
try {
const templateStats = await fs6.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 = path6.join(currentFileDirectory, templateRoot, codeLanguage, "_mcp");
const resolvedMcpTemplateDirectory = path6.resolve(mcpTemplateDirectory);
try {
const templateStats = await fs6.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 = path6.join(currentFileDirectory, templateRoot, codeLanguage, "_commands");
const resolvedCommandsTemplateDirectory = path6.resolve(commandsTemplateDirectory);
try {
const templateStats = await fs6.stat(resolvedCommandsTemplateDirectory);
if (!templateStats.isDirectory()) {
return null;
}
return resolvedCommandsTemplateDirectory;
} catch (error) {
return null;
}
}
__name(resolveCommandsTemplateDirectory, "resolveCommandsTemplateDirectory");
async function createTargetDirectory(directory) {
const resolvedTargetDirectory = path6.resolve(directory);
await fs6.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");
export {
getAiAppDirectory,
resolveCommandsTemplateDirectory,
resolveMcpTemplateDirectory,
resolveTemplateDirectory,
scaffoldAiAppInstructions
};