@bobmatnyc/ai-trackdown-tools
Version:
Professional CLI tool for ai-trackdown functionality with comprehensive PR management system
1 lines ⢠1.75 MB
Source Map (JSON)
{"version":3,"sources":["../node_modules/tsup/assets/cjs_shims.js","../src/utils/unified-path-resolver.ts","../src/utils/template-manager.ts","../src/utils/config-manager.ts","../src/utils/colors.ts","../src/utils/formatter.ts","../src/utils/frontmatter-parser.ts","../src/utils/relationship-manager.ts","../src/commands/ai/context.ts","../src/commands/ai/generate-llms-txt.ts","../src/commands/ai/track-tokens.ts","../src/commands/ai.ts","../src/commands/backlog.ts","../src/utils/project-detector.ts","../src/utils/path-resolver.ts","../src/utils/project-context-manager.ts","../src/utils/trackdown-index-manager.ts","../src/commands/backlog-enhanced.ts","../src/commands/epic/complete.ts","../src/utils/simple-id-generator.ts","../src/commands/epic/create.ts","../src/commands/epic/delete.ts","../src/utils/state-migration.ts","../src/types/ai-trackdown.ts","../src/commands/epic/list.ts","../src/commands/epic/show.ts","../src/commands/epic/update.ts","../src/commands/epic.ts","../src/utils/config.ts","../src/utils/validation.ts","../src/commands/export.ts","../src/utils/universal-ticketing-interface.ts","../src/commands/health.ts","../src/commands/index-health.ts","../src/utils/id-generator.ts","../src/commands/init.ts","../src/commands/issue/assign.ts","../src/commands/issue/close.ts","../src/commands/issue/complete.ts","../src/commands/issue/create.ts","../src/commands/issue/delete.ts","../src/commands/issue/list.ts","../src/utils/formatters.ts","../src/commands/issue/reopen.ts","../src/commands/issue/show.ts","../src/commands/issue/update.ts","../src/commands/issue.ts","../src/commands/migrate.ts","../src/commands/migrate-structure.ts","../src/commands/resolve.ts","../src/commands/state.ts","../src/commands/migrate-state.ts","../src/commands/portfolio.ts","../src/commands/pr/approve.ts","../src/utils/pr-status-manager.ts","../src/utils/pr-file-manager.ts","../src/commands/pr/archive.ts","../src/commands/pr/close.ts","../src/commands/pr/merge.ts","../src/commands/pr/batch.ts","../src/commands/pr/create.ts","../src/commands/pr/dependencies.ts","../src/commands/pr/list.ts","../src/commands/pr/review.ts","../src/commands/pr/show.ts","../src/commands/pr/sync.ts","../src/commands/pr/update.ts","../src/commands/pr.ts","../src/utils/git.ts","../src/utils/git-metadata-extractor.ts","../src/commands/project/create.ts","../src/commands/project/list.ts","../src/commands/project/show.ts","../src/commands/project/switch.ts","../src/commands/project.ts","../src/commands/status.ts","../src/commands/status-enhanced.ts","../src/commands/sync/auto.ts","../node_modules/@octokit/request-error/dist-src/index.js","../src/utils/github-client.ts","../src/integrations/github-sync.ts","../src/commands/sync/bidirectional.ts","../src/commands/sync/pull.ts","../src/commands/sync/push.ts","../src/commands/sync/setup.ts","../src/commands/sync/status.ts","../src/commands/sync.ts","../src/commands/task/complete.ts","../src/commands/task/create.ts","../src/commands/task/delete.ts","../src/commands/task/list.ts","../src/commands/task/show.ts","../src/commands/task/update.ts","../src/commands/task.ts","../src/commands/track.ts","../src/utils/version.ts","../src/utils/changelog.ts","../src/commands/version/bump.ts","../src/commands/version/changelog.ts","../src/commands/version/release.ts","../src/commands/version/show.ts","../src/commands/version/sync.ts","../src/commands/version/tag.ts","../src/commands/version.ts","../src/index.ts","../src/cli.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","/**\n * Unified Path Resolver for AI-Trackdown\n * Implements single root directory structure: tasks/{type}/\n * ATT-004: Fix Task Directory Structure - Single Root Directory Implementation\n */\n\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { ProjectConfig } from '../types/ai-trackdown.js';\n\nexport interface UnifiedPaths {\n projectRoot: string;\n configDir: string;\n tasksRoot: string; // The single configurable root (default: \"tasks\")\n epicsDir: string; // {tasksRoot}/epics/\n issuesDir: string; // {tasksRoot}/issues/\n tasksDir: string; // {tasksRoot}/tasks/\n prsDir: string; // {tasksRoot}/prs/\n templatesDir: string; // {tasksRoot}/templates/\n}\n\n/**\n * Unified Path Resolver implementing the required directory structure\n * All task types are organized under a single configurable root directory\n */\nexport class UnifiedPathResolver {\n private config: ProjectConfig;\n private projectRoot: string;\n private cliTasksDir?: string; // CLI override via --tasks-dir or --root-dir\n\n constructor(config: ProjectConfig, projectRoot: string, cliTasksDir?: string) {\n this.config = config;\n this.projectRoot = projectRoot;\n this.cliTasksDir = cliTasksDir;\n }\n\n /**\n * Get the tasks root directory with proper priority resolution:\n * 1. CLI option override (--root-dir, --tasks-dir)\n * 2. Environment variable (AITRACKDOWN_TASKS_DIR)\n * 3. Config file setting (tasks_directory)\n * 4. Default to \"tasks\"\n */\n getTasksRootDirectory(): string {\n // 1. CLI option takes highest priority\n if (this.cliTasksDir) {\n return this.cliTasksDir;\n }\n\n // 2. Environment variable override\n const envTasksDir = process.env.AITRACKDOWN_TASKS_DIR || process.env.AITRACKDOWN_ROOT_DIR;\n if (envTasksDir) {\n return envTasksDir;\n }\n\n // 3. Config file setting\n if (this.config.tasks_directory) {\n return this.config.tasks_directory;\n }\n\n // 4. Default to \"tasks\"\n return 'tasks';\n }\n\n /**\n * Get all unified paths following the required structure\n */\n getUnifiedPaths(): UnifiedPaths {\n const tasksRoot = this.getTasksRootDirectory();\n\n return {\n projectRoot: this.projectRoot,\n configDir: join(this.projectRoot, '.ai-trackdown'),\n tasksRoot: join(this.projectRoot, tasksRoot),\n epicsDir: join(this.projectRoot, tasksRoot, this.config.structure.epics_dir),\n issuesDir: join(this.projectRoot, tasksRoot, this.config.structure.issues_dir),\n tasksDir: join(this.projectRoot, tasksRoot, this.config.structure.tasks_dir),\n prsDir: join(this.projectRoot, tasksRoot, this.config.structure.prs_dir || 'prs'),\n templatesDir: join(this.projectRoot, tasksRoot, this.config.structure.templates_dir),\n };\n }\n\n /**\n * Get path for specific item type\n */\n getItemTypeDirectory(type: 'project' | 'epic' | 'issue' | 'task' | 'pr'): string {\n const paths = this.getUnifiedPaths();\n\n switch (type) {\n case 'project':\n return join(paths.tasksRoot, 'projects');\n case 'epic':\n return paths.epicsDir;\n case 'issue':\n return paths.issuesDir;\n case 'task':\n return paths.tasksDir;\n case 'pr':\n return paths.prsDir;\n default:\n throw new Error(`Unknown item type: ${type}`);\n }\n }\n\n /**\n * Get all directories that should be created for the unified structure\n */\n getRequiredDirectories(): string[] {\n const paths = this.getUnifiedPaths();\n\n return [\n paths.configDir,\n paths.tasksRoot,\n paths.epicsDir,\n paths.issuesDir,\n paths.tasksDir,\n paths.prsDir,\n paths.templatesDir,\n ];\n }\n\n /**\n * Check if legacy directory structure exists (separate root directories)\n */\n detectLegacyStructure(): {\n hasLegacy: boolean;\n legacyDirs: string[];\n suggestions: string[];\n } {\n const legacyDirs: string[] = [];\n const suggestions: string[] = [];\n\n // Check for old separate root directories\n const potentialLegacyDirs = [\n join(this.projectRoot, 'epics'),\n join(this.projectRoot, 'issues'),\n join(this.projectRoot, 'tasks'),\n join(this.projectRoot, 'prs'),\n join(this.projectRoot, 'trackdown'), // Old trackdown structure\n ];\n\n for (const dir of potentialLegacyDirs) {\n if (existsSync(dir)) {\n legacyDirs.push(dir);\n }\n }\n\n if (legacyDirs.length > 0) {\n const tasksRoot = this.getTasksRootDirectory();\n\n suggestions.push(\n `# Detected legacy directory structure. Migration options:`,\n ``,\n `# Option 1: Use CLI override to maintain current structure`,\n `export AITRACKDOWN_TASKS_DIR=\"\" # Use project root`,\n ``,\n `# Option 2: Migrate to unified structure`,\n `mkdir -p ${tasksRoot}`,\n ...legacyDirs.map((dir) => {\n const dirName = dir.split('/').pop();\n return `mv ${dirName} ${tasksRoot}/${dirName} 2>/dev/null || true`;\n }),\n ``,\n `# Option 3: Update configuration`,\n `# Edit .ai-trackdown/config.yaml and set:`,\n `# tasks_directory: \"\" # Use project root`\n );\n }\n\n return {\n hasLegacy: legacyDirs.length > 0,\n legacyDirs,\n suggestions,\n };\n }\n\n /**\n * Get migration commands for moving to unified structure\n */\n getMigrationCommands(): string[] {\n const legacy = this.detectLegacyStructure();\n\n if (!legacy.hasLegacy) {\n return [];\n }\n\n return legacy.suggestions;\n }\n\n /**\n * Validate current directory structure\n */\n validateStructure(): {\n valid: boolean;\n issues: string[];\n missingDirs: string[];\n } {\n const _paths = this.getUnifiedPaths();\n const issues: string[] = [];\n const missingDirs: string[] = [];\n\n // Check if required directories exist\n const requiredDirs = this.getRequiredDirectories();\n\n for (const dir of requiredDirs) {\n if (!existsSync(dir)) {\n missingDirs.push(dir);\n }\n }\n\n // Check for legacy structure conflicts\n const legacy = this.detectLegacyStructure();\n if (legacy.hasLegacy) {\n issues.push(`Legacy directory structure detected: ${legacy.legacyDirs.join(', ')}`);\n }\n\n return {\n valid: issues.length === 0 && missingDirs.length === 0,\n issues,\n missingDirs,\n };\n }\n\n /**\n * Update CLI tasks directory override\n */\n setCliTasksDir(tasksDir: string): void {\n this.cliTasksDir = tasksDir;\n }\n\n /**\n * Clear CLI tasks directory override\n */\n clearCliTasksDir(): void {\n this.cliTasksDir = undefined;\n }\n\n /**\n * Show structure information for debugging\n */\n showStructureInfo(): void {\n const paths = this.getUnifiedPaths();\n const validation = this.validateStructure();\n\n console.log(`\\nšļø AI-Trackdown Directory Structure`);\n console.log(`š Tasks Root: ${paths.tasksRoot}`);\n console.log(` āāā š epics/ ā ${paths.epicsDir}`);\n console.log(` āāā š issues/ ā ${paths.issuesDir}`);\n console.log(` āāā š tasks/ ā ${paths.tasksDir}`);\n console.log(` āāā š prs/ ā ${paths.prsDir}`);\n console.log(` āāā š templates/ ā ${paths.templatesDir}`);\n\n if (validation.missingDirs.length > 0) {\n console.log(`\\nā ļø Missing directories:`);\n validation.missingDirs.forEach((dir) => console.log(` ⢠${dir}`));\n }\n\n if (validation.issues.length > 0) {\n console.log(`\\nšØ Issues detected:`);\n validation.issues.forEach((issue) => console.log(` ⢠${issue}`));\n }\n\n const legacy = this.detectLegacyStructure();\n if (legacy.hasLegacy) {\n console.log(`\\nš Migration suggestions:`);\n legacy.suggestions.forEach((suggestion) => console.log(` ${suggestion}`));\n }\n }\n}\n","/**\n * Template Manager for AI-Trackdown\n * Handles bundled template deployment and management\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport * as YAML from 'yaml';\nimport type { ItemTemplate } from '../types/ai-trackdown.js';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\n/**\n * Template Manager class for handling bundled templates\n */\nexport class TemplateManager {\n private bundledTemplatesDir: string;\n\n constructor() {\n // Path to bundled templates in the package\n // Try multiple possible locations for bundled templates\n const possiblePaths = [\n path.join(__dirname, '../../templates'), // Development: src/utils -> templates\n path.join(__dirname, '../templates'), // Compiled: dist/utils -> dist/templates\n path.join(__dirname, 'templates'), // Compiled: dist -> dist/templates\n path.resolve(__dirname, '..', 'templates'), // Alternative dist structure\n ];\n\n // Find the first path that exists\n this.bundledTemplatesDir =\n possiblePaths.find((dir) => {\n try {\n return fs.existsSync(dir);\n } catch {\n return false;\n }\n }) || path.join(__dirname, '../../templates'); // fallback to original\n }\n\n /**\n * Get the path to bundled templates\n */\n public getBundledTemplatesDir(): string {\n return this.bundledTemplatesDir;\n }\n\n /**\n * Check if bundled templates exist\n */\n public hasBundledTemplates(): boolean {\n return fs.existsSync(this.bundledTemplatesDir);\n }\n\n /**\n * List all bundled template files\n */\n public listBundledTemplates(): string[] {\n if (!this.hasBundledTemplates()) {\n return [];\n }\n\n try {\n return fs\n .readdirSync(this.bundledTemplatesDir)\n .filter((file) => file.endsWith('.yaml'))\n .sort();\n } catch (error) {\n console.warn(\n `Failed to list bundled templates: ${error instanceof Error ? error.message : 'Unknown error'}`\n );\n return [];\n }\n }\n\n /**\n * Deploy bundled templates to project's templates directory\n */\n public deployTemplates(projectTemplatesDir: string, force: boolean = false): void {\n if (!this.hasBundledTemplates()) {\n console.warn('No bundled templates found. Creating default templates programmatically.');\n this.createDefaultTemplates(projectTemplatesDir, force);\n return;\n }\n\n // Ensure project templates directory exists\n if (!fs.existsSync(projectTemplatesDir)) {\n fs.mkdirSync(projectTemplatesDir, { recursive: true });\n }\n\n const bundledFiles = this.listBundledTemplates();\n let deployedCount = 0;\n\n for (const templateFile of bundledFiles) {\n const sourcePath = path.join(this.bundledTemplatesDir, templateFile);\n const destPath = path.join(projectTemplatesDir, templateFile);\n\n try {\n // Check if file exists and force flag\n if (fs.existsSync(destPath) && !force) {\n console.log(`āļø Skipping ${templateFile} (already exists)`);\n continue;\n }\n\n // Copy template file\n fs.copyFileSync(sourcePath, destPath);\n console.log(`ā
Deployed ${templateFile}`);\n deployedCount++;\n } catch (error) {\n console.error(\n `ā Failed to deploy ${templateFile}: ${error instanceof Error ? error.message : 'Unknown error'}`\n );\n }\n }\n\n console.log(`š¦ Deployed ${deployedCount} template(s) to ${projectTemplatesDir}`);\n }\n\n /**\n * Create default templates programmatically if bundled templates are not available\n */\n private createDefaultTemplates(projectTemplatesDir: string, force: boolean = false): void {\n if (!fs.existsSync(projectTemplatesDir)) {\n fs.mkdirSync(projectTemplatesDir, { recursive: true });\n }\n\n const defaultTemplates: ItemTemplate[] = [\n {\n type: 'epic',\n name: 'default',\n description: 'Default epic template',\n frontmatter_template: {\n title: 'Epic Title',\n description: 'Epic description',\n status: 'planning',\n priority: 'medium',\n assignee: 'unassigned',\n created_date: '',\n updated_date: '',\n estimated_tokens: 0,\n actual_tokens: 0,\n ai_context: [\n 'context/requirements',\n 'context/constraints',\n 'context/assumptions',\n 'context/dependencies',\n ],\n sync_status: 'local',\n },\n content_template: `# Epic: {{title}}\n\n## Overview\n{{description}}\n\n## Objectives\n- [ ] Objective 1\n- [ ] Objective 2\n- [ ] Objective 3\n\n## Acceptance Criteria\n- [ ] Criteria 1\n- [ ] Criteria 2\n\n## Related Issues\n{{#related_issues}}\n- {{.}}\n{{/related_issues}}\n\n## Notes\nAdd any additional notes here.`,\n },\n {\n type: 'issue',\n name: 'default',\n description: 'Default issue template',\n frontmatter_template: {\n title: 'Issue Title',\n description: 'Issue description',\n status: 'planning',\n priority: 'medium',\n assignee: 'unassigned',\n created_date: '',\n updated_date: '',\n estimated_tokens: 0,\n actual_tokens: 0,\n ai_context: [\n 'context/requirements',\n 'context/constraints',\n 'context/assumptions',\n 'context/dependencies',\n ],\n sync_status: 'local',\n },\n content_template: `# Issue: {{title}}\n\n## Description\n{{description}}\n\n## Tasks\n{{#related_tasks}}\n- [ ] {{.}}\n{{/related_tasks}}\n\n## Acceptance Criteria\n- [ ] Criteria 1\n- [ ] Criteria 2\n\n## Notes\nAdd any additional notes here.`,\n },\n {\n type: 'task',\n name: 'default',\n description: 'Default task template',\n frontmatter_template: {\n title: 'Task Title',\n description: 'Task description',\n status: 'planning',\n priority: 'medium',\n assignee: 'unassigned',\n created_date: '',\n updated_date: '',\n estimated_tokens: 0,\n actual_tokens: 0,\n ai_context: [\n 'context/requirements',\n 'context/constraints',\n 'context/assumptions',\n 'context/dependencies',\n ],\n sync_status: 'local',\n },\n content_template: `# Task: {{title}}\n\n## Description\n{{description}}\n\n## Steps\n1. Step 1\n2. Step 2\n3. Step 3\n\n## Acceptance Criteria\n- [ ] Criteria 1\n- [ ] Criteria 2\n\n## Notes\nAdd any additional notes here.`,\n },\n {\n type: 'pr',\n name: 'default',\n description: 'Default PR template',\n frontmatter_template: {\n title: 'PR Title',\n description: 'PR description',\n status: 'planning',\n priority: 'medium',\n assignee: 'unassigned',\n created_date: '',\n updated_date: '',\n estimated_tokens: 0,\n actual_tokens: 0,\n ai_context: [\n 'context/requirements',\n 'context/constraints',\n 'context/assumptions',\n 'context/dependencies',\n ],\n sync_status: 'local',\n },\n content_template: `# PR: {{title}}\n\n## Description\n{{description}}\n\n## Changes\n- Change 1\n- Change 2\n- Change 3\n\n## Testing\n- [ ] Unit tests pass\n- [ ] Integration tests pass\n- [ ] Manual testing completed\n\n## Checklist\n- [ ] Code follows style guidelines\n- [ ] Self-review completed\n- [ ] Documentation updated\n- [ ] Tests added/updated\n\n## Related\n- Issue: {{issue_id}}\n- Branch: {{branch_name}}\n- Target: {{target_branch}}\n\n## Notes\nAdd any additional notes here.`,\n },\n ];\n\n let deployedCount = 0;\n\n for (const template of defaultTemplates) {\n const templatePath = path.join(projectTemplatesDir, `${template.type}-${template.name}.yaml`);\n\n try {\n // Check if file exists and force flag\n if (fs.existsSync(templatePath) && !force) {\n console.log(`āļø Skipping ${template.type}-${template.name}.yaml (already exists)`);\n continue;\n }\n\n const templateContent = YAML.stringify(template, {\n indent: 2,\n lineWidth: 120,\n });\n\n fs.writeFileSync(templatePath, templateContent, 'utf8');\n console.log(`ā
Created ${template.type}-${template.name}.yaml`);\n deployedCount++;\n } catch (error) {\n console.error(\n `ā Failed to create ${template.type}-${template.name}.yaml: ${error instanceof Error ? error.message : 'Unknown error'}`\n );\n }\n }\n\n console.log(`š¦ Created ${deployedCount} default template(s) in ${projectTemplatesDir}`);\n }\n\n /**\n * Get template by type and name, with fallback to bundled templates\n */\n public getTemplate(\n projectTemplatesDir: string,\n type: 'epic' | 'issue' | 'task' | 'pr',\n name: string = 'default'\n ): ItemTemplate | null {\n const templateFileName = `${type}-${name}.yaml`;\n\n // First, try to load from project templates directory\n const projectTemplatePath = path.join(projectTemplatesDir, templateFileName);\n if (fs.existsSync(projectTemplatePath)) {\n try {\n const templateContent = fs.readFileSync(projectTemplatePath, 'utf8');\n return YAML.parse(templateContent) as ItemTemplate;\n } catch (error) {\n console.warn(\n `Failed to load project template ${projectTemplatePath}: ${error instanceof Error ? error.message : 'Unknown error'}`\n );\n }\n }\n\n // Fallback to bundled templates\n if (this.hasBundledTemplates()) {\n const bundledTemplatePath = path.join(this.bundledTemplatesDir, templateFileName);\n if (fs.existsSync(bundledTemplatePath)) {\n try {\n const templateContent = fs.readFileSync(bundledTemplatePath, 'utf8');\n return YAML.parse(templateContent) as ItemTemplate;\n } catch (error) {\n console.warn(\n `Failed to load bundled template ${bundledTemplatePath}: ${error instanceof Error ? error.message : 'Unknown error'}`\n );\n }\n }\n }\n\n return null;\n }\n\n /**\n * Validate template file structure\n */\n public validateTemplate(templatePath: string): boolean {\n try {\n const content = fs.readFileSync(templatePath, 'utf8');\n const template = YAML.parse(content) as ItemTemplate;\n\n // Check required fields\n const requiredFields = [\n 'type',\n 'name',\n 'description',\n 'frontmatter_template',\n 'content_template',\n ];\n for (const field of requiredFields) {\n if (!template[field as keyof ItemTemplate]) {\n console.error(`Template ${templatePath} missing required field: ${field}`);\n return false;\n }\n }\n\n // Check valid type\n const validTypes = ['epic', 'issue', 'task', 'pr'];\n if (!validTypes.includes(template.type)) {\n console.error(`Template ${templatePath} has invalid type: ${template.type}`);\n return false;\n }\n\n return true;\n } catch (error) {\n console.error(\n `Failed to validate template ${templatePath}: ${error instanceof Error ? error.message : 'Unknown error'}`\n );\n return false;\n }\n }\n}\n","/**\n * Configuration Manager for AI-Trackdown\n * Handles .ai-trackdown/config.yaml configuration system\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport * as YAML from 'yaml';\nimport type { ItemTemplate, ProjectConfig } from '../types/ai-trackdown.js';\nimport { UnifiedPathResolver } from './unified-path-resolver.js';\n\nconst DEFAULT_CONFIG_DIR = '.ai-trackdown';\nconst DEFAULT_CONFIG_FILE = 'config.yaml';\nconst _DEFAULT_TEMPLATES_DIR = 'templates';\n\nexport class ConfigManager {\n private configPath: string;\n private config: ProjectConfig | null = null;\n\n constructor(projectRoot?: string) {\n const root = projectRoot || this.findProjectRoot();\n this.configPath = path.join(root, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILE);\n }\n\n /**\n * Load configuration from file\n */\n public loadConfig(): ProjectConfig {\n if (this.config) {\n return this.config;\n }\n\n if (!fs.existsSync(this.configPath)) {\n throw new Error(\n `AI-Trackdown configuration not found at ${this.configPath}. Run 'aitrackdown init' to create a new project.`\n );\n }\n\n try {\n const configContent = fs.readFileSync(this.configPath, 'utf8');\n const rawConfig = YAML.parse(configContent) as any;\n\n // Handle both old format (project.name) and new format (name)\n if (rawConfig.project?.name && !rawConfig.name) {\n // Convert old format to new format\n this.config = {\n name: rawConfig.project.name,\n description: rawConfig.project.description,\n version: rawConfig.version || '1.0.0',\n tasks_directory: rawConfig.tasks_directory || 'tasks',\n structure: rawConfig.structure || {\n epics_dir: 'epics',\n issues_dir: 'issues',\n tasks_dir: 'tasks',\n templates_dir: 'templates',\n prs_dir: 'prs',\n },\n naming_conventions: rawConfig.naming_conventions || {\n epic_prefix: 'EP',\n issue_prefix: 'ISS',\n task_prefix: 'TSK',\n pr_prefix: 'PR',\n file_extension: '.md',\n },\n default_assignee: rawConfig.default_assignee || 'unassigned',\n ai_context_templates: rawConfig.ai_context_templates || [],\n automation: rawConfig.automation || {\n auto_update_timestamps: true,\n auto_calculate_tokens: false,\n auto_sync_status: true,\n },\n };\n } else {\n this.config = rawConfig as ProjectConfig;\n }\n\n // Validate and normalize config\n this.validateConfig(this.config);\n this.normalizeConfig(this.config);\n\n return this.config;\n } catch (error) {\n throw new Error(\n `Failed to load AI-Trackdown configuration: ${error instanceof Error ? error.message : 'Unknown error'}`\n );\n }\n }\n\n /**\n * Save configuration to file\n */\n public saveConfig(config: ProjectConfig): void {\n this.validateConfig(config);\n\n const configDir = path.dirname(this.configPath);\n if (!fs.existsSync(configDir)) {\n fs.mkdirSync(configDir, { recursive: true });\n }\n\n const yamlContent = YAML.stringify(config, {\n indent: 2,\n lineWidth: 120,\n minContentWidth: 20,\n });\n\n fs.writeFileSync(this.configPath, yamlContent, 'utf8');\n this.config = config;\n }\n\n /**\n * Create default configuration\n */\n public createDefaultConfig(\n projectName: string,\n options: Partial<ProjectConfig> = {}\n ): ProjectConfig {\n const defaultConfig: ProjectConfig = {\n name: projectName,\n description: options.description || `AI-Trackdown project: ${projectName}`,\n version: '1.0.0',\n // NEW: Default tasks directory for unified structure\n tasks_directory: options.tasks_directory || 'tasks',\n structure: {\n epics_dir: 'epics',\n issues_dir: 'issues',\n tasks_dir: 'tasks',\n templates_dir: 'templates',\n prs_dir: 'prs', // NEW: PR directory\n },\n naming_conventions: {\n epic_prefix: 'EP',\n issue_prefix: 'ISS',\n task_prefix: 'TSK',\n pr_prefix: 'PR', // NEW: PR prefix\n file_extension: '.md',\n },\n default_assignee: options.default_assignee || 'unassigned',\n ai_context_templates: [\n 'context/requirements',\n 'context/constraints',\n 'context/assumptions',\n 'context/dependencies',\n ],\n automation: {\n auto_update_timestamps: true,\n auto_calculate_tokens: false,\n auto_sync_status: true,\n },\n ...options,\n };\n\n return defaultConfig;\n }\n\n /**\n * Initialize new project with default structure\n */\n public initializeProject(\n projectName: string,\n options: Partial<ProjectConfig> = {}\n ): ProjectConfig {\n const config = this.createDefaultConfig(projectName, options);\n\n // Create directory structure\n this.createProjectStructure(config);\n\n // Create default templates\n this.createDefaultTemplates(config);\n\n // Save configuration\n this.saveConfig(config);\n\n return config;\n }\n\n /**\n * Initialize new project with structure only (no template creation)\n */\n public initializeProjectStructure(\n projectName: string,\n options: Partial<ProjectConfig> = {}\n ): ProjectConfig {\n const config = this.createDefaultConfig(projectName, options);\n\n // Create directory structure only\n this.createProjectStructure(config);\n\n // Save configuration\n this.saveConfig(config);\n\n return config;\n }\n\n /**\n * Update specific configuration values\n */\n public updateConfig(updates: Partial<ProjectConfig>): ProjectConfig {\n const currentConfig = this.loadConfig();\n const updatedConfig = this.deepMerge(currentConfig, updates);\n\n this.saveConfig(updatedConfig);\n return updatedConfig;\n }\n\n /**\n * Get configuration with environment overrides\n */\n public getConfig(): ProjectConfig {\n const config = this.loadConfig();\n\n // Apply environment variable overrides\n if (process.env.ATD_DEFAULT_ASSIGNEE) {\n config.default_assignee = process.env.ATD_DEFAULT_ASSIGNEE;\n }\n\n if (process.env.ATD_AUTO_TIMESTAMPS === 'false') {\n config.automation!.auto_update_timestamps = false;\n }\n\n if (process.env.ATD_AUTO_CALCULATE_TOKENS === 'true') {\n config.automation!.auto_calculate_tokens = true;\n }\n\n return config;\n }\n\n /**\n * Get absolute paths for project structure using unified directory layout\n */\n public getAbsolutePaths(cliTasksDir?: string): {\n projectRoot: string;\n configDir: string;\n tasksRoot: string;\n epicsDir: string;\n issuesDir: string;\n tasksDir: string;\n prsDir: string;\n templatesDir: string;\n } {\n const config = this.getConfig();\n const projectRoot = path.dirname(path.dirname(this.configPath));\n\n // Import UnifiedPathResolver dynamically to avoid circular dependencies\n // UnifiedPathResolver already imported at the top\n const pathResolver = new UnifiedPathResolver(config, projectRoot, cliTasksDir);\n const unifiedPaths = pathResolver.getUnifiedPaths();\n\n return {\n projectRoot: unifiedPaths.projectRoot,\n configDir: unifiedPaths.configDir,\n tasksRoot: unifiedPaths.tasksRoot,\n epicsDir: unifiedPaths.epicsDir,\n issuesDir: unifiedPaths.issuesDir,\n tasksDir: unifiedPaths.tasksDir,\n prsDir: unifiedPaths.prsDir,\n templatesDir: unifiedPaths.templatesDir,\n };\n }\n\n /**\n * Check if current directory is an AI-Trackdown project\n */\n public isProjectDirectory(dir?: string): boolean {\n const checkDir = dir || process.cwd();\n const configPath = path.join(checkDir, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILE);\n return fs.existsSync(configPath);\n }\n\n /**\n * Find project root by walking up directory tree\n */\n public findProjectRoot(startDir?: string): string {\n let currentDir = startDir || process.cwd();\n\n while (currentDir !== path.dirname(currentDir)) {\n const configPath = path.join(currentDir, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILE);\n if (fs.existsSync(configPath)) {\n return currentDir;\n }\n currentDir = path.dirname(currentDir);\n }\n\n // If not found, return the starting directory\n return startDir || process.cwd();\n }\n\n /**\n * Validate configuration structure\n */\n private validateConfig(config: ProjectConfig): void {\n const required = ['name', 'version', 'structure', 'naming_conventions'];\n\n for (const field of required) {\n if (!config[field as keyof ProjectConfig]) {\n throw new Error(`Configuration missing required field: ${field}`);\n }\n }\n\n // Validate structure paths\n const structureFields = ['epics_dir', 'issues_dir', 'tasks_dir', 'templates_dir'];\n for (const field of structureFields) {\n if (!config.structure[field as keyof typeof config.structure]) {\n throw new Error(`Configuration structure missing required field: ${field}`);\n }\n }\n\n // Validate naming conventions\n const namingFields = ['epic_prefix', 'issue_prefix', 'task_prefix', 'file_extension'];\n for (const field of namingFields) {\n if (!config.naming_conventions[field as keyof typeof config.naming_conventions]) {\n throw new Error(`Configuration naming_conventions missing required field: ${field}`);\n }\n }\n }\n\n /**\n * Normalize configuration (ensure defaults and proper types)\n */\n private normalizeConfig(config: ProjectConfig): void {\n // Ensure automation defaults\n if (!config.automation) {\n config.automation = {\n auto_update_timestamps: true,\n auto_calculate_tokens: false,\n auto_sync_status: true,\n };\n }\n\n // Ensure arrays are arrays\n if (!config.ai_context_templates) {\n config.ai_context_templates = [];\n }\n\n // Normalize paths (remove leading/trailing slashes)\n config.structure.epics_dir = config.structure.epics_dir.replace(/^\\/|\\/$/g, '');\n config.structure.issues_dir = config.structure.issues_dir.replace(/^\\/|\\/$/g, '');\n config.structure.tasks_dir = config.structure.tasks_dir.replace(/^\\/|\\/$/g, '');\n config.structure.templates_dir = config.structure.templates_dir.replace(/^\\/|\\/$/g, '');\n\n // Ensure file extension starts with dot\n if (!config.naming_conventions.file_extension.startsWith('.')) {\n config.naming_conventions.file_extension = `.${config.naming_conventions.file_extension}`;\n }\n }\n\n /**\n * Create project directory structure using unified layout\n */\n public createProjectStructure(config: ProjectConfig): void {\n const projectRoot = path.dirname(path.dirname(this.configPath));\n\n // Import UnifiedPathResolver dynamically to avoid circular dependencies\n // UnifiedPathResolver already imported at the top\n const pathResolver = new UnifiedPathResolver(config, projectRoot);\n const requiredDirs = pathResolver.getRequiredDirectories();\n\n for (const dir of requiredDirs) {\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n }\n }\n\n /**\n * Create default templates\n */\n private createDefaultTemplates(config: ProjectConfig): void {\n const projectRoot = path.dirname(path.dirname(this.configPath));\n\n // Import UnifiedPathResolver dynamically to avoid circular dependencies\n // UnifiedPathResolver already imported at the top\n const pathResolver = new UnifiedPathResolver(config, projectRoot);\n const paths = pathResolver.getUnifiedPaths();\n const templatesDir = paths.templatesDir;\n\n const templates: ItemTemplate[] = [\n {\n type: 'epic',\n name: 'default',\n description: 'Default epic template',\n frontmatter_template: {\n title: 'Epic Title',\n description: 'Epic description',\n status: 'planning',\n priority: 'medium',\n assignee: config.default_assignee || 'unassigned',\n created_date: '',\n updated_date: '',\n estimated_tokens: 0,\n actual_tokens: 0,\n ai_context: config.ai_context_templates || [],\n sync_status: 'local',\n },\n content_template: `# Epic: {{title}}\n\n## Overview\n{{description}}\n\n## Objectives\n- [ ] Objective 1\n- [ ] Objective 2\n- [ ] Objective 3\n\n## Acceptance Criteria\n- [ ] Criteria 1\n- [ ] Criteria 2\n\n## Related Issues\n{{#related_issues}}\n- {{.}}\n{{/related_issues}}\n\n## Notes\nAdd any additional notes here.`,\n },\n {\n type: 'issue',\n name: 'default',\n description: 'Default issue template',\n frontmatter_template: {\n title: 'Issue Title',\n description: 'Issue description',\n status: 'planning',\n priority: 'medium',\n assignee: config.default_assignee || 'unassigned',\n created_date: '',\n updated_date: '',\n estimated_tokens: 0,\n actual_tokens: 0,\n ai_context: config.ai_context_templates || [],\n sync_status: 'local',\n },\n content_template: `# Issue: {{title}}\n\n## Description\n{{description}}\n\n## Tasks\n{{#related_tasks}}\n- [ ] {{.}}\n{{/related_tasks}}\n\n## Acceptance Criteria\n- [ ] Criteria 1\n- [ ] Criteria 2\n\n## Notes\nAdd any additional notes here.`,\n },\n {\n type: 'task',\n name: 'default',\n description: 'Default task template',\n frontmatter_template: {\n title: 'Task Title',\n description: 'Task description',\n status: 'planning',\n priority: 'medium',\n assignee: config.default_assignee || 'unassigned',\n created_date: '',\n updated_date: '',\n estimated_tokens: 0,\n actual_tokens: 0,\n ai_context: config.ai_context_templates || [],\n sync_status: 'local',\n },\n content_template: `# Task: {{title}}\n\n## Description\n{{description}}\n\n## Steps\n1. Step 1\n2. Step 2\n3. Step 3\n\n## Acceptance Criteria\n- [ ] Criteria 1\n- [ ] Criteria 2\n\n## Notes\nAdd any additional notes here.`,\n },\n {\n type: 'pr',\n name: 'default',\n description: 'Default PR template',\n frontmatter_template: {\n title: 'PR Title',\n description: 'PR description',\n status: 'planning',\n priority: 'medium',\n assignee: config.default_assignee || 'unassigned',\n created_date: '',\n updated_date: '',\n estimated_tokens: 0,\n actual_tokens: 0,\n ai_context: config.ai_context_templates || [],\n sync_status: 'local',\n },\n content_template: `# PR: {{title}}\n\n## Description\n{{description}}\n\n## Changes\n- Change 1\n- Change 2\n- Change 3\n\n## Testing\n- [ ] Unit tests pass\n- [ ] Integration tests pass\n- [ ] Manual testing completed\n\n## Checklist\n- [ ] Code follows style guidelines\n- [ ] Self-review completed\n- [ ] Documentation updated\n- [ ] Tests added/updated\n\n## Related\n- Issue: {{issue_id}}\n- Branch: {{branch_name}}\n- Target: {{target_branch}}\n\n## Notes\nAdd any additional notes here.`,\n },\n ];\n\n for (const template of templates) {\n const templatePath = path.join(templatesDir, `${template.type}-${template.name}.yaml`);\n if (!fs.existsSync(templatePath)) {\n const templateContent = YAML.stringify(template, {\n indent: 2,\n lineWidth: 120,\n });\n fs.writeFileSync(templatePath, templateContent, 'utf8');\n }\n }\n }\n\n /**\n * Deep merge two objects\n */\n private deepMerge(target: any, source: any): any {\n const result = { ...target };\n\n for (const key in source) {\n if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {\n result[key] = this.deepMerge(result[key] || {}, source[key]);\n } else {\n result[key] = source[key];\n }\n }\n\n return result;\n }\n\n /**\n * Get template by type and name\n */\n public getTemplate(\n type: 'epic' | 'issue' | 'task' | 'pr',\n name: string = 'default'\n ): ItemTemplate | null {\n const config = this.getConfig();\n const projectRoot = path.dirname(path.dirname(this.configPath));\n\n // Import UnifiedPathResolver dynamically to avoid circular dependencies\n // UnifiedPathResolver already imported at the top\n const pathResolver = new UnifiedPathResolver(config, projectRoot);\n const paths = pathResolver.getUnifiedPaths();\n const templatesDir = paths.templatesDir;\n\n const templatePath = path.join(templatesDir, `${type}-${name}.yaml`);\n\n if (!fs.existsSync(templatePath)) {\n return null;\n }\n\n try {\n const templateContent = fs.readFileSync(templatePath, 'utf8');\n return YAML.parse(templateContent) as ItemTemplate;\n } catch (error) {\n console.warn(\n `Failed to load template ${templatePath}: ${error instanceof Error ? error.message : 'Unknown error'}`\n );\n return null;\n }\n }\n\n /**\n * Get template by type and name with fallback to bundled templates\n */\n public getTemplateWithFallback(\n type: 'epic' | 'issue' | 'task' | 'pr',\n name: string = 'default'\n ): ItemTemplate | null {\n const config = this.getConfig();\n const projectRoot = path.dirname(path.dirname(this.configPath));\n\n // Import UnifiedPathResolver dynamically to avoid circular dependencies\n // UnifiedPathResolver already imported at the top\n const pathResolver = new UnifiedPathResolver(config, projectRoot);\n const paths = pathResolver.getUnifiedPaths();\n const templatesDir = paths.templatesDir;\n\n // Try to load from project templates first\n const projectTemplate = this.getTemplate(type, name);\n if (projectTemplate) {\n return projectTemplate;\n }\n\n // Fallback to bundled templates using TemplateManager\n const TemplateManager = require('./template-manager.js').TemplateManager;\n const templateManager = new TemplateManager();\n return templateManager.getTemplate(templatesDir, type, name);\n }\n\n /**\n * List available templates\n */\n public listTemplates(): { type: string; name: string; description: string }[] {\n const config = this.getConfig();\n const projectRoot = path.dirname(path.dirname(this.configPath));\n\n // Import UnifiedPathResolver dynamically to avoid circular dependencies\n // UnifiedPathResolver already imported at the top\n const pathResolver = new UnifiedPathResolver(config, projectRoot);\n const paths = pathResolver.getUnifiedPaths();\n const templatesDir = paths.templatesDir;\n\n if (!fs.existsSync(templatesDir)) {\n return [];\n }\n\n const templates: { type: string; name: string; description: string }[] = [];\n const files = fs.readdirSync(templatesDir).filter((file) => file.endsWith('.yaml'));\n\n for (const file of files) {\n try {\n const templateContent = fs.readFileSync(path.join(templatesDir, file), 'utf8');\n const template = YAML.parse(templateContent) as ItemTemplate;\n templates.push({\n type: template.type,\n name: template.name,\n description: template.description,\n });\n } catch (error) {\n console.warn(\n `Failed to parse template ${file}: ${error instanceof Error ? error.message : 'Unknown error'}`\n );\n }\n }\n\n return templates;\n }\n}\n","import chalk from 'chalk';\nimport type { Colors } from '../types/index.js';\n\n/**\n * Professional color scheme for the CLI\n * Provides consistent, accessible colors across the application\n */\nexport const colors: Colors = {\n // Main brand color - cyan for professional tech feel\n primary: chalk.cyan,\n\n // Success operations and positive feedback\n success: chalk.green,\n\n // Warnings and cautionary messages\n warning: chalk.yellow,\n\n // Errors and critical issues\n error: chalk.red,\n\n // Informational messages and tips\n info: chalk.blue,\n\n // Secondary text and less important information\n muted: chalk.gray,\n\n // Important highlights and emphasis\n highlight: chalk.bold.white,\n};\n\n/**\n * Specialized color functions for specific UI elements\n */\nexport class ColorTheme {\n // Priority-based colors\n static priority(level: string): (text: string) => string {\n switch (level.toLowerCase()) {\n case 'low':\n return chalk.gray;\n case 'medium':\n return chalk.yellow;\n case 'high':\n return chalk.magenta;\n case 'critical':\n return chalk.red.bold;\n default:\n return chalk.whiteBright;\n }\n }\n\n // Status-based colors\n static status(status: string): (text: string) => string {\n switch (status.toLowerCase()) {\n case 'todo':\n return chalk.gray;\n case 'in-progress':\n return chalk.blue;\n case 'done':\n return chalk.green;\n case 'blocked':\n return chalk.red;\n default:\n return chalk.whiteBright;\n }\n }\n\n // Command-specific colors\n static command(command: string): string {\n return chalk.cyan.bold(command);\n }\n\n // Option colors\n static option(option: string): string {\n return chalk.yellow(option);\n }\n\n // Argument colors\n static argument(arg: string): string {\n return chalk.green(arg);\n }\n\n // Header styling\n static header(text: string): string {\n return chalk.bold.cyan(`\\n${text}\\n${'='.repeat(text.length)}`);\n }\n\n // Subheader styling\n static subheader(text: string): string {\n return chalk.bold.white(`\\n${text}\\n${'-'.repeat(text.length)}`);\n }\n\n // Badge styling for tags, labels, etc.\n static badge(text: string, variant: 'info' | 'success' | 'warning' | 'error' = 'info'): string {\n const colorFn = colors[variant];\n return colorFn(` ${text} `);\n }\n\n // Create a bordered box for important messages\n static box(text: string, variant: 'info' | 'success' | 'warning' | 'error' = 'info'): string {\n const lines = text.split('\\n');\n const maxLength = Math.max(...lines.map((line) => line.length));\n const colorFn = colors[variant];\n\n const border = 'ā'.repeat(maxLength + 2);\n const top = `ā${border}ā`;\n const bottom = `ā${border}ā`;\n\n const content = lines.map((line) => `ā ${line.padEnd(maxLength)} ā`).join('\\n');\n\n return colorFn(`${top}\\n${content}\\n${bottom}`);\n }\n\n // Progress indicators\n static progress(current: number, total: number): string {\n const percentage = Math.round((current / total) * 100);\n const filled = Math.round((current / total) * 20);\n const empty = 20 - filled;\n\n const bar = 'ā'.repeat(filled) + 'ā'.repeat(empty);\n\n if (percentage < 30) {\n return chalk.red(`[${bar}] ${percentage}%`);\n } else if (percentage < 70) {\n return chalk.yellow(`[${bar}] ${percentage}%`);\n } else {\n return chalk.green(`[${bar}] ${percentage}%`);\n }\n }\n\n // Create a separator line\n static separator(char = 'ā', length = 50): string {\n return chalk.gray(char.repeat(length));\n }\n\n // Timestamp formatting\n static timestamp(date: Date): string {\n return chalk.dim(date.toISOString().replace('T', ' ').substring(0, 19));\n }\n\n // File path formatting\n static path(path: string): string {\n return chalk.cyan.underline(path);\n }\n\n // Code formatting\n static code(code: string): string {\n return chalk.gray.inverse(` ${code} `);\n }\n\n // URL formatting\n static url(url: string): string {\n return chalk.blue.underline(url);\n }\n\n // Keyboard shortcut formatting\n static key(key: string): string {\n return chalk.inverse(` ${key} `);\n }\n}\n\n/**\n * Check if colors should be disabled based on environment\n */\nexport function shouldUseColors(): boolean {\n // Check environment variables\n if (process.env.NO_COLOR || process.env.FORCE_COLOR === '0') {\n return false;\n }\n\n if (\n process.env.FORCE_COLOR === '1' ||\n process.env.FORCE_COLOR === '2' ||\n process.env.FORCE_COLOR === '3'\n ) {\n return true;\n }\n\n // Check if stdout is a TTY\n return process.stdout.isTTY;\n}\n\n/**\n * Disable colors globally\n */\nexport function disableColors(): void {\n chalk.level = 0;\n}\n\n/**\n * Enable colors globally\n */\nexport function enableColors(): void {\n chalk.level = 3;\n}\n","import boxen from 'boxen';\nimport figlet from 'figlet';\nimport type { LogLevel, TrackdownItem } from '../types/index.js';\nimport { ColorTheme, colors } from './colors.js';\n\nexport class Formatter {\n static success(message: string): string {\n return colors.success(`ā
${message}`);\n }\n\n static error(message: string): string {\n return colors.error(`ā ${message}`);\n }\n\n static warning(message: string): string {\n return colors.warning(`ā ļø ${message}`);\n }\n\n static info(message: string): string {\n return colors.info(`ā¹ļø ${message}`);\n }\n\n static debug(message: string): string {\n return colors.muted(`š ${message}`);\n }\n\n static header(text: string): string {\n return ColorTheme.header(text);\n }\n\n static subheader(text: string): string {\n return ColorTheme.subheader(text);\n }\n\n static highlight(text: string): string {\n return colors.highlight(text);\n }\n\n static dim(text: string): string {\n return colors.muted(text);\n }\n\n // Enhanced banner for CLI startup\n static banner(text: string): string {\n try {\n const ascii = figlet.textSync(text, {\n font: 'ANSI Shadow',\n horizontalLayout: 'default',\n verticalLayout: 'default',\n width: 80,\n whitespaceBreak: true,\n });\n return colors.primary(ascii);\n } catch {\n // Fallback if figlet fails\n ret