UNPKG

dotagent

Version:

Multi-file AI agent configuration manager with .agent directory support

1,367 lines (1,335 loc) 53.8 kB
#!/usr/bin/env node var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // node_modules/.pnpm/tsup@8.5.0_postcss@8.5.6_typescript@5.8.3/node_modules/tsup/assets/esm_shims.js import path from "path"; import { fileURLToPath } from "url"; var init_esm_shims = __esm({ "node_modules/.pnpm/tsup@8.5.0_postcss@8.5.6_typescript@5.8.3/node_modules/tsup/assets/esm_shims.js"() { "use strict"; } }); // src/yaml-parser.ts import yaml2 from "js-yaml"; function createSafeYamlParser() { return { parse: (str) => { const processedStr = str.replace( /^(\s*\w+:\s*)(\*[^\n\r"']*?)(\s*(?:\r?\n|$))/gm, (match, prefix, value, suffix) => { if (value.startsWith('"') || value.startsWith("'")) { return match; } return `${prefix}"${value}"${suffix}`; } ); const fullyProcessedStr = processedStr.replace( /^(\s*-\s+)(\*[^\n\r"']*?)(\s*(?:\r?\n|$))/gm, (match, prefix, value, suffix) => { if (value.startsWith('"') || value.startsWith("'")) { return match; } return `${prefix}"${value}"${suffix}`; } ); try { return yaml2.load(fullyProcessedStr); } catch (error) { return yaml2.load(str); } }, stringify: (data) => yaml2.dump(data) }; } var grayMatterOptions; var init_yaml_parser = __esm({ "src/yaml-parser.ts"() { "use strict"; init_esm_shims(); grayMatterOptions = { engines: { yaml: createSafeYamlParser() } }; } }); // src/importers.ts var importers_exports = {}; __export(importers_exports, { importAgent: () => importAgent, importAider: () => importAider, importAll: () => importAll, importAmazonQ: () => importAmazonQ, importClaudeCode: () => importClaudeCode, importCline: () => importCline, importCodex: () => importCodex, importCopilot: () => importCopilot, importCursor: () => importCursor, importCursorLegacy: () => importCursorLegacy, importGemini: () => importGemini, importQodo: () => importQodo, importWindsurf: () => importWindsurf, importZed: () => importZed }); import { readFileSync, existsSync, readdirSync, statSync } from "fs"; import { join, basename } from "path"; import matter from "gray-matter"; function isPrivateRule(filePath) { const lowerPath = filePath.toLowerCase(); return lowerPath.includes(".local.") || lowerPath.includes("/private/") || lowerPath.includes("\\private\\"); } async function importAll(repoPath) { const results = []; const errors = []; const agentDir = join(repoPath, ".agent"); if (existsSync(agentDir)) { try { results.push(importAgent(agentDir)); } catch (e) { errors.push({ file: agentDir, error: String(e) }); } } const copilotPath = join(repoPath, ".github", "copilot-instructions.md"); if (existsSync(copilotPath)) { try { results.push(importCopilot(copilotPath)); } catch (e) { errors.push({ file: copilotPath, error: String(e) }); } } const copilotLocalPath = join(repoPath, ".github", "copilot-instructions.local.md"); if (existsSync(copilotLocalPath)) { try { results.push(importCopilot(copilotLocalPath)); } catch (e) { errors.push({ file: copilotLocalPath, error: String(e) }); } } const cursorDir = join(repoPath, ".cursor"); if (existsSync(cursorDir)) { try { results.push(importCursor(cursorDir)); } catch (e) { errors.push({ file: cursorDir, error: String(e) }); } } const cursorRulesFile = join(repoPath, ".cursorrules"); if (existsSync(cursorRulesFile)) { try { results.push(importCursorLegacy(cursorRulesFile)); } catch (e) { errors.push({ file: cursorRulesFile, error: String(e) }); } } const clinerules = join(repoPath, ".clinerules"); if (existsSync(clinerules)) { try { results.push(importCline(clinerules)); } catch (e) { errors.push({ file: clinerules, error: String(e) }); } } const clinerulesLocal = join(repoPath, ".clinerules.local"); if (existsSync(clinerulesLocal)) { try { results.push(importCline(clinerulesLocal)); } catch (e) { errors.push({ file: clinerulesLocal, error: String(e) }); } } const windsurfRules = join(repoPath, ".windsurfrules"); if (existsSync(windsurfRules)) { try { results.push(importWindsurf(windsurfRules)); } catch (e) { errors.push({ file: windsurfRules, error: String(e) }); } } const windsurfRulesLocal = join(repoPath, ".windsurfrules.local"); if (existsSync(windsurfRulesLocal)) { try { results.push(importWindsurf(windsurfRulesLocal)); } catch (e) { errors.push({ file: windsurfRulesLocal, error: String(e) }); } } const zedRules = join(repoPath, ".rules"); if (existsSync(zedRules)) { try { results.push(importZed(zedRules)); } catch (e) { errors.push({ file: zedRules, error: String(e) }); } } const zedRulesLocal = join(repoPath, ".rules.local"); if (existsSync(zedRulesLocal)) { try { results.push(importZed(zedRulesLocal)); } catch (e) { errors.push({ file: zedRulesLocal, error: String(e) }); } } const agentsMd = join(repoPath, "AGENTS.md"); if (existsSync(agentsMd)) { try { results.push(importCodex(agentsMd)); } catch (e) { errors.push({ file: agentsMd, error: String(e) }); } } const agentsLocalMd = join(repoPath, "AGENTS.local.md"); if (existsSync(agentsLocalMd)) { try { results.push(importCodex(agentsLocalMd)); } catch (e) { errors.push({ file: agentsLocalMd, error: String(e) }); } } const claudeMd = join(repoPath, "CLAUDE.md"); if (existsSync(claudeMd)) { try { results.push(importClaudeCode(claudeMd)); } catch (e) { errors.push({ file: claudeMd, error: String(e) }); } } const geminiMd = join(repoPath, "GEMINI.md"); if (existsSync(geminiMd)) { try { results.push(importGemini(geminiMd)); } catch (e) { errors.push({ file: geminiMd, error: String(e) }); } } const bestPracticesMd = join(repoPath, "best_practices.md"); if (existsSync(bestPracticesMd)) { try { results.push(importQodo(bestPracticesMd)); } catch (e) { errors.push({ file: bestPracticesMd, error: String(e) }); } } const claudeLocalMd = join(repoPath, "CLAUDE.local.md"); if (existsSync(claudeLocalMd)) { try { results.push(importClaudeCode(claudeLocalMd)); } catch (e) { errors.push({ file: claudeLocalMd, error: String(e) }); } } const geminiLocalMd = join(repoPath, "GEMINI.local.md"); if (existsSync(geminiLocalMd)) { try { results.push(importGemini(geminiLocalMd)); } catch (e) { errors.push({ file: geminiLocalMd, error: String(e) }); } } const conventionsMd = join(repoPath, "CONVENTIONS.md"); if (existsSync(conventionsMd)) { try { results.push(importAider(conventionsMd)); } catch (e) { errors.push({ file: conventionsMd, error: String(e) }); } } const conventionsLocalMd = join(repoPath, "CONVENTIONS.local.md"); if (existsSync(conventionsLocalMd)) { try { results.push(importAider(conventionsLocalMd)); } catch (e) { errors.push({ file: conventionsLocalMd, error: String(e) }); } } const amazonqRulesDir = join(repoPath, ".amazonq", "rules"); if (existsSync(amazonqRulesDir)) { try { results.push(importAmazonQ(amazonqRulesDir)); } catch (e) { errors.push({ file: amazonqRulesDir, error: String(e) }); } } return { results, errors }; } function importCopilot(filePath) { const content = readFileSync(filePath, "utf-8"); const isPrivate = isPrivateRule(filePath); const metadata = { id: "copilot-instructions", alwaysApply: true, description: "GitHub Copilot custom instructions" }; if (isPrivate) { metadata.private = true; } const rules = [{ metadata, content: content.trim() }]; return { format: "copilot", filePath, rules, raw: content }; } function importAgent(agentDir) { const rules = []; function findMarkdownFiles(dir, relativePath = "") { const entries = readdirSync(dir, { withFileTypes: true }); entries.sort((a, b) => { if (a.isDirectory() && !b.isDirectory()) return -1; if (!a.isDirectory() && b.isDirectory()) return 1; return a.name.localeCompare(b.name); }); for (const entry of entries) { const fullPath = join(dir, entry.name); const relPath = relativePath ? join(relativePath, entry.name) : entry.name; if (entry.isDirectory()) { findMarkdownFiles(fullPath, relPath); } else if (entry.isFile() && entry.name.endsWith(".md")) { const content = readFileSync(fullPath, "utf-8"); const { data, content: body } = matter(content, grayMatterOptions); let segments = relPath.replace(/\.md$/, "").replace(/\\/g, "/").split("/").map((s) => s.replace(/^\d{2,}-/, "").replace(/\.local$/, "")); if (segments[0] === "private") segments = segments.slice(1); const defaultId = segments.join("/"); const isPrivateFile = isPrivateRule(fullPath); const metadata = { id: data.id || defaultId, ...data }; if (metadata.alwaysApply === void 0) { metadata.alwaysApply = false; } if (data.private === true || data.private === void 0 && isPrivateFile) { metadata.private = true; } rules.push({ metadata, content: body.trim() }); } } } findMarkdownFiles(agentDir); return { format: "agent", filePath: agentDir, rules }; } function importCursor(cursorDir) { const rules = []; function findCursorFiles(dir, relativePath = "") { const entries = readdirSync(dir, { withFileTypes: true }); entries.sort((a, b) => { if (a.isDirectory() && !b.isDirectory()) return -1; if (!a.isDirectory() && b.isDirectory()) return 1; return a.name.localeCompare(b.name); }); for (const entry of entries) { const fullPath = join(dir, entry.name); const relPath = relativePath ? join(relativePath, entry.name) : entry.name; if (entry.isDirectory()) { findCursorFiles(fullPath, relPath); } else if (entry.isFile() && (entry.name.endsWith(".mdc") || entry.name.endsWith(".md"))) { const content = readFileSync(fullPath, "utf-8"); const { data, content: body } = matter(content, grayMatterOptions); let segments = relPath.replace(/\.(mdc|md)$/, "").replace(/\\/g, "/").split("/").map((s) => s.replace(/^\d{2,}-/, "").replace(/\.local$/, "")); if (segments[0] === "private") segments = segments.slice(1); if (segments[0] === "rules" && segments.length === 2) segments = segments.slice(1); const defaultId = segments.join("/"); const isPrivateFile = isPrivateRule(fullPath); const metadata = { id: data.id || defaultId, ...data }; if (metadata.alwaysApply === void 0) { metadata.alwaysApply = false; } if (data.private === true || data.private === void 0 && isPrivateFile) { metadata.private = true; } rules.push({ metadata, content: body.trim() }); } } } findCursorFiles(cursorDir); return { format: "cursor", filePath: cursorDir, rules }; } function importCursorLegacy(filePath) { const content = readFileSync(filePath, "utf-8"); const rules = [{ metadata: { id: "cursor-rules-legacy", alwaysApply: true, description: "Legacy Cursor rules" }, content: content.trim() }]; return { format: "cursor", filePath, rules, raw: content }; } function importCline(rulesPath) { const rules = []; if (existsSync(rulesPath) && statSync(rulesPath).isDirectory()) { let findMdFiles2 = function(dir, relativePath = "") { const entries = readdirSync(dir, { withFileTypes: true }); entries.sort((a, b) => { if (a.isDirectory() && !b.isDirectory()) return -1; if (!a.isDirectory() && b.isDirectory()) return 1; return a.name.localeCompare(b.name); }); for (const entry of entries) { const fullPath = join(dir, entry.name); const relPath = relativePath ? join(relativePath, entry.name) : entry.name; if (entry.isDirectory()) { findMdFiles2(fullPath, relPath); } else if (entry.isFile() && entry.name.endsWith(".md")) { const content = readFileSync(fullPath, "utf-8"); const isPrivateFile = isPrivateRule(fullPath); let segments = relPath.replace(/\.md$/, "").replace(/\\/g, "/").split("/").map((s) => s.replace(/^\d{2,}-/, "").replace(/\.local$/, "")); if (segments[0] === "private") segments = segments.slice(1); const defaultId = segments.join("/"); const metadata = { id: defaultId, alwaysApply: true, description: `Cline rules from ${relPath}` }; if (isPrivateFile) { metadata.private = true; } rules.push({ metadata, content: content.trim() }); } } }; var findMdFiles = findMdFiles2; findMdFiles2(rulesPath); } else { const content = readFileSync(rulesPath, "utf-8"); const isPrivateFile = isPrivateRule(rulesPath); const metadata = { id: "cline-rules", alwaysApply: true, description: "Cline project rules" }; if (isPrivateFile) { metadata.private = true; } rules.push({ metadata, content: content.trim() }); } return { format: "cline", filePath: rulesPath, rules }; } function importWindsurf(filePath) { const content = readFileSync(filePath, "utf-8"); const isPrivateFile = isPrivateRule(filePath); const metadata = { id: "windsurf-rules", alwaysApply: true, description: "Windsurf AI rules" }; if (isPrivateFile) { metadata.private = true; } const rules = [{ metadata, content: content.trim() }]; return { format: "windsurf", filePath, rules, raw: content }; } function importZed(filePath) { const content = readFileSync(filePath, "utf-8"); const isPrivateFile = isPrivateRule(filePath); const metadata = { id: "zed-rules", alwaysApply: true, description: "Zed editor rules" }; if (isPrivateFile) { metadata.private = true; } const rules = [{ metadata, content: content.trim() }]; return { format: "zed", filePath, rules, raw: content }; } function importCodex(filePath) { const content = readFileSync(filePath, "utf-8"); const format = basename(filePath) === "AGENTS.md" || basename(filePath) === "AGENTS.local.md" ? "codex" : "unknown"; const isPrivateFile = isPrivateRule(filePath); const metadata = { id: format === "codex" ? "codex-agents" : "claude-rules", alwaysApply: true, description: format === "codex" ? "OpenAI Codex agent instructions" : "Claude AI instructions" }; if (isPrivateFile) { metadata.private = true; } const rules = [{ metadata, content: content.trim() }]; return { format, filePath, rules, raw: content }; } function importAider(filePath) { const content = readFileSync(filePath, "utf-8"); const isPrivateFile = isPrivateRule(filePath); const metadata = { id: "aider-conventions", alwaysApply: true, description: "Aider CLI conventions" }; if (isPrivateFile) { metadata.private = true; } const rules = [{ metadata, content: content.trim() }]; return { format: "aider", filePath, rules, raw: content }; } function importClaudeCode(filePath) { const content = readFileSync(filePath, "utf-8"); const isPrivateFile = isPrivateRule(filePath); const metadata = { id: "claude-code-instructions", alwaysApply: true, description: "Claude Code context and instructions" }; if (isPrivateFile) { metadata.private = true; } const rules = [{ metadata, content: content.trim() }]; return { format: "claude", filePath, rules, raw: content }; } function importGemini(filePath) { const content = readFileSync(filePath, "utf-8"); const isPrivateFile = isPrivateRule(filePath); const metadata = { id: "gemini-instructions", alwaysApply: true, description: "Gemini CLI context and instructions" }; if (isPrivateFile) { metadata.private = true; } const rules = [{ metadata, content: content.trim() }]; return { format: "gemini", filePath, rules, raw: content }; } function importQodo(filePath) { const content = readFileSync(filePath, "utf-8"); const rules = [{ metadata: { id: "qodo-best-practices", alwaysApply: true, description: "Qodo best practices and coding standards", scope: "**/*", priority: "high" }, content: content.trim() }]; return { format: "qodo", filePath, rules, raw: content }; } function importAmazonQ(rulesDir) { const rules = []; function findMdFiles(dir, relativePath = "") { const entries = readdirSync(dir, { withFileTypes: true }); entries.sort((a, b) => { if (a.isDirectory() && !b.isDirectory()) return -1; if (!a.isDirectory() && b.isDirectory()) return 1; return a.name.localeCompare(b.name); }); for (const entry of entries) { const fullPath = join(dir, entry.name); const relPath = relativePath ? join(relativePath, entry.name) : entry.name; if (entry.isDirectory()) { findMdFiles(fullPath, relPath); } else if (entry.isFile() && entry.name.endsWith(".md")) { const content = readFileSync(fullPath, "utf-8"); const isPrivateFile = isPrivateRule(fullPath); let segments = relPath.replace(/\.md$/, "").replace(/\\/g, "/").split("/").map((s) => s.replace(/^\d{2,}-/, "").replace(/\.local$/, "")); if (segments[0] === "private") segments = segments.slice(1); const defaultId = segments.join("/"); const metadata = { id: `amazonq-${defaultId}`, alwaysApply: true, description: `Amazon Q rules from ${relPath}` }; if (isPrivateFile) { metadata.private = true; } rules.push({ metadata, content: content.trim() }); } } } findMdFiles(rulesDir); return { format: "amazonq", filePath: rulesDir, rules }; } var init_importers = __esm({ "src/importers.ts"() { "use strict"; init_esm_shims(); init_yaml_parser(); } }); // src/cli.ts init_esm_shims(); import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2, appendFileSync, rmSync } from "fs"; import { join as join3, resolve, dirname as dirname2 } from "path"; import { parseArgs } from "util"; // src/index.ts init_esm_shims(); // src/parser.ts init_esm_shims(); import { unified } from "unified"; import remarkParse from "remark-parse"; import { toMarkdown } from "mdast-util-to-markdown"; import yaml from "js-yaml"; // src/index.ts init_importers(); // src/exporters.ts init_esm_shims(); init_yaml_parser(); import { writeFileSync, mkdirSync, existsSync as existsSync2 } from "fs"; import { join as join2, dirname } from "path"; import yaml3 from "js-yaml"; import matter2 from "gray-matter"; function generateConditionalRulesSection(rules, repoPath) { const sections = []; const alwaysApplyRules = rules.filter((r) => r.metadata.alwaysApply !== false); const conditionalRules = rules.filter((r) => r.metadata.alwaysApply === false); if (conditionalRules.length === 0) { return ""; } const rulesByFolder = {}; const rulesWithScope = []; const rulesWithDescription = []; conditionalRules.forEach((rule) => { if (rule.metadata.id && rule.metadata.id.includes("/")) { const folder = rule.metadata.id.split("/")[0]; if (!rulesByFolder[folder]) { rulesByFolder[folder] = []; } rulesByFolder[folder].push(rule); } if (rule.metadata.scope) { rulesWithScope.push(rule); } else if (rule.metadata.description && !rule.metadata.scope && !rule.metadata.id?.includes("/")) { rulesWithDescription.push(rule); } }); sections.push("## Context-Specific Rules"); sections.push(""); if (rulesWithScope.length > 0) { rulesWithScope.forEach((rule) => { const scopes = Array.isArray(rule.metadata.scope) ? rule.metadata.scope : [rule.metadata.scope]; scopes.forEach((scope) => { const rulePath = `.agent/${rule.metadata.id}.md`; const description = rule.metadata.description ? ` - ${rule.metadata.description}` : ""; sections.push(`When working with files matching \`${scope}\`, also apply:`); sections.push(`\u2192 [${rule.metadata.id}](${rulePath})${description}`); sections.push(""); }); }); } if (rulesWithDescription.length > 0) { rulesWithDescription.forEach((rule) => { const rulePath = `.agent/${rule.metadata.id}.md`; sections.push(`When working with ${rule.metadata.description}, also apply:`); sections.push(`\u2192 [${rule.metadata.id}](${rulePath})`); sections.push(""); }); } Object.entries(rulesByFolder).forEach(([folder, folderRules]) => { const unhandledRules = folderRules.filter( (r) => !rulesWithScope.includes(r) && !rulesWithDescription.includes(r) ); if (unhandledRules.length > 0) { const sectionTitle = folder.charAt(0).toUpperCase() + folder.slice(1); sections.push(`## ${sectionTitle}`); sections.push(""); unhandledRules.forEach((rule) => { const rulePath = `.agent/${rule.metadata.id}.md`; const description = rule.metadata.description ? ` - ${rule.metadata.description}` : ""; sections.push(`\u2192 [${rule.metadata.id}](${rulePath})${description}`); }); sections.push(""); } }); return sections.join("\n"); } function exportToCopilot(rules, outputPath, options) { const filteredRules = rules.filter((rule) => !rule.metadata.private || options?.includePrivate); const alwaysApplyRules = filteredRules.filter((r) => r.metadata.alwaysApply !== false); const conditionalSection = generateConditionalRulesSection(filteredRules, dirname(outputPath)); const mainContent = alwaysApplyRules.map((rule) => rule.content).join("\n\n---\n\n"); const fullContent = conditionalSection ? `${mainContent} --- ${conditionalSection}` : mainContent; ensureDirectoryExists(outputPath); writeFileSync(outputPath, fullContent, "utf-8"); } function exportToAgent(rules, outputDir, options) { const agentDir = join2(outputDir, ".agent"); mkdirSync(agentDir, { recursive: true }); let topIndex = 1; rules.forEach((rule) => { let filename; let filePath; if (rule.metadata.id && rule.metadata.id.includes("/")) { const parts = rule.metadata.id.split("/"); const fileName = parts.pop() + ".md"; const subDir = join2(agentDir, ...parts); mkdirSync(subDir, { recursive: true }); filePath = join2(subDir, fileName); } else { if (rule.metadata.private) { const prefix = String(topIndex).padStart(3, "0") + "-"; topIndex++; filename = `${prefix}${rule.metadata.id || "rule"}.md`; const privDir = join2(agentDir, "private"); mkdirSync(privDir, { recursive: true }); filePath = join2(privDir, filename); } else { filename = `${rule.metadata.id || "rule"}.md`; filePath = join2(agentDir, filename); } } const frontMatterBase = {}; if (rule.metadata.description !== void 0 && rule.metadata.description !== null) frontMatterBase.description = rule.metadata.description; if (rule.metadata.alwaysApply !== void 0) frontMatterBase.alwaysApply = rule.metadata.alwaysApply; if (rule.metadata.globs !== void 0 && rule.metadata.globs !== null) frontMatterBase.globs = rule.metadata.globs; if (rule.metadata.manual !== void 0 && rule.metadata.manual !== null) frontMatterBase.manual = rule.metadata.manual; if (rule.metadata.scope !== void 0 && rule.metadata.scope !== null) frontMatterBase.scope = rule.metadata.scope; if (rule.metadata.priority !== void 0 && rule.metadata.priority !== null) frontMatterBase.priority = rule.metadata.priority; if (rule.metadata.triggers !== void 0 && rule.metadata.triggers !== null) frontMatterBase.triggers = rule.metadata.triggers; for (const [key, value] of Object.entries(rule.metadata)) { if (!["id", "description", "alwaysApply", "globs", "manual", "scope", "priority", "triggers"].includes(key) && value !== void 0 && value !== null) { if (key === "private" && value === false) continue; frontMatterBase[key] = value; } } const frontMatter = frontMatterBase; const mdContent = matter2.stringify(rule.content, frontMatter, grayMatterOptions); writeFileSync(filePath, mdContent, "utf-8"); }); } function exportToCursor(rules, outputDir, options) { const rulesDir = join2(outputDir, ".cursor", "rules"); mkdirSync(rulesDir, { recursive: true }); const filteredRules = rules.filter((rule) => !rule.metadata.private || options?.includePrivate); for (const rule of filteredRules) { let filePath; if (rule.metadata.id && rule.metadata.id.includes("/")) { const parts = rule.metadata.id.split("/"); const fileName = parts.pop() + ".mdc"; const subDir = join2(rulesDir, ...parts); mkdirSync(subDir, { recursive: true }); filePath = join2(subDir, fileName); } else { const filename = `${rule.metadata.id || "rule"}.mdc`; filePath = join2(rulesDir, filename); } const frontMatterBase = {}; if (rule.metadata.description !== void 0 && rule.metadata.description !== null) frontMatterBase.description = rule.metadata.description; if (rule.metadata.alwaysApply !== void 0) frontMatterBase.alwaysApply = rule.metadata.alwaysApply; if (rule.metadata.globs !== void 0 && rule.metadata.globs !== null) frontMatterBase.globs = rule.metadata.globs; if (rule.metadata.manual !== void 0 && rule.metadata.manual !== null) frontMatterBase.manual = rule.metadata.manual; if (rule.metadata.scope !== void 0 && rule.metadata.scope !== null) frontMatterBase.scope = rule.metadata.scope; if (rule.metadata.priority !== void 0 && rule.metadata.priority !== null) frontMatterBase.priority = rule.metadata.priority; if (rule.metadata.triggers !== void 0 && rule.metadata.triggers !== null) frontMatterBase.triggers = rule.metadata.triggers; for (const [key, value] of Object.entries(rule.metadata)) { if (!["id", "description", "alwaysApply", "globs", "manual", "scope", "priority", "triggers"].includes(key) && value !== void 0 && value !== null) { if (key === "private" && value === false) continue; frontMatterBase[key] = value; } } const frontMatter = frontMatterBase; const mdcContent = matter2.stringify(rule.content, frontMatter, grayMatterOptions); writeFileSync(filePath, mdcContent, "utf-8"); } } function exportToCline(rules, outputPath, options) { const filteredRules = rules.filter((rule) => !rule.metadata.private || options?.includePrivate); if (outputPath.endsWith(".clinerules")) { const alwaysApplyRules = filteredRules.filter((r) => r.metadata.alwaysApply !== false); const conditionalSection = generateConditionalRulesSection(filteredRules, dirname(outputPath)); const mainContent = alwaysApplyRules.map((rule) => { const header2 = rule.metadata.description ? `## ${rule.metadata.description} ` : ""; return header2 + rule.content; }).join("\n\n"); const fullContent = conditionalSection ? `${mainContent} ${conditionalSection}` : mainContent; ensureDirectoryExists(outputPath); writeFileSync(outputPath, fullContent, "utf-8"); } else { const rulesDir = join2(outputPath, ".clinerules"); mkdirSync(rulesDir, { recursive: true }); filteredRules.forEach((rule, index) => { const filename = `${String(index + 1).padStart(2, "0")}-${rule.metadata.id || "rule"}.md`; const filePath = join2(rulesDir, filename); writeFileSync(filePath, rule.content, "utf-8"); }); } } function exportToWindsurf(rules, outputPath, options) { const filteredRules = rules.filter((rule) => !rule.metadata.private || options?.includePrivate); const alwaysApplyRules = filteredRules.filter((r) => r.metadata.alwaysApply !== false); const conditionalSection = generateConditionalRulesSection(filteredRules, dirname(outputPath)); const mainContent = alwaysApplyRules.map((rule) => rule.content).join("\n\n"); const fullContent = conditionalSection ? `${mainContent} ${conditionalSection}` : mainContent; ensureDirectoryExists(outputPath); writeFileSync(outputPath, fullContent, "utf-8"); } function exportToZed(rules, outputPath, options) { const filteredRules = rules.filter((rule) => !rule.metadata.private || options?.includePrivate); const alwaysApplyRules = filteredRules.filter((r) => r.metadata.alwaysApply !== false); const conditionalSection = generateConditionalRulesSection(filteredRules, dirname(outputPath)); const mainContent = alwaysApplyRules.map((rule) => rule.content).join("\n\n"); const fullContent = conditionalSection ? `${mainContent} ${conditionalSection}` : mainContent; ensureDirectoryExists(outputPath); writeFileSync(outputPath, fullContent, "utf-8"); } function exportToCodex(rules, outputPath, options) { const filteredRules = rules.filter((rule) => !rule.metadata.private || options?.includePrivate); const alwaysApplyRules = filteredRules.filter((r) => r.metadata.alwaysApply !== false); const conditionalSection = generateConditionalRulesSection(filteredRules, dirname(outputPath)); const mainContent = alwaysApplyRules.map((rule) => { const header2 = rule.metadata.description ? `# ${rule.metadata.description} ` : ""; return header2 + rule.content; }).join("\n\n"); const fullContent = conditionalSection ? `${mainContent} ${conditionalSection}` : mainContent; ensureDirectoryExists(outputPath); writeFileSync(outputPath, fullContent, "utf-8"); } function exportToAider(rules, outputPath, options) { const filteredRules = rules.filter((rule) => !rule.metadata.private || options?.includePrivate); const alwaysApplyRules = filteredRules.filter((r) => r.metadata.alwaysApply !== false); const conditionalSection = generateConditionalRulesSection(filteredRules, dirname(outputPath)); const mainContent = alwaysApplyRules.map((rule) => rule.content).join("\n\n"); const fullContent = conditionalSection ? `${mainContent} ${conditionalSection}` : mainContent; ensureDirectoryExists(outputPath); writeFileSync(outputPath, fullContent, "utf-8"); } function exportToClaudeCode(rules, outputPath, options) { const filteredRules = rules.filter((rule) => !rule.metadata.private || options?.includePrivate); const alwaysApplyRules = filteredRules.filter((r) => r.metadata.alwaysApply !== false); const conditionalSection = generateConditionalRulesSection(filteredRules, dirname(outputPath)); const mainContent = alwaysApplyRules.map((rule) => { const header2 = rule.metadata.description ? `# ${rule.metadata.description} ` : ""; return header2 + rule.content; }).join("\n\n"); const fullContent = conditionalSection ? `${mainContent} ${conditionalSection}` : mainContent; ensureDirectoryExists(outputPath); writeFileSync(outputPath, fullContent, "utf-8"); } function exportToGemini(rules, outputPath, options) { const filteredRules = rules.filter((rule) => !rule.metadata.private || options?.includePrivate); const content = filteredRules.map((rule) => { const header2 = rule.metadata.description ? `# ${rule.metadata.description} ` : ""; return header2 + rule.content; }).join("\n\n"); ensureDirectoryExists(outputPath); writeFileSync(outputPath, content, "utf-8"); } function exportToQodo(rules, outputPath, options) { const filteredRules = rules.filter((rule) => !rule.metadata.private || options?.includePrivate); const alwaysApplyRules = filteredRules.filter((r) => r.metadata.alwaysApply !== false); const conditionalSection = generateConditionalRulesSection(filteredRules, dirname(outputPath)); const mainContent = alwaysApplyRules.map((rule) => { const header2 = rule.metadata.description ? `# ${rule.metadata.description} ` : ""; return header2 + rule.content; }).join("\n\n---\n\n"); const fullContent = conditionalSection ? `${mainContent} --- ${conditionalSection}` : mainContent; ensureDirectoryExists(outputPath); writeFileSync(outputPath, fullContent, "utf-8"); } function exportToAmazonQ(rules, outputDir, options) { const rulesDir = join2(outputDir, ".amazonq", "rules"); mkdirSync(rulesDir, { recursive: true }); const filteredRules = rules.filter((rule) => !rule.metadata.private || options?.includePrivate); for (const rule of filteredRules) { let filePath; if (rule.metadata.id && rule.metadata.id.includes("/")) { const parts = rule.metadata.id.split("/"); const fileName = parts.pop() + ".md"; const subDir = join2(rulesDir, ...parts); mkdirSync(subDir, { recursive: true }); filePath = join2(subDir, fileName); } else { const cleanId = rule.metadata.id?.startsWith("amazonq-") ? rule.metadata.id.substring(8) : rule.metadata.id || "rule"; const filename = `${cleanId}.md`; filePath = join2(rulesDir, filename); } writeFileSync(filePath, rule.content, "utf-8"); } } function exportAll(rules, repoPath, dryRun = false, options = { includePrivate: false }) { if (!dryRun) { exportToAgent(rules, repoPath, options); exportToCopilot(rules, join2(repoPath, ".github", "copilot-instructions.md"), options); exportToCursor(rules, repoPath, options); exportToCline(rules, join2(repoPath, ".clinerules"), options); exportToWindsurf(rules, join2(repoPath, ".windsurfrules"), options); exportToZed(rules, join2(repoPath, ".rules"), options); exportToCodex(rules, join2(repoPath, "AGENTS.md"), options); exportToAider(rules, join2(repoPath, "CONVENTIONS.md"), options); exportToClaudeCode(rules, join2(repoPath, "CLAUDE.md"), options); exportToGemini(rules, join2(repoPath, "GEMINI.md"), options); exportToQodo(rules, join2(repoPath, "best_practices.md"), options); exportToAmazonQ(rules, repoPath, options); } } function ensureDirectoryExists(filePath) { const dir = dirname(filePath); if (!existsSync2(dir)) { mkdirSync(dir, { recursive: true }); } } // src/utils/colors.ts init_esm_shims(); var colors = { reset: "\x1B[0m", bright: "\x1B[1m", dim: "\x1B[2m", // Foreground colors red: "\x1B[31m", green: "\x1B[32m", yellow: "\x1B[33m", blue: "\x1B[34m", magenta: "\x1B[35m", cyan: "\x1B[36m", gray: "\x1B[90m", // Background colors bgRed: "\x1B[41m", bgGreen: "\x1B[42m", bgYellow: "\x1B[43m" }; function supportsColor() { if (process.env.NO_COLOR) return false; if (process.env.TERM === "dumb") return false; if (process.env.COLORTERM) return true; if (process.env.TERM?.includes("color")) return true; return false; } function colorize(text, color2) { if (!supportsColor()) return text; return `${colors[color2]}${text}${colors.reset}`; } var color = { // Status messages success: (text) => colorize(`\u2713 ${text}`, "green"), error: (text) => colorize(`\u2717 ${text}`, "red"), warning: (text) => colorize(`\u26A0 ${text}`, "yellow"), info: (text) => colorize(`\u2139 ${text}`, "blue"), // Text formatting bold: (text) => colorize(text, "bright"), dim: (text) => colorize(text, "dim"), // Semantic colors path: (text) => colorize(text, "cyan"), command: (text) => colorize(text, "magenta"), number: (text) => colorize(text, "yellow"), format: (text) => colorize(text, "blue"), // Raw colors red: (text) => colorize(text, "red"), green: (text) => colorize(text, "green"), yellow: (text) => colorize(text, "yellow"), blue: (text) => colorize(text, "blue"), gray: (text) => colorize(text, "gray") }; function formatList(items, prefix = " ") { return items.map((item) => `${prefix}${color.dim("\u2022")} ${item}`).join("\n"); } function header(text) { const line = color.dim("\u2500".repeat(text.length + 4)); return ` ${line} ${color.bold(` ${text} `)} ${line} `; } // src/utils/prompt.ts init_esm_shims(); import { select as inquirerSelect, confirm as inquirerConfirm } from "@inquirer/prompts"; async function confirm(question, defaultValue = false) { return await inquirerConfirm({ message: question, default: defaultValue }); } async function select(message, choices, defaultIndex = 0) { const inquirerChoices = choices.map((choice, index) => ({ name: choice.name, value: choice.value, // Set the default based on index ...index === defaultIndex ? { default: true } : {} })); return await inquirerSelect({ message, choices: inquirerChoices }); } // src/cli.ts var { values, positionals } = parseArgs({ args: process.argv.slice(2), options: { help: { type: "boolean", short: "h" }, output: { type: "string", short: "o" }, format: { type: "string", short: "f" }, formats: { type: "string" }, overwrite: { type: "boolean", short: "w" }, "dry-run": { type: "boolean", short: "d" }, "include-private": { type: "boolean" }, "skip-private": { type: "boolean" }, "no-gitignore": { type: "boolean" } }, allowPositionals: true }); function showHelp() { console.log(` ${color.bold("dotagent")} - Multi-file AI agent configuration manager ${color.bold("Usage:")} ${color.command("dotagent import")} ${color.dim("<repo-path>")} Import all rule files from a repository ${color.command("dotagent export")} ${color.dim("[repo-path]")} Export .agent/ directory to all supported formats ${color.command("dotagent convert")} ${color.dim("<file>")} Convert a specific rule file ${color.bold("Options:")} ${color.yellow("-h, --help")} Show this help message ${color.yellow("-o, --output")} Output file path (for convert command) ${color.yellow("-f, --format")} Specify format (copilot|cursor|cline|windsurf|zed|codex|aider|claude|gemini|qodo) ${color.yellow("--formats")} Specify multiple formats (comma-separated) ${color.yellow("-w, --overwrite")} Overwrite existing files ${color.yellow("-d, --dry-run")} Preview operations without making changes ${color.yellow("--no-gitignore")} Skip gitignore prompt ${color.bold("Examples:")} ${color.dim("# Import all rules from current directory (creates .agent/)")} ${color.command("dotagent import .")} ${color.dim("# Export .agent/ directory to all formats")} ${color.command("dotagent export")} ${color.dim("# Export from specific directory")} ${color.command("dotagent export /path/to/repo")} ${color.dim("# Preview what would be imported without creating files")} ${color.command("dotagent import . --dry-run")} `); } async function main() { if (values.help || positionals.length === 0) { showHelp(); process.exit(0); } const command = positionals[0]; const target = positionals[1]; const isDryRun = values["dry-run"]; if (isDryRun) { console.log(color.info("Running in dry-run mode - no files will be modified")); } switch (command) { case "import": { const importTarget = target || "."; const repoPath = resolve(importTarget); if (!existsSync3(repoPath)) { console.error(color.error(`Path does not exist: ${color.path(repoPath)}`)); console.error(color.dim('Hint: Check if the path is correct or use "." for current directory')); process.exit(1); } console.log(header("Importing Rules")); console.log(`Scanning: ${color.path(repoPath)}`); const { results, errors } = await importAll(repoPath); if (results.length === 0) { console.log(color.warning("No rule files found")); console.log(color.dim("Hint: DotAgent looks for:")); console.log(formatList([ ".agent/**/*.md", ".github/copilot-instructions.md", ".cursor/**/*.{mdc,md}", ".clinerules", ".windsurfrules", ".rules", "AGENTS.md", "CLAUDE.md", "GEMINI.md", "best_practices.md" ])); } else { console.log(color.success(`Found ${color.number(results.length.toString())} rule file(s):`)); for (const result of results) { const ruleCount = color.number(`${result.rules.length} rule(s)`); console.log(` ${color.format(result.format)}: ${color.path(result.filePath)} ${color.dim(`(${ruleCount})`)}`); } const allRules = results.flatMap((r) => r.rules); const agentDir = join3(repoPath, ".agent"); if (existsSync3(agentDir)) { const existingAgent = importAgent(agentDir); console.log(color.info(`Found existing .agent/ directory with ${color.number(existingAgent.rules.length.toString())} rule(s)`)); } if (isDryRun) { console.log(color.info(`Would export to: ${color.path(agentDir)}`)); console.log(color.dim(`Total rules: ${allRules.length}`)); } else { const outputDir = values.output || repoPath; exportToAgent(allRules, outputDir); console.log(color.success(`Created .agent/ directory with ${color.number(allRules.length.toString())} rule(s)`)); } } if (errors.length > 0) { console.log(color.warning("Import errors:")); for (const error of errors) { console.log(` ${color.red("\xD7")} ${color.path(error.file)}: ${error.error}`); } } break; } case "export": { const repoPath = target ? resolve(target) : process.cwd(); const agentDir = join3(repoPath, ".agent"); if (!existsSync3(agentDir)) { console.error(color.error(`No .agent/ directory found in: ${color.path(repoPath)}`)); console.error(color.dim('Hint: Run "dotagent import ." first to create .agent/ directory')); process.exit(1); } const agentConfigPath = join3(repoPath, ".agentconfig"); if (existsSync3(agentConfigPath)) { console.error(color.error("Found deprecated .agentconfig file")); console.error(color.dim('The single-file .agentconfig format is deprecated. Please run "dotagent import ." to migrate to .agent/ directory.')); process.exit(1); } console.log(header("Exporting Rules")); const result = importAgent(agentDir); const rules = result.rules; console.log(color.success(`Found ${color.number(rules.length.toString())} rule(s) in ${color.path(agentDir)}`)); const privateRuleCount = rules.filter((r) => r.metadata.private).length; if (privateRuleCount > 0) { console.log(color.dim(`Including ${privateRuleCount} private rule(s)`)); } const outputDir = values.output || repoPath; const exportFormats = [ { name: "All formats", value: "all" }, { name: "VS Code Copilot (.github/copilot-instructions.md)", value: "copilot" }, { name: "Cursor (.cursor/rules/)", value: "cursor" }, { name: "Cline (.clinerules)", value: "cline" }, { name: "Windsurf (.windsurfrules)", value: "windsurf" }, { name: "Zed (.rules)", value: "zed" }, { name: "OpenAI Codex (AGENTS.md)", value: "codex" }, { name: "Aider (CONVENTIONS.md)", value: "aider" }, { name: "Claude Code (CLAUDE.md)", value: "claude" }, { name: "Gemini CLI (GEMINI.md)", value: "gemini" }, { name: "Qodo Merge (best_practices.md)", value: "qodo" } ]; let selectedFormats = []; if (values.formats) { selectedFormats = values.formats.split(",").map((f) => f.trim()); } else if (values.format) { selectedFormats = [values.format]; } else { console.log(); const selectedFormat = await select("Select export format:", exportFormats, 0); selectedFormats = selectedFormat === "all" ? ["all"] : [selectedFormat]; } const validFormats = ["all", "copilot", "cursor", "cline", "windsurf", "zed", "codex", "aider", "claude", "gemini", "qodo"]; const invalidFormats = selectedFormats.filter((f) => !validFormats.includes(f)); if (invalidFormats.length > 0) { console.error(color.error(`Invalid format(s): ${invalidFormats.join(", ")}`)); console.error(color.dim(`Valid formats: ${validFormats.slice(1).join(", ")}, all`)); process.exit(1); } if (isDryRun) { console.log(color.info("Dry run mode - no files will be written")); } const options = { includePrivate: values["include-private"] }; const exportedPaths = []; for (const selectedFormat of selectedFormats) { if (selectedFormat === "all") { if (!isDryRun) { exportAll(rules, outputDir, false, options); } console.log(color.success("Exported to all formats")); exportedPaths.push( ".github/copilot-instructions.md", ".cursor/rules/", ".clinerules", ".windsurfrules", ".rules", "AGENTS.md", "CONVENTIONS.md", "CLAUDE.md", "GEMINI.md", "best_practices.md" ); } else { let exportPath = ""; switch (selectedFormat) { case "copilot": exportPath = join3(outputDir, ".github", "copilot-instructions.md"); if (!isDryRun) exportToCopilot(rules, exportPath, options); exportedPaths.push(".github/copilot-instructions.md"); break; case "cursor": if (!isDryRun) exportToCursor(rules, outputDir, options); exportPath = join3(outputDir, ".cursor/rules/"); exportedPaths.push(".cursor/rules/"); break; case "cline": exportPath = join3(outputDir, ".clinerules"); if (!isDryRun) exportToCline(rules, exportPath, options); exportedPaths.push(".clinerules"); break; case "windsurf": exportPath = join3(outputDir, ".windsurfrules"); if (!isDryRun) exportToWindsurf(rules, exportPath, options); exportedPaths.push(".windsurfrules"); break; case "zed": exportPath = join3(outputDir, ".rules"); if (!isDryRun) exportToZed(rules, exportPath, options); exportedPaths.push(".rules"); break; case "codex": exportPath = join3(outputDir, "AGENTS.md"); if (!isDryRun) exportToCodex(rules, exportPath, options); exportedPaths.push("AGENTS.md"); break; case "aider": exportPath = join3(outputDir, "CONVENTIONS.md"); if (!isDryRun) exportToAider(rules, exportPath, options); exportedPaths.push("CONVENTIONS.md"); break; case "claude": exportPath = join3(outputDir, "CLAUDE.md"); if (!isDryRun) exportToClaudeCode(rules, exportPath, options); exportedPaths.push("CLAUDE.md"); break; case "gemini": exportPath = join3(outputDir, "GEMINI.md"); if (!isDryRun) exportToGemini(rules, exportPath, options); exportedPaths.push("GEMINI.md"); break; case "qodo": exportPath = join3(outputDir, "best_practices.md"); if (!isDryRun) exportToQodo(rules, exportPath, options); exportedPaths.push("best_practices.md"); break; } if (exportPath) { console.log(color.success(`Exported to: ${color.path(exportPath)}`)); } } } if (!values["include-private"] && privateRuleCount > 0) { console.log(color.dim(` Excluded ${privateRuleCount} private rule(s). Use --include-private to include them.`)); } if (!isDryRun && exportedPaths.length > 0 && !values["no-gitignore"]) { console.log(); const shouldUpdateGitignore = await confirm("Add exported files to .gitignore?", true); if (shouldUpdateGitignore) { updateGitignoreWithPaths(outputDir, exportedPaths); console.log(color.success("Updated .gitignore")); } } break; } case "convert": { if (!target) { console.error(color.error("Input file path required")); process.exit(1); } const inputPath = resolve(target); if (!existsSync3(inputPath)) { console.error(color.error(`File does not exist: ${color.path(inputPath)}`)); process.exit(1); } console.log(header("Converting File")); let format = values.format; if (!format) { if (inputPath.includes("copilot-instructions")) format = "copilot"; else if (inputPath.endsWith(".mdc")) format = "cursor"; else if (inputPath.includes(".clinerules")) format = "cline"; else if (inputPath.includes(".windsurfrules")) format