feat(rc): implement rc command with .homesick.d script execution
- App.Rc runs all executable files in <castle>/.homesick.d in sorted (lexicographic) order with the castle root as cwd - Non-executable files are skipped - stdout/stderr from scripts forward to App writers - If .homesickrc exists and parity.rb does not yet exist in .homesick.d, a Ruby wrapper (parity.rb) is generated before execution - Existing parity.rb is never overwritten - Wire rcCmd in CLI with optional CASTLE argument (defaults to dotfiles)
This commit is contained in:
@@ -531,6 +531,82 @@ func gitOutput(dir string, args ...string) (string, error) {
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
// Rc runs the rc hooks for the given castle. It looks for executable files
|
||||
// inside <castle>/.homesick.d and runs them in sorted (lexicographic) order
|
||||
// with the castle root as the working directory, forwarding stdout and stderr
|
||||
// to the App writers.
|
||||
//
|
||||
// If a .homesickrc file exists in the castle root and no parity.rb wrapper
|
||||
// already exists in .homesick.d, a Ruby wrapper script named parity.rb is
|
||||
// written there before execution so that it sorts first.
|
||||
func (a *App) Rc(castle string) error {
|
||||
castleRoot := filepath.Join(a.ReposDir, castle)
|
||||
if _, err := os.Stat(castleRoot); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("castle %q not found", castle)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
homesickD := filepath.Join(castleRoot, ".homesick.d")
|
||||
homesickRc := filepath.Join(castleRoot, ".homesickrc")
|
||||
|
||||
// If .homesickrc exists, ensure .homesick.d/parity.rb wrapper is created
|
||||
// (but do not overwrite an existing parity.rb).
|
||||
if _, err := os.Stat(homesickRc); err == nil {
|
||||
wrapperPath := filepath.Join(homesickD, "parity.rb")
|
||||
if _, err := os.Stat(wrapperPath); errors.Is(err, os.ErrNotExist) {
|
||||
if mkErr := os.MkdirAll(homesickD, 0o755); mkErr != nil {
|
||||
return fmt.Errorf("create .homesick.d: %w", mkErr)
|
||||
}
|
||||
wrapperContent := "#!/usr/bin/env ruby\n" +
|
||||
"# parity.rb — generated wrapper for legacy .homesickrc\n" +
|
||||
"# Evaluates .homesickrc in the context of the castle root.\n" +
|
||||
"rc_file = File.join(__dir__, '..', '.homesickrc')\n" +
|
||||
"eval(File.read(rc_file), binding, rc_file) if File.exist?(rc_file)\n"
|
||||
if writeErr := os.WriteFile(wrapperPath, []byte(wrapperContent), 0o755); writeErr != nil {
|
||||
return fmt.Errorf("write parity.rb: %w", writeErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(homesickD); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(homesickD)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ReadDir returns entries in sorted order already.
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
info, infoErr := entry.Info()
|
||||
if infoErr != nil {
|
||||
return infoErr
|
||||
}
|
||||
if info.Mode()&0o111 == 0 {
|
||||
// Not executable — skip.
|
||||
continue
|
||||
}
|
||||
scriptPath := filepath.Join(homesickD, entry.Name())
|
||||
cmd := exec.Command(scriptPath)
|
||||
cmd.Dir = castleRoot
|
||||
cmd.Stdout = a.Stdout
|
||||
cmd.Stderr = a.Stderr
|
||||
if runErr := cmd.Run(); runErr != nil {
|
||||
return fmt.Errorf("rc script %q failed: %w", entry.Name(), runErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deriveDestination(uri string) string {
|
||||
candidate := strings.TrimSpace(uri)
|
||||
candidate = strings.TrimPrefix(candidate, "https://github.com/")
|
||||
|
||||
Reference in New Issue
Block a user