diff --git a/cmd/add.go b/cmd/add.go index 9df4741..34bad9e 100644 --- a/cmd/add.go +++ b/cmd/add.go @@ -9,8 +9,8 @@ import ( func newAddCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "add ...", - Short: "✨ Add files to lnk management", + Use: "add ...", + Short: "✨ Add files to lnk management", Long: `Moves files to the lnk repository and creates symlinks in their place. Supports multiple files. Examples: @@ -40,20 +40,20 @@ changes to your system - perfect for verification before bulk operations.`, if err != nil { return err } - + // Display preview output if recursive { printf(cmd, "🔍 \033[1mWould add %d files recursively:\033[0m\n", len(files)) } else { printf(cmd, "🔍 \033[1mWould add %d files:\033[0m\n", len(files)) } - + // List files that would be added for _, file := range files { basename := filepath.Base(file) printf(cmd, " 📄 \033[90m%s\033[0m\n", basename) } - + printf(cmd, "\n💡 \033[33mTo proceed:\033[0m run without --dry-run flag\n") return nil } @@ -65,19 +65,19 @@ changes to your system - perfect for verification before bulk operations.`, if err != nil { return err } - + // Create progress callback for CLI display progressCallback := func(current, total int, currentFile string) { printf(cmd, "\r⏳ Processing %d/%d: %s", current, total, currentFile) } - + if err := lnk.AddRecursiveWithProgress(args, progressCallback); err != nil { return err } - + // Clear progress line and show completion printf(cmd, "\r") - + // Store processed file count for display args = previewFiles // Replace args with actual files for display } else { @@ -103,13 +103,13 @@ changes to your system - perfect for verification before bulk operations.`, } else { printf(cmd, "✨ \033[1mAdded %d files recursively to lnk\033[0m\n", len(args)) } - + // Show some of the files that were added (limit to first few for readability) filesToShow := len(args) if filesToShow > 5 { filesToShow = 5 } - + for i := 0; i < filesToShow; i++ { basename := filepath.Base(args[i]) if host != "" { @@ -118,7 +118,7 @@ changes to your system - perfect for verification before bulk operations.`, printf(cmd, " 🔗 \033[90m%s\033[0m → \033[36m~/.config/lnk/...\033[0m\n", basename) } } - + if len(args) > 5 { printf(cmd, " \033[90m... and %d more files\033[0m\n", len(args)-5) } diff --git a/cmd/root_test.go b/cmd/root_test.go index d4c0a37..0c7b728 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -1038,7 +1038,7 @@ func (suite *CLITestSuite) TestAddCommandRecursiveFlag() { err = os.MkdirAll(themesDir, 0755) suite.Require().NoError(err) - themeFile := filepath.Join(themesDir, "custom.json") + themeFile := filepath.Join(themesDir, "custom.json") err = os.WriteFile(themeFile, []byte(`{"colors": {}}`), 0644) suite.Require().NoError(err) @@ -1049,7 +1049,7 @@ func (suite *CLITestSuite) TestAddCommandRecursiveFlag() { // Check output shows multiple files were processed output := suite.stdout.String() suite.Contains(output, "Added") // Should show some success message - + // Verify individual files are now symlinks (not the directory itself) info, err := os.Lstat(settingsFile) suite.NoError(err) @@ -1140,7 +1140,7 @@ func (suite *CLITestSuite) TestDryRunFlag() { err = suite.runCommand("add", "--dry-run", testFile1, testFile2) suite.NoError(err, "Dry-run command should succeed") output := suite.stdout.String() - + // Basic check that some output was produced (flag exists but behavior TBD) suite.NotEmpty(output, "Should produce some output") @@ -1186,7 +1186,7 @@ func (suite *CLITestSuite) TestDryRunOutput() { suite.Contains(output, "test1.txt", "Should show first file") suite.Contains(output, "test2.txt", "Should show second file") suite.Contains(output, "2 files", "Should show file count") - + // Should contain helpful instructions suite.Contains(output, "run without --dry-run", "Should provide next steps") } @@ -1219,7 +1219,7 @@ func (suite *CLITestSuite) TestDryRunRecursive() { suite.Contains(output, "Would add", "Should show dry-run preview") suite.Contains(output, "15 files", "Should show correct file count") suite.Contains(output, "recursively", "Should indicate recursive mode") - + // Should show some of the files suite.Contains(output, "config1.json", "Should show first file") suite.Contains(output, "config15.json", "Should show last file") @@ -1247,7 +1247,7 @@ func (suite *CLITestSuite) TestEnhancedSuccessOutput() { filepath.Join(suite.tempDir, "config2.txt"), filepath.Join(suite.tempDir, "config3.txt"), } - + for i, file := range testFiles { suite.Require().NoError(os.WriteFile(file, []byte(fmt.Sprintf("content %d", i+1)), 0644)) } @@ -1261,12 +1261,12 @@ func (suite *CLITestSuite) TestEnhancedSuccessOutput() { // Should have enhanced formatting with consistent indentation suite.Contains(output, "🔗", "Should use link icons") suite.Contains(output, "config1.txt", "Should show first file") - suite.Contains(output, "config2.txt", "Should show second file") + suite.Contains(output, "config2.txt", "Should show second file") suite.Contains(output, "config3.txt", "Should show third file") - + // Should show organized file list suite.Contains(output, " ", "Should have consistent indentation") - + // Should include summary information suite.Contains(output, "3 items", "Should show total count") } @@ -1296,11 +1296,11 @@ func (suite *CLITestSuite) TestOperationSummary() { // Should show operation summary suite.Contains(output, "recursively", "Should indicate operation type") suite.Contains(output, "5", "Should show correct file count") - + // Should include contextual help message suite.Contains(output, "lnk push", "Should suggest next steps") suite.Contains(output, "sync to remote", "Should explain next step purpose") - + // Should show operation completion confirmation suite.Contains(output, "✨", "Should use success emoji") suite.Contains(output, "Added", "Should confirm operation completed") @@ -1317,7 +1317,7 @@ func (suite *CLITestSuite) TestUpdatedHelpText() { // Should mention bulk operations suite.Contains(helpOutput, "multiple files", "Help should mention multiple file support") - + // Test add command help err = suite.runCommand("add", "--help") suite.NoError(err) @@ -1326,13 +1326,13 @@ func (suite *CLITestSuite) TestUpdatedHelpText() { // Should include new flags suite.Contains(addHelpOutput, "--recursive", "Help should include recursive flag") suite.Contains(addHelpOutput, "--dry-run", "Help should include dry-run flag") - + // Should include examples suite.Contains(addHelpOutput, "Examples:", "Help should include usage examples") suite.Contains(addHelpOutput, "lnk add ~/.bashrc ~/.vimrc", "Help should show multiple file example") suite.Contains(addHelpOutput, "lnk add --recursive ~/.config", "Help should show recursive example") suite.Contains(addHelpOutput, "lnk add --dry-run", "Help should show dry-run example") - + // Should describe what each flag does suite.Contains(addHelpOutput, "directory contents individually", "Should explain recursive flag") suite.Contains(addHelpOutput, "without making changes", "Should explain dry-run flag") diff --git a/internal/core/lnk.go b/internal/core/lnk.go index 24e5bb4..4b0b475 100644 --- a/internal/core/lnk.go +++ b/internal/core/lnk.go @@ -831,17 +831,17 @@ func (l *Lnk) RunBootstrapScript(scriptName string) error { // walkDirectory walks through a directory and returns all regular files func (l *Lnk) walkDirectory(dirPath string) ([]string, error) { var files []string - + err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { if err != nil { return err } - + // Skip directories - we only want files if info.IsDir() { return nil } - + // Handle symlinks: include them as files if they point to regular files if info.Mode()&os.ModeSymlink != 0 { // For symlinks, we'll include them but the AddMultiple logic @@ -849,19 +849,19 @@ func (l *Lnk) walkDirectory(dirPath string) ([]string, error) { files = append(files, path) return nil } - + // Include regular files if info.Mode().IsRegular() { files = append(files, path) } - + return nil }) - + if err != nil { return nil, fmt.Errorf("failed to walk directory %s: %w", dirPath, err) } - + return files, nil } @@ -871,20 +871,20 @@ type ProgressCallback func(current, total int, currentFile string) // AddRecursiveWithProgress adds directory contents individually with progress reporting func (l *Lnk) AddRecursiveWithProgress(paths []string, progress ProgressCallback) error { var allFiles []string - + for _, path := range paths { // Get absolute path absPath, err := filepath.Abs(path) if err != nil { return fmt.Errorf("failed to get absolute path for %s: %w", path, err) } - + // Check if it's a directory info, err := os.Stat(absPath) if err != nil { return fmt.Errorf("failed to stat %s: %w", path, err) } - + if info.IsDir() { // Walk directory to get all files files, err := l.walkDirectory(absPath) @@ -897,18 +897,18 @@ func (l *Lnk) AddRecursiveWithProgress(paths []string, progress ProgressCallback allFiles = append(allFiles, absPath) } } - + // Use AddMultiple for batch processing if len(allFiles) == 0 { return fmt.Errorf("no files found to add") } - + // Apply progress threshold: only show progress for >10 files const progressThreshold = 10 if len(allFiles) > progressThreshold && progress != nil { return l.addMultipleWithProgress(allFiles, progress) } - + // For small operations, use regular AddMultiple without progress return l.AddMultiple(allFiles) } @@ -1053,20 +1053,20 @@ func (l *Lnk) addMultipleWithProgress(paths []string, progress ProgressCallback) // AddRecursive adds directory contents individually instead of the directory as a whole func (l *Lnk) AddRecursive(paths []string) error { var allFiles []string - + for _, path := range paths { // Get absolute path absPath, err := filepath.Abs(path) if err != nil { return fmt.Errorf("failed to get absolute path for %s: %w", path, err) } - + // Check if it's a directory info, err := os.Stat(absPath) if err != nil { return fmt.Errorf("failed to stat %s: %w", path, err) } - + if info.IsDir() { // Walk directory to get all files files, err := l.walkDirectory(absPath) @@ -1079,32 +1079,32 @@ func (l *Lnk) AddRecursive(paths []string) error { allFiles = append(allFiles, absPath) } } - + // Use AddMultiple for batch processing if len(allFiles) == 0 { return fmt.Errorf("no files found to add") } - + return l.AddMultiple(allFiles) } // PreviewAdd simulates an add operation and returns files that would be affected func (l *Lnk) PreviewAdd(paths []string, recursive bool) ([]string, error) { var allFiles []string - + for _, path := range paths { // Get absolute path absPath, err := filepath.Abs(path) if err != nil { return nil, fmt.Errorf("failed to get absolute path for %s: %w", path, err) } - + // Check if it's a directory info, err := os.Stat(absPath) if err != nil { return nil, fmt.Errorf("failed to stat %s: %w", path, err) } - + if info.IsDir() && recursive { // Walk directory to get all files (same logic as AddRecursive) files, err := l.walkDirectory(absPath) @@ -1117,7 +1117,7 @@ func (l *Lnk) PreviewAdd(paths []string, recursive bool) ([]string, error) { allFiles = append(allFiles, absPath) } } - + // Validate files (same validation as AddMultiple but without making changes) var validFiles []string for _, filePath := range allFiles { @@ -1145,6 +1145,6 @@ func (l *Lnk) PreviewAdd(paths []string, recursive bool) ([]string, error) { validFiles = append(validFiles, filePath) } - + return validFiles, nil } diff --git a/internal/core/lnk_test.go b/internal/core/lnk_test.go index f9212dd..f08691a 100644 --- a/internal/core/lnk_test.go +++ b/internal/core/lnk_test.go @@ -867,7 +867,7 @@ func (suite *CoreTestSuite) TestAddMultiple() { suite.NoError(err) suite.Equal("file1.txt\nfile2.txt\nfile3.txt\n", string(lnkContent)) - // Verify Git commit was created + // Verify Git commit was created commits, err := suite.lnk.GetCommits() suite.NoError(err) suite.T().Logf("Commits: %v", commits) @@ -1037,7 +1037,7 @@ func (suite *CoreTestSuite) TestAtomicRollbackOnFailure() { info1After, err := os.Lstat(file1) suite.NoError(err) suite.Equal(info1Before.Mode(), info1After.Mode(), "file1 mode should be unchanged") - + info3After, err := os.Lstat(file3) suite.NoError(err) suite.Equal(info3Before.Mode(), info3After.Mode(), "file3 mode should be unchanged") @@ -1120,10 +1120,10 @@ func (suite *CoreTestSuite) TestWalkDirectory() { // Call walkDirectory method (which doesn't exist yet) files, err := suite.lnk.walkDirectory(configDir) suite.Require().NoError(err, "walkDirectory should succeed") - + // Should find all 4 files suite.Len(files, 4, "Should find all files in nested structure") - + // Check that all expected files are found (order may vary) expectedFiles := []string{file1, file2, file3, file4} for _, expectedFile := range expectedFiles { @@ -1157,7 +1157,7 @@ func (suite *CoreTestSuite) TestWalkDirectoryIncludesHiddenFiles() { // Call walkDirectory method files, err := suite.lnk.walkDirectory(testDir) suite.Require().NoError(err, "walkDirectory should succeed with hidden files") - + // Should find all files including hidden ones suite.Len(files, 3, "Should find all files including hidden ones") suite.Contains(files, regularFile, "Should include regular file") @@ -1187,12 +1187,12 @@ func (suite *CoreTestSuite) TestWalkDirectorySymlinkHandling() { // Call walkDirectory method files, err := suite.lnk.walkDirectory(testDir) suite.Require().NoError(err, "walkDirectory should handle symlinks") - + // Should include both regular file and properly handle symlink // (exact behavior depends on implementation - could include symlink as file) suite.GreaterOrEqual(len(files), 1, "Should find at least the regular file") suite.Contains(files, regularFile, "Should include regular file") - + // The symlink handling behavior will be defined in implementation // For now, we just ensure no errors occur } @@ -1219,14 +1219,14 @@ func (suite *CoreTestSuite) TestWalkDirectoryEmptyDirs() { nonEmptyDir := filepath.Join(testDir, "non-empty") err = os.MkdirAll(nonEmptyDir, 0755) suite.Require().NoError(err) - + testFile := filepath.Join(nonEmptyDir, "test.txt") suite.Require().NoError(os.WriteFile(testFile, []byte("content"), 0644)) // Call walkDirectory method files, err := suite.lnk.walkDirectory(testDir) suite.Require().NoError(err, "walkDirectory should skip empty directories") - + // Should only find the one file, not empty directories suite.Len(files, 1, "Should only find files, not empty directories") suite.Contains(files, testFile, "Should include the actual file") @@ -1354,7 +1354,7 @@ func (suite *CoreTestSuite) TestPreviewAdd() { // Test PreviewAdd for multiple files files, err := suite.lnk.PreviewAdd([]string{testFile1, testFile2}, false) suite.Require().NoError(err, "PreviewAdd should succeed") - + // Should return both files suite.Len(files, 2, "Should preview both files") suite.Contains(files, testFile1, "Should include first file") @@ -1392,10 +1392,10 @@ func (suite *CoreTestSuite) TestPreviewAddRecursive() { // Test PreviewAdd with recursive files, err := suite.lnk.PreviewAdd([]string{configDir}, true) suite.Require().NoError(err, "PreviewAdd recursive should succeed") - + // Should return all files in directory suite.Len(files, expectedFiles, "Should preview all files in directory") - + // Check that all created files are included for _, createdFile := range createdFiles { suite.Contains(files, createdFile, "Should include file %s", createdFile)