From 4a275ce4cae8d6e08e263a6ef92a0d4f796a06ec Mon Sep 17 00:00:00 2001 From: Yar Kravtsov Date: Mon, 26 May 2025 05:57:45 +0300 Subject: [PATCH] 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 --- README.md | 5 +++ cmd/list.go | 43 ++++++++++++++++++++++++ cmd/root.go | 1 + cmd/root_test.go | 60 +++++++++++++++++++++++++++++++++ internal/core/lnk.go | 16 +++++++++ internal/core/lnk_test.go | 70 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 195 insertions(+) create mode 100644 cmd/list.go diff --git a/README.md b/README.md index 5ffe300..547b6e1 100644 --- a/README.md +++ b/README.md @@ -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 ` - Move files to repo, create symlinks - `lnk rm ` - 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 diff --git a/cmd/list.go b/cmd/list.go new file mode 100644 index 0000000..2c60857 --- /dev/null +++ b/cmd/list.go @@ -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 \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 + }, + } +} diff --git a/cmd/root.go b/cmd/root.go index 0802e59..a8ac2b5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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()) diff --git a/cmd/root_test.go b/cmd/root_test.go index 1eeae6e..caf8568 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -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 ") + 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 { diff --git a/internal/core/lnk.go b/internal/core/lnk.go index be4f28c..7de632d 100644 --- a/internal/core/lnk.go +++ b/internal/core/lnk.go @@ -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 diff --git a/internal/core/lnk_test.go b/internal/core/lnk_test.go index cde65fc..a766c5d 100644 --- a/internal/core/lnk_test.go +++ b/internal/core/lnk_test.go @@ -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)) }