From 9633f9ad4cb62bf634d053e31c207b69f5b906fe Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Sat, 23 May 2026 11:25:31 +0100 Subject: [PATCH] fix: pre-select all in init prompt, preserve agent paths with slashes, fix rollback off-by-one --- internal/cli/init.go | 41 +++++++++++++++++++++++++++++++++++++---- internal/gitx/repo.go | 7 ++++++- internal/ui/prompts.go | 13 +++++++++++-- 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/internal/cli/init.go b/internal/cli/init.go index c0eb0ea..257fbdc 100644 --- a/internal/cli/init.go +++ b/internal/cli/init.go @@ -49,7 +49,11 @@ func NewInit(reg *adapter.Registry) *cobra.Command { var picked []string switch { case len(from) > 0: - picked = from + for _, f := range from { + if f != "" { + picked = append(picked, f) + } + } case noPrompt: picked = candidates default: @@ -58,6 +62,14 @@ func NewInit(reg *adapter.Registry) *cobra.Command { return err } } + if len(picked) == 0 { + return fmt.Errorf("no harnesses selected; rerun with --no-prompt or --from ") + } + for _, name := range picked { + if _, ok := reg.Get(name); !ok { + return fmt.Errorf("unknown adapter %q (try `harness-sync adapter list`)", name) + } + } return runImport(cmd, reg, r, home, picked) }, } @@ -134,7 +146,10 @@ func runImport(cmd *cobra.Command, reg *adapter.Registry, root, home string, pic if err := repo.Commit(fmt.Sprintf("import from %s", strings.Join(picked, ", "))); err != nil { return err } - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "imported from %v into %s\n", picked, root) + _, _ = fmt.Fprintf(cmd.OutOrStdout(), + "imported from %v into %s\n skills: %d\n agents: %d\n mcp servers: %d\n instructions: %d source(s)\n", + picked, root, len(bundle.Skills), len(bundle.Agents), len(bundle.MCP.Servers), len(instructions), + ) return nil } @@ -183,7 +198,11 @@ func writeCanonical(root string, b *canonical.Bundle, picked []string) error { } for _, s := range b.Skills { - path := filepath.Join(root, "skills", s.Name, "SKILL.md") + rel := s.Path + if rel == "" { + rel = filepath.Join(safeFilename(s.Name), "SKILL.md") + } + path := filepath.Join(root, "skills", rel) if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil { return err } @@ -192,7 +211,14 @@ func writeCanonical(root string, b *canonical.Bundle, picked []string) error { } } for _, a := range b.Agents { - path := filepath.Join(root, "agents", a.Name+".md") + rel := a.Path + if rel == "" { + rel = safeFilename(a.Name) + ".md" + } + path := filepath.Join(root, "agents", rel) + if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil { + return err + } if err := os.WriteFile(path, []byte(a.Body), 0o600); err != nil { return err } @@ -205,3 +231,10 @@ func writeCanonical(root string, b *canonical.Bundle, picked []string) error { } return nil } + +// safeFilename strips path separators and other characters that would break +// when used as a basename. +func safeFilename(s string) string { + r := strings.NewReplacer("/", "-", "\\", "-", ":", "-", "\x00", "") + return r.Replace(s) +} diff --git a/internal/gitx/repo.go b/internal/gitx/repo.go index d784a67..a168043 100644 --- a/internal/gitx/repo.go +++ b/internal/gitx/repo.go @@ -68,7 +68,12 @@ func (r *Repo) IsRepo() bool { return err == nil } +// Revert undoes the last N commits with `git revert --no-edit HEAD~N..HEAD`. +// n must be >= 1. func (r *Repo) Revert(n int) error { - _, err := r.run("revert", "--no-edit", fmt.Sprintf("HEAD~%d..HEAD", n-1)) + if n < 1 { + return fmt.Errorf("revert: n must be >= 1, got %d", n) + } + _, err := r.run("revert", "--no-edit", fmt.Sprintf("HEAD~%d..HEAD", n)) return err } diff --git a/internal/ui/prompts.go b/internal/ui/prompts.go index ed74b08..085ffe9 100644 --- a/internal/ui/prompts.go +++ b/internal/ui/prompts.go @@ -44,19 +44,28 @@ func MultiSelect(title string, choices []string, opts ...Option) ([]string, erro } return o.nonInteractive, nil } - var picked []string + // Pre-select every choice. Users deselect what they don't want; hitting + // enter without toggling thus accepts all (the common case) instead of + // silently producing an empty result. + picked := make([]string, len(choices)) + copy(picked, choices) + opts2 := make([]huh.Option[string], 0, len(choices)) for _, c := range choices { - opts2 = append(opts2, huh.NewOption(c, c)) + opts2 = append(opts2, huh.NewOption(c, c).Selected(true)) } form := huh.NewForm(huh.NewGroup( huh.NewMultiSelect[string](). Title(title). + Description("space to toggle · enter to confirm · all pre-selected"). Options(opts2...). Value(&picked), )) if err := form.Run(); err != nil { return nil, err } + if len(picked) == 0 { + return nil, fmt.Errorf("no items selected") + } return picked, nil }