mirror of
https://github.com/yarlson/lnk.git
synced 2025-09-01 18:02:34 +02:00
feat: enhance CLI output with colorful and informative messages
This commit is contained in:
13
cmd/add.go
13
cmd/add.go
@@ -9,10 +9,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var addCmd = &cobra.Command{
|
var addCmd = &cobra.Command{
|
||||||
Use: "add <file>",
|
Use: "add <file>",
|
||||||
Short: "Add a file to lnk management",
|
Short: "✨ Add a file to lnk management",
|
||||||
Long: "Moves a file to the lnk repository and creates a symlink in its place.",
|
Long: "Moves a file to the lnk repository and creates a symlink in its place.",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
|
SilenceUsage: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
filePath := args[0]
|
filePath := args[0]
|
||||||
|
|
||||||
@@ -22,7 +23,9 @@ var addCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
basename := filepath.Base(filePath)
|
basename := filepath.Base(filePath)
|
||||||
fmt.Printf("Added %s to lnk\n", basename)
|
fmt.Printf("✨ \033[1mAdded %s to lnk\033[0m\n", basename)
|
||||||
|
fmt.Printf(" 🔗 \033[90m%s\033[0m → \033[36m~/.config/lnk/%s\033[0m\n", filePath, basename)
|
||||||
|
fmt.Printf(" 📝 Use \033[1mlnk push\033[0m to sync to remote\n")
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
20
cmd/init.go
20
cmd/init.go
@@ -8,9 +8,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var initCmd = &cobra.Command{
|
var initCmd = &cobra.Command{
|
||||||
Use: "init",
|
Use: "init",
|
||||||
Short: "Initialize a new lnk repository",
|
Short: "🎯 Initialize a new lnk repository",
|
||||||
Long: "Creates the lnk directory and initializes a Git repository for managing dotfiles.",
|
Long: "Creates the lnk directory and initializes a Git repository for managing dotfiles.",
|
||||||
|
SilenceUsage: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
remote, _ := cmd.Flags().GetString("remote")
|
remote, _ := cmd.Flags().GetString("remote")
|
||||||
|
|
||||||
@@ -20,9 +21,18 @@ var initCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if remote != "" {
|
if remote != "" {
|
||||||
fmt.Printf("Initialized lnk repository by cloning: %s\n", remote)
|
fmt.Printf("🎯 \033[1mInitialized lnk repository\033[0m\n")
|
||||||
|
fmt.Printf(" 📦 Cloned from: \033[36m%s\033[0m\n", remote)
|
||||||
|
fmt.Printf(" 📁 Location: \033[90m~/.config/lnk\033[0m\n")
|
||||||
|
fmt.Printf("\n💡 \033[33mNext steps:\033[0m\n")
|
||||||
|
fmt.Printf(" • Run \033[1mlnk pull\033[0m to restore symlinks\n")
|
||||||
|
fmt.Printf(" • Use \033[1mlnk add <file>\033[0m to manage new files\n")
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Initialized lnk repository")
|
fmt.Printf("🎯 \033[1mInitialized empty lnk repository\033[0m\n")
|
||||||
|
fmt.Printf(" 📁 Location: \033[90m~/.config/lnk\033[0m\n")
|
||||||
|
fmt.Printf("\n💡 \033[33mNext steps:\033[0m\n")
|
||||||
|
fmt.Printf(" • Run \033[1mlnk add <file>\033[0m to start managing dotfiles\n")
|
||||||
|
fmt.Printf(" • Add a remote with: \033[1mgit remote add origin <url>\033[0m\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
23
cmd/pull.go
23
cmd/pull.go
@@ -8,9 +8,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var pullCmd = &cobra.Command{
|
var pullCmd = &cobra.Command{
|
||||||
Use: "pull",
|
Use: "pull",
|
||||||
Short: "Pull changes from remote and restore symlinks",
|
Short: "⬇️ Pull changes from remote and restore symlinks",
|
||||||
Long: "Fetches changes from remote repository and automatically restores symlinks for all managed files.",
|
Long: "Fetches changes from remote repository and automatically restores symlinks for all managed files.",
|
||||||
|
SilenceUsage: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
lnk := core.NewLnk()
|
lnk := core.NewLnk()
|
||||||
restored, err := lnk.Pull()
|
restored, err := lnk.Pull()
|
||||||
@@ -19,12 +20,20 @@ var pullCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(restored) > 0 {
|
if len(restored) > 0 {
|
||||||
fmt.Printf("Successfully pulled changes and restored %d symlink(s):\n", len(restored))
|
fmt.Printf("⬇️ \033[1;32mSuccessfully pulled changes\033[0m\n")
|
||||||
for _, file := range restored {
|
fmt.Printf(" 🔗 Restored \033[1m%d symlink", len(restored))
|
||||||
fmt.Printf(" - %s\n", file)
|
if len(restored) > 1 {
|
||||||
|
fmt.Printf("s")
|
||||||
}
|
}
|
||||||
|
fmt.Printf("\033[0m:\n")
|
||||||
|
for _, file := range restored {
|
||||||
|
fmt.Printf(" ✨ \033[36m%s\033[0m\n", file)
|
||||||
|
}
|
||||||
|
fmt.Printf("\n 🎉 Your dotfiles are synced and ready!\n")
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Successfully pulled changes (no symlinks needed restoration)")
|
fmt.Printf("⬇️ \033[1;32mSuccessfully pulled changes\033[0m\n")
|
||||||
|
fmt.Printf(" ✅ All symlinks already in place\n")
|
||||||
|
fmt.Printf(" 🎉 Everything is up to date!\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
14
cmd/push.go
14
cmd/push.go
@@ -8,10 +8,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var pushCmd = &cobra.Command{
|
var pushCmd = &cobra.Command{
|
||||||
Use: "push [message]",
|
Use: "push [message]",
|
||||||
Short: "Push local changes to remote repository",
|
Short: "🚀 Push local changes to remote repository",
|
||||||
Long: "Stages all changes, creates a sync commit with the provided message, and pushes to remote.",
|
Long: "Stages all changes, creates a sync commit with the provided message, and pushes to remote.",
|
||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
SilenceUsage: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
message := "lnk: sync configuration files"
|
message := "lnk: sync configuration files"
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
@@ -23,7 +24,10 @@ var pushCmd = &cobra.Command{
|
|||||||
return fmt.Errorf("failed to push changes: %w", err)
|
return fmt.Errorf("failed to push changes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Successfully pushed changes to remote")
|
fmt.Printf("🚀 \033[1;32mSuccessfully pushed changes\033[0m\n")
|
||||||
|
fmt.Printf(" 💾 Commit: \033[90m%s\033[0m\n", message)
|
||||||
|
fmt.Printf(" 📡 Synced to remote\n")
|
||||||
|
fmt.Printf(" ✨ Your dotfiles are up to date!\n")
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
13
cmd/rm.go
13
cmd/rm.go
@@ -9,10 +9,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var rmCmd = &cobra.Command{
|
var rmCmd = &cobra.Command{
|
||||||
Use: "rm <file>",
|
Use: "rm <file>",
|
||||||
Short: "Remove a file from lnk management",
|
Short: "🗑️ Remove a file from lnk management",
|
||||||
Long: "Removes a symlink and restores the original file from the lnk repository.",
|
Long: "Removes a symlink and restores the original file from the lnk repository.",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
|
SilenceUsage: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
filePath := args[0]
|
filePath := args[0]
|
||||||
|
|
||||||
@@ -22,7 +23,9 @@ var rmCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
basename := filepath.Base(filePath)
|
basename := filepath.Base(filePath)
|
||||||
fmt.Printf("Removed %s from lnk\n", basename)
|
fmt.Printf("🗑️ \033[1mRemoved %s from lnk\033[0m\n", basename)
|
||||||
|
fmt.Printf(" ↩️ \033[90m~/.config/lnk/%s\033[0m → \033[36m%s\033[0m\n", basename, filePath)
|
||||||
|
fmt.Printf(" 📄 Original file restored\n")
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
17
cmd/root.go
17
cmd/root.go
@@ -14,8 +14,21 @@ var (
|
|||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "lnk",
|
Use: "lnk",
|
||||||
Short: "Dotfiles, linked. No fluff.",
|
Short: "🔗 Dotfiles, linked. No fluff.",
|
||||||
Long: "Lnk is a minimalist CLI tool for managing dotfiles using symlinks and Git.",
|
Long: `🔗 Lnk - Git-native dotfiles management that doesn't suck.
|
||||||
|
|
||||||
|
Move your dotfiles to ~/.config/lnk, symlink them back, and use Git like normal.
|
||||||
|
That's it.
|
||||||
|
|
||||||
|
✨ Examples:
|
||||||
|
lnk init # Fresh start
|
||||||
|
lnk init -r <repo-url> # Clone existing dotfiles
|
||||||
|
lnk add ~/.vimrc ~/.bashrc # Start managing files
|
||||||
|
lnk push "setup complete" # Sync to remote
|
||||||
|
lnk pull # Get latest changes
|
||||||
|
|
||||||
|
🎯 Simple, fast, and Git-native.`,
|
||||||
|
SilenceUsage: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetVersion sets the version information for the CLI
|
// SetVersion sets the version information for the CLI
|
||||||
|
@@ -8,9 +8,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var statusCmd = &cobra.Command{
|
var statusCmd = &cobra.Command{
|
||||||
Use: "status",
|
Use: "status",
|
||||||
Short: "Show repository sync status",
|
Short: "📊 Show repository sync status",
|
||||||
Long: "Display how many commits ahead/behind the local repository is relative to the remote.",
|
Long: "Display how many commits ahead/behind the local repository is relative to the remote.",
|
||||||
|
SilenceUsage: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
lnk := core.NewLnk()
|
lnk := core.NewLnk()
|
||||||
status, err := lnk.Status()
|
status, err := lnk.Status()
|
||||||
@@ -19,13 +20,32 @@ var statusCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if status.Ahead == 0 && status.Behind == 0 {
|
if status.Ahead == 0 && status.Behind == 0 {
|
||||||
fmt.Println("Repository is up to date with remote")
|
fmt.Printf("✅ \033[1;32mRepository is up to date\033[0m\n")
|
||||||
|
fmt.Printf(" 📡 Synced with \033[36m%s\033[0m\n", status.Remote)
|
||||||
} else {
|
} else {
|
||||||
|
fmt.Printf("📊 \033[1mRepository Status\033[0m\n")
|
||||||
|
fmt.Printf(" 📡 Remote: \033[36m%s\033[0m\n", status.Remote)
|
||||||
|
fmt.Printf("\n")
|
||||||
|
|
||||||
if status.Ahead > 0 {
|
if status.Ahead > 0 {
|
||||||
fmt.Printf("Your branch is ahead of '%s' by %d commit(s)\n", status.Remote, status.Ahead)
|
commitText := "commit"
|
||||||
|
if status.Ahead > 1 {
|
||||||
|
commitText = "commits"
|
||||||
|
}
|
||||||
|
fmt.Printf(" ⬆️ \033[1;33m%d %s ahead\033[0m - ready to push\n", status.Ahead, commitText)
|
||||||
}
|
}
|
||||||
if status.Behind > 0 {
|
if status.Behind > 0 {
|
||||||
fmt.Printf("Your branch is behind '%s' by %d commit(s)\n", status.Remote, status.Behind)
|
commitText := "commit"
|
||||||
|
if status.Behind > 1 {
|
||||||
|
commitText = "commits"
|
||||||
|
}
|
||||||
|
fmt.Printf(" ⬇️ \033[1;31m%d %s behind\033[0m - run \033[1mlnk pull\033[0m\n", status.Behind, commitText)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.Ahead > 0 && status.Behind == 0 {
|
||||||
|
fmt.Printf("\n💡 Run \033[1mlnk push\033[0m to sync your changes")
|
||||||
|
} else if status.Behind > 0 {
|
||||||
|
fmt.Printf("\n💡 Run \033[1mlnk pull\033[0m to get latest changes")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -68,7 +68,7 @@ func (l *Lnk) InitWithRemote(remoteURL string) error {
|
|||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
// It's not a lnk repository, error to prevent data loss
|
// It's not a lnk repository, error to prevent data loss
|
||||||
return fmt.Errorf("directory %s appears to contain an existing Git repository that is not managed by lnk. Please backup or move the existing repository before initializing lnk", l.repoPath)
|
return fmt.Errorf("❌ Directory \033[31m%s\033[0m contains an existing Git repository\n 💡 Please backup or move the existing repository before initializing lnk", l.repoPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +282,7 @@ type StatusInfo struct {
|
|||||||
func (l *Lnk) Status() (*StatusInfo, error) {
|
func (l *Lnk) Status() (*StatusInfo, error) {
|
||||||
// Check if repository is initialized
|
// Check if repository is initialized
|
||||||
if !l.git.IsGitRepository() {
|
if !l.git.IsGitRepository() {
|
||||||
return nil, fmt.Errorf("lnk repository not initialized - run 'lnk init' first")
|
return nil, fmt.Errorf("❌ Lnk repository not initialized\n 💡 Run \033[1mlnk init\033[0m first")
|
||||||
}
|
}
|
||||||
|
|
||||||
gitStatus, err := l.git.GetStatus()
|
gitStatus, err := l.git.GetStatus()
|
||||||
@@ -301,7 +301,7 @@ func (l *Lnk) Status() (*StatusInfo, error) {
|
|||||||
func (l *Lnk) Push(message string) error {
|
func (l *Lnk) Push(message string) error {
|
||||||
// Check if repository is initialized
|
// Check if repository is initialized
|
||||||
if !l.git.IsGitRepository() {
|
if !l.git.IsGitRepository() {
|
||||||
return fmt.Errorf("lnk repository not initialized - run 'lnk init' first")
|
return fmt.Errorf("❌ Lnk repository not initialized\n 💡 Run \033[1mlnk init\033[0m first")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there are any changes
|
// Check if there are any changes
|
||||||
@@ -335,7 +335,7 @@ func (l *Lnk) Push(message string) error {
|
|||||||
func (l *Lnk) Pull() ([]string, error) {
|
func (l *Lnk) Pull() ([]string, error) {
|
||||||
// Check if repository is initialized
|
// Check if repository is initialized
|
||||||
if !l.git.IsGitRepository() {
|
if !l.git.IsGitRepository() {
|
||||||
return nil, fmt.Errorf("lnk repository not initialized - run 'lnk init' first")
|
return nil, fmt.Errorf("❌ Lnk repository not initialized\n 💡 Run \033[1mlnk init\033[0m first")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pull changes from remote (this will be a no-op in tests since we don't have real remotes)
|
// Pull changes from remote (this will be a no-op in tests since we don't have real remotes)
|
||||||
|
@@ -21,14 +21,14 @@ func (fs *FileSystem) ValidateFileForAdd(filePath string) error {
|
|||||||
info, err := os.Stat(filePath)
|
info, err := os.Stat(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return fmt.Errorf("file does not exist: %s", filePath)
|
return fmt.Errorf("❌ File does not exist: \033[31m%s\033[0m", filePath)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("failed to stat file: %w", err)
|
return fmt.Errorf("❌ Failed to check file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow both regular files and directories
|
// Allow both regular files and directories
|
||||||
if !info.Mode().IsRegular() && !info.IsDir() {
|
if !info.Mode().IsRegular() && !info.IsDir() {
|
||||||
return fmt.Errorf("only regular files and directories are supported: %s", filePath)
|
return fmt.Errorf("❌ Only regular files and directories are supported: \033[31m%s\033[0m", filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -40,14 +40,14 @@ func (fs *FileSystem) ValidateSymlinkForRemove(filePath, repoPath string) error
|
|||||||
info, err := os.Lstat(filePath) // Use Lstat to not follow symlinks
|
info, err := os.Lstat(filePath) // Use Lstat to not follow symlinks
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return fmt.Errorf("file does not exist: %s", filePath)
|
return fmt.Errorf("❌ File does not exist: \033[31m%s\033[0m", filePath)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("failed to stat file: %w", err)
|
return fmt.Errorf("❌ Failed to check file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a symlink
|
// Check if it's a symlink
|
||||||
if info.Mode()&os.ModeSymlink == 0 {
|
if info.Mode()&os.ModeSymlink == 0 {
|
||||||
return fmt.Errorf("file is not managed by lnk: %s", filePath)
|
return fmt.Errorf("❌ File is not managed by lnk: \033[31m%s\033[0m\n 💡 Use \033[1mlnk add\033[0m to manage this file first", filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if symlink points to the repository
|
// Check if symlink points to the repository
|
||||||
@@ -67,7 +67,7 @@ func (fs *FileSystem) ValidateSymlinkForRemove(filePath, repoPath string) error
|
|||||||
|
|
||||||
// Check if target is inside the repository
|
// Check if target is inside the repository
|
||||||
if !strings.HasPrefix(target, repoPath+string(filepath.Separator)) && target != repoPath {
|
if !strings.HasPrefix(target, repoPath+string(filepath.Separator)) && target != repoPath {
|
||||||
return fmt.Errorf("file is not managed by lnk: %s", filePath)
|
return fmt.Errorf("❌ File is not managed by lnk: \033[31m%s\033[0m", filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@@ -162,7 +162,7 @@ func (suite *LnkIntegrationTestSuite) TestAddNonexistentFile() {
|
|||||||
|
|
||||||
err = suite.lnk.Add("/nonexistent/file")
|
err = suite.lnk.Add("/nonexistent/file")
|
||||||
suite.Error(err)
|
suite.Error(err)
|
||||||
suite.Contains(err.Error(), "file does not exist")
|
suite.Contains(err.Error(), "File does not exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *LnkIntegrationTestSuite) TestAddDirectory() {
|
func (suite *LnkIntegrationTestSuite) TestAddDirectory() {
|
||||||
@@ -406,7 +406,7 @@ func (suite *LnkIntegrationTestSuite) TestRemoveNonSymlink() {
|
|||||||
|
|
||||||
err = suite.lnk.Remove(testFile)
|
err = suite.lnk.Remove(testFile)
|
||||||
suite.Error(err)
|
suite.Error(err)
|
||||||
suite.Contains(err.Error(), "file is not managed by lnk")
|
suite.Contains(err.Error(), "File is not managed by lnk")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *LnkIntegrationTestSuite) TestXDGConfigHomeFallback() {
|
func (suite *LnkIntegrationTestSuite) TestXDGConfigHomeFallback() {
|
||||||
@@ -550,7 +550,7 @@ func (suite *LnkIntegrationTestSuite) TestInitWithNonLnkRepo() {
|
|||||||
// Now try to init lnk - should error to protect existing repo
|
// Now try to init lnk - should error to protect existing repo
|
||||||
err = suite.lnk.Init()
|
err = suite.lnk.Init()
|
||||||
suite.Error(err)
|
suite.Error(err)
|
||||||
suite.Contains(err.Error(), "appears to contain an existing Git repository")
|
suite.Contains(err.Error(), "contains an existing Git repository")
|
||||||
|
|
||||||
// Verify the original file is still there
|
// Verify the original file is still there
|
||||||
suite.FileExists(testFile)
|
suite.FileExists(testFile)
|
||||||
|
Reference in New Issue
Block a user