mirror of
https://github.com/yarlson/lnk.git
synced 2025-08-31 18:01:41 +02:00
feat(bootstrap): add automatic environment setup with bootstrap scripts
Implement bootstrap functionality for streamlined dotfiles setup: - Add 'bootstrap' command to run setup scripts manually - Auto-execute bootstrap on 'init' with remote (--no-bootstrap to skip) - Update README with bootstrap usage and examples - Extend tests to cover bootstrap scenarios
This commit is contained in:
82
README.md
82
README.md
@@ -2,12 +2,12 @@
|
||||
|
||||
**Git-native dotfiles management that doesn't suck.**
|
||||
|
||||
Move your dotfiles to `~/.config/lnk`, symlink them back, and use Git like normal. Supports both common configurations and host-specific setups.
|
||||
Move your dotfiles to `~/.config/lnk`, symlink them back, and use Git like normal. Supports both common configurations and host-specific setups. Automatically runs bootstrap scripts to set up your environment.
|
||||
|
||||
```bash
|
||||
lnk init
|
||||
lnk add ~/.vimrc ~/.bashrc # Common config
|
||||
lnk add --host work ~/.ssh/config # Host-specific config
|
||||
lnk init -r git@github.com:user/dotfiles.git # Clones & runs bootstrap automatically
|
||||
lnk add ~/.vimrc ~/.bashrc # Common config
|
||||
lnk add --host work ~/.ssh/config # Host-specific config
|
||||
lnk push "setup"
|
||||
```
|
||||
|
||||
@@ -43,8 +43,14 @@ git clone https://github.com/yarlson/lnk.git && cd lnk && go build . && sudo mv
|
||||
# Fresh start
|
||||
lnk init
|
||||
|
||||
# With existing repo
|
||||
# With existing repo (runs bootstrap automatically)
|
||||
lnk init -r git@github.com:user/dotfiles.git
|
||||
|
||||
# Skip automatic bootstrap
|
||||
lnk init -r git@github.com:user/dotfiles.git --no-bootstrap
|
||||
|
||||
# Run bootstrap script manually
|
||||
lnk bootstrap
|
||||
```
|
||||
|
||||
### Daily workflow
|
||||
@@ -85,6 +91,44 @@ After: ~/.ssh/config -> ~/.config/lnk/laptop.lnk/.ssh/config (symlink)
|
||||
|
||||
Your files live in `~/.config/lnk` (a Git repo). Common files go in the root, host-specific files go in `<host>.lnk/` subdirectories. Lnk creates symlinks back to original locations. Edit files normally, use Git normally.
|
||||
|
||||
## Bootstrap Support
|
||||
|
||||
Lnk automatically runs bootstrap scripts when cloning dotfiles repositories, making it easy to set up your development environment. Just add a `bootstrap.sh` file to your dotfiles repo.
|
||||
|
||||
### Examples
|
||||
|
||||
**Simple bootstrap script:**
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# bootstrap.sh
|
||||
echo "Setting up development environment..."
|
||||
|
||||
# Install Homebrew (macOS)
|
||||
if ! command -v brew &> /dev/null; then
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
fi
|
||||
|
||||
# Install packages
|
||||
brew install git vim tmux
|
||||
|
||||
echo "✅ Setup complete!"
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
# Automatic bootstrap on clone
|
||||
lnk init -r git@github.com:you/dotfiles.git
|
||||
# → Clones repo and runs bootstrap script automatically
|
||||
|
||||
# Skip bootstrap if needed
|
||||
lnk init -r git@github.com:you/dotfiles.git --no-bootstrap
|
||||
|
||||
# Run bootstrap manually later
|
||||
lnk bootstrap
|
||||
```
|
||||
|
||||
## Multihost Support
|
||||
|
||||
Lnk supports both **common configurations** (shared across all machines) and **host-specific configurations** (unique per machine).
|
||||
@@ -142,7 +186,9 @@ You could `git init ~/.config/lnk` and manually symlink everything. Lnk just aut
|
||||
### First time setup
|
||||
|
||||
```bash
|
||||
# Clone dotfiles and run bootstrap automatically
|
||||
lnk init -r git@github.com:you/dotfiles.git
|
||||
# → Downloads dependencies, installs packages, configures environment
|
||||
|
||||
# Add common config (shared across all machines)
|
||||
lnk add ~/.bashrc ~/.vimrc ~/.gitconfig
|
||||
@@ -156,13 +202,18 @@ lnk push "initial setup"
|
||||
### On a new machine
|
||||
|
||||
```bash
|
||||
# Bootstrap runs automatically
|
||||
lnk init -r git@github.com:you/dotfiles.git
|
||||
# → Sets up environment, installs dependencies
|
||||
|
||||
# Pull common config
|
||||
lnk pull
|
||||
|
||||
# Pull host-specific config (if it exists)
|
||||
lnk pull --host $(hostname)
|
||||
|
||||
# Or run bootstrap manually if needed
|
||||
lnk bootstrap
|
||||
```
|
||||
|
||||
### Daily edits
|
||||
@@ -184,7 +235,7 @@ lnk add --host laptop ~/.ssh/config
|
||||
lnk add ~/.vimrc # Common config
|
||||
lnk push "laptop ssh config"
|
||||
|
||||
# On your work machine
|
||||
# On your work machine
|
||||
lnk pull # Get common config
|
||||
lnk add --host work ~/.gitconfig
|
||||
lnk push "work git config"
|
||||
@@ -195,19 +246,21 @@ lnk pull # Get updates (work config won't affect laptop)
|
||||
|
||||
## Commands
|
||||
|
||||
- `lnk init [-r remote]` - Create repo
|
||||
- `lnk init [-r remote] [--no-bootstrap]` - Create repo (runs bootstrap automatically)
|
||||
- `lnk add [--host HOST] <files>` - Move files to repo, create symlinks
|
||||
- `lnk rm [--host HOST] <files>` - Move files back, remove symlinks
|
||||
- `lnk list [--host HOST] [--all]` - List files managed by lnk
|
||||
- `lnk status` - Git status + sync info
|
||||
- `lnk push [msg]` - Stage all, commit, push
|
||||
- `lnk pull [--host HOST]` - Pull + restore missing symlinks
|
||||
- `lnk bootstrap` - Run bootstrap script manually
|
||||
|
||||
### Command Options
|
||||
|
||||
- `--host HOST` - Manage files for specific host (default: common configuration)
|
||||
- `--all` - Show all configurations (common + all hosts) when listing
|
||||
- `-r, --remote URL` - Clone from remote URL when initializing
|
||||
- `--no-bootstrap` - Skip automatic execution of bootstrap script after cloning
|
||||
|
||||
## Technical bits
|
||||
|
||||
@@ -215,17 +268,18 @@ lnk pull # Get updates (work config won't affect laptop)
|
||||
- **Relative symlinks** (portable)
|
||||
- **XDG compliant** (`~/.config/lnk`)
|
||||
- **Multihost support** (common + host-specific configs)
|
||||
- **Bootstrap support** (automatic environment setup)
|
||||
- **Git-native** (standard Git repo, no special formats)
|
||||
|
||||
## Alternatives
|
||||
|
||||
| Tool | Complexity | Why choose it |
|
||||
| ------- | ---------- | -------------------------------------------- |
|
||||
| **lnk** | Minimal | Just works, no config, Git-native, multihost |
|
||||
| chezmoi | High | Templates, encryption, cross-platform |
|
||||
| yadm | Medium | Git power user, encryption |
|
||||
| dotbot | Low | YAML config, basic features |
|
||||
| stow | Low | Perl, symlink only |
|
||||
| Tool | Complexity | Why choose it |
|
||||
| ------- | ---------- | ------------------------------------------------------- |
|
||||
| **lnk** | Minimal | Just works, no config, Git-native, multihost, bootstrap |
|
||||
| chezmoi | High | Templates, encryption, cross-platform |
|
||||
| yadm | Medium | Git power user, encryption |
|
||||
| dotbot | Low | YAML config, basic features |
|
||||
| stow | Low | Perl, symlink only |
|
||||
|
||||
## Contributing
|
||||
|
||||
|
45
cmd/bootstrap.go
Normal file
45
cmd/bootstrap.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/yarlson/lnk/internal/core"
|
||||
)
|
||||
|
||||
func newBootstrapCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "bootstrap",
|
||||
Short: "🚀 Run the bootstrap script to set up your environment",
|
||||
Long: "Executes the bootstrap script from your dotfiles repository to install dependencies and configure your system.",
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
lnk := core.NewLnk()
|
||||
|
||||
scriptPath, err := lnk.FindBootstrapScript()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if scriptPath == "" {
|
||||
printf(cmd, "💡 \033[33mNo bootstrap script found\033[0m\n")
|
||||
printf(cmd, " 📝 Create a \033[1mbootstrap.sh\033[0m file in your dotfiles repository:\n")
|
||||
printf(cmd, " \033[90m#!/bin/bash\033[0m\n")
|
||||
printf(cmd, " \033[90mecho \"Setting up environment...\"\033[0m\n")
|
||||
printf(cmd, " \033[90m# Your setup commands here\033[0m\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
printf(cmd, "🚀 \033[1mRunning bootstrap script\033[0m\n")
|
||||
printf(cmd, " 📄 Script: \033[36m%s\033[0m\n", scriptPath)
|
||||
printf(cmd, "\n")
|
||||
|
||||
if err := lnk.RunBootstrapScript(scriptPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printf(cmd, "\n✅ \033[1;32mBootstrap completed successfully!\033[0m\n")
|
||||
printf(cmd, " 🎉 Your environment is ready to use\n")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
29
cmd/init.go
29
cmd/init.go
@@ -14,6 +14,7 @@ func newInitCmd() *cobra.Command {
|
||||
SilenceErrors: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
remote, _ := cmd.Flags().GetString("remote")
|
||||
noBootstrap, _ := cmd.Flags().GetBool("no-bootstrap")
|
||||
|
||||
lnk := core.NewLnk()
|
||||
if err := lnk.InitWithRemote(remote); err != nil {
|
||||
@@ -24,6 +25,33 @@ func newInitCmd() *cobra.Command {
|
||||
printf(cmd, "🎯 \033[1mInitialized lnk repository\033[0m\n")
|
||||
printf(cmd, " 📦 Cloned from: \033[36m%s\033[0m\n", remote)
|
||||
printf(cmd, " 📁 Location: \033[90m~/.config/lnk\033[0m\n")
|
||||
|
||||
// Try to run bootstrap script if not disabled
|
||||
if !noBootstrap {
|
||||
printf(cmd, "\n🔍 \033[1mLooking for bootstrap script...\033[0m\n")
|
||||
|
||||
scriptPath, err := lnk.FindBootstrapScript()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if scriptPath != "" {
|
||||
printf(cmd, " ✅ Found bootstrap script: \033[36m%s\033[0m\n", scriptPath)
|
||||
printf(cmd, "\n🚀 \033[1mRunning bootstrap script...\033[0m\n")
|
||||
printf(cmd, "\n")
|
||||
|
||||
if err := lnk.RunBootstrapScript(scriptPath); err != nil {
|
||||
printf(cmd, "\n⚠️ \033[33mBootstrap script failed, but repository was initialized successfully\033[0m\n")
|
||||
printf(cmd, " 💡 You can run it manually with: \033[1mlnk bootstrap\033[0m\n")
|
||||
printf(cmd, " 🔧 Error: %v\n", err)
|
||||
} else {
|
||||
printf(cmd, "\n✅ \033[1;32mBootstrap completed successfully!\033[0m\n")
|
||||
}
|
||||
} else {
|
||||
printf(cmd, " 💡 No bootstrap script found\n")
|
||||
}
|
||||
}
|
||||
|
||||
printf(cmd, "\n💡 \033[33mNext steps:\033[0m\n")
|
||||
printf(cmd, " • Run \033[1mlnk pull\033[0m to restore symlinks\n")
|
||||
printf(cmd, " • Use \033[1mlnk add <file>\033[0m to manage new files\n")
|
||||
@@ -40,5 +68,6 @@ func newInitCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
cmd.Flags().StringP("remote", "r", "", "Clone from remote URL instead of creating empty repository")
|
||||
cmd.Flags().Bool("no-bootstrap", false, "Skip automatic execution of bootstrap script after cloning")
|
||||
return cmd
|
||||
}
|
||||
|
@@ -24,12 +24,17 @@ Supports both common configurations and host-specific setups.
|
||||
|
||||
✨ Examples:
|
||||
lnk init # Fresh start
|
||||
lnk init -r <repo-url> # Clone existing dotfiles
|
||||
lnk init -r <repo-url> # Clone existing dotfiles (runs bootstrap automatically)
|
||||
lnk add ~/.vimrc ~/.bashrc # Start managing common files
|
||||
lnk add --host work ~/.ssh/config # Manage host-specific files
|
||||
lnk list --all # Show all configurations
|
||||
lnk pull --host work # Pull host-specific changes
|
||||
lnk push "setup complete" # Sync to remote
|
||||
lnk bootstrap # Run bootstrap script manually
|
||||
|
||||
🚀 Bootstrap Support:
|
||||
Automatically runs bootstrap.sh when cloning a repository.
|
||||
Use --no-bootstrap to disable.
|
||||
|
||||
🎯 Simple, fast, Git-native, and multi-host ready.`,
|
||||
SilenceUsage: true,
|
||||
@@ -45,6 +50,7 @@ Supports both common configurations and host-specific setups.
|
||||
rootCmd.AddCommand(newStatusCmd())
|
||||
rootCmd.AddCommand(newPushCmd())
|
||||
rootCmd.AddCommand(newPullCmd())
|
||||
rootCmd.AddCommand(newBootstrapCmd())
|
||||
|
||||
return rootCmd
|
||||
}
|
||||
|
171
cmd/root_test.go
171
cmd/root_test.go
@@ -746,6 +746,177 @@ func (suite *CLITestSuite) TestMultihostErrorHandling() {
|
||||
suite.Contains(output, "No files currently managed by lnk (host: nonexistent)")
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestBootstrapCommand() {
|
||||
// Initialize repository
|
||||
err := suite.runCommand("init")
|
||||
suite.Require().NoError(err)
|
||||
suite.stdout.Reset()
|
||||
|
||||
// Test bootstrap command with no script
|
||||
err = suite.runCommand("bootstrap")
|
||||
suite.NoError(err)
|
||||
output := suite.stdout.String()
|
||||
suite.Contains(output, "No bootstrap script found")
|
||||
suite.Contains(output, "bootstrap.sh")
|
||||
suite.stdout.Reset()
|
||||
|
||||
// Create a bootstrap script
|
||||
lnkDir := filepath.Join(suite.tempDir, ".config", "lnk")
|
||||
bootstrapScript := filepath.Join(lnkDir, "bootstrap.sh")
|
||||
scriptContent := `#!/bin/bash
|
||||
echo "Bootstrap script executed!"
|
||||
echo "Working directory: $(pwd)"
|
||||
touch bootstrap-ran.txt
|
||||
`
|
||||
err = os.WriteFile(bootstrapScript, []byte(scriptContent), 0755)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Test bootstrap command with script
|
||||
err = suite.runCommand("bootstrap")
|
||||
suite.NoError(err)
|
||||
output = suite.stdout.String()
|
||||
suite.Contains(output, "Running bootstrap script")
|
||||
suite.Contains(output, "bootstrap.sh")
|
||||
suite.Contains(output, "Bootstrap completed successfully")
|
||||
|
||||
// Verify script actually ran
|
||||
markerFile := filepath.Join(lnkDir, "bootstrap-ran.txt")
|
||||
suite.FileExists(markerFile)
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestInitWithBootstrap() {
|
||||
// Create a temporary remote repository with bootstrap script
|
||||
remoteDir := filepath.Join(suite.tempDir, "remote")
|
||||
err := os.MkdirAll(remoteDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Initialize git repo in remote
|
||||
cmd := exec.Command("git", "init", "--bare")
|
||||
cmd.Dir = remoteDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create a working repo to populate the remote
|
||||
workingDir := filepath.Join(suite.tempDir, "working")
|
||||
err = os.MkdirAll(workingDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
cmd = exec.Command("git", "clone", remoteDir, workingDir)
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add a bootstrap script to the working repo
|
||||
bootstrapScript := filepath.Join(workingDir, "bootstrap.sh")
|
||||
scriptContent := `#!/bin/bash
|
||||
echo "Remote bootstrap script executed!"
|
||||
touch remote-bootstrap-ran.txt
|
||||
`
|
||||
err = os.WriteFile(bootstrapScript, []byte(scriptContent), 0755)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add a dummy config file
|
||||
configFile := filepath.Join(workingDir, ".bashrc")
|
||||
err = os.WriteFile(configFile, []byte("echo 'Hello from remote!'"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add .lnk file to track the config
|
||||
lnkFile := filepath.Join(workingDir, ".lnk")
|
||||
err = os.WriteFile(lnkFile, []byte(".bashrc\n"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Commit and push to remote
|
||||
cmd = exec.Command("git", "add", ".")
|
||||
cmd.Dir = workingDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
cmd = exec.Command("git", "-c", "user.email=test@example.com", "-c", "user.name=Test User", "commit", "-m", "Add bootstrap and config")
|
||||
cmd.Dir = workingDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
cmd = exec.Command("git", "push", "origin", "master")
|
||||
cmd.Dir = workingDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Now test init with remote and automatic bootstrap
|
||||
err = suite.runCommand("init", "-r", remoteDir)
|
||||
suite.NoError(err)
|
||||
output := suite.stdout.String()
|
||||
suite.Contains(output, "Cloned from:")
|
||||
suite.Contains(output, "Looking for bootstrap script")
|
||||
suite.Contains(output, "Found bootstrap script:")
|
||||
suite.Contains(output, "bootstrap.sh")
|
||||
suite.Contains(output, "Running bootstrap script")
|
||||
suite.Contains(output, "Bootstrap completed successfully")
|
||||
|
||||
// Verify bootstrap actually ran
|
||||
lnkDir := filepath.Join(suite.tempDir, ".config", "lnk")
|
||||
markerFile := filepath.Join(lnkDir, "remote-bootstrap-ran.txt")
|
||||
suite.FileExists(markerFile)
|
||||
}
|
||||
|
||||
func (suite *CLITestSuite) TestInitWithBootstrapDisabled() {
|
||||
// Create a temporary remote repository with bootstrap script
|
||||
remoteDir := filepath.Join(suite.tempDir, "remote")
|
||||
err := os.MkdirAll(remoteDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Initialize git repo in remote
|
||||
cmd := exec.Command("git", "init", "--bare")
|
||||
cmd.Dir = remoteDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create a working repo to populate the remote
|
||||
workingDir := filepath.Join(suite.tempDir, "working")
|
||||
err = os.MkdirAll(workingDir, 0755)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
cmd = exec.Command("git", "clone", remoteDir, workingDir)
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Add a bootstrap script
|
||||
bootstrapScript := filepath.Join(workingDir, "bootstrap.sh")
|
||||
scriptContent := `#!/bin/bash
|
||||
echo "This should not run!"
|
||||
touch should-not-exist.txt
|
||||
`
|
||||
err = os.WriteFile(bootstrapScript, []byte(scriptContent), 0755)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Commit and push
|
||||
cmd = exec.Command("git", "add", ".")
|
||||
cmd.Dir = workingDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
cmd = exec.Command("git", "-c", "user.email=test@example.com", "-c", "user.name=Test User", "commit", "-m", "Add bootstrap")
|
||||
cmd.Dir = workingDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
cmd = exec.Command("git", "push", "origin", "master")
|
||||
cmd.Dir = workingDir
|
||||
err = cmd.Run()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Test init with --no-bootstrap flag
|
||||
err = suite.runCommand("init", "-r", remoteDir, "--no-bootstrap")
|
||||
suite.NoError(err)
|
||||
output := suite.stdout.String()
|
||||
suite.Contains(output, "Cloned from:")
|
||||
suite.NotContains(output, "Looking for bootstrap script")
|
||||
suite.NotContains(output, "Running bootstrap script")
|
||||
|
||||
// Verify bootstrap did NOT run
|
||||
lnkDir := filepath.Join(suite.tempDir, ".config", "lnk")
|
||||
markerFile := filepath.Join(lnkDir, "should-not-exist.txt")
|
||||
suite.NoFileExists(markerFile)
|
||||
}
|
||||
|
||||
func TestCLISuite(t *testing.T) {
|
||||
suite.Run(t, new(CLITestSuite))
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ package core
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -630,3 +631,52 @@ func (l *Lnk) writeManagedItems(items []string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindBootstrapScript searches for a bootstrap script in the repository
|
||||
func (l *Lnk) FindBootstrapScript() (string, error) {
|
||||
// Check if repository is initialized
|
||||
if !l.git.IsGitRepository() {
|
||||
return "", fmt.Errorf("❌ Lnk repository not initialized\n 💡 Run \033[1mlnk init\033[0m first")
|
||||
}
|
||||
|
||||
// Look for bootstrap.sh - simple, opinionated choice
|
||||
scriptPath := filepath.Join(l.repoPath, "bootstrap.sh")
|
||||
if _, err := os.Stat(scriptPath); err == nil {
|
||||
return "bootstrap.sh", nil
|
||||
}
|
||||
|
||||
return "", nil // No bootstrap script found
|
||||
}
|
||||
|
||||
// RunBootstrapScript executes the bootstrap script
|
||||
func (l *Lnk) RunBootstrapScript(scriptName string) error {
|
||||
scriptPath := filepath.Join(l.repoPath, scriptName)
|
||||
|
||||
// Verify the script exists
|
||||
if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("❌ Bootstrap script not found: \033[31m%s\033[0m", scriptName)
|
||||
}
|
||||
|
||||
// Make sure it's executable
|
||||
if err := os.Chmod(scriptPath, 0755); err != nil {
|
||||
return fmt.Errorf("❌ Failed to make bootstrap script executable: %w", err)
|
||||
}
|
||||
|
||||
// Run with bash (since we only support bootstrap.sh)
|
||||
cmd := exec.Command("bash", scriptPath)
|
||||
|
||||
// Set working directory to the repository
|
||||
cmd.Dir = l.repoPath
|
||||
|
||||
// Connect to stdout/stderr for user to see output
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
|
||||
// Run the script
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("❌ Bootstrap script failed with error: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -749,6 +750,76 @@ func (suite *CoreTestSuite) TestMultihostIsolation() {
|
||||
suite.Equal(hostContent, string(symlinkContent))
|
||||
}
|
||||
|
||||
// Test bootstrap script detection
|
||||
func (suite *CoreTestSuite) TestFindBootstrapScript() {
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Test with no bootstrap script
|
||||
scriptPath, err := suite.lnk.FindBootstrapScript()
|
||||
suite.NoError(err)
|
||||
suite.Empty(scriptPath)
|
||||
|
||||
// Test with bootstrap.sh
|
||||
bootstrapScript := filepath.Join(suite.tempDir, "lnk", "bootstrap.sh")
|
||||
err = os.WriteFile(bootstrapScript, []byte("#!/bin/bash\necho 'test'"), 0644)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
scriptPath, err = suite.lnk.FindBootstrapScript()
|
||||
suite.NoError(err)
|
||||
suite.Equal("bootstrap.sh", scriptPath)
|
||||
}
|
||||
|
||||
// Test bootstrap script execution
|
||||
func (suite *CoreTestSuite) TestRunBootstrapScript() {
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create a test script that creates a marker file
|
||||
bootstrapScript := filepath.Join(suite.tempDir, "lnk", "test.sh")
|
||||
markerFile := filepath.Join(suite.tempDir, "lnk", "bootstrap-executed.txt")
|
||||
scriptContent := fmt.Sprintf("#!/bin/bash\ntouch %s\necho 'Bootstrap executed'", markerFile)
|
||||
|
||||
err = os.WriteFile(bootstrapScript, []byte(scriptContent), 0755)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Run the bootstrap script
|
||||
err = suite.lnk.RunBootstrapScript("test.sh")
|
||||
suite.NoError(err)
|
||||
|
||||
// Verify the marker file was created
|
||||
suite.FileExists(markerFile)
|
||||
}
|
||||
|
||||
// Test bootstrap script execution with error
|
||||
func (suite *CoreTestSuite) TestRunBootstrapScriptWithError() {
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Create a script that will fail
|
||||
bootstrapScript := filepath.Join(suite.tempDir, "lnk", "failing.sh")
|
||||
scriptContent := "#!/bin/bash\nexit 1"
|
||||
|
||||
err = os.WriteFile(bootstrapScript, []byte(scriptContent), 0755)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Run the bootstrap script - should fail
|
||||
err = suite.lnk.RunBootstrapScript("failing.sh")
|
||||
suite.Error(err)
|
||||
suite.Contains(err.Error(), "Bootstrap script failed")
|
||||
}
|
||||
|
||||
// Test running bootstrap on non-existent script
|
||||
func (suite *CoreTestSuite) TestRunBootstrapScriptNotFound() {
|
||||
err := suite.lnk.Init()
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Try to run non-existent script
|
||||
err = suite.lnk.RunBootstrapScript("nonexistent.sh")
|
||||
suite.Error(err)
|
||||
suite.Contains(err.Error(), "Bootstrap script not found")
|
||||
}
|
||||
|
||||
func TestCoreSuite(t *testing.T) {
|
||||
suite.Run(t, new(CoreTestSuite))
|
||||
}
|
||||
|
Reference in New Issue
Block a user