feat(cmd): add 'list' command to display managed files

Implements a new 'list' command that shows all files and directories managed by lnk, improving visibility and user experience.

fixes #4
This commit is contained in:
Yar Kravtsov
2025-05-26 05:57:45 +03:00
parent 69c1038f3e
commit 4a275ce4ca
6 changed files with 195 additions and 0 deletions

View File

@@ -52,6 +52,9 @@ lnk init -r git@github.com:user/dotfiles.git
# Add files/directories
lnk add ~/.vimrc ~/.config/nvim ~/.gitconfig
# List managed files
lnk list
# Check status
lnk status
@@ -99,6 +102,7 @@ lnk pull # auto-creates symlinks
```bash
vim ~/.vimrc # edit normally
lnk list # see what's managed
lnk status # check what changed
lnk push "new plugins" # commit & push
```
@@ -108,6 +112,7 @@ lnk push "new plugins" # commit & push
- `lnk init [-r remote]` - Create repo
- `lnk add <files>` - Move files to repo, create symlinks
- `lnk rm <files>` - Move files back, remove symlinks
- `lnk list` - List files managed by lnk
- `lnk status` - Git status + sync info
- `lnk push [msg]` - Stage all, commit, push
- `lnk pull` - Pull + restore missing symlinks

43
cmd/list.go Normal file
View File

@@ -0,0 +1,43 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/yarlson/lnk/internal/core"
)
func newListCmd() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "📋 List files managed by lnk",
Long: "Display all files and directories currently managed by lnk.",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
lnk := core.NewLnk()
managedItems, err := lnk.List()
if err != nil {
return fmt.Errorf("failed to list managed items: %w", err)
}
if len(managedItems) == 0 {
printf(cmd, "📋 \033[1mNo files currently managed by lnk\033[0m\n")
printf(cmd, " 💡 Use \033[1mlnk add <file>\033[0m to start managing files\n")
return nil
}
printf(cmd, "📋 \033[1mFiles managed by lnk\033[0m (\033[36m%d item", len(managedItems))
if len(managedItems) > 1 {
printf(cmd, "s")
}
printf(cmd, "\033[0m):\n\n")
for _, item := range managedItems {
printf(cmd, " 🔗 \033[36m%s\033[0m\n", item)
}
printf(cmd, "\n💡 Use \033[1mlnk status\033[0m to check sync status\n")
return nil
},
}
}

View File

@@ -38,6 +38,7 @@ That's it.
rootCmd.AddCommand(newInitCmd())
rootCmd.AddCommand(newAddCmd())
rootCmd.AddCommand(newRemoveCmd())
rootCmd.AddCommand(newListCmd())
rootCmd.AddCommand(newStatusCmd())
rootCmd.AddCommand(newPushCmd())
rootCmd.AddCommand(newPullCmd())

View File

@@ -163,6 +163,60 @@ func (suite *CLITestSuite) TestStatusCommand() {
suite.Contains(err.Error(), "no remote configured")
}
func (suite *CLITestSuite) TestListCommand() {
// Test list without init - should fail
err := suite.runCommand("list")
suite.Error(err)
suite.Contains(err.Error(), "Lnk repository not initialized")
// Initialize first
err = suite.runCommand("init")
suite.Require().NoError(err)
suite.stdout.Reset()
// Test list with no managed files
err = suite.runCommand("list")
suite.NoError(err)
output := suite.stdout.String()
suite.Contains(output, "No files currently managed by lnk")
suite.Contains(output, "lnk add <file>")
suite.stdout.Reset()
// Add a file
testFile := filepath.Join(suite.tempDir, ".bashrc")
err = os.WriteFile(testFile, []byte("export PATH=/usr/local/bin:$PATH"), 0644)
suite.Require().NoError(err)
err = suite.runCommand("add", testFile)
suite.Require().NoError(err)
suite.stdout.Reset()
// Test list with one managed file
err = suite.runCommand("list")
suite.NoError(err)
output = suite.stdout.String()
suite.Contains(output, "Files managed by lnk")
suite.Contains(output, "1 item")
suite.Contains(output, ".bashrc")
suite.stdout.Reset()
// Add another file
testFile2 := filepath.Join(suite.tempDir, ".vimrc")
err = os.WriteFile(testFile2, []byte("set number"), 0644)
suite.Require().NoError(err)
err = suite.runCommand("add", testFile2)
suite.Require().NoError(err)
suite.stdout.Reset()
// Test list with multiple managed files
err = suite.runCommand("list")
suite.NoError(err)
output = suite.stdout.String()
suite.Contains(output, "Files managed by lnk")
suite.Contains(output, "2 items")
suite.Contains(output, ".bashrc")
suite.Contains(output, ".vimrc")
}
func (suite *CLITestSuite) TestErrorHandling() {
tests := []struct {
name string
@@ -207,6 +261,12 @@ func (suite *CLITestSuite) TestErrorHandling() {
wantErr: false,
outContains: "Moves a file to the lnk repository",
},
{
name: "list help",
args: []string{"list", "--help"},
wantErr: false,
outContains: "Display all files and directories",
},
}
for _, tt := range tests {

View File

@@ -428,6 +428,22 @@ func (l *Lnk) Pull() ([]string, error) {
return restored, nil
}
// List returns the list of files and directories currently managed by lnk
func (l *Lnk) List() ([]string, error) {
// Check if repository is initialized
if !l.git.IsGitRepository() {
return nil, fmt.Errorf("❌ Lnk repository not initialized\n 💡 Run \033[1mlnk init\033[0m first")
}
// Get managed items from .lnk file
managedItems, err := l.getManagedItems()
if err != nil {
return nil, fmt.Errorf("failed to get managed items: %w", err)
}
return managedItems, nil
}
// RestoreSymlinks finds all managed items from .lnk file and ensures they have proper symlinks
func (l *Lnk) RestoreSymlinks() ([]string, error) {
var restored []string

View File

@@ -516,6 +516,76 @@ func (suite *CoreTestSuite) TestStatusDetectsDirtyRepo() {
suite.True(status.Dirty, "Repository should be dirty after editing managed file")
}
// Test list functionality
func (suite *CoreTestSuite) TestListManagedItems() {
// Test list without init - should fail
_, err := suite.lnk.List()
suite.Error(err)
suite.Contains(err.Error(), "Lnk repository not initialized")
// Initialize repository
err = suite.lnk.Init()
suite.Require().NoError(err)
// Test list with no managed files
items, err := suite.lnk.List()
suite.Require().NoError(err)
suite.Empty(items)
// Add a file
testFile := filepath.Join(suite.tempDir, ".bashrc")
content := "export PATH=$PATH:/usr/local/bin"
err = os.WriteFile(testFile, []byte(content), 0644)
suite.Require().NoError(err)
err = suite.lnk.Add(testFile)
suite.Require().NoError(err)
// Test list with one managed file
items, err = suite.lnk.List()
suite.Require().NoError(err)
suite.Len(items, 1)
suite.Contains(items[0], ".bashrc")
// Add a directory
testDir := filepath.Join(suite.tempDir, ".config")
err = os.MkdirAll(testDir, 0755)
suite.Require().NoError(err)
configFile := filepath.Join(testDir, "app.conf")
err = os.WriteFile(configFile, []byte("setting=value"), 0644)
suite.Require().NoError(err)
err = suite.lnk.Add(testDir)
suite.Require().NoError(err)
// Test list with multiple managed items
items, err = suite.lnk.List()
suite.Require().NoError(err)
suite.Len(items, 2)
// Check that both items are present
found := make(map[string]bool)
for _, item := range items {
if strings.Contains(item, ".bashrc") {
found[".bashrc"] = true
}
if strings.Contains(item, ".config") {
found[".config"] = true
}
}
suite.True(found[".bashrc"], "Should contain .bashrc")
suite.True(found[".config"], "Should contain .config")
// Remove one item and verify list is updated
err = suite.lnk.Remove(testFile)
suite.Require().NoError(err)
items, err = suite.lnk.List()
suite.Require().NoError(err)
suite.Len(items, 1)
suite.Contains(items[0], ".config")
}
func TestCoreSuite(t *testing.T) {
suite.Run(t, new(CoreTestSuite))
}