UNPKG

dotagent

Version:

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

1 lines 111 kB
{"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 *\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) => yaml.dump(data)\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 yaml from 'js-yaml'\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 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 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 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","#!/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 } 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 'no-gitignore': { type: 'boolean' },\n },\n allowPositionals: true\n}) as { values: any; positionals: string[] }\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)\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('--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 ]\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']\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 'qodo':\n exportPath = join(outputDir, 'best_practices.md')\n if (!isDryRun) exportToQodo(rules, exportPath, options)\n exportedPaths.push('best_practices.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 // Ask about gitignore unless --no-gitignore is specified\n if (!isDryRun && exportedPaths.length > 0 && !values['no-gitignore']) {\n console.log()\n const shouldUpdateGitignore = await confirm('Add exported files to .gitignore?', true)\n \n if (shouldUpdateGitignore) {\n updateGitignoreWithPaths(outputDir, exportedPaths)\n console.log(color.success('Updated .gitignore'))\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 {\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)'))\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 'gemini':\n result = importGemini(inputPath)\n break\n case 'qodo':\n result = importQodo(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 command: ${command}`))\n showHelp()\n process.exit(1)\n }\n}\n\nfunction updateGitignoreWithPaths(repoPath: string, paths: string[]): void {\n const gitignorePath = join(repoPath, '.gitignore')\n \n const patterns = [\n '',\n '# Added by dotagent: ignore exported AI rule files',\n ...paths.map(p => p.endsWith('/') ? p + '**' : p),\n ''\n ].join('\\n')\n \n if (existsSync(gitignorePath)) {\n const content = readFileSync(gitignorePath, 'utf-8')\n \n // Check if any of the patterns already exist\n const newPatterns = paths.filter(p => {\n const pattern = p.endsWith('/') ? p + '**' : p\n return !content.includes(pattern)\n })\n \n if (newPatterns.length > 0) {\n appendFileSync(gitignorePath, patterns)\n }\n } else {\n writeFileSync(gitignorePath, patterns.trim() + '\\n')\n }\n}\n\nfunction updateGitignore(repoPath: string): void {\n const gitignorePath = join(repoPath, '.gitignore')\n const privatePatterns = [\n '# Added by dotagent: ignore private AI rule files',\n '.agent/**/*.local.md',\n '.agent/private/**',\n '.github/copilot-instructions.local.md',\n '.cursor/rules/**/*.local.{mdc,md}',\n '.cursor/rules-private/**',\n '.clinerules.local',\n '.clinerules/private/**',\n '.windsurfrules.local',\n '.rules.local',\n 'AGENTS.local.md',\n 'CONVENTIONS.local.md',\n 'CLAUDE.local.md',\n 'GEMINI.local.md'\n ].join('\\n')\n \n if (existsSync(gitignorePath)) {\n const content = readFileSync(gitignorePath, 'utf-8')\n \n // Check if patterns are already present\n if (!content.includes('# Added by dotagent:')) {\n console.log(color.info('Updating .gitignore with private file patterns'))\n appendFileSync(gitignorePath, '\\n\\n' + privatePatterns + '\\n', 'utf-8')\n }\n } else {\n // Create new .gitignore\n console.log(color.info('Creating .gitignore with private file patterns'))\n writeFileSync(gitignorePath, privatePatterns + '\\n', 'utf-8')\n }\n}\n\nmain().catch(error => {\n console.error(color.error('Unexpected error:'))\n console.error(error)\n process.exit(1)\n})","export {\n parseAgentMarkdown,\n parseFenceEncodedMarkdown\n} from './parser.js'\n\nexport {\n importAll,\n importAgent,\n importCopilot,\n importCursor,\n importCursorLegacy,\n importCline,\n importWindsurf,\n importZed,\n importCodex,\n importAider,\n importClaudeCode,\n importGemini,\n importQodo,\n importAmazonQ\n} from './importers.js'\n\nexport {\n toAgentMarkdown,\n exportToAgent,\n exportToCopilot,\n exportToCursor,\n exportToCline,\n exportToWindsurf,\n exportToZed,\n exportToCodex,\n exportToAider,\n exportToClaudeCode,\n exportToGemini,\n exportToQodo,\n exportToAmazonQ,\n exportAll\n} from './exporters.js'\n\nexport type {\n RuleMetadata,\n RuleBlock,\n ImportResult,\n ImportResults,\n ExportOptions,\n ParserOptions\n} from './types.js'","import { unified } from 'unified'\nimport remarkParse from 'remark-parse'\nimport { toMarkdown } from 'mdast-util-to-markdown'\nimport yaml from 'js-yaml'\nimport type { Root, RootContent } from 'mdast'\nimport type { RuleBlock, RuleMetadata, ParserOptions } from './types.js'\n\n/**\n * @deprecated Use importAgent() instead. Single-file .agentconfig format is deprecated.\n */\nexport function parseAgentMarkdown(\n markdown: string,\n options: ParserOptions = {}\n): RuleBlock[] {\n console.warn('Warning: parseAgentMarkdown() is deprecated. Use importAgent() to import from .agent/ directory instead.')\n const processor = unified().use(remarkParse)\n const tree = processor.parse(markdown) as Root\n\n const rules: RuleBlock[] = []\n let currentMetadata: RuleMetadata | null = null\n let currentContent: RootContent[] = []\n let currentPosition: RuleBlock['position'] | undefined\n\n for (let i = 0; i < tree.children.length; i++) {\n const node = tree.children[i]\n\n // Check for HTML comment with @<id> directive\n if (node.type === 'html' && isRuleComment(node.value)) {\n // If we have accumulated content, save the previous rule\n if (currentMetadata && currentContent.length > 0) {\n rules.push({\n metadata: currentMetadata,\n content: nodesToMarkdown(currentContent),\n position: currentPosition\n })\n }\n\n // Parse the new metadata\n currentMetadata = parseRuleComment(node.value)\n currentContent = []\n currentPosition = node.position ? {\n start: { ...node.position.start },\n end: { ...node.position.end }\n } : undefined\n }\n // Accumulate content\n else if (currentMetadata) {\n currentContent.push(node)\n if (currentPosition && node.position) {\n currentPosition.end = { ...node.position.end }\n }\n }\n }\n\n // Don't forget the last rule\n if (currentMetadata && currentContent.length > 0) {\n rules.push({\n metadata: currentMetadata,\n content: nodesToMarkdown(currentContent),\n position: currentPosition\n })\n }\n\n return rules\n}\n\nfunction isRuleComment(html: string): boolean {\n // Check if it contains @<id> pattern (@ followed by alphanumeric and hyphens)\n return /<!--\\s*@[a-zA-Z0-9-]+(\\s|$)/.test(html)\n}\n\nfunction parseRuleComment(html: string): RuleMetadata {\n // Extract @<id> and any additional metadata\n const match = html.match(/<!--\\s*@([a-zA-Z0-9-]+)\\s*([\\s\\S]*?)\\s*-->/)\n if (!match) {\n throw new Error('Invalid rule comment format')\n }\n\n const id = match[1]\n const metaContent = match[2].trim()\n\n // Start with the ID from the @<id> pattern\n const metadata: RuleMetadata = { id }\n\n // If there's no additional content, return just the ID\n if (!metaContent) {\n return metadata\n }\n\n // Check if it looks like YAML (has newlines or starts with a YAML indicator)\n if (metaContent.includes('\\n') || metaContent.startsWith('-') || metaContent.includes(': ')) {\n // Try to parse as YAML\n try {\n const parsed = yaml.load(metaContent) as Record<string, unknown>\n if (typeof parsed === 'object' && parsed !== null) {\n // Merge with existing metadata, but preserve the ID from @<id>\n return { ...parsed, id } as RuleMetadata\n }\n } catch {\n // Fall through to key:value parsing\n }\n }\n\n // Parse as key:value pairs\n // First check if it's all on one line (inline format)\n if (!metaContent.includes('\\n')) {\n // Inline format: key:value pairs separated by spaces\n const pairs = metaContent.matchAll(/(\\w+):(\\S+)(?:\\s|$)/g);\n for (const [, key, value] of pairs) {\n // Skip 'id' since we already have it from @<id>\n if (key === 'scope' && value.includes(',')) {\n metadata[key] = value.split(',').map(s => s.trim())\n } else if (key === 'alwaysApply' || key === 'manual') {\n metadata[key] = value === 'true'\n } else if (key !== 'id') {\n metadata[key] = value\n }\n }\n } else {\n // Multi-line format: one key:value per line\n const lines = metaContent.split('\\n');\n for (const line of lines) {\n const colonIndex = line.indexOf(':');\n if (colonIndex > 0) {\n const key = line.substring(0, colonIndex).trim();\n const value = line.substring(colonIndex + 1).trim();\n \n // Skip 'id' since we already have it from @<id>\n if (key === 'scope' && value.includes(',')) {\n metadata[key] = value.split(',').map(s => s.trim())\n } else if (key === 'alwaysApply' || key === 'manual') {\n metadata[key] = value === 'true'\n } else if (key !== 'id' && value) {\n metadata[key] = value\n }\n }\n }\n }\n\n return metadata\n}\n\nfunction nodesToMarkdown(nodes: RootContent[]): string {\n const tree: Root = {\n type: 'root',\n children: nodes\n }\n\n return toMarkdown(tree, {\n bullet: '-',\n emphasis: '*',\n rule: '-'\n }).trim()\n}\n\n// Alternative parser for fence-encoded format\nexport function parseFenceEncodedMarkdown(\n markdown: string,\n options: ParserOptions = {}\n): RuleBlock[] {\n const processor = unified().use(remarkParse)\n const tree = processor.parse(markdown) as Root\n\n const rules: RuleBlock[] = []\n let currentMetadata: RuleMetadata | null = null\n let currentContent: RootContent[] = []\n let currentPosition: RuleBlock['position'] | undefined\n\n for (let i = 0; i < tree.children.length; i++) {\n const node = tree.children[i]\n\n // Check for code block with 'rule' language\n if (node.type === 'code' && node.lang === 'rule') {\n // Save previous rule if exists\n if (currentMetadata && currentContent.length > 0) {\n rules.push({\n metadata: