Skip to content
Merged
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,18 @@ go install github.com/nnutter/timber@latest

Run `timber config` to show settings and create the config file if absent.
The file is `$XDG_CONFIG_HOME/timber/config.toml` (fallback: `~/.config/timber/config.toml`).
Automatic mise trust is disabled by default.
On the first eligible worktree creation in an interactive terminal, Timber offers to enable it.
The prompt defaults to No and saves your answer, so Timber does not ask again.
After No, the prompt shows `timber config --auto-mise-trust` so you can enable it later.
An explicit `timber config --no-auto-mise-trust` also suppresses the prompt.
Automatic mise trust and automatic hk hook installation are disabled by default.
On the first eligible worktree creation in an interactive terminal, Timber offers to enable each feature.
The prompts default to No and save your answers, so Timber does not ask again.
After No, a prompt shows the matching `timber config` flag so you can enable the feature later.
An explicit `--no-auto-mise-trust` or `--no-auto-hk-install` also suppresses its prompt.
Non-interactive creation does not prompt or record a choice.

```bash
timber config --auto-mise-trust
timber config --no-auto-mise-trust
timber config --auto-hk-install
timber config --no-auto-hk-install
```

Enabling automatic trust permits mise configuration in eligible worktrees to execute code.
Expand All @@ -101,11 +103,18 @@ Remove this cache after switching GitHub accounts or changing organization membe
If Timber cannot refresh an invalid cache, it warns and skips trust without failing worktree creation.
A failed `mise trust` also produces a warning and keeps the worktree.

Enabling automatic hk hook installation runs `hk install` in eligible worktrees.
Installed hooks run repository-defined commands on Git events.
Eligibility requires a root-level, regular `hk.pkl` and a GitHub upstream owned by you or one of your organizations.
Timber adds `--mise` when the worktree also has a root-level, regular `mise.toml`, so hooks can use mise-managed tools.
A failed `hk install` also produces a warning and keeps the worktree.

Entry points:

- [internal/timber/config.go](internal/timber/config.go): settings and config command
- [internal/timber/github_identity.go](internal/timber/github_identity.go): identity cache
- [internal/timber/mise_trust.go](internal/timber/mise_trust.go): eligibility and trust
- [internal/timber/hk_install.go](internal/timber/hk_install.go): hk hook installation

## Herdr Plugin

Expand Down Expand Up @@ -246,7 +255,7 @@ For full flags and examples, see `timber --help` and `timber <command> --help`.
| `repo import` | Convert an existing clone into a managed repository |
| `repo remove` | Unregister a bare repository once its worktrees are gone |
| `repo rename` | Rename a repository and move its worktree directories |
| `config` | Show settings or change them with `--auto-mise-trust` / `--no-auto-mise-trust` |
| `config` | Show settings or change them with `--auto-mise-trust` / `--no-auto-mise-trust` / `--auto-hk-install` / `--no-auto-hk-install` |
| `create` | Create a worktree for a branch (`--no-herdr` skips the Herdr space) |
| `switch` | Shell-only: `cd` into a worktree, creating it first with `-c` |
| `tui` | Interactively open an existing worktree or create a new one |
Expand Down
19 changes: 18 additions & 1 deletion internal/timber/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import (
type config struct {
AutoMiseTrust bool `toml:"auto_mise_trust"`
AutoMiseTrustDecided bool `toml:"auto_mise_trust_decided,omitempty"`
AutoHKInstall bool `toml:"auto_hk_install"`
AutoHKInstallDecided bool `toml:"auto_hk_install_decided,omitempty"`
}

func NewConfigCommand(runtime Runtime) *cobra.Command {
var enable, disable bool
var enable, disable, enableHK, disableHK bool
command := &cobra.Command{
Use: "config",
Short: "Show or change Timber settings",
Expand All @@ -27,12 +29,24 @@ func NewConfigCommand(runtime Runtime) *cobra.Command {
if err != nil {
return err
}
changed := false
if command.Flags().Changed("auto-mise-trust") || command.Flags().Changed("no-auto-mise-trust") {
settings.AutoMiseTrust = enable
if command.Flags().Changed("no-auto-mise-trust") {
settings.AutoMiseTrust = !disable
}
settings.AutoMiseTrustDecided = true
changed = true
}
if command.Flags().Changed("auto-hk-install") || command.Flags().Changed("no-auto-hk-install") {
settings.AutoHKInstall = enableHK
if command.Flags().Changed("no-auto-hk-install") {
settings.AutoHKInstall = !disableHK
}
settings.AutoHKInstallDecided = true
changed = true
}
if changed {
if err := writeConfig(runtime.configPath(), settings); err != nil {
return err
}
Expand All @@ -43,6 +57,9 @@ func NewConfigCommand(runtime Runtime) *cobra.Command {
command.Flags().BoolVar(&enable, "auto-mise-trust", false, "Trust mise.toml in worktrees owned by you or your GitHub organizations")
command.Flags().BoolVar(&disable, "no-auto-mise-trust", false, "Disable automatic mise trust")
command.MarkFlagsMutuallyExclusive("auto-mise-trust", "no-auto-mise-trust")
command.Flags().BoolVar(&enableHK, "auto-hk-install", false, "Install hk hooks in worktrees owned by you or your GitHub organizations")
command.Flags().BoolVar(&disableHK, "no-auto-hk-install", false, "Disable automatic hk hook installation")
command.MarkFlagsMutuallyExclusive("auto-hk-install", "no-auto-hk-install")
return command
}

Expand Down
17 changes: 12 additions & 5 deletions internal/timber/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ func TestConfigPersistsSettingsUnderXDGConfigHome(t *testing.T) {
args []string
want string
}{
{[]string{"config"}, "auto_mise_trust = false\n"},
{[]string{"config", "--auto-mise-trust"}, "auto_mise_trust = true\nauto_mise_trust_decided = true\n"},
{[]string{"config"}, "auto_mise_trust = true\nauto_mise_trust_decided = true\n"},
{[]string{"config", "--no-auto-mise-trust"}, "auto_mise_trust = false\nauto_mise_trust_decided = true\n"},
{[]string{"config"}, "auto_mise_trust = false\nauto_mise_trust_decided = true\n"},
{[]string{"config"}, "auto_mise_trust = false\nauto_hk_install = false\n"},
{[]string{"config", "--auto-mise-trust"}, "auto_mise_trust = true\nauto_mise_trust_decided = true\nauto_hk_install = false\n"},
{[]string{"config"}, "auto_mise_trust = true\nauto_mise_trust_decided = true\nauto_hk_install = false\n"},
{[]string{"config", "--no-auto-mise-trust"}, "auto_mise_trust = false\nauto_mise_trust_decided = true\nauto_hk_install = false\n"},
{[]string{"config"}, "auto_mise_trust = false\nauto_mise_trust_decided = true\nauto_hk_install = false\n"},
{[]string{"config", "--auto-hk-install"}, "auto_mise_trust = false\nauto_mise_trust_decided = true\nauto_hk_install = true\nauto_hk_install_decided = true\n"},
{[]string{"config"}, "auto_mise_trust = false\nauto_mise_trust_decided = true\nauto_hk_install = true\nauto_hk_install_decided = true\n"},
{[]string{"config", "--no-auto-hk-install"}, "auto_mise_trust = false\nauto_mise_trust_decided = true\nauto_hk_install = false\nauto_hk_install_decided = true\n"},
{[]string{"config"}, "auto_mise_trust = false\nauto_mise_trust_decided = true\nauto_hk_install = false\nauto_hk_install_decided = true\n"},
} {
result := runTimberCommandWithRuntime(t, runtime, step.args...)
require.NoError(t, result.err)
Expand All @@ -32,6 +36,9 @@ func TestConfigPersistsSettingsUnderXDGConfigHome(t *testing.T) {
result := runTimberCommandWithRuntime(t, runtime, "config", "--auto-mise-trust", "--no-auto-mise-trust")
require.Error(t, result.err)
assert.Contains(t, result.err.Error(), "none of the others can be")
result = runTimberCommandWithRuntime(t, runtime, "config", "--auto-hk-install", "--no-auto-hk-install")
require.Error(t, result.err)
assert.Contains(t, result.err.Error(), "none of the others can be")
}

func TestConfigDoesNotOverwriteInvalidOrUnknownSettings(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions internal/timber/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ func (x *createCommandOptions) createWorktree(command *cobra.Command, args []str
}

x.runtime.autoTrustMise(command, repository, upstreamBranch, worktreePath)
x.runtime.autoInstallHKHooks(command, repository, upstreamBranch, worktreePath)

if !x.shouldCreateHerdrWorkspace() {
return worktreePath, nil
Expand Down
105 changes: 105 additions & 0 deletions internal/timber/hk_install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package timber

import (
"bufio"
"fmt"
"path/filepath"
"strings"

"github.com/spf13/cobra"
)

func (x Runtime) autoInstallHKHooks(command *cobra.Command, repository *Repository, upstream, worktreePath string) {
warn := func(err error) {
fmt.Fprintf(command.ErrOrStderr(), "warning: automatic hk install skipped: %v\n", err)
}
settings, err := x.loadConfig()
if err != nil {
warn(err)
return
}
if !settings.AutoHKInstall && (settings.AutoHKInstallDecided || !isInteractiveTerminal(command.InOrStdin())) {
return
}
regular, err := regularRootFile(filepath.Join(worktreePath, "hk.pkl"))
if err != nil {
warn(err)
return
}
if !regular {
return
}
identity, owned, err := x.githubOwnedWorktree(command, repository, upstream)
if err != nil {
warn(err)
return
}
if !owned {
return
}
if !settings.AutoHKInstall {
enabled, err := x.promptAutoHKInstall(command, settings, identity)
if err != nil {
warn(err)
return
}
if !enabled {
return
}
}
args := []string{"install"}
hasMise, err := regularRootFile(filepath.Join(worktreePath, "mise.toml"))
if err != nil {
warn(err)
return
}
if hasMise {
args = append(args, "--mise")
}
install := x.command(command.Context(), "hk", args...)
install.Dir = worktreePath
output, err := install.CombinedOutput()
if err != nil {
warn(fmt.Errorf("hk install failed: %w: %s", err, strings.TrimSpace(string(output))))
}
}

func (x Runtime) promptAutoHKInstall(command *cobra.Command, settings config, identity githubIdentity) (bool, error) {
output := command.ErrOrStderr()
if _, err := fmt.Fprintln(output, "This worktree qualifies for automatic hk hook installation. Installing hk hooks permits code execution."); err != nil {
return false, err
}
if err := writeGitHubIdentity(output, identity); err != nil {
return false, err
}
reader := bufio.NewReader(command.InOrStdin())
for {
if _, err := fmt.Fprint(output, "Enable automatic hk hook installation for repositories owned by you or your GitHub organizations? [y/N] "); err != nil {
return false, err
}
answer, err := reader.ReadString('\n')
if err != nil {
return false, fmt.Errorf("read hk install choice: %w", err)
}
switch strings.ToLower(strings.TrimSpace(answer)) {
case "y", "yes":
settings.AutoHKInstall = true
case "", "n", "no":
settings.AutoHKInstall = false
default:
if _, err := fmt.Fprintln(output, "Enter yes or no."); err != nil {
return false, err
}
continue
}
settings.AutoHKInstallDecided = true
if err := writeConfig(x.configPath(), settings); err != nil {
return false, fmt.Errorf("save hk install choice: %w", err)
}
if !settings.AutoHKInstall {
_, err := fmt.Fprintln(output, "Automatic hk hook installation remains disabled. Enable it later with: timber config --auto-hk-install")
return false, err
}
return true, nil
}
}
Loading
Loading