dotagent
Version:
Multi-file AI agent configuration manager with .agent directory support
1 lines • 132 kB
Source Map (JSON)
{"version":3,"sources":["../node_modules/.pnpm/tsup@8.5.0_postcss@8.5.6_typescript@5.8.3/node_modules/tsup/assets/cjs_shims.js","../src/yaml-parser.ts","../src/importers.ts","../src/cli.ts","../src/index.ts","../src/parser.ts","../src/exporters.ts","../src/utils/colors.ts","../src/utils/prompt.ts"],"sourcesContent":["// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () =>\n typeof document === 'undefined'\n ? new URL(`file:${__filename}`).href\n : (document.currentScript && document.currentScript.src) ||\n new URL('main.js', document.baseURI).href\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","import yaml from 'js-yaml'\nimport type { GrayMatterOption } from 'gray-matter'\n\n/**\n * Custom YAML parser that handles glob patterns starting with *\n * by pre-processing the YAML to quote unquoted strings that start with * during parsing\n * and removing quotes from glob patterns during stringification\n */\nexport function createSafeYamlParser() {\n return {\n parse: (str: string): object => {\n // Pre-process the YAML string to quote glob patterns\n // This regex looks for unquoted strings starting with * in YAML values\n const processedStr = str.replace(\n /^(\\s*\\w+:\\s*)(\\*[^\\n\\r\"']*?)(\\s*(?:\\r?\\n|$))/gm,\n (match, prefix, value, suffix) => {\n // Check if the value is already quoted\n if (value.startsWith('\"') || value.startsWith(\"'\")) {\n return match\n }\n // Quote the value to prevent it from being interpreted as a YAML alias\n return `${prefix}\"${value}\"${suffix}`\n }\n )\n \n // Also handle array items that start with *\n const fullyProcessedStr = processedStr.replace(\n /^(\\s*-\\s+)(\\*[^\\n\\r\"']*?)(\\s*(?:\\r?\\n|$))/gm,\n (match, prefix, value, suffix) => {\n // Check if the value is already quoted\n if (value.startsWith('\"') || value.startsWith(\"'\")) {\n return match\n }\n // Quote the value\n return `${prefix}\"${value}\"${suffix}`\n }\n )\n \n try {\n return yaml.load(fullyProcessedStr) as object\n } catch (error) {\n // If preprocessing fails, try the original string\n return yaml.load(str) as object\n }\n },\n stringify: (data: object) => {\n const yamlOutput = yaml.dump(data)\n const lines = yamlOutput.split(/\\r?\\n/)\n const out: string[] = []\n let inGlobsArray = false\n let globsIndent = ''\n const containsGlob = (s: string) => s.includes('*')\n\n for (let i = 0; i < lines.length; i++) {\n let line = lines[i]\n\n // Detect the globs key\n const globsMatch = line.match(/^(\\s*)globs:\\s*(.*)$/)\n if (globsMatch) {\n globsIndent = globsMatch[1]\n const value = globsMatch[2]\n\n // Array style begins on next lines\n if (value === '') {\n inGlobsArray = true\n out.push(line)\n continue\n }\n\n // Scalar style on same line: globs: \"...\"\n const scalar = value.match(/^(['\"])(.+)\\1(\\s*(?:#.*)?)$/)\n if (scalar && containsGlob(scalar[2])) {\n line = `${globsIndent}globs: ${scalar[2]}${scalar[3] ?? ''}`\n }\n out.push(line)\n continue\n }\n\n if (inGlobsArray) {\n // End of the globs array when we dedent\n if (!line.startsWith(globsIndent + ' ')) {\n inGlobsArray = false\n i-- // reprocess this line outside array handling\n continue\n }\n // Sequence item: - \"...\"\n const item = line.match(/^(\\s*-\\s*)(['\"])(.+)\\2(\\s*(?:#.*)?)$/)\n if (item && containsGlob(item[3])) {\n line = `${item[1]}${item[3]}${item[4] ?? ''}`\n }\n out.push(line)\n continue\n }\n\n out.push(line)\n }\n return out.join('\\n')\n }\n }\n}\n\n/**\n * Gray-matter options with custom YAML parser for handling glob patterns\n */\nexport const grayMatterOptions: GrayMatterOption<string, object> = {\n engines: {\n yaml: createSafeYamlParser()\n }\n}","import { readFileSync, existsSync, readdirSync, statSync, Dirent } from 'fs'\nimport { join, basename } from 'path'\nimport matter from 'gray-matter'\nimport type { ImportResult, ImportResults, RuleBlock } from './types.js'\nimport { grayMatterOptions } from './yaml-parser.js'\n\n// Helper function to detect if a file/path indicates a private rule\nfunction isPrivateRule(filePath: string): boolean {\n const lowerPath = filePath.toLowerCase()\n return lowerPath.includes('.local.') || lowerPath.includes('/private/') || lowerPath.includes('\\\\private\\\\')\n}\n\nexport async function importAll(repoPath: string): Promise<ImportResults> {\n const results: ImportResult[] = []\n const errors: Array<{ file: string; error: string }> = []\n \n // Check for Agent directory (.agent/)\n const agentDir = join(repoPath, '.agent')\n if (existsSync(agentDir)) {\n try {\n results.push(importAgent(agentDir))\n } catch (e) {\n errors.push({ file: agentDir, error: String(e) })\n }\n }\n \n // Check for VS Code Copilot instructions\n const copilotPath = join(repoPath, '.github', 'copilot-instructions.md')\n if (existsSync(copilotPath)) {\n try {\n results.push(importCopilot(copilotPath))\n } catch (e) {\n errors.push({ file: copilotPath, error: String(e) })\n }\n }\n \n // Check for local VS Code Copilot instructions\n const copilotLocalPath = join(repoPath, '.github', 'copilot-instructions.local.md')\n if (existsSync(copilotLocalPath)) {\n try {\n results.push(importCopilot(copilotLocalPath))\n } catch (e) {\n errors.push({ file: copilotLocalPath, error: String(e) })\n }\n }\n \n // Check for Cursor directory (.cursor/)\n const cursorDir = join(repoPath, '.cursor')\n if (existsSync(cursorDir)) {\n try {\n results.push(importCursor(cursorDir))\n } catch (e) {\n errors.push({ file: cursorDir, error: String(e) })\n }\n }\n \n // Legacy single .cursorrules file\n const cursorRulesFile = join(repoPath, '.cursorrules')\n if (existsSync(cursorRulesFile)) {\n try {\n results.push(importCursorLegacy(cursorRulesFile))\n } catch (e) {\n errors.push({ file: cursorRulesFile, error: String(e) })\n }\n }\n \n // Check for Cline rules\n const clinerules = join(repoPath, '.clinerules')\n if (existsSync(clinerules)) {\n try {\n results.push(importCline(clinerules))\n } catch (e) {\n errors.push({ file: clinerules, error: String(e) })\n }\n }\n \n // Check for local Cline rules\n const clinerulesLocal = join(repoPath, '.clinerules.local')\n if (existsSync(clinerulesLocal)) {\n try {\n results.push(importCline(clinerulesLocal))\n } catch (e) {\n errors.push({ file: clinerulesLocal, error: String(e) })\n }\n }\n \n // Check for Windsurf rules\n const windsurfRules = join(repoPath, '.windsurfrules')\n if (existsSync(windsurfRules)) {\n try {\n results.push(importWindsurf(windsurfRules))\n } catch (e) {\n errors.push({ file: windsurfRules, error: String(e) })\n }\n }\n \n // Check for local Windsurf rules\n const windsurfRulesLocal = join(repoPath, '.windsurfrules.local')\n if (existsSync(windsurfRulesLocal)) {\n try {\n results.push(importWindsurf(windsurfRulesLocal))\n } catch (e) {\n errors.push({ file: windsurfRulesLocal, error: String(e) })\n }\n }\n \n // Check for Zed rules\n const zedRules = join(repoPath, '.rules')\n if (existsSync(zedRules)) {\n try {\n results.push(importZed(zedRules))\n } catch (e) {\n errors.push({ file: zedRules, error: String(e) })\n }\n }\n \n // Check for local Zed rules\n const zedRulesLocal = join(repoPath, '.rules.local')\n if (existsSync(zedRulesLocal)) {\n try {\n results.push(importZed(zedRulesLocal))\n } catch (e) {\n errors.push({ file: zedRulesLocal, error: String(e) })\n }\n }\n \n // Check for OpenAI Codex AGENTS.md\n const agentsMd = join(repoPath, 'AGENTS.md')\n if (existsSync(agentsMd)) {\n try {\n results.push(importCodex(agentsMd))\n } catch (e) {\n errors.push({ file: agentsMd, error: String(e) })\n }\n }\n \n // Check for local AGENTS.md\n const agentsLocalMd = join(repoPath, 'AGENTS.local.md')\n if (existsSync(agentsLocalMd)) {\n try {\n results.push(importCodex(agentsLocalMd))\n } catch (e) {\n errors.push({ file: agentsLocalMd, error: String(e) })\n }\n }\n \n // Check for CLAUDE.md (Claude Code)\n const claudeMd = join(repoPath, 'CLAUDE.md')\n if (existsSync(claudeMd)) {\n try {\n results.push(importClaudeCode(claudeMd))\n } catch (e) {\n errors.push({ file: claudeMd, error: String(e) })\n }\n }\n \n // Check for AGENTS.md (OpenCode)\n const opencodeMd = join(repoPath, 'AGENTS.md')\n if (existsSync(opencodeMd)) {\n try {\n results.push(importOpenCode(opencodeMd))\n } catch (e) {\n errors.push({ file: opencodeMd, error: String(e) })\n }\n }\n \n // Check for AGENTS.md (OpenAI Codex) - Note: This conflicts with OpenCode,\n // so we need to handle this carefully. For now, we'll prioritize OpenAI Codex\n // since it was implemented first, but this could be made configurable.\n // Users can explicitly specify the format using the CLI if needed.\n \n // Check for GEMINI.md (Gemini CLI)\n const geminiMd = join(repoPath, 'GEMINI.md')\n if (existsSync(geminiMd)) {\n try {\n results.push(importGemini(geminiMd))\n } catch (e) {\n errors.push({ file: geminiMd, error: String(e) })\n }\n }\n\n // Check for best_practices.md (Qodo)\n const bestPracticesMd = join(repoPath, 'best_practices.md')\n if (existsSync(bestPracticesMd)) {\n try {\n results.push(importQodo(bestPracticesMd))\n } catch (e) {\n errors.push({ file: bestPracticesMd, error: String(e) })\n }\n }\n\n // Check for local CLAUDE.md\n const claudeLocalMd = join(repoPath, 'CLAUDE.local.md')\n if (existsSync(claudeLocalMd)) {\n try {\n results.push(importClaudeCode(claudeLocalMd))\n } catch (e) {\n errors.push({ file: claudeLocalMd, error: String(e) })\n }\n }\n \n // Check for local GEMINI.md\n const geminiLocalMd = join(repoPath, 'GEMINI.local.md')\n if (existsSync(geminiLocalMd)) {\n try {\n results.push(importGemini(geminiLocalMd))\n } catch (e) {\n errors.push({ file: geminiLocalMd, error: String(e) })\n }\n }\n \n // Check for CONVENTIONS.md (Aider)\n const conventionsMd = join(repoPath, 'CONVENTIONS.md')\n if (existsSync(conventionsMd)) {\n try {\n results.push(importAider(conventionsMd))\n } catch (e) {\n errors.push({ file: conventionsMd, error: String(e) })\n }\n }\n \n // Check for local CONVENTIONS.md\n const conventionsLocalMd = join(repoPath, 'CONVENTIONS.local.md')\n if (existsSync(conventionsLocalMd)) {\n try {\n results.push(importAider(conventionsLocalMd))\n } catch (e) {\n errors.push({ file: conventionsLocalMd, error: String(e) })\n }\n }\n \n // Check for Amazon Q rules\n const amazonqRulesDir = join(repoPath, '.amazonq', 'rules')\n if (existsSync(amazonqRulesDir)) {\n try {\n results.push(importAmazonQ(amazonqRulesDir))\n } catch (e) {\n errors.push({ file: amazonqRulesDir, error: String(e) })\n }\n }\n\n // Check for Roo rules\n const rooRulesDir = join(repoPath, '.roo', 'rules')\n if (existsSync(rooRulesDir)) {\n try {\n results.push(importRoo(rooRulesDir))\n } catch (e) {\n errors.push({ file: rooRulesDir, error: String(e) })\n }\n }\n\n // Check for Junie guidelines\n const junieGuidelines = join(repoPath, '.junie', 'guidelines.md')\n if (existsSync(junieGuidelines)) {\n try {\n results.push(importJunie(junieGuidelines))\n } catch (e) {\n errors.push({ file: junieGuidelines, error: String(e) })\n }\n }\n \n return { results, errors }\n}\n\nexport function importCopilot(filePath: string): ImportResult {\n const content = readFileSync(filePath, 'utf-8')\n const isPrivate = isPrivateRule(filePath)\n \n const metadata: any = {\n id: 'copilot-instructions',\n alwaysApply: true,\n description: 'GitHub Copilot custom instructions'\n }\n \n if (isPrivate) {\n metadata.private = true\n }\n \n const rules: RuleBlock[] = [{\n metadata,\n content: content.trim()\n }]\n \n return {\n format: 'copilot',\n filePath,\n rules,\n raw: content\n }\n}\n\nexport function importAgent(agentDir: string): ImportResult {\n const rules: RuleBlock[] = []\n \n // Recursively find all .md files in the agent directory\n function findMarkdownFiles(dir: string, relativePath = ''): void {\n const entries = readdirSync(dir, { withFileTypes: true })\n \n // Ensure deterministic ordering: process directories before files, then sort alphabetically\n entries.sort((a: Dirent, b: Dirent) => {\n if (a.isDirectory() && !b.isDirectory()) return -1;\n if (!a.isDirectory() && b.isDirectory()) return 1;\n return a.name.localeCompare(b.name);\n })\n \n for (const entry of entries) {\n const fullPath = join(dir, entry.name)\n const relPath = relativePath ? join(relativePath, entry.name) : entry.name\n \n if (entry.isDirectory()) {\n // Recursively search subdirectories\n findMarkdownFiles(fullPath, relPath)\n } else if (entry.isFile() && entry.name.endsWith('.md')) {\n const content = readFileSync(fullPath, 'utf-8')\n const { data, content: body } = matter(content, grayMatterOptions)\n \n // Remove any leading numeric ordering prefixes (e.g., \"001-\" or \"12-\") from each path segment\n let segments = relPath\n .replace(/\\.md$/, '')\n .replace(/\\\\/g, '/')\n .split('/')\n .map((s: string) => s.replace(/^\\d{2,}-/, '').replace(/\\.local$/, ''))\n if (segments[0] === 'private') segments = segments.slice(1)\n const defaultId = segments.join('/')\n \n // Check if this is a private rule (either by path or frontmatter)\n const isPrivateFile = isPrivateRule(fullPath)\n \n const metadata: any = {\n id: data.id || defaultId,\n ...data\n }\n \n // Set default alwaysApply to false if not specified\n if (metadata.alwaysApply === undefined) {\n metadata.alwaysApply = false\n }\n \n // Only set private if it's true (from file pattern or frontmatter)\n if (data.private === true || (data.private === undefined && isPrivateFile)) {\n metadata.private = true\n }\n \n rules.push({\n metadata,\n content: body.trim()\n })\n }\n }\n }\n \n findMarkdownFiles(agentDir)\n \n return {\n format: 'agent',\n filePath: agentDir,\n rules\n }\n}\n\nexport function importCursor(cursorDir: string): ImportResult {\n const rules: RuleBlock[] = []\n \n // Recursively find all .mdc and .md files in the .cursor directory\n function findCursorFiles(dir: string, relativePath = ''): void {\n const entries = readdirSync(dir, { withFileTypes: true })\n \n // Ensure deterministic ordering: process directories before files, then sort alphabetically\n entries.sort((a: Dirent, b: Dirent) => {\n if (a.isDirectory() && !b.isDirectory()) return -1;\n if (!a.isDirectory() && b.isDirectory()) return 1;\n return a.name.localeCompare(b.name);\n })\n \n for (const entry of entries) {\n const fullPath = join(dir, entry.name)\n const relPath = relativePath ? join(relativePath, entry.name) : entry.name\n \n if (entry.isDirectory()) {\n // Recursively search subdirectories\n findCursorFiles(fullPath, relPath)\n } else if (entry.isFile() && (entry.name.endsWith('.mdc') || entry.name.endsWith('.md'))) {\n const content = readFileSync(fullPath, 'utf-8')\n const { data, content: body } = matter(content, grayMatterOptions)\n \n // Remove any leading numeric ordering prefixes (e.g., \"001-\" or \"12-\") from each path segment\n let segments = relPath\n .replace(/\\.(mdc|md)$/, '')\n .replace(/\\\\/g, '/')\n .split('/')\n .map((s: string) => s.replace(/^\\d{2,}-/, '').replace(/\\.local$/, ''))\n \n // Special handling for backward compatibility\n if (segments[0] === 'private') segments = segments.slice(1)\n // If the file is directly in the 'rules' directory, don't include 'rules' in the ID\n if (segments[0] === 'rules' && segments.length === 2) segments = segments.slice(1)\n \n const defaultId = segments.join('/')\n \n // Check if this is a private rule\n const isPrivateFile = isPrivateRule(fullPath)\n \n const metadata: any = {\n id: data.id || defaultId,\n ...data\n }\n \n // Set default alwaysApply to false if not specified\n if (metadata.alwaysApply === undefined) {\n metadata.alwaysApply = false\n }\n \n // Only set private if it's true (from file pattern or frontmatter)\n if (data.private === true || (data.private === undefined && isPrivateFile)) {\n metadata.private = true\n }\n \n rules.push({\n metadata,\n content: body.trim()\n })\n }\n }\n }\n \n findCursorFiles(cursorDir)\n \n return {\n format: 'cursor',\n filePath: cursorDir,\n rules\n }\n}\n\nexport function importCursorLegacy(filePath: string): ImportResult {\n const content = readFileSync(filePath, 'utf-8')\n const rules: RuleBlock[] = [{\n metadata: {\n id: 'cursor-rules-legacy',\n alwaysApply: true,\n description: 'Legacy Cursor rules'\n },\n content: content.trim()\n }]\n \n return {\n format: 'cursor',\n filePath,\n rules,\n raw: content\n }\n}\n\nexport function importCline(rulesPath: string): ImportResult {\n const rules: RuleBlock[] = []\n \n // Check if it's a directory\n if (existsSync(rulesPath) && statSync(rulesPath).isDirectory()) {\n // Recursively find all .md files\n function findMdFiles(dir: string, relativePath = ''): void {\n const entries = readdirSync(dir, { withFileTypes: true })\n \n // Ensure deterministic ordering: process directories before files, then sort alphabetically\n entries.sort((a: Dirent, b: Dirent) => {\n if (a.isDirectory() && !b.isDirectory()) return -1;\n if (!a.isDirectory() && b.isDirectory()) return 1;\n return a.name.localeCompare(b.name);\n })\n \n for (const entry of entries) {\n const fullPath = join(dir, entry.name)\n const relPath = relativePath ? join(relativePath, entry.name) : entry.name\n \n if (entry.isDirectory()) {\n findMdFiles(fullPath, relPath)\n } else if (entry.isFile() && entry.name.endsWith('.md')) {\n const content = readFileSync(fullPath, 'utf-8')\n const isPrivateFile = isPrivateRule(fullPath)\n // Remove any leading numeric ordering prefixes (e.g., \"001-\" or \"12-\") from each path segment\n let segments = relPath\n .replace(/\\.md$/, '')\n .replace(/\\\\/g, '/')\n .split('/')\n .map((s: string) => s.replace(/^\\d{2,}-/, '').replace(/\\.local$/, ''))\n if (segments[0] === 'private') segments = segments.slice(1)\n const defaultId = segments.join('/')\n \n const metadata: any = {\n id: defaultId,\n alwaysApply: true,\n description: `Cline rules from ${relPath}`\n }\n \n if (isPrivateFile) {\n metadata.private = true\n }\n \n rules.push({\n metadata,\n content: content.trim()\n })\n }\n }\n }\n \n findMdFiles(rulesPath)\n } else {\n // Single .clinerules file\n const content = readFileSync(rulesPath, 'utf-8')\n const isPrivateFile = isPrivateRule(rulesPath)\n \n const metadata: any = {\n id: 'cline-rules',\n alwaysApply: true,\n description: 'Cline project rules'\n }\n \n if (isPrivateFile) {\n metadata.private = true\n }\n \n rules.push({\n metadata,\n content: content.trim()\n })\n }\n \n return {\n format: 'cline',\n filePath: rulesPath,\n rules\n }\n}\n\nexport function importWindsurf(filePath: string): ImportResult {\n const content = readFileSync(filePath, 'utf-8')\n const isPrivateFile = isPrivateRule(filePath)\n \n const metadata: any = {\n id: 'windsurf-rules',\n alwaysApply: true,\n description: 'Windsurf AI rules'\n }\n \n if (isPrivateFile) {\n metadata.private = true\n }\n \n const rules: RuleBlock[] = [{\n metadata,\n content: content.trim()\n }]\n \n return {\n format: 'windsurf',\n filePath,\n rules,\n raw: content\n }\n}\n\nexport function importZed(filePath: string): ImportResult {\n const content = readFileSync(filePath, 'utf-8')\n const isPrivateFile = isPrivateRule(filePath)\n \n const metadata: any = {\n id: 'zed-rules',\n alwaysApply: true,\n description: 'Zed editor rules'\n }\n \n if (isPrivateFile) {\n metadata.private = true\n }\n \n const rules: RuleBlock[] = [{\n metadata,\n content: content.trim()\n }]\n \n return {\n format: 'zed',\n filePath,\n rules,\n raw: content\n }\n}\n\nexport function importCodex(filePath: string): ImportResult {\n const content = readFileSync(filePath, 'utf-8')\n const format = basename(filePath) === 'AGENTS.md' || basename(filePath) === 'AGENTS.local.md' ? 'codex' : 'unknown'\n const isPrivateFile = isPrivateRule(filePath)\n \n const metadata: any = {\n id: format === 'codex' ? 'codex-agents' : 'claude-rules',\n alwaysApply: true,\n description: format === 'codex' ? 'OpenAI Codex agent instructions' : 'Claude AI instructions'\n }\n \n if (isPrivateFile) {\n metadata.private = true\n }\n \n const rules: RuleBlock[] = [{\n metadata,\n content: content.trim()\n }]\n \n return {\n format,\n filePath,\n rules,\n raw: content\n }\n}\n\nexport function importAider(filePath: string): ImportResult {\n const content = readFileSync(filePath, 'utf-8')\n const isPrivateFile = isPrivateRule(filePath)\n \n const metadata: any = {\n id: 'aider-conventions',\n alwaysApply: true,\n description: 'Aider CLI conventions'\n }\n \n if (isPrivateFile) {\n metadata.private = true\n }\n \n const rules: RuleBlock[] = [{\n metadata,\n content: content.trim()\n }]\n \n return {\n format: 'aider',\n filePath,\n rules,\n raw: content\n }\n}\n\nexport function importClaudeCode(filePath: string): ImportResult {\n const content = readFileSync(filePath, 'utf-8')\n const isPrivateFile = isPrivateRule(filePath)\n \n const metadata: any = {\n id: 'claude-code-instructions',\n alwaysApply: true,\n description: 'Claude Code context and instructions'\n }\n \n if (isPrivateFile) {\n metadata.private = true\n }\n \n const rules: RuleBlock[] = [{\n metadata,\n content: content.trim()\n }]\n \n return {\n format: 'claude',\n filePath,\n rules,\n raw: content\n }\n}\n\nexport function importOpenCode(filePath: string): ImportResult {\n const content = readFileSync(filePath, 'utf-8')\n const isPrivateFile = isPrivateRule(filePath)\n \n const metadata: any = {\n id: 'opencode-agents',\n alwaysApply: true,\n description: 'OpenCode agents and instructions'\n }\n \n if (isPrivateFile) {\n metadata.private = true\n }\n \n const rules: RuleBlock[] = [{\n metadata,\n content: content.trim()\n }]\n \n return {\n format: 'opencode',\n filePath,\n rules,\n raw: content\n }\n}\n\nexport function importGemini(filePath: string): ImportResult {\n const content = readFileSync(filePath, 'utf-8')\n const isPrivateFile = isPrivateRule(filePath)\n \n const metadata: any = {\n id: 'gemini-instructions',\n alwaysApply: true,\n description: 'Gemini CLI context and instructions'\n }\n \n if (isPrivateFile) {\n metadata.private = true\n }\n \n const rules: RuleBlock[] = [{\n metadata,\n content: content.trim()\n }]\n \n return {\n format: 'gemini',\n filePath,\n rules,\n raw: content\n }\n}\n\nexport function importQodo(filePath: string): ImportResult {\n const content = readFileSync(filePath, 'utf-8')\n const rules: RuleBlock[] = [{\n metadata: {\n id: 'qodo-best-practices',\n alwaysApply: true,\n description: 'Qodo best practices and coding standards',\n scope: '**/*',\n priority: 'high'\n },\n content: content.trim()\n }]\n \n return {\n format: 'qodo',\n filePath,\n rules,\n raw: content\n }\n}\n\nexport function importAmazonQ(rulesDir: string): ImportResult {\n const rules: RuleBlock[] = []\n \n // Recursively find all .md files in the Amazon Q rules directory\n function findMdFiles(dir: string, relativePath = ''): void {\n const entries = readdirSync(dir, { withFileTypes: true })\n \n // Ensure deterministic ordering: process directories before files, then sort alphabetically\n entries.sort((a: Dirent, b: Dirent) => {\n if (a.isDirectory() && !b.isDirectory()) return -1;\n if (!a.isDirectory() && b.isDirectory()) return 1;\n return a.name.localeCompare(b.name);\n })\n \n for (const entry of entries) {\n const fullPath = join(dir, entry.name)\n const relPath = relativePath ? join(relativePath, entry.name) : entry.name\n \n if (entry.isDirectory()) {\n // Recursively search subdirectories\n findMdFiles(fullPath, relPath)\n } else if (entry.isFile() && entry.name.endsWith('.md')) {\n const content = readFileSync(fullPath, 'utf-8')\n const isPrivateFile = isPrivateRule(fullPath)\n \n // Remove any leading numeric ordering prefixes (e.g., \"001-\" or \"12-\") from each path segment\n let segments = relPath\n .replace(/\\.md$/, '')\n .replace(/\\\\/g, '/')\n .split('/')\n .map((s: string) => s.replace(/^\\d{2,}-/, '').replace(/\\.local$/, ''))\n if (segments[0] === 'private') segments = segments.slice(1)\n const defaultId = segments.join('/')\n \n const metadata: any = {\n id: `amazonq-${defaultId}`,\n alwaysApply: true,\n description: `Amazon Q rules from ${relPath}`\n }\n \n if (isPrivateFile) {\n metadata.private = true\n }\n \n rules.push({\n metadata,\n content: content.trim()\n })\n }\n }\n }\n \n findMdFiles(rulesDir)\n \n return {\n format: 'amazonq',\n filePath: rulesDir,\n rules\n }\n}\n\nexport function importRoo(rulesDir: string): ImportResult {\n const rules: RuleBlock[] = []\n \n // Recursively find all .md files in the Roo rules directory\n function findMdFiles(dir: string, relativePath = ''): void {\n const entries = readdirSync(dir, { withFileTypes: true })\n \n // Ensure deterministic ordering: process directories before files, then sort alphabetically\n entries.sort((a: Dirent, b: Dirent) => {\n if (a.isDirectory() && !b.isDirectory()) return -1;\n if (!a.isDirectory() && b.isDirectory()) return 1;\n return a.name.localeCompare(b.name);\n })\n \n for (const entry of entries) {\n const fullPath = join(dir, entry.name)\n const relPath = relativePath ? join(relativePath, entry.name) : entry.name\n \n if (entry.isDirectory()) {\n // Recursively search subdirectories\n findMdFiles(fullPath, relPath)\n } else if (entry.isFile() && entry.name.endsWith('.md')) {\n const content = readFileSync(fullPath, 'utf-8')\n const { data, content: body } = matter(content, grayMatterOptions)\n \n // Remove any leading numeric ordering prefixes (e.g., \"001-\" or \"12-\") from each path segment\n let segments = relPath\n .replace(/\\.md$/, '')\n .replace(/\\\\/g, '/')\n .split('/')\n .map((s: string) => s.replace(/^\\d{2,}-/, '').replace(/\\.local$/, ''))\n if (segments[0] === 'private') segments = segments.slice(1)\n const defaultId = segments.join('/')\n \n // Check if this is a private rule (either by path or frontmatter)\n const isPrivateFile = isPrivateRule(fullPath)\n \n const metadata: any = {\n id: data.id || defaultId,\n ...data\n }\n \n // Set default alwaysApply to false if not specified\n if (metadata.alwaysApply === undefined) {\n metadata.alwaysApply = false\n }\n \n // Only set private if it's true (from file pattern or frontmatter)\n if (data.private === true || (data.private === undefined && isPrivateFile)) {\n metadata.private = true\n }\n \n rules.push({\n metadata,\n content: body.trim()\n })\n }\n }\n }\n \n findMdFiles(rulesDir)\n \n return {\n format: 'roo',\n filePath: rulesDir,\n rules\n }\n}\n\nexport function importJunie(filePath: string): ImportResult {\n const content = readFileSync(filePath, 'utf-8')\n const isPrivateFile = isPrivateRule(filePath)\n \n const metadata: any = {\n id: 'junie-guidelines',\n alwaysApply: true,\n description: 'JetBrains Junie guidelines and instructions'\n }\n \n if (isPrivateFile) {\n metadata.private = true\n }\n \n const rules: RuleBlock[] = [{\n metadata,\n content: content.trim()\n }]\n \n return {\n format: 'junie' as any,\n filePath,\n rules,\n raw: content\n }\n}","#!/usr/bin/env node\n\nimport { existsSync, readFileSync, writeFileSync, appendFileSync, rmSync } from 'fs'\nimport { join, resolve, dirname } from 'path'\nimport { parseArgs } from 'util'\nimport { importAll, importAgent, exportToAgent, exportAll, exportToCopilot, exportToCursor, exportToCline, exportToWindsurf, exportToZed, exportToCodex, exportToAider, exportToClaudeCode, exportToGemini, exportToQodo, importRoo, exportToRoo, exportToJunie, importOpenCode, exportToOpenCode } from './index.js'\nimport { color, header, formatList } from './utils/colors.js'\nimport { select, confirm } from './utils/prompt.js'\n\nconst { values, positionals } = parseArgs({\n args: process.argv.slice(2),\n options: {\n help: { type: 'boolean', short: 'h' },\n output: { type: 'string', short: 'o' },\n format: { type: 'string', short: 'f' },\n formats: { type: 'string' },\n overwrite: { type: 'boolean', short: 'w' },\n 'dry-run': { type: 'boolean', short: 'd' },\n 'include-private': { type: 'boolean' },\n 'skip-private': { type: 'boolean' },\n 'gitignore': { type: 'boolean' },\n 'no-gitignore': { type: 'boolean' },\n },\n allowPositionals: true\n}) as { values: any; positionals: string[] }\n\n// Validate mutually exclusive flags\nif (values['gitignore'] && values['no-gitignore']) {\n console.error(color.error('Cannot use both --gitignore and --no-gitignore flags together'))\n process.exit(1)\n}\n\nfunction showHelp() {\n console.log(`\n${color.bold('dotagent')} - Multi-file AI agent configuration manager\n\n${color.bold('Usage:')}\n ${color.command('dotagent import')} ${color.dim('<repo-path>')} Import all rule files from a repository\n ${color.command('dotagent export')} ${color.dim('[repo-path]')} Export .agent/ directory to all supported formats\n ${color.command('dotagent convert')} ${color.dim('<file>')} Convert a specific rule file\n\n${color.bold('Options:')}\n ${color.yellow('-h, --help')} Show this help message\n ${color.yellow('-o, --output')} Output file path (for convert command)\n ${color.yellow('-f, --format')} Specify format (copilot|cursor|cline|windsurf|zed|codex|aider|claude|gemini|qodo|roo|junie|opencode)\n ${color.yellow('--formats')} Specify multiple formats (comma-separated)\n ${color.yellow('-w, --overwrite')} Overwrite existing files\n ${color.yellow('-d, --dry-run')} Preview operations without making changes\n ${color.yellow('--gitignore')} Auto-update gitignore (skip prompt)\n ${color.yellow('--no-gitignore')} Skip gitignore prompt\n\n${color.bold('Examples:')}\n ${color.dim('# Import all rules from current directory (creates .agent/)')}\n ${color.command('dotagent import .')}\n\n ${color.dim('# Export .agent/ directory to all formats')}\n ${color.command('dotagent export')}\n \n ${color.dim('# Export from specific directory')}\n ${color.command('dotagent export /path/to/repo')}\n\n ${color.dim('# Preview what would be imported without creating files')}\n ${color.command('dotagent import . --dry-run')}\n`)\n}\n\nasync function main() {\n if (values.help || positionals.length === 0) {\n showHelp()\n process.exit(0)\n }\n\n const command = positionals[0]\n const target = positionals[1]\n const isDryRun = values['dry-run']\n\n if (isDryRun) {\n console.log(color.info('Running in dry-run mode - no files will be modified'))\n }\n\n switch (command) {\n case 'import': {\n // Default to current directory if no target specified\n const importTarget = target || '.'\n\n const repoPath = resolve(importTarget)\n if (!existsSync(repoPath)) {\n console.error(color.error(`Path does not exist: ${color.path(repoPath)}`))\n console.error(color.dim('Hint: Check if the path is correct or use \".\" for current directory'))\n process.exit(1)\n }\n\n console.log(header('Importing Rules'))\n console.log(`Scanning: ${color.path(repoPath)}`)\n \n const { results, errors } = await importAll(repoPath)\n\n if (results.length === 0) {\n console.log(color.warning('No rule files found'))\n console.log(color.dim('Hint: DotAgent looks for:'))\n console.log(formatList([\n '.agent/**/*.md',\n '.github/copilot-instructions.md',\n '.cursor/**/*.{mdc,md}',\n '.clinerules',\n '.windsurfrules',\n '.rules',\n 'AGENTS.md',\n 'CLAUDE.md',\n 'GEMINI.md',\n 'best_practices.md'\n ]))\n } else {\n console.log(color.success(`Found ${color.number(results.length.toString())} rule file(s):`))\n \n for (const result of results) {\n const ruleCount = color.number(`${result.rules.length} rule(s)`)\n console.log(` ${color.format(result.format)}: ${color.path(result.filePath)} ${color.dim(`(${ruleCount})`)}`)\n }\n\n // Combine all rules\n const allRules = results.flatMap(r => r.rules)\n \n // Check if .agent directory exists\n const agentDir = join(repoPath, '.agent')\n if (existsSync(agentDir)) {\n const existingAgent = importAgent(agentDir)\n console.log(color.info(`Found existing .agent/ directory with ${color.number(existingAgent.rules.length.toString())} rule(s)`))\n }\n\n if (isDryRun) {\n console.log(color.info(`Would export to: ${color.path(agentDir)}`))\n console.log(color.dim(`Total rules: ${allRules.length}`))\n } else {\n const outputDir = values.output || repoPath\n exportToAgent(allRules, outputDir)\n console.log(color.success(`Created .agent/ directory with ${color.number(allRules.length.toString())} rule(s)`))\n }\n }\n\n if (errors.length > 0) {\n console.log(color.warning('Import errors:'))\n for (const error of errors) {\n console.log(` ${color.red('×')} ${color.path(error.file)}: ${error.error}`)\n }\n }\n break\n }\n\n case 'export': {\n // Default to current directory if no target specified\n const repoPath = target ? resolve(target) : process.cwd()\n const agentDir = join(repoPath, '.agent')\n \n if (!existsSync(agentDir)) {\n console.error(color.error(`No .agent/ directory found in: ${color.path(repoPath)}`))\n console.error(color.dim('Hint: Run \"dotagent import .\" first to create .agent/ directory'))\n process.exit(1)\n }\n\n // Check for legacy .agentconfig file\n const agentConfigPath = join(repoPath, '.agentconfig')\n if (existsSync(agentConfigPath)) {\n console.error(color.error('Found deprecated .agentconfig file'))\n console.error(color.dim('The single-file .agentconfig format is deprecated. Please run \"dotagent import .\" to migrate to .agent/ directory.'))\n process.exit(1)\n }\n\n console.log(header('Exporting Rules'))\n \n const result = importAgent(agentDir)\n const rules = result.rules\n \n console.log(color.success(`Found ${color.number(rules.length.toString())} rule(s) in ${color.path(agentDir)}`))\n \n // Count private rules\n const privateRuleCount = rules.filter(r => r.metadata.private).length\n if (privateRuleCount > 0) {\n console.log(color.dim(`Including ${privateRuleCount} private rule(s)`))\n }\n\n const outputDir = values.output || repoPath\n \n const exportFormats = [\n { name: 'All formats', value: 'all' },\n { name: 'VS Code Copilot (.github/copilot-instructions.md)', value: 'copilot' },\n { name: 'Cursor (.cursor/rules/)', value: 'cursor' },\n { name: 'Cline (.clinerules)', value: 'cline' },\n { name: 'Windsurf (.windsurfrules)', value: 'windsurf' },\n { name: 'Zed (.rules)', value: 'zed' },\n { name: 'OpenAI Codex (AGENTS.md)', value: 'codex' },\n { name: 'Aider (CONVENTIONS.md)', value: 'aider' },\n { name: 'Claude Code (CLAUDE.md)', value: 'claude' },\n { name: 'Gemini CLI (GEMINI.md)', value: 'gemini' },\n { name: 'Qodo Merge (best_practices.md)', value: 'qodo' },\n { name: 'Roo Code (.roo/rules/)', value: 'roo' },\n { name: 'JetBrains Junie (.junie/guidelines.md)', value: 'junie' },\n { name: 'OpenCode (AGENTS.md)', value: 'opencode' }\n ]\n\n // Handle format parameter or show interactive menu\n let selectedFormats: string[] = []\n \n if (values.formats) {\n // Parse comma-separated formats\n selectedFormats = values.formats.split(',').map((f: string) => f.trim())\n } else if (values.format) {\n // Single format from -f flag\n selectedFormats = [values.format]\n } else {\n // Interactive menu\n console.log()\n const selectedFormat = await select('Select export format:', exportFormats, 0)\n selectedFormats = selectedFormat === 'all' ? ['all'] : [selectedFormat]\n }\n\n // Validate formats\n const validFormats = ['all', 'copilot', 'cursor', 'cline', 'windsurf', 'zed', 'codex', 'aider', 'claude', 'gemini', 'qodo', 'roo', 'junie', 'opencode']\n const invalidFormats = selectedFormats.filter(f => !validFormats.includes(f))\n if (invalidFormats.length > 0) {\n console.error(color.error(`Invalid format(s): ${invalidFormats.join(', ')}`))\n console.error(color.dim(`Valid formats: ${validFormats.slice(1).join(', ')}, all`))\n process.exit(1)\n }\n \n if (isDryRun) {\n console.log(color.info('Dry run mode - no files will be written'))\n }\n \n const options = { includePrivate: values['include-private'] }\n const exportedPaths: string[] = []\n \n // Handle multiple formats\n for (const selectedFormat of selectedFormats) {\n if (selectedFormat === 'all') {\n if (!isDryRun) {\n exportAll(rules, outputDir, false, options)\n }\n console.log(color.success('Exported to all formats'))\n exportedPaths.push(\n '.github/copilot-instructions.md',\n '.cursor/rules/',\n '.clinerules',\n '.windsurfrules',\n '.rules',\n 'AGENTS.md',\n 'CONVENTIONS.md',\n 'CLAUDE.md',\n 'GEMINI.md',\n 'best_practices.md'\n )\n } else {\n // Export to specific format\n let exportPath = ''\n \n switch (selectedFormat) {\n case 'copilot':\n exportPath = join(outputDir, '.github', 'copilot-instructions.md')\n if (!isDryRun) exportToCopilot(rules, exportPath, options)\n exportedPaths.push('.github/copilot-instructions.md')\n break\n case 'cursor':\n if (!isDryRun) exportToCursor(rules, outputDir, options)\n exportPath = join(outputDir, '.cursor/rules/')\n exportedPaths.push('.cursor/rules/')\n break\n case 'cline':\n exportPath = join(outputDir, '.clinerules')\n if (!isDryRun) exportToCline(rules, exportPath, options)\n exportedPaths.push('.clinerules')\n break\n case 'windsurf':\n exportPath = join(outputDir, '.windsurfrules')\n if (!isDryRun) exportToWindsurf(rules, exportPath, options)\n exportedPaths.push('.windsurfrules')\n break\n case 'zed':\n exportPath = join(outputDir, '.rules')\n if (!isDryRun) exportToZed(rules, exportPath, options)\n exportedPaths.push('.rules')\n break\n case 'codex':\n exportPath = join(outputDir, 'AGENTS.md')\n if (!isDryRun) exportToCodex(rules, exportPath, options)\n exportedPaths.push('AGENTS.md')\n break\n case 'aider':\n exportPath = join(outputDir, 'CONVENTIONS.md')\n if (!isDryRun) exportToAider(rules, exportPath, options)\n exportedPaths.push('CONVENTIONS.md')\n break\n case 'claude':\n exportPath = join(outputDir, 'CLAUDE.md')\n if (!isDryRun) exportToClaudeCode(rules, exportPath, options)\n exportedPaths.push('CLAUDE.md')\n break\n case 'gemini':\n exportPath = join(outputDir, 'GEMINI.md')\n if (!isDryRun) exportToGemini(rules, exportPath, options)\n exportedPaths.push('GEMINI.md')\n break\n case 'opencode':\n exportPath = join(outputDir, 'AGENTS.md')\n if (!isDryRun) exportToOpenCode(rules, exportPath, options)\n exportedPaths.push('AGENTS.md')\n break\n case 'qodo':\n exportPath = join(outputDir, 'best_practices.md')\n if (!isDryRun) exportToQodo(rules, exportPath, options)\n exportedPaths.push('best_practices.md')\n break\n case 'roo':\n if (!isDryRun) exportToRoo(rules, outputDir, options)\n exportPath = join(outputDir, '.roo/rules/')\n exportedPaths.push('.roo/rules/')\n break\n case 'junie':\n if (!isDryRun) exportToJunie(rules, outputDir, options)\n exportPath = join(outputDir, '.junie/guidelines.md')\n exportedPaths.push('.junie/guidelines.md')\n break\n }\n \n if (exportPath) {\n console.log(color.success(`Exported to: ${color.path(exportPath)}`))\n }\n }\n }\n \n if (!values['include-private'] && privateRuleCount > 0) {\n console.log(color.dim(`\\nExcluded ${privateRuleCount} private rule(s). Use --include-private to include them.`))\n }\n \n // Handle gitignore updates\n if (!isDryRun && exportedPaths.length > 0) {\n let shouldUpdateGitignore = false\n \n // Check if there are actually new patterns to add\n const hasNewPatterns = checkForNewGitignorePatterns(outputDir, exportedPaths)\n \n if (values['gitignore']) {\n // --gitignore flag: automatically update gitignore (answer yes)\n shouldUpdateGitignore = true\n console.log(color.info('Updating .gitignore (auto-enabled by --gitignore flag)'))\n } else if (!values['no-gitignore'] && hasNewPatterns) {\n // Default behavior: ask user only if there are new patterns\n console.log()\n shouldUpdateGitignore = await confirm('Add exported files to .gitignore?', true)\n }\n // If --no-gitignore is specified or no new patterns, shouldUpdateGitignore remains false\n\n if (shouldUpdateGitignore) {\n const wasUpdated = updateGitignoreWithPaths(outputDir, exportedPaths)\n if (wasUpdated) {\n console.log(color.success('Updated .gitignore'))\n }\n }\n }\n break\n }\n\n case 'convert': {\n if (!target) {\n console.error(color.error('Input file path required'))\n process.exit(1)\n }\n\n const inputPath = resolve(target)\n if (!existsSync(inputPath)) {\n console.error(color.error(`File does not exist: ${color.path(inputPath)}`))\n process.exit(1)\n }\n\n console.log(header('Converting File'))\n\n // Auto-detect format or use specified\n let format = values.format\n if (!format) {\n if (inputPath.includes('copilot-instructions')) format = 'copilot'\n else if (inputPath.endsWith('.mdc')) format = 'cursor'\n else if (inputPath.includes('.clinerules')) format = 'cline'\n else if (inputPath.includes('.windsurfrules')) format = 'windsurf'\n else if (inputPath.endsWith('.rules')) format = 'zed'\n else if (inputPath.endsWith('AGENTS.md')) format = 'codex'\n else if (inputPath.endsWith('CLAUDE.md')) format = 'claude'\n else if (inputPath.endsWith('GEMINI.md')) format = 'gemini'\n else if (inputPath.endsWith('CONVENTIONS.md')) format = 'aider'\n else if (inputPath.endsWith('best_practices.md')) format = 'qodo'\n else if (inputPath.includes('.roo/rules')) format = 'roo'\n else {\n console.error(color.error('Cannot auto-detect format'))\n console.error(color.dim('Hint: Specify format with -f (copilot|cursor|cline|windsurf|zed|codex|aider|claude|gemini|qodo|roo|opencode)'))\n process.exit(1)\n }\n }\n\n console.log(`Format: ${color.format(format)}`)\n console.log(`Input: ${color.path(inputPath)}`)\n\n // Import using appropriate importer\n const { importCopilot, importCursor, importCline, importWindsurf, importZed, importCodex, importAider, importClaudeCode, importGemini, importQodo } = await import('./importers.js')\n \n let result\n switch (format) {\n case 'copilot':\n result = importCopilot(inputPath)\n break\n case 'cursor':\n result = importCursor(inputPath)\n break\n case 'cline':\n result = importCline(inputPath)\n break\n case 'windsurf':\n result = importWindsurf(inputPath)\n break\n case 'zed':\n result = importZed(inputPath)\n break\n case 'codex':\n result = importCodex(inputPath)\n break\n case 'aider':\n result = importAider(inputPath)\n break\n case 'claude':\n result = importClaudeCode(inputPath)\n break\n case 'opencode':\n result = importOpenCode(inputPath)\n break\n case 'gemini':\n result = importGemini(inputPath)\n break\n case 'qodo':\n result = importQodo(inputPath)\n break\n case 'roo':\n result = importRoo(inputPath)\n break\n default:\n console.error(color.error(`Unknown format: ${format}`))\n process.exit(1)\n }\n\n const outputDir = values.output || dirname(inputPath)\n const agentDir = join(outputDir, '.agent')\n\n // If an .agent directory already exists and overwrite flag is NOT set,\n // remove it to ensure a clean conversion output. This prevents stale\n // rules from previous operations (e.g., example workspaces) from\n // contaminating the converted output and breaking ordering-sensitive tests.\n if (existsSync(agentDir) && !values.overwrite) {\n rmSync(agentDir, { recursive: true, force: true })\n }\n\n if (isDryRun) {\n console.log(color.info(`Would export to: ${color.path(agentDir)}`))\n console.log(color.dim(`Rules found: ${result.rules.length}`))\n } else {\n exportToAgent(result.rules, outputDir)\n console.log(color.success(`Exported to: ${color.path(agentDir)}`))\n console.log(color.dim(`Created ${result.rules.length} .mdc file(s)`))\n }\n break\n }\n\n default:\n console.error(color.error(`Unknown