UNPKG

pgit-cli

Version:

Private file tracking with dual git repositories

459 lines 17.5 kB
import * as path from 'node:path'; import chalk from 'chalk'; import { DEFAULT_PATHS, } from '../types/config.types.js'; import { ConfigManager } from '../core/config.manager.js'; import { FileSystemService } from '../core/filesystem.service.js'; import { GitService } from '../core/git.service.js'; import { BaseError } from '../errors/base.error.js'; /** * Status command specific errors */ export class StatusError extends BaseError { constructor() { super(...arguments); this.code = 'STATUS_ERROR'; this.recoverable = true; } } export class NotInitializedError extends BaseError { constructor() { super(...arguments); this.code = 'NOT_INITIALIZED'; this.recoverable = false; } } /** * Status command for showing repository and system health */ export class StatusCommand { constructor(workingDir) { this.workingDir = workingDir || process.cwd(); this.fileSystem = new FileSystemService(); this.configManager = new ConfigManager(this.workingDir, this.fileSystem); } /** * Execute status command (show both repositories) */ async execute(options = {}) { try { const systemStatus = await this.getSystemStatus(); if (!systemStatus.initialized) { throw new NotInitializedError('Private git tracking is not initialized. Run "private init" first.'); } // Display status this.displayCombinedStatus(systemStatus, options.verbose); return { success: true, message: 'Status retrieved successfully', data: systemStatus, exitCode: 0, }; } catch (error) { if (error instanceof BaseError) { return { success: false, message: error.message, error, exitCode: 1, }; } return { success: false, message: 'Failed to get status', error: error instanceof Error ? error : new Error(String(error)), exitCode: 1, }; } } /** * Execute private-only status command */ async executePrivateOnly(options = {}) { try { const systemStatus = await this.getSystemStatus(); if (!systemStatus.initialized) { throw new NotInitializedError('Private git tracking is not initialized. Run "private init" first.'); } // Display private-only status this.displayPrivateStatus(systemStatus, options.verbose); return { success: true, message: 'Private status retrieved successfully', data: systemStatus.privateRepo, exitCode: 0, }; } catch (error) { if (error instanceof BaseError) { return { success: false, message: error.message, error, exitCode: 1, }; } return { success: false, message: 'Failed to get private status', error: error instanceof Error ? error : new Error(String(error)), exitCode: 1, }; } } /** * Get complete system status */ async getSystemStatus() { const systemStatus = { initialized: false, mainRepo: await this.getMainRepositoryStatus(), privateRepo: await this.getPrivateRepositoryStatus(), symlinks: await this.getSymlinkHealth(), config: await this.getConfigHealth(), isHealthy: false, issues: [], }; // Check if initialized systemStatus.initialized = await this.configManager.exists(); // Determine overall health systemStatus.isHealthy = this.determineOverallHealth(systemStatus); // Collect issues systemStatus.issues = this.collectSystemIssues(systemStatus); return systemStatus; } /** * Get main repository status */ async getMainRepositoryStatus() { const status = { type: 'main', branch: 'unknown', isClean: false, stagedFiles: 0, modifiedFiles: 0, untrackedFiles: 0, deletedFiles: 0, exists: false, issues: [], }; try { const gitService = new GitService(this.workingDir, this.fileSystem); if (await gitService.isRepository()) { status.exists = true; const gitStatus = await gitService.getStatus(); status.branch = gitStatus.current || 'unknown'; status.isClean = gitStatus.isClean; status.stagedFiles = gitStatus.staged.length; status.modifiedFiles = gitStatus.modified.length; status.untrackedFiles = gitStatus.untracked.length; status.deletedFiles = gitStatus.deleted.length; } else { status.issues.push('Directory is not a git repository'); } } catch (error) { status.issues.push(`Failed to get main repository status: ${error instanceof Error ? error.message : String(error)}`); } return status; } /** * Get private repository status */ async getPrivateRepositoryStatus() { const status = { type: 'private', branch: 'unknown', isClean: false, stagedFiles: 0, modifiedFiles: 0, untrackedFiles: 0, deletedFiles: 0, exists: false, issues: [], }; try { if (!(await this.configManager.exists())) { status.issues.push('Private git tracking not initialized'); return status; } const storagePath = path.join(this.workingDir, DEFAULT_PATHS.storage); if (!(await this.fileSystem.pathExists(storagePath))) { status.issues.push('Private storage directory does not exist'); return status; } const gitService = new GitService(storagePath, this.fileSystem); if (await gitService.isRepository()) { status.exists = true; const gitStatus = await gitService.getStatus(); status.branch = gitStatus.current || 'unknown'; status.isClean = gitStatus.isClean; status.stagedFiles = gitStatus.staged.length; status.modifiedFiles = gitStatus.modified.length; status.untrackedFiles = gitStatus.untracked.length; status.deletedFiles = gitStatus.deleted.length; } else { status.issues.push('Private storage is not a git repository'); } } catch (error) { status.issues.push(`Failed to get private repository status: ${error instanceof Error ? error.message : String(error)}`); } return status; } /** * Get symbolic link health information */ async getSymlinkHealth() { const health = { total: 0, healthy: 0, broken: 0, brokenLinks: [], }; try { if (!(await this.configManager.exists())) { return health; } const config = await this.configManager.load(); health.total = config.trackedPaths.length; for (const trackedPath of config.trackedPaths) { const fullPath = path.join(this.workingDir, trackedPath); const targetPath = path.join(this.workingDir, config.storagePath, trackedPath); try { if (await this.fileSystem.pathExists(fullPath)) { const stats = await this.fileSystem.getStats(fullPath); if (stats.isSymbolicLink()) { // Check if target exists if (await this.fileSystem.pathExists(targetPath)) { health.healthy++; } else { health.broken++; health.brokenLinks.push({ linkPath: fullPath, targetPath, reason: 'Target file does not exist', repairable: false, }); } } else { health.broken++; health.brokenLinks.push({ linkPath: fullPath, targetPath, reason: 'Path exists but is not a symbolic link', repairable: true, }); } } else { health.broken++; health.brokenLinks.push({ linkPath: fullPath, targetPath, reason: 'Symbolic link does not exist', repairable: await this.fileSystem.pathExists(targetPath), }); } } catch (error) { health.broken++; health.brokenLinks.push({ linkPath: fullPath, targetPath, reason: `Error checking symbolic link: ${error instanceof Error ? error.message : String(error)}`, repairable: false, }); } } } catch { // If we can't load config, we can't check symlinks } return health; } /** * Get configuration health */ async getConfigHealth() { return this.configManager.getHealth(); } /** * Determine overall system health */ determineOverallHealth(status) { return (status.initialized && status.mainRepo.exists && status.privateRepo.exists && status.config.valid && status.symlinks.broken === 0 && status.issues.length === 0); } /** * Collect all system issues */ collectSystemIssues(status) { const issues = []; if (!status.initialized) { issues.push('Private git tracking not initialized'); } if (!status.mainRepo.exists) { issues.push('Main repository not found'); } if (!status.privateRepo.exists) { issues.push('Private repository not found'); } if (!status.config.valid) { issues.push('Configuration is invalid'); } if (status.symlinks.broken > 0) { issues.push(`${status.symlinks.broken} broken symbolic links found`); } // Add repository-specific issues issues.push(...status.mainRepo.issues); issues.push(...status.privateRepo.issues); issues.push(...status.config.errors); return issues; } /** * Display combined status (both repositories) */ displayCombinedStatus(status, verbose) { console.log(chalk.blue.bold('📊 Private Git Tracking Status')); console.log(); // Overall health if (status.isHealthy) { console.log(chalk.green('✓ System is healthy')); } else { console.log(chalk.red('✗ System has issues')); } console.log(); // Main repository status console.log(chalk.blue.bold('📋 Main Repository')); this.displayRepositoryStatus(status.mainRepo, verbose); console.log(); // Private repository status console.log(chalk.blue.bold('🔒 Private Repository')); this.displayRepositoryStatus(status.privateRepo, verbose); console.log(); // Symbolic links health if (status.symlinks.total > 0) { console.log(chalk.blue.bold('🔗 Symbolic Links')); console.log(` Total: ${status.symlinks.total}`); console.log(` Healthy: ${chalk.green(status.symlinks.healthy)}`); console.log(` Broken: ${status.symlinks.broken > 0 ? chalk.red(status.symlinks.broken) : chalk.green(status.symlinks.broken)}`); if (verbose && status.symlinks.brokenLinks.length > 0) { console.log(' Broken links:'); for (const brokenLink of status.symlinks.brokenLinks) { console.log(` ${chalk.red('✗')} ${brokenLink.linkPath}`); console.log(` Reason: ${brokenLink.reason}`); console.log(` Repairable: ${brokenLink.repairable ? chalk.green('Yes') : chalk.red('No')}`); } } console.log(); } // Issues if (status.issues.length > 0) { console.log(chalk.red.bold('⚠️ Issues Found')); for (const issue of status.issues) { console.log(` ${chalk.red('•')} ${issue}`); } console.log(); } } /** * Display private repository status only */ displayPrivateStatus(status, verbose) { console.log(chalk.blue.bold('🔒 Private Repository Status')); console.log(); this.displayRepositoryStatus(status.privateRepo, verbose); // Show tracked files if (verbose) { this.displayTrackedFiles(); } // Symbolic links health if (status.symlinks.total > 0) { console.log(); console.log(chalk.blue.bold('🔗 Tracked Files Summary')); console.log(` Total tracked files: ${status.symlinks.total}`); console.log(` Healthy symbolic links: ${chalk.green(status.symlinks.healthy)}`); console.log(` Broken symbolic links: ${status.symlinks.broken > 0 ? chalk.red(status.symlinks.broken) : chalk.green(status.symlinks.broken)}`); } } /** * Display repository status information */ displayRepositoryStatus(repo, verbose) { if (!repo.exists) { console.log(chalk.red(' ✗ Repository not found')); return; } console.log(` Branch: ${chalk.cyan(repo.branch)}`); console.log(` Status: ${repo.isClean ? chalk.green('Clean') : chalk.yellow('Has changes')}`); if (!repo.isClean || verbose) { if (repo.stagedFiles > 0) { console.log(` Staged files: ${chalk.green(repo.stagedFiles)}`); } if (repo.modifiedFiles > 0) { console.log(` Modified files: ${chalk.yellow(repo.modifiedFiles)}`); } if (repo.untrackedFiles > 0) { console.log(` Untracked files: ${chalk.cyan(repo.untrackedFiles)}`); } if (repo.deletedFiles > 0) { console.log(` Deleted files: ${chalk.red(repo.deletedFiles)}`); } } if (repo.issues.length > 0) { console.log(chalk.red(' Issues:')); for (const issue of repo.issues) { console.log(` ${chalk.red('•')} ${issue}`); } } } /** * Display tracked files information */ async displayTrackedFiles() { try { const config = await this.configManager.load(); if (config.trackedPaths.length === 0) { console.log(); console.log(chalk.gray(' No files are currently tracked')); return; } console.log(); console.log(chalk.blue.bold('📁 Tracked Files')); for (const trackedPath of config.trackedPaths) { const fullPath = path.join(this.workingDir, trackedPath); const targetPath = path.join(this.workingDir, config.storagePath, trackedPath); const linkExists = await this.fileSystem.pathExists(fullPath); const targetExists = await this.fileSystem.pathExists(targetPath); let status = ''; if (linkExists && targetExists) { status = chalk.green('✓'); } else if (!linkExists && targetExists) { status = chalk.red('✗ Missing link'); } else if (linkExists && !targetExists) { status = chalk.red('✗ Missing target'); } else { status = chalk.red('✗ Both missing'); } console.log(` ${status} ${trackedPath}`); } } catch { console.log(chalk.red(' Failed to load tracked files information')); } } } //# sourceMappingURL=status.command.js.map