├── .npmrc ├── .gitignore ├── tsconfig.json ├── src ├── components │ ├── Scanner.tsx │ ├── Summary.tsx │ ├── ResultsTable.tsx │ ├── ActionMenu.tsx │ └── App.tsx ├── utils │ ├── templates.ts │ └── paths.ts ├── types │ └── index.ts ├── cli.tsx └── services │ ├── scanner.ts │ ├── analyzer.ts │ └── executor.ts ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── package.json ├── README.md ├── AGENTS.md └── bun.lock /.npmrc: -------------------------------------------------------------------------------- 1 | # Ensure bin is executable after install 2 | bin-links=true 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | *.log 4 | .DS_Store 5 | *.swp 6 | *.bak 7 | .env 8 | .vscode/ 9 | .idea/ 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ES2022", 5 | "moduleResolution": "bundler", 6 | "lib": ["ES2022"], 7 | "outDir": "./dist", 8 | "rootDir": "./src", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "resolveJsonModule": true, 14 | "declaration": true, 15 | "declarationMap": true, 16 | "sourceMap": true, 17 | "jsx": "react" 18 | }, 19 | "include": ["src/**/*"], 20 | "exclude": ["node_modules", "dist"] 21 | } 22 | -------------------------------------------------------------------------------- /src/components/Scanner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Text } from 'ink'; 3 | import Spinner from 'ink-spinner'; 4 | 5 | interface ScannerProps { 6 | root: string; 7 | isScanning: boolean; 8 | } 9 | 10 | export const Scanner: React.FC = ({ root, isScanning }) => { 11 | if (!isScanning) { 12 | return null; 13 | } 14 | 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | Scanning {root}... 22 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/utils/templates.ts: -------------------------------------------------------------------------------- 1 | export const AGENTS_TEMPLATE = `# Agent Instructions 2 | 3 | This file contains instructions for AI agents working on this project. 4 | 5 | ## Project Overview 6 | 7 | [Add project description here] 8 | 9 | ## Key Guidelines 10 | 11 | - [Add important guidelines here] 12 | 13 | ## File Structure 14 | 15 | [Describe the project structure] 16 | 17 | ## Development Workflow 18 | 19 | [Describe how to work with this project] 20 | `; 21 | 22 | export const CLAUDE_TEMPLATE = `@AGENTS.md 23 | 24 | # Claude-Specific Instructions 25 | 26 | Add any Claude-specific customizations below this line. 27 | `; 28 | 29 | export const CLAUDE_SOURCING_ONLY = `@AGENTS.md 30 | `; 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main, master ] 6 | pull_request: 7 | branches: [ main, master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Setup Bun 16 | uses: oven-sh/setup-bun@v2 17 | with: 18 | bun-version: latest 19 | 20 | - name: Install dependencies 21 | run: bun install --frozen-lockfile 22 | 23 | - name: Type-check 24 | run: bun run type-check 25 | 26 | - name: Build 27 | run: bun run build 28 | 29 | - name: Smoke test CLI 30 | run: node dist/cli.js --help 31 | -------------------------------------------------------------------------------- /src/utils/paths.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | import path from 'path'; 3 | 4 | export function shortenPath(fullPath: string, maxLength: number): string { 5 | const homeDir = os.homedir(); 6 | let displayPath = fullPath.startsWith(homeDir) 7 | ? `~${fullPath.slice(homeDir.length)}` 8 | : fullPath; 9 | 10 | if (displayPath.length <= maxLength) { 11 | return displayPath; 12 | } 13 | 14 | // Truncate with ellipsis 15 | if (maxLength <= 3) { 16 | return displayPath.slice(0, 1); 17 | } 18 | 19 | return displayPath.slice(0, maxLength - 3) + '...'; 20 | } 21 | 22 | export function expandPath(inputPath: string): string { 23 | if (inputPath.startsWith('~')) { 24 | return path.join(os.homedir(), inputPath.slice(1)); 25 | } 26 | return path.resolve(inputPath); 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Ian Nuttall 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "source-agents", 3 | "version": "1.0.0", 4 | "description": "TUI for managing CLAUDE.md and AGENTS.md files with intelligent sourcing", 5 | "main": "dist/cli.js", 6 | "type": "module", 7 | "bin": { 8 | "source-agents": "./dist/cli.js" 9 | }, 10 | "scripts": { 11 | "build": "bun build src/cli.tsx --outdir dist --target node --format esm --external react --external ink --external ink-spinner --external commander --external fast-glob --external fs-extra", 12 | "dev": "bun run src/cli.tsx", 13 | "start": "node dist/cli.js", 14 | "type-check": "tsc --noEmit", 15 | "link": "npm link" 16 | }, 17 | "keywords": [ 18 | "claude", 19 | "agents", 20 | "tui", 21 | "cli", 22 | "markdown", 23 | "source" 24 | ], 25 | "author": "", 26 | "license": "MIT", 27 | "dependencies": { 28 | "commander": "^12.1.0", 29 | "fast-glob": "^3.3.2", 30 | "fs-extra": "^11.2.0", 31 | "ink": "^5.0.1", 32 | "ink-spinner": "^5.0.0", 33 | "react": "^18.3.1" 34 | }, 35 | "devDependencies": { 36 | "@types/fs-extra": "^11.0.4", 37 | "@types/node": "^22.10.2", 38 | "@types/react": "^18.3.18", 39 | "tsx": "^4.19.2", 40 | "typescript": "^5.7.2" 41 | }, 42 | "engines": { 43 | "node": ">=18.0.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export type FileStatus = 'exists' | 'missing' | 'symlink' | 'broken' | 'sourcing'; 2 | 3 | export interface DirectoryInfo { 4 | path: string; 5 | agentsStatus: FileStatus; 6 | claudeStatus: FileStatus; 7 | needsAction: boolean; 8 | scenario: Scenario; 9 | } 10 | 11 | export type Scenario = 12 | | 'optimal' // Both exist, CLAUDE sources AGENTS 13 | | 'both-no-source' // Both exist, CLAUDE doesn't source AGENTS 14 | | 'only-claude' // Only CLAUDE.md exists 15 | | 'only-agents' // Only AGENTS.md exists 16 | | 'both-symlinks' // Both are symlinks (legacy) 17 | | 'mixed-symlinks' // One symlink, one regular 18 | | 'broken-symlinks'; // Has broken symlinks 19 | 20 | export type ActionType = 21 | | 'create-agents-empty' 22 | | 'create-agents-from-claude' 23 | | 'create-claude-sourcing' 24 | | 'create-claude-empty' 25 | | 'add-source-to-claude' 26 | | 'convert-symlinks' 27 | | 'remove-broken' 28 | | 'do-nothing' 29 | | 'review-manually'; 30 | 31 | export interface Action { 32 | type: ActionType; 33 | directory: string; 34 | description: string; 35 | recommended: boolean; 36 | } 37 | 38 | export interface ScanOptions { 39 | root: string; 40 | exclude?: string[]; 41 | verbose?: boolean; 42 | } 43 | 44 | export interface ScanResult { 45 | directories: DirectoryInfo[]; 46 | totalScanned: number; 47 | duration: number; 48 | } 49 | -------------------------------------------------------------------------------- /src/cli.tsx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import React from 'react'; 4 | import { render } from 'ink'; 5 | import { Command } from 'commander'; 6 | import os from 'os'; 7 | import fs from 'fs'; 8 | import { App } from './components/App.js'; 9 | import { expandPath } from './utils/paths.js'; 10 | 11 | const program = new Command(); 12 | 13 | program 14 | .name('source-agents') 15 | .description('Manage CLAUDE.md and AGENTS.md files with intelligent sourcing') 16 | .version('1.0.0') 17 | .option('-r, --root ', 'Root directory to scan', os.homedir()) 18 | .option('-e, --exclude ', 'Additional exclude patterns') 19 | .option('-d, --dry-run', 'Preview changes without executing') 20 | .option('-a, --auto', 'Automatically apply recommended actions') 21 | .option('-v, --verbose', 'Show detailed output') 22 | .parse(process.argv); 23 | 24 | const opts = program.opts(); 25 | 26 | const root = expandPath(opts.root || os.homedir()); 27 | 28 | // Validate root exists and is a directory before rendering the TUI 29 | try { 30 | const stat = fs.statSync(root); 31 | if (!stat.isDirectory()) { 32 | console.error(`Error: --root path is not a directory: ${root}`); 33 | process.exit(1); 34 | } 35 | } catch { 36 | console.error(`Error: --root path does not exist: ${root}`); 37 | process.exit(1); 38 | } 39 | 40 | const scanOptions = { 41 | root, 42 | exclude: opts.exclude || [], 43 | verbose: opts.verbose || false, 44 | }; 45 | 46 | // Render the Ink app 47 | render( 48 | 53 | ); 54 | -------------------------------------------------------------------------------- /src/services/scanner.ts: -------------------------------------------------------------------------------- 1 | import fg from 'fast-glob'; 2 | import path from 'path'; 3 | import { ScanOptions, ScanResult, DirectoryInfo } from '../types/index.js'; 4 | import { analyzeDirectory } from './analyzer.js'; 5 | 6 | const DEFAULT_EXCLUDE = [ 7 | '**/node_modules/**', 8 | '**/.git/**', 9 | '**/dist/**', 10 | '**/build/**', 11 | '**/.next/**', 12 | '**/.vscode/**', 13 | '**/.idea/**', 14 | '**/target/**', 15 | '**/__pycache__/**', 16 | '**/.pytest_cache/**', 17 | '**/venv/**', 18 | '**/.venv/**', 19 | ]; 20 | 21 | export async function scanDirectories(options: ScanOptions): Promise { 22 | const startTime = Date.now(); 23 | const excludePatterns = [...DEFAULT_EXCLUDE, ...(options.exclude || [])]; 24 | 25 | // Find all CLAUDE.md and AGENTS.md files 26 | const files = await fg(['**/CLAUDE.md', '**/AGENTS.md'], { 27 | cwd: options.root, 28 | absolute: true, 29 | ignore: excludePatterns, 30 | onlyFiles: true, 31 | followSymbolicLinks: false, 32 | }); 33 | 34 | // Group files by directory 35 | const dirMap = new Map>(); 36 | for (const file of files) { 37 | const dir = path.dirname(file); 38 | if (!dirMap.has(dir)) { 39 | dirMap.set(dir, new Set()); 40 | } 41 | dirMap.get(dir)!.add(path.basename(file)); 42 | } 43 | 44 | // Analyze each directory 45 | const directories: DirectoryInfo[] = []; 46 | for (const [dir] of dirMap) { 47 | const info = await analyzeDirectory(dir); 48 | directories.push(info); 49 | } 50 | 51 | // Sort by path for consistent display 52 | directories.sort((a, b) => a.path.localeCompare(b.path)); 53 | 54 | const duration = Date.now() - startTime; 55 | 56 | return { 57 | directories, 58 | totalScanned: dirMap.size, 59 | duration, 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | source‑agents 3 |
4 | Keep AGENTS.md and CLAUDE.md clean, consistent, and sourcing correctly. 5 | 6 |

7 | 8 | Scan projects • Fix sourcing • Convert symlinks • Dry‑run preview 9 | 10 |
11 | 12 | ## Quick Start 13 | 14 | Requirements: Node 18+, Bun 1.1+. 15 | 16 | Clone the repo: 17 | ``` 18 | git clone https://github.com/iannuttall/source-agents.git 19 | ``` 20 | 21 | Enter the folder: 22 | ``` 23 | cd source-agents 24 | ``` 25 | 26 | Install dependencies: 27 | ``` 28 | bun install 29 | ``` 30 | 31 | Run interactively on a specific folder (recommended): 32 | ``` 33 | bun run dev --root ~/projects 34 | ``` 35 | 36 | Auto‑fix recommended actions: 37 | ``` 38 | bun run dev --root ~/projects --auto 39 | ``` 40 | 41 | Preview without making changes: 42 | ``` 43 | bun run dev --root ~/projects --dry-run 44 | ``` 45 | 46 | Exclude extra directories (in addition to node_modules, .git, dist, etc.): 47 | ``` 48 | bun run dev --root ~/projects --exclude '**/temp/**' '**/backup/**' 49 | ``` 50 | 51 | ## Global CLI (optional) 52 | 53 | Build the CLI: 54 | ``` 55 | bun run build 56 | ``` 57 | 58 | Link globally: 59 | ``` 60 | npm link 61 | ``` 62 | 63 | Run from anywhere: 64 | ``` 65 | source-agents --help 66 | ``` 67 | 68 | Example run: 69 | ``` 70 | source-agents --root ~/projects --auto 71 | ``` 72 | 73 | ## Development 74 | 75 | Run the TUI in dev mode: 76 | ``` 77 | bun run dev 78 | ``` 79 | 80 | Type‑check: 81 | ``` 82 | bun run type-check 83 | ``` 84 | 85 | Build: 86 | ``` 87 | bun run build 88 | ``` 89 | 90 | ## Keyboard & Tips 91 | - ↑↓ to navigate, Enter to select, 's' to skip, Esc/← to go back, Ctrl+C to exit. 92 | - Default root is your home directory; pass --root to limit scope. 93 | 94 | ## License 95 | 96 | MIT 97 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # Repository Guidelines 2 | 3 | ## Project Structure & Modules 4 | - Source lives in `src/` with ESM TypeScript. 5 | - CLI entry: `src/cli.tsx` → built to `dist/cli.js`. 6 | - UI: `src/components/` (Ink + React). 7 | - Logic: `src/services/` (scanner, analyzer, executor). 8 | - Helpers: `src/utils/`, shared types in `src/types/`. 9 | - Build output in `dist/` (committed artifacts are not expected). 10 | 11 | ## Build, Test, and Dev 12 | - Install: `bun install` 13 | - Dev TUI: `bun run dev --root ~/projects` (use `--auto`, `--dry-run`, `--exclude` as needed). 14 | - Type‑check: `bun run type-check` 15 | - Build: `bun run build` (emits ESM to `dist/`). 16 | - Start built CLI: `node dist/cli.js --help` 17 | - CI runs type‑check, build, and a smoke CLI help check. 18 | 19 | ## Coding Style & Naming 20 | - TypeScript strict, ESM imports, 2‑space indent. 21 | - Components: PascalCase files (`ResultsTable.tsx`, `ActionMenu.tsx`). 22 | - Services/Utils/Types: lowercase filenames (`scanner.ts`, `paths.ts`, `index.ts`). 23 | - Use camelCase for variables/functions, UPPER_SNAKE_CASE for constants, named exports preferred. 24 | - Keep functions small and pure in `services/`; UI logic stays in `components/`. 25 | 26 | ## Testing Guidelines 27 | - No formal test suite yet; contributions welcome. 28 | - Prefer Vitest for unit tests; place tests under `src/**/__tests__` with `*.test.ts`. 29 | - Target high‑value coverage for `services/` (scanner/analyzer/executor). Aim for 80%+ on touched files. 30 | - Add lightweight CLI smoke tests if feasible. 31 | 32 | ## Commit & Pull Requests 33 | - Use Conventional Commits: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`. 34 | - Scope clearly and write imperative, present‑tense summaries (≤72 chars). 35 | - PRs should include: summary, motivation, screenshots or sample CLI output, steps to verify, and linked issues. 36 | - Keep diffs focused; update README or this file if behavior changes. 37 | 38 | ## Security & Configuration 39 | - Requires Node 18+ and Bun 1.1+. ESM only. 40 | - Use `--dry-run` for potentially destructive actions; limit scans with `--root`. 41 | - Default excludes: `node_modules`, `.git`, `dist`, common build caches. Add extras via `--exclude`. 42 | 43 | ## Agent‑Specific Notes 44 | - Follow these guidelines for any code changes in this repo tree. 45 | - Do not introduce unrelated tooling or broad refactors in a single PR. 46 | -------------------------------------------------------------------------------- /src/components/Summary.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Text } from 'ink'; 3 | import { shortenPath } from '../utils/paths.js'; 4 | 5 | interface SummaryProps { 6 | results: Map; 7 | maxPathWidth?: number; 8 | resultWidth?: number; 9 | } 10 | 11 | function truncateOneLine(text: string, max: number): string { 12 | const clean = text.replace(/\s+/g, ' '); 13 | if (clean.length <= max) return clean; 14 | return clean.slice(0, Math.max(0, max - 3)) + '...'; 15 | } 16 | 17 | export const Summary: React.FC = ({ results, maxPathWidth = 50, resultWidth = 60 }) => { 18 | const entries = Array.from(results.entries()); 19 | const successful = entries.filter(([, r]) => r.success).length; 20 | const failed = entries.length - successful; 21 | 22 | return ( 23 | 24 | 25 | Summary 26 | 27 | 28 | {/* Header */} 29 | 30 | 31 | DIRECTORY 32 | 33 | 34 | RESULT 35 | 36 | 37 | 38 | {/* Separator */} 39 | 40 | 41 | {'─'.repeat(maxPathWidth)} 42 | 43 | 44 | {'─'.repeat(resultWidth)} 45 | 46 | 47 | 48 | {/* Rows */} 49 | {entries.map(([dir, result], idx) => { 50 | const displayPath = shortenPath(dir, maxPathWidth - 2); 51 | const resultText = truncateOneLine(result.message, resultWidth - 2); 52 | 53 | const msg = result.message.toLowerCase(); 54 | let resultColor: string = 'green'; 55 | if (!result.success) { 56 | resultColor = 'red'; 57 | } else if (msg.includes('[dry run]')) { 58 | resultColor = 'yellow'; 59 | } else if (msg.includes('no changes') || msg.includes('already sources') || msg.includes('no symlinks')) { 60 | resultColor = 'gray'; 61 | } else if (msg.includes('created')) { 62 | resultColor = 'cyan'; 63 | } else if (msg.includes('added')) { 64 | resultColor = 'green'; 65 | } else if (msg.includes('moved')) { 66 | resultColor = 'magenta'; 67 | } else if (msg.includes('converted')) { 68 | resultColor = 'blue'; 69 | } else if (msg.includes('removed')) { 70 | resultColor = 'yellow'; 71 | } 72 | return ( 73 | 74 | 75 | {displayPath} 76 | 77 | 78 | {resultText} 79 | 80 | 81 | ); 82 | })} 83 | 84 | {/* Footer summary */} 85 | 86 | 87 | Completed: {successful} successful 88 | {failed > 0 && (<>{', '}{failed} failed)} 89 | 90 | 91 | 92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /src/components/ResultsTable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Text } from 'ink'; 3 | import { DirectoryInfo, FileStatus } from '../types/index.js'; 4 | import { shortenPath } from '../utils/paths.js'; 5 | 6 | interface ResultsTableProps { 7 | directories: DirectoryInfo[]; 8 | maxPathWidth?: number; 9 | } 10 | 11 | function getStatusColor(status: FileStatus): string { 12 | switch (status) { 13 | case 'exists': 14 | return 'green'; 15 | case 'sourcing': 16 | return 'cyan'; 17 | case 'symlink': 18 | return 'blue'; 19 | case 'missing': 20 | return 'red'; 21 | case 'broken': 22 | return 'red'; 23 | default: 24 | return 'white'; 25 | } 26 | } 27 | 28 | function getActionIndicator(needsAction: boolean): { symbol: string; color: string } { 29 | return needsAction 30 | ? { symbol: '⚠', color: 'yellow' } 31 | : { symbol: '✓', color: 'green' }; 32 | } 33 | 34 | export const ResultsTable: React.FC = ({ directories, maxPathWidth = 50 }) => { 35 | if (directories.length === 0) { 36 | return ( 37 | 38 | No directories found with CLAUDE.md or AGENTS.md files. 39 | 40 | ); 41 | } 42 | 43 | return ( 44 | 45 | {/* Header */} 46 | 47 | 48 | 49 | 50 | 51 | DIRECTORY 52 | 53 | 54 | AGENTS.md 55 | 56 | 57 | CLAUDE.md 58 | 59 | 60 | 61 | {/* Separator */} 62 | 63 | 64 | {'─'.repeat(3)} 65 | 66 | 67 | {'─'.repeat(maxPathWidth)} 68 | 69 | 70 | {'─'.repeat(15)} 71 | 72 | 73 | {'─'.repeat(15)} 74 | 75 | 76 | 77 | {/* Rows */} 78 | {directories.map((dir, index) => { 79 | const indicator = getActionIndicator(dir.needsAction); 80 | const displayPath = shortenPath(dir.path, maxPathWidth - 2); 81 | 82 | return ( 83 | 84 | 85 | {indicator.symbol} 86 | 87 | 88 | {displayPath} 89 | 90 | 91 | {dir.agentsStatus} 92 | 93 | 94 | {dir.claudeStatus} 95 | 96 | 97 | ); 98 | })} 99 | 100 | {/* Summary */} 101 | 102 | 103 | Found {directories.length} {directories.length === 1 ? 'directory' : 'directories'} 104 | {' - '} 105 | {directories.filter(d => d.needsAction).length} need attention 106 | 107 | 108 | 109 | ); 110 | }; 111 | -------------------------------------------------------------------------------- /src/services/analyzer.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra'; 2 | import path from 'path'; 3 | import { DirectoryInfo, FileStatus, Scenario } from '../types/index.js'; 4 | 5 | async function getFileStatus(filePath: string): Promise { 6 | try { 7 | const stats = await fs.lstat(filePath); 8 | 9 | if (stats.isSymbolicLink()) { 10 | // Check if symlink is broken 11 | try { 12 | await fs.access(filePath); 13 | return 'symlink'; 14 | } catch { 15 | return 'broken'; 16 | } 17 | } 18 | 19 | if (stats.isFile()) { 20 | return 'exists'; 21 | } 22 | 23 | return 'missing'; 24 | } catch (error: any) { 25 | if (error.code === 'ENOENT') { 26 | return 'missing'; 27 | } 28 | throw error; 29 | } 30 | } 31 | 32 | async function checkIfClaudeSources(claudePath: string): Promise { 33 | try { 34 | const content = await fs.readFile(claudePath, 'utf-8'); 35 | // Check if CLAUDE.md contains @AGENTS.md sourcing directive 36 | return content.includes('@AGENTS.md'); 37 | } catch { 38 | return false; 39 | } 40 | } 41 | 42 | function determineScenario( 43 | agentsStatus: FileStatus, 44 | claudeStatus: FileStatus, 45 | claudeSources: boolean 46 | ): Scenario { 47 | const claudeExists = claudeStatus === 'exists' || claudeStatus === 'sourcing'; 48 | 49 | // Both exist and CLAUDE sources AGENTS - optimal 50 | if (agentsStatus === 'exists' && claudeExists && claudeSources) { 51 | return 'optimal'; 52 | } 53 | 54 | // Both exist but CLAUDE doesn't source AGENTS 55 | if (agentsStatus === 'exists' && claudeExists && !claudeSources) { 56 | return 'both-no-source'; 57 | } 58 | 59 | // Only CLAUDE exists 60 | if (claudeExists && agentsStatus === 'missing') { 61 | return 'only-claude'; 62 | } 63 | 64 | // Only AGENTS exists 65 | if (agentsStatus === 'exists' && claudeStatus === 'missing') { 66 | return 'only-agents'; 67 | } 68 | 69 | // Both are symlinks 70 | if (agentsStatus === 'symlink' && claudeStatus === 'symlink') { 71 | return 'both-symlinks'; 72 | } 73 | 74 | // Mixed symlinks 75 | if ( 76 | (agentsStatus === 'symlink' && claudeStatus === 'exists') || 77 | (agentsStatus === 'exists' && claudeStatus === 'symlink') 78 | ) { 79 | return 'mixed-symlinks'; 80 | } 81 | 82 | // Broken symlinks 83 | if (agentsStatus === 'broken' || claudeStatus === 'broken') { 84 | return 'broken-symlinks'; 85 | } 86 | 87 | // Default case 88 | return 'both-no-source'; 89 | } 90 | 91 | export async function analyzeDirectory(dirPath: string): Promise { 92 | const agentsPath = path.join(dirPath, 'AGENTS.md'); 93 | const claudePath = path.join(dirPath, 'CLAUDE.md'); 94 | 95 | const agentsStatus = await getFileStatus(agentsPath); 96 | let claudeStatus = await getFileStatus(claudePath); 97 | 98 | // Check if CLAUDE.md sources AGENTS.md 99 | const claudeSources = claudeStatus === 'exists' ? await checkIfClaudeSources(claudePath) : false; 100 | 101 | // Update claudeStatus to 'sourcing' if it sources AGENTS.md 102 | if (claudeStatus === 'exists' && claudeSources) { 103 | claudeStatus = 'sourcing'; 104 | } 105 | 106 | const scenario = determineScenario(agentsStatus, claudeStatus, claudeSources); 107 | const needsAction = scenario !== 'optimal'; 108 | 109 | return { 110 | path: dirPath, 111 | agentsStatus, 112 | claudeStatus, 113 | needsAction, 114 | scenario, 115 | }; 116 | } 117 | -------------------------------------------------------------------------------- /src/services/executor.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra'; 2 | import path from 'path'; 3 | import { Action, ActionType } from '../types/index.js'; 4 | import { AGENTS_TEMPLATE, CLAUDE_TEMPLATE, CLAUDE_SOURCING_ONLY } from '../utils/templates.js'; 5 | 6 | const SOURCING_DIRECTIVE = '@AGENTS.md\n\n'; 7 | 8 | export async function executeAction(action: Action, dryRun: boolean = false): Promise { 9 | const agentsPath = path.join(action.directory, 'AGENTS.md'); 10 | const claudePath = path.join(action.directory, 'CLAUDE.md'); 11 | 12 | if (dryRun) { 13 | return `[DRY RUN] ${action.description}`; 14 | } 15 | 16 | try { 17 | switch (action.type) { 18 | case 'create-agents-empty': 19 | await fs.writeFile(agentsPath, AGENTS_TEMPLATE); 20 | // Ensure CLAUDE.md sources AGENTS.md without altering existing content 21 | try { 22 | let existingClaude = await fs.readFile(claudePath, 'utf-8'); 23 | if (!existingClaude.includes('@AGENTS.md')) { 24 | await fs.writeFile(claudePath, SOURCING_DIRECTIVE + existingClaude); 25 | } 26 | } catch { 27 | // CLAUDE.md may not exist; that's fine in some scenarios 28 | } 29 | return `Created empty AGENTS.md and ensured CLAUDE.md sources it`; 30 | 31 | case 'create-agents-from-claude': 32 | const claudeContent = await fs.readFile(claudePath, 'utf-8'); 33 | await fs.writeFile(agentsPath, claudeContent); 34 | await fs.writeFile(claudePath, SOURCING_DIRECTIVE); 35 | return `Moved content from CLAUDE.md to AGENTS.md and added sourcing`; 36 | 37 | case 'create-claude-sourcing': 38 | await fs.writeFile(claudePath, CLAUDE_SOURCING_ONLY); 39 | return `Created CLAUDE.md with @AGENTS.md sourcing`; 40 | 41 | case 'create-claude-empty': 42 | await fs.writeFile(claudePath, CLAUDE_TEMPLATE); 43 | return `Created empty CLAUDE.md`; 44 | 45 | case 'add-source-to-claude': 46 | let content = await fs.readFile(claudePath, 'utf-8'); 47 | // Add sourcing directive at the top if not already present 48 | if (!content.includes('@AGENTS.md')) { 49 | content = SOURCING_DIRECTIVE + content; 50 | await fs.writeFile(claudePath, content); 51 | return `Added @AGENTS.md sourcing to CLAUDE.md`; 52 | } 53 | return `CLAUDE.md already sources AGENTS.md`; 54 | 55 | case 'convert-symlinks': 56 | const results: string[] = []; 57 | 58 | // Handle AGENTS.md symlink 59 | const agentsStats = await fs.lstat(agentsPath).catch(() => null); 60 | if (agentsStats?.isSymbolicLink()) { 61 | const target = await fs.readlink(agentsPath); 62 | const targetPath = path.resolve(path.dirname(agentsPath), target); 63 | const targetContent = await fs.readFile(targetPath, 'utf-8'); 64 | await fs.unlink(agentsPath); 65 | await fs.writeFile(agentsPath, targetContent); 66 | results.push('Converted AGENTS.md symlink to real file'); 67 | } 68 | 69 | // Handle CLAUDE.md symlink 70 | const claudeStats = await fs.lstat(claudePath).catch(() => null); 71 | if (claudeStats?.isSymbolicLink()) { 72 | const target = await fs.readlink(claudePath); 73 | const targetPath = path.resolve(path.dirname(claudePath), target); 74 | const targetContent = await fs.readFile(targetPath, 'utf-8'); 75 | await fs.unlink(claudePath); 76 | await fs.writeFile(claudePath, targetContent); 77 | results.push('Converted CLAUDE.md symlink to real file'); 78 | } 79 | 80 | return results.join('; ') || 'No symlinks to convert'; 81 | 82 | case 'remove-broken': 83 | const removed: string[] = []; 84 | 85 | // Check and remove broken AGENTS.md 86 | try { 87 | const aStats = await fs.lstat(agentsPath); 88 | if (aStats.isSymbolicLink()) { 89 | try { 90 | await fs.access(agentsPath); 91 | } catch { 92 | await fs.unlink(agentsPath); 93 | removed.push('AGENTS.md'); 94 | } 95 | } 96 | } catch {} 97 | 98 | // Check and remove broken CLAUDE.md 99 | try { 100 | const cStats = await fs.lstat(claudePath); 101 | if (cStats.isSymbolicLink()) { 102 | try { 103 | await fs.access(claudePath); 104 | } catch { 105 | await fs.unlink(claudePath); 106 | removed.push('CLAUDE.md'); 107 | } 108 | } 109 | } catch {} 110 | 111 | return removed.length > 0 112 | ? `Removed broken symlinks: ${removed.join(', ')}` 113 | : 'No broken symlinks found'; 114 | 115 | case 'do-nothing': 116 | return 'No changes made'; 117 | 118 | case 'review-manually': 119 | return 'Skipped - requires manual review'; 120 | 121 | default: 122 | return `Unknown action type: ${action.type}`; 123 | } 124 | } catch (error: any) { 125 | throw new Error(`Failed to execute action: ${error.message}`); 126 | } 127 | } 128 | 129 | export async function executeActions( 130 | actions: Action[], 131 | dryRun: boolean = false, 132 | onProgress?: (index: number, total: number, message: string) => void 133 | ): Promise> { 134 | const results = new Map(); 135 | 136 | for (let i = 0; i < actions.length; i++) { 137 | const action = actions[i]; 138 | 139 | if (onProgress) { 140 | onProgress(i + 1, actions.length, `Processing ${path.basename(action.directory)}...`); 141 | } 142 | 143 | try { 144 | const message = await executeAction(action, dryRun); 145 | results.set(action.directory, { success: true, message }); 146 | } catch (error: any) { 147 | results.set(action.directory, { success: false, message: error.message }); 148 | } 149 | } 150 | 151 | return results; 152 | } 153 | -------------------------------------------------------------------------------- /src/components/ActionMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Box, Text, useInput } from 'ink'; 3 | import { DirectoryInfo, Action, ActionType } from '../types/index.js'; 4 | import { shortenPath } from '../utils/paths.js'; 5 | 6 | interface ActionMenuProps { 7 | directory: DirectoryInfo; 8 | onSelect: (action: Action) => void; 9 | onSkip: () => void; 10 | onBack: () => void; 11 | onExit: () => void; // exit to main list 12 | canGoBack: boolean; 13 | } 14 | 15 | interface ActionOption { 16 | type: ActionType; 17 | label: string; 18 | description: string; 19 | recommended?: boolean; 20 | } 21 | 22 | function getActionOptions(dir: DirectoryInfo): ActionOption[] { 23 | const options: ActionOption[] = []; 24 | 25 | switch (dir.scenario) { 26 | case 'optimal': 27 | options.push({ 28 | type: 'do-nothing', 29 | label: 'No action needed', 30 | description: 'Both files exist and CLAUDE.md sources AGENTS.md', 31 | recommended: true, 32 | }); 33 | break; 34 | 35 | case 'both-no-source': 36 | options.push( 37 | { 38 | type: 'add-source-to-claude', 39 | label: 'Add @AGENTS.md to CLAUDE.md', 40 | description: 'Add sourcing directive to existing CLAUDE.md', 41 | recommended: true, 42 | }, 43 | { 44 | type: 'do-nothing', 45 | label: 'Leave as-is', 46 | description: 'Keep custom setup without sourcing', 47 | } 48 | ); 49 | break; 50 | 51 | case 'only-claude': 52 | options.push( 53 | { 54 | type: 'create-agents-empty', 55 | label: 'Create empty AGENTS.md', 56 | description: 'Create empty AGENTS.md, add @AGENTS.md to CLAUDE.md', 57 | recommended: true, 58 | }, 59 | { 60 | type: 'create-agents-from-claude', 61 | label: 'Move content to AGENTS.md', 62 | description: 'Move CLAUDE.md content to AGENTS.md and add sourcing', 63 | }, 64 | { 65 | type: 'do-nothing', 66 | label: 'Keep CLAUDE.md only', 67 | description: 'No changes', 68 | } 69 | ); 70 | break; 71 | 72 | case 'only-agents': 73 | options.push( 74 | { 75 | type: 'create-claude-sourcing', 76 | label: 'Create CLAUDE.md with sourcing', 77 | description: 'Create CLAUDE.md that sources AGENTS.md', 78 | recommended: true, 79 | }, 80 | { 81 | type: 'create-claude-empty', 82 | label: 'Create empty CLAUDE.md', 83 | description: 'Create empty CLAUDE.md without sourcing', 84 | }, 85 | { 86 | type: 'do-nothing', 87 | label: 'Keep AGENTS.md only', 88 | description: 'No changes', 89 | } 90 | ); 91 | break; 92 | 93 | case 'both-symlinks': 94 | case 'mixed-symlinks': 95 | options.push( 96 | { 97 | type: 'convert-symlinks', 98 | label: 'Convert symlinks to real files', 99 | description: 'Replace symlinks with real files', 100 | recommended: true, 101 | }, 102 | { 103 | type: 'do-nothing', 104 | label: 'Keep symlinks', 105 | description: 'No changes', 106 | } 107 | ); 108 | break; 109 | 110 | case 'broken-symlinks': 111 | options.push( 112 | { 113 | type: 'remove-broken', 114 | label: 'Remove broken symlinks', 115 | description: 'Delete broken symbolic links', 116 | recommended: true, 117 | }, 118 | { 119 | type: 'review-manually', 120 | label: 'Review manually', 121 | description: 'Skip automated fix', 122 | } 123 | ); 124 | break; 125 | 126 | default: 127 | options.push({ 128 | type: 'review-manually', 129 | label: 'Review manually', 130 | description: 'Requires manual review', 131 | recommended: true, 132 | }); 133 | } 134 | 135 | return options; 136 | } 137 | 138 | export const ActionMenu: React.FC = ({ directory, onSelect, onSkip, onBack, onExit, canGoBack }) => { 139 | const options = getActionOptions(directory); 140 | const [selectedIndex, setSelectedIndex] = useState(0); 141 | 142 | useInput((input, key) => { 143 | if (key.upArrow) { 144 | setSelectedIndex((prev) => (prev > 0 ? prev - 1 : options.length - 1)); 145 | } else if (key.downArrow) { 146 | setSelectedIndex((prev) => (prev < options.length - 1 ? prev + 1 : 0)); 147 | } else if (key.return) { 148 | const selected = options[selectedIndex]; 149 | onSelect({ 150 | type: selected.type, 151 | directory: directory.path, 152 | description: selected.description, 153 | recommended: selected.recommended || false, 154 | }); 155 | } else if (input === 's' || input === 'S') { 156 | onSkip(); 157 | } else if (key.leftArrow && canGoBack) { 158 | onBack(); 159 | } else if (key.escape) { 160 | onExit(); 161 | } 162 | }); 163 | 164 | const displayPath = shortenPath(directory.path, 60); 165 | 166 | return ( 167 | 168 | 169 | 170 | {displayPath} 171 | 172 | 173 | 174 | 175 | 176 | AGENTS: {directory.agentsStatus} 177 | {' | '} 178 | CLAUDE: {directory.claudeStatus} 179 | 180 | 181 | 182 | 183 | Choose an action: 184 | 185 | 186 | {options.map((option, index) => { 187 | const isSelected = index === selectedIndex; 188 | const prefix = isSelected ? '❯ ' : ' '; 189 | const recommendedBadge = option.recommended ? ' [recommended]' : ''; 190 | 191 | return ( 192 | 193 | 194 | {prefix}{option.label}{recommendedBadge} 195 | 196 | 197 | ); 198 | })} 199 | 200 | 201 | 202 | ↑↓ navigate • Enter select • 's' skip{canGoBack ? ' • ← prev' : ''} • Esc list 203 | 204 | 205 | 206 | ); 207 | }; 208 | -------------------------------------------------------------------------------- /src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Box, Text, useInput } from 'ink'; 3 | import { Scanner } from './Scanner.js'; 4 | import { ResultsTable } from './ResultsTable.js'; 5 | import { ActionMenu } from './ActionMenu.js'; 6 | import { Summary } from './Summary.js'; 7 | import { scanDirectories } from '../services/scanner.js'; 8 | import { executeAction } from '../services/executor.js'; 9 | import { analyzeDirectory } from '../services/analyzer.js'; 10 | import { DirectoryInfo, Action, ScanOptions } from '../types/index.js'; 11 | 12 | interface AppProps { 13 | options: ScanOptions; 14 | dryRun?: boolean; 15 | auto?: boolean; 16 | } 17 | 18 | type AppState = 'scanning' | 'displaying' | 'selecting' | 'executing' | 'complete'; 19 | 20 | export const App: React.FC = ({ options, dryRun = false, auto = false }) => { 21 | const [state, setState] = useState('scanning'); 22 | const [directories, setDirectories] = useState([]); 23 | const [currentDirIndex, setCurrentDirIndex] = useState(0); 24 | const [actions, setActions] = useState([]); 25 | const [results, setResults] = useState>( 26 | new Map() 27 | ); 28 | const [error, setError] = useState(null); 29 | 30 | // Initial scan 31 | useEffect(() => { 32 | const doScan = async () => { 33 | try { 34 | const scanResult = await scanDirectories(options); 35 | setDirectories(scanResult.directories); 36 | 37 | if (scanResult.directories.length === 0) { 38 | setState('complete'); 39 | return; 40 | } 41 | 42 | const needsAction = scanResult.directories.filter((d) => d.needsAction); 43 | 44 | if (needsAction.length === 0) { 45 | setState('displaying'); 46 | setTimeout(() => setState('complete'), 2000); 47 | return; 48 | } 49 | 50 | if (auto) { 51 | // Auto mode: apply recommended actions 52 | const autoActions = needsAction.map((dir) => 53 | getRecommendedAction(dir) 54 | ); 55 | setActions(autoActions); 56 | setState('executing'); 57 | } else { 58 | setState('displaying'); 59 | } 60 | } catch (err: any) { 61 | setError(err.message); 62 | setState('complete'); 63 | } 64 | }; 65 | 66 | doScan(); 67 | }, [options, auto]); 68 | 69 | // Execute actions 70 | useEffect(() => { 71 | if (state === 'executing' && actions.length > 0) { 72 | const execute = async () => { 73 | const newResults = new Map(); 74 | 75 | for (const action of actions) { 76 | try { 77 | const message = await executeAction(action, dryRun); 78 | newResults.set(action.directory, { success: true, message }); 79 | } catch (err: any) { 80 | newResults.set(action.directory, { success: false, message: err.message }); 81 | } 82 | } 83 | 84 | setResults(newResults); 85 | setState('complete'); 86 | }; 87 | 88 | execute(); 89 | } 90 | }, [state, actions, dryRun]); 91 | 92 | const handleActionSelect = async (action: Action) => { 93 | // Treat no-op choices as resolved for this session 94 | if (action.type === 'do-nothing' || action.type === 'review-manually') { 95 | try { 96 | const message = await executeAction(action, dryRun); 97 | setResults((prev) => new Map(prev).set(action.directory, { success: true, message })); 98 | } catch (err: any) { 99 | setResults((prev) => new Map(prev).set(action.directory, { success: false, message: err.message })); 100 | } 101 | 102 | let updated: DirectoryInfo[] = []; 103 | setDirectories((prev) => { 104 | updated = prev.map((d) => (d.path === action.directory ? { ...d, needsAction: false } : d)); 105 | return updated; 106 | }); 107 | 108 | const remainingCount = updated.filter((d) => d.needsAction).length; 109 | const nextIdx = Math.max(0, Math.min(currentDirIndex, Math.max(remainingCount - 1, 0))); 110 | setCurrentDirIndex(nextIdx); 111 | setState(remainingCount === 0 ? 'complete' : 'selecting'); 112 | return; 113 | } 114 | 115 | // Apply real changes immediately so the user sees results 116 | setState('executing'); 117 | try { 118 | const message = await executeAction(action, dryRun); 119 | setResults((prev) => new Map(prev).set(action.directory, { success: true, message })); 120 | 121 | let nextDirs: DirectoryInfo[] = []; 122 | const refreshed = await analyzeDirectory(action.directory); 123 | setDirectories((prev) => { 124 | nextDirs = prev.map((d) => (d.path === action.directory ? refreshed : d)); 125 | return nextDirs; 126 | }); 127 | 128 | const needsAfter = nextDirs.filter((d) => d.needsAction); 129 | if (needsAfter.length === 0) { 130 | setState('complete'); 131 | } else { 132 | const nextIdx = Math.max(0, Math.min(currentDirIndex, needsAfter.length - 1)); 133 | setCurrentDirIndex(nextIdx); 134 | setState('selecting'); 135 | } 136 | } catch (err: any) { 137 | setResults((prev) => new Map(prev).set(action.directory, { success: false, message: err.message })); 138 | setState('selecting'); 139 | } 140 | }; 141 | 142 | const handleSkip = () => { 143 | const needsAction = directories.filter((d) => d.needsAction); 144 | const nextIndex = currentDirIndex + 1; 145 | 146 | if (nextIndex >= needsAction.length) { 147 | if (actions.length > 0) { 148 | setState('executing'); 149 | } else { 150 | setState('complete'); 151 | } 152 | } else { 153 | setCurrentDirIndex(nextIndex); 154 | } 155 | }; 156 | 157 | const handleBack = () => { 158 | if (currentDirIndex > 0) { 159 | // Remove the last action if we added one for the current directory 160 | const needsAction = directories.filter((d) => d.needsAction); 161 | const currentDir = needsAction[currentDirIndex]; 162 | 163 | // Check if there's an action for the previous directory 164 | const previousDir = needsAction[currentDirIndex - 1]; 165 | const hasActionForPrevious = actions.some(a => a.directory === previousDir.path); 166 | 167 | if (hasActionForPrevious) { 168 | // Remove the last action 169 | setActions(actions.slice(0, -1)); 170 | } 171 | 172 | setCurrentDirIndex(currentDirIndex - 1); 173 | } 174 | }; 175 | 176 | const handleExitToList = () => { 177 | // Return to the table view; discard any staged actions and reset index 178 | setActions([]); 179 | setCurrentDirIndex(0); 180 | setState('displaying'); 181 | }; 182 | 183 | // Handle input in displaying state to start interactive mode (only in interactive mode) 184 | useInput( 185 | (input, key) => { 186 | if (state === 'displaying') { 187 | const needsAction = directories.filter((d) => d.needsAction); 188 | if (needsAction.length > 0 && !key.escape && !key.ctrl && input !== 'c') { 189 | setState('selecting'); 190 | } 191 | } 192 | }, 193 | { isActive: !auto && state === 'displaying' } 194 | ); 195 | 196 | if (error) { 197 | return ( 198 | 199 | 200 | Error: {error} 201 | 202 | 203 | ); 204 | } 205 | 206 | if (state === 'scanning') { 207 | return ; 208 | } 209 | 210 | if (state === 'displaying') { 211 | const needsAction = directories.filter((d) => d.needsAction); 212 | 213 | return ( 214 | 215 | 216 | 217 | {needsAction.length > 0 && ( 218 | 219 | 220 | Press any key to start interactive mode, or Ctrl+C to exit... 221 | 222 | 223 | )} 224 | 225 | ); 226 | } 227 | 228 | if (state === 'selecting') { 229 | const needsAction = directories.filter((d) => d.needsAction); 230 | const currentDir = needsAction[currentDirIndex]; 231 | 232 | if (!currentDir) { 233 | setState('complete'); 234 | return null; 235 | } 236 | 237 | return ( 238 | 239 | 240 | 241 | Directory {currentDirIndex + 1} of {needsAction.length} 242 | 243 | 244 | 0} 251 | /> 252 | 253 | ); 254 | } 255 | 256 | if (state === 'executing') { 257 | return ( 258 | 259 | 260 | {dryRun ? '[DRY RUN] ' : ''} 261 | {actions.length > 0 ? `Executing ${actions.length} action${actions.length !== 1 ? 's' : ''}...` : 'Applying action...'} 262 | 263 | 264 | ); 265 | } 266 | 267 | if (state === 'complete') { 268 | if (results.size > 0) { 269 | return ; 270 | } 271 | 272 | if (directories.length === 0) { 273 | return ( 274 | 275 | No directories found with CLAUDE.md or AGENTS.md files. 276 | 277 | ); 278 | } 279 | 280 | return ( 281 | 282 | 283 | 284 | ✓ All directories are in optimal state! 285 | 286 | 287 | ); 288 | } 289 | 290 | return null; 291 | }; 292 | 293 | function getRecommendedAction(dir: DirectoryInfo): Action { 294 | switch (dir.scenario) { 295 | case 'both-no-source': 296 | return { 297 | type: 'add-source-to-claude', 298 | directory: dir.path, 299 | description: 'Add @AGENTS.md to CLAUDE.md', 300 | recommended: true, 301 | }; 302 | 303 | case 'only-claude': 304 | return { 305 | type: 'create-agents-from-claude', 306 | directory: dir.path, 307 | description: 'Move content to AGENTS.md', 308 | recommended: true, 309 | }; 310 | 311 | case 'only-agents': 312 | return { 313 | type: 'create-claude-sourcing', 314 | directory: dir.path, 315 | description: 'Create CLAUDE.md with sourcing', 316 | recommended: true, 317 | }; 318 | 319 | case 'both-symlinks': 320 | case 'mixed-symlinks': 321 | return { 322 | type: 'convert-symlinks', 323 | directory: dir.path, 324 | description: 'Convert symlinks to real files', 325 | recommended: true, 326 | }; 327 | 328 | case 'broken-symlinks': 329 | return { 330 | type: 'remove-broken', 331 | directory: dir.path, 332 | description: 'Remove broken symlinks', 333 | recommended: true, 334 | }; 335 | 336 | default: 337 | return { 338 | type: 'do-nothing', 339 | directory: dir.path, 340 | description: 'No action needed', 341 | recommended: true, 342 | }; 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "source-agents", 6 | "dependencies": { 7 | "commander": "^12.1.0", 8 | "fast-glob": "^3.3.2", 9 | "fs-extra": "^11.2.0", 10 | "ink": "^5.0.1", 11 | "ink-spinner": "^5.0.0", 12 | "react": "^18.3.1", 13 | }, 14 | "devDependencies": { 15 | "@types/fs-extra": "^11.0.4", 16 | "@types/node": "^22.10.2", 17 | "@types/react": "^18.3.18", 18 | "tsx": "^4.19.2", 19 | "typescript": "^5.7.2", 20 | }, 21 | }, 22 | }, 23 | "packages": { 24 | "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.1.3", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw=="], 25 | 26 | "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.11", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg=="], 27 | 28 | "@esbuild/android-arm": ["@esbuild/android-arm@0.25.11", "", { "os": "android", "cpu": "arm" }, "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg=="], 29 | 30 | "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.11", "", { "os": "android", "cpu": "arm64" }, "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ=="], 31 | 32 | "@esbuild/android-x64": ["@esbuild/android-x64@0.25.11", "", { "os": "android", "cpu": "x64" }, "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g=="], 33 | 34 | "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w=="], 35 | 36 | "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ=="], 37 | 38 | "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.11", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA=="], 39 | 40 | "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw=="], 41 | 42 | "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.11", "", { "os": "linux", "cpu": "arm" }, "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw=="], 43 | 44 | "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA=="], 45 | 46 | "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.11", "", { "os": "linux", "cpu": "ia32" }, "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw=="], 47 | 48 | "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw=="], 49 | 50 | "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ=="], 51 | 52 | "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.11", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw=="], 53 | 54 | "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww=="], 55 | 56 | "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.11", "", { "os": "linux", "cpu": "s390x" }, "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw=="], 57 | 58 | "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.11", "", { "os": "linux", "cpu": "x64" }, "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ=="], 59 | 60 | "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.11", "", { "os": "none", "cpu": "arm64" }, "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg=="], 61 | 62 | "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.11", "", { "os": "none", "cpu": "x64" }, "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A=="], 63 | 64 | "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.11", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg=="], 65 | 66 | "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.11", "", { "os": "openbsd", "cpu": "x64" }, "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw=="], 67 | 68 | "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.11", "", { "os": "none", "cpu": "arm64" }, "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ=="], 69 | 70 | "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.11", "", { "os": "sunos", "cpu": "x64" }, "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA=="], 71 | 72 | "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q=="], 73 | 74 | "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.11", "", { "os": "win32", "cpu": "ia32" }, "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA=="], 75 | 76 | "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.11", "", { "os": "win32", "cpu": "x64" }, "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA=="], 77 | 78 | "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], 79 | 80 | "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], 81 | 82 | "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], 83 | 84 | "@types/fs-extra": ["@types/fs-extra@11.0.4", "", { "dependencies": { "@types/jsonfile": "*", "@types/node": "*" } }, "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ=="], 85 | 86 | "@types/jsonfile": ["@types/jsonfile@6.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ=="], 87 | 88 | "@types/node": ["@types/node@22.18.12", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog=="], 89 | 90 | "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], 91 | 92 | "@types/react": ["@types/react@18.3.26", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA=="], 93 | 94 | "ansi-escapes": ["ansi-escapes@7.1.1", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q=="], 95 | 96 | "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], 97 | 98 | "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], 99 | 100 | "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="], 101 | 102 | "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], 103 | 104 | "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], 105 | 106 | "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], 107 | 108 | "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="], 109 | 110 | "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], 111 | 112 | "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], 113 | 114 | "code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="], 115 | 116 | "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], 117 | 118 | "convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="], 119 | 120 | "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 121 | 122 | "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], 123 | 124 | "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], 125 | 126 | "es-toolkit": ["es-toolkit@1.41.0", "", {}, "sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA=="], 127 | 128 | "esbuild": ["esbuild@0.25.11", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.11", "@esbuild/android-arm": "0.25.11", "@esbuild/android-arm64": "0.25.11", "@esbuild/android-x64": "0.25.11", "@esbuild/darwin-arm64": "0.25.11", "@esbuild/darwin-x64": "0.25.11", "@esbuild/freebsd-arm64": "0.25.11", "@esbuild/freebsd-x64": "0.25.11", "@esbuild/linux-arm": "0.25.11", "@esbuild/linux-arm64": "0.25.11", "@esbuild/linux-ia32": "0.25.11", "@esbuild/linux-loong64": "0.25.11", "@esbuild/linux-mips64el": "0.25.11", "@esbuild/linux-ppc64": "0.25.11", "@esbuild/linux-riscv64": "0.25.11", "@esbuild/linux-s390x": "0.25.11", "@esbuild/linux-x64": "0.25.11", "@esbuild/netbsd-arm64": "0.25.11", "@esbuild/netbsd-x64": "0.25.11", "@esbuild/openbsd-arm64": "0.25.11", "@esbuild/openbsd-x64": "0.25.11", "@esbuild/openharmony-arm64": "0.25.11", "@esbuild/sunos-x64": "0.25.11", "@esbuild/win32-arm64": "0.25.11", "@esbuild/win32-ia32": "0.25.11", "@esbuild/win32-x64": "0.25.11" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q=="], 129 | 130 | "escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], 131 | 132 | "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], 133 | 134 | "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], 135 | 136 | "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], 137 | 138 | "fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="], 139 | 140 | "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 141 | 142 | "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], 143 | 144 | "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], 145 | 146 | "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], 147 | 148 | "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], 149 | 150 | "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], 151 | 152 | "ink": ["ink@5.2.1", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.1.3", "ansi-escapes": "^7.0.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^4.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.22.0", "indent-string": "^5.0.0", "is-in-ci": "^1.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.29.0", "scheduler": "^0.23.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^7.2.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=18.0.0", "react": ">=18.0.0", "react-devtools-core": "^4.19.1" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg=="], 153 | 154 | "ink-spinner": ["ink-spinner@5.0.0", "", { "dependencies": { "cli-spinners": "^2.7.0" }, "peerDependencies": { "ink": ">=4.0.0", "react": ">=18.0.0" } }, "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA=="], 155 | 156 | "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], 157 | 158 | "is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], 159 | 160 | "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], 161 | 162 | "is-in-ci": ["is-in-ci@1.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg=="], 163 | 164 | "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], 165 | 166 | "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], 167 | 168 | "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], 169 | 170 | "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], 171 | 172 | "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], 173 | 174 | "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], 175 | 176 | "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], 177 | 178 | "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], 179 | 180 | "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], 181 | 182 | "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], 183 | 184 | "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], 185 | 186 | "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], 187 | 188 | "react-reconciler": ["react-reconciler@0.29.2", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg=="], 189 | 190 | "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], 191 | 192 | "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="], 193 | 194 | "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], 195 | 196 | "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], 197 | 198 | "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], 199 | 200 | "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], 201 | 202 | "slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], 203 | 204 | "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], 205 | 206 | "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], 207 | 208 | "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], 209 | 210 | "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], 211 | 212 | "tsx": ["tsx@4.20.6", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg=="], 213 | 214 | "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], 215 | 216 | "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 217 | 218 | "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 219 | 220 | "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], 221 | 222 | "widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="], 223 | 224 | "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], 225 | 226 | "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], 227 | 228 | "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], 229 | 230 | "cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], 231 | 232 | "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], 233 | } 234 | } 235 | --------------------------------------------------------------------------------