Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,26 @@ case $1 in
esac
```

### Pre-Run Hooks

Execute custom scripts before your main scripts run. Perfect for:
- Environment validation and setup
- Dependency checking
- Authentication checks
- Audit logging

Create a `.hooks.d/` directory in your scripts root and add numbered hooks:

```bash
# Executable hook - runs as separate process
.hooks.d/00-check-deps

# Sourced hook - runs in same shell, can modify environment
.hooks.d/05-set-env.source
```

See [docs/hooks.md](./docs/hooks.md) for complete guide with examples.

### Flexible Root Detection

tome-cli determines the scripts root directory from multiple sources (in order of precedence):
Expand All @@ -200,10 +220,10 @@ This flexibility allows team members to customize locations without changing the
- ✅ Environment variable injection
- ✅ Structured logging with levels
- ✅ Generated documentation
- ✅ Pre-run hooks (.hooks.d folder execution)

### Planned
- ⏳ ActiveHelp integration for contextual assistance
- ⏳ Pre/post hooks (hooks.d folder execution)
- ⏳ Enhanced directory help (show all subcommands in tree)
- ⏳ Improved completion output filtering

Expand Down Expand Up @@ -318,6 +338,7 @@ Make sure:
- [Your First Script](#your-first-script) - Create your first script
- [Writing Scripts Guide](./docs/writing-scripts.md) - Comprehensive guide to writing scripts
- [Completion Guide](./docs/completion-guide.md) - Implement custom tab completions
- [Pre-Run Hooks Guide](./docs/hooks.md) - Add validation and setup hooks
- [Migration Guide](./docs/migration.md) - Migrate from original tome/sub

### Core Documentation
Expand Down
64 changes: 62 additions & 2 deletions cmd/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package cmd
import (
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
Expand Down Expand Up @@ -80,11 +81,67 @@ func ExecRunE(cmd *cobra.Command, args []string) error {
envs = append(envs, fmt.Sprintf("%s_ROOT=%s", executableAsEnvPrefix, absRootDir))
envs = append(envs, fmt.Sprintf("%s_EXECUTABLE=%s", executableAsEnvPrefix, config.ExecutableName()))

args = append([]string{maybeFile}, maybeArgs...)
execOrLog(maybeFile, args, envs)
// Check for hooks and generate wrapper if needed
var execTarget string
var execArgs []string

if !skipHooks {
hookRunner := NewHookRunner(config)
hooks, err := hookRunner.DiscoverHooks()
if err != nil {
fmt.Printf("Error discovering hooks: %v\n", err)
os.Exit(1)
}

if len(hooks) > 0 {
// Generate wrapper script content
wrapperContent, err := hookRunner.GenerateWrapperScriptContent(hooks, executable, maybeArgs)
if err != nil {
fmt.Printf("Error generating wrapper script: %v\n", err)
os.Exit(1)
}

// Execute shell with inline script instead of script directly
shellPath, err := findShell()
if err != nil {
fmt.Printf("Error finding shell: %v\n", err)
os.Exit(1)
}
execTarget = shellPath
// Use basename of shell path for argv[0]
shellName := filepath.Base(shellPath)
execArgs = []string{shellName, "-c", wrapperContent}
} else {
// No hooks, execute script directly
execTarget = executable
execArgs = append([]string{executable}, maybeArgs...)
}
} else {
// Skip hooks, execute script directly
execTarget = executable
execArgs = append([]string{executable}, maybeArgs...)
}

execOrLog(execTarget, execArgs, envs)
return nil
}

// findShell locates a POSIX shell, preferring bash but falling back to sh if unavailable
func findShell() (string, error) {
// Try bash first
if bashPath, err := exec.LookPath("bash"); err == nil {
return bashPath, nil
}

// Fall back to sh (POSIX standard)
if shPath, err := exec.LookPath("sh"); err == nil {
log.Debugw("bash not found, using sh as fallback", "path", shPath)
return shPath, nil
}

return "", fmt.Errorf("neither bash nor sh found")
}

func execOrLog(arv0 string, argv []string, env []string) {
if dryRun {
fmt.Printf("dry run:\nbinary: %s\nargs: %+v\nenv (injected):\n%+v\n", arv0, strings.Join(argv, " "), strings.Join(env, "\n"))
Expand Down Expand Up @@ -128,9 +185,12 @@ var execCmd = &cobra.Command{
}

var dryRun bool
var skipHooks bool

func init() {
execCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Dry run the exec command")
execCmd.Flags().BoolVar(&skipHooks, "skip-hooks", false, "Skip pre-execution hooks")
viper.BindPFlag("dry-run", execCmd.Flags().Lookup("dry-run"))
viper.BindPFlag("skip-hooks", execCmd.Flags().Lookup("skip-hooks"))
rootCmd.AddCommand(execCmd)
}
Loading