// Command agentws (agentic workspace) manages git worktrees for the unkin-agent // user so agents can work on isolated branches without disturbing Ben's shared // checkouts. // // Repositories are cloned into the source root (default ~/src/prodenv/) so // branches created here are visible in the main checkout too. Worktrees live // under the worktree root (default ~/.cache/agentws/__). Auth for // clone/fetch/push comes from a short-lived Gitea token minted from Vault via // agent.GiteaToken(); it is supplied through an ephemeral git credential helper // (`agentws credential get`) rather than being persisted in any remote URL or // config, since the tokens expire in about an hour. // // agentws new [--branch benvin/] [--from ] // agentws list // agentws rm [--delete-branch] // agentws clean // agentws token // agentws credential get # git credential-helper protocol on stdin package main import ( "bufio" "fmt" "io" "os" "path/filepath" "strings" "git.unkin.net/unkin/agent-tools/internal/agent" "github.com/spf13/cobra" ) var version = "dev" func main() { if err := newRootCmd().Execute(); err != nil { os.Exit(1) } } // newRootCmd builds the agentws command tree. Separated from main so tests can // invoke Execute and assert behaviour without spawning a process. func newRootCmd() *cobra.Command { root := &cobra.Command{ Use: "agentws", Short: "Manage git worktrees for the unkin-agent user.", Long: "agentws manages per-branch git worktrees for unkin-agent. Repos are cloned into\nthe source root (~/src/prodenv) and worktrees live under the worktree root\n(~/.cache/agentws), authenticated by an ephemeral Vault-minted Gitea token.", Version: version, SilenceUsage: true, } root.SetVersionTemplate("{{.Version}}\n") root.AddCommand( newNewCmd(), newListCmd(), newRmCmd(), newCleanCmd(), newTokenCmd(), newCredentialCmd(), newVersionCmd(), ) return root } // --- configuration (env-overridable) -------------------------------------- // srcRoot is where source-of-truth checkouts live (default ~/src/prodenv). func srcRoot() (string, error) { if v := os.Getenv("AGENTWS_SRC_ROOT"); v != "" { return v, nil } home, err := os.UserHomeDir() if err != nil { return "", err } return filepath.Join(home, "src", "prodenv"), nil } // worktreeRoot is where managed worktrees live (default ~/.cache/agentws). func worktreeRoot() (string, error) { if v := os.Getenv("AGENTWS_ROOT"); v != "" { return v, nil } cache, err := os.UserCacheDir() if err != nil { return "", err } return filepath.Join(cache, "agentws"), nil } // owner is the Gitea org that owns the repos (default unkin). func owner() string { if v := os.Getenv("AGENTWS_OWNER"); v != "" { return v } return "unkin" } // cloneURL builds the (token-free) HTTPS clone URL for a repo. func cloneURL(repo string) string { return strings.TrimRight(agent.GiteaURL(), "/") + "/" + owner() + "/" + repo + ".git" } // credentialHelperArgs returns git global args that wire this binary as an // ephemeral credential helper, so clone/fetch/push authenticate without // persisting a token anywhere. func credentialHelperArgs() []string { exe, err := os.Executable() if err != nil || exe == "" { exe = "agentws" } return []string{"-c", "credential.helper=!" + exe + " credential"} } // --- new ------------------------------------------------------------------ func newNewCmd() *cobra.Command { var branch, from string cmd := &cobra.Command{ Use: "new ", Short: "Clone (if needed) and create a worktree for a branch", Args: cobra.ExactArgs(1), SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { repo := strings.TrimSpace(args[0]) if repo == "" || strings.Contains(repo, "/") { return fmt.Errorf("repo must be a bare repository name (owner comes from AGENTWS_OWNER, default %q)", owner()) } if branch == "" { return fmt.Errorf("--branch is required (e.g. benvin/)") } sr, err := srcRoot() if err != nil { return err } wr, err := worktreeRoot() if err != nil { return err } srcDir := filepath.Join(sr, repo) auth := credentialHelperArgs() // a. Clone the source-of-truth checkout if missing. if _, statErr := os.Stat(srcDir); statErr != nil { if !os.IsNotExist(statErr) { return statErr } fmt.Fprintf(cmd.OutOrStdout(), "cloning %s into %s\n", cloneURL(repo), srcDir) if err := agent.GitClone(cloneURL(repo), srcDir, auth...); err != nil { return err } } // b. Refresh so the base branch is current. if err := agent.GitFetch(srcDir, "origin", auth...); err != nil { return err } // c. Base branch: --from or the remote default. base := from if base == "" { base, err = agent.GitRemoteDefaultBranch(srcDir, "origin") if err != nil { return err } } // d. Create the worktree FROM the source checkout so the branch is // visible in the main checkout too. wtPath := filepath.Join(wr, agent.WorktreeDirName(repo, branch)) if _, statErr := os.Stat(wtPath); statErr == nil { return fmt.Errorf("worktree already exists at %s", wtPath) } if err := agent.GitWorktreeAdd(srcDir, wtPath, branch, "origin/"+base); err != nil { return err } // e. Set the agent identity + auth WITHOUT polluting the shared // checkout: per-worktree config only. if err := agent.GitConfigSet(srcDir, false, "extensions.worktreeConfig", "true"); err != nil { return err } if err := agent.GitConfigSet(wtPath, true, "user.name", agent.AgentLogin()); err != nil { return err } if err := agent.GitConfigSet(wtPath, true, "user.email", agent.AgentLogin()+"@unkin.net"); err != nil { return err } exe, _ := os.Executable() if exe == "" { exe = "agentws" } if err := agent.GitConfigSet(wtPath, true, "credential.helper", "!"+exe+" credential"); err != nil { return err } // f. Report the worktree path and branch. fmt.Fprintf(cmd.OutOrStdout(), "%s\n", wtPath) fmt.Fprintf(cmd.OutOrStdout(), "branch %s (from origin/%s)\n", branch, base) return nil }, } f := cmd.Flags() f.StringVar(&branch, "branch", "", "Branch to check out/create (e.g. benvin/) (required)") f.StringVar(&from, "from", "", "Base branch to branch from (default: remote default branch)") _ = cmd.MarkFlagRequired("branch") return cmd } // --- list ----------------------------------------------------------------- func newListCmd() *cobra.Command { return &cobra.Command{ Use: "list", Short: "List managed worktrees under the worktree root", SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { managed, err := managedWorktrees() if err != nil { return err } out := cmd.OutOrStdout() if len(managed) == 0 { fmt.Fprintln(out, "no managed worktrees") return nil } for _, w := range managed { fmt.Fprintf(out, "%s\t%s\t%s\n", w.repo, w.branch, w.path) } return nil }, } } // managedWt describes one worktree living under the worktree root. type managedWt struct { repo string branch string path string srcDir string } // managedWorktrees scans the worktree root and resolves each entry's repo and // branch from git so branch names are accurate (not the sanitized dir name). func managedWorktrees() ([]managedWt, error) { wr, err := worktreeRoot() if err != nil { return nil, err } entries, err := os.ReadDir(wr) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } var out []managedWt for _, e := range entries { if !e.IsDir() { continue } path := filepath.Join(wr, e.Name()) branch, err := agent.GitCurrentBranch(path) if err != nil { continue // not a git worktree; skip } srcDir, err := agent.SourceRepoDir(path) if err != nil { continue } out = append(out, managedWt{ repo: filepath.Base(srcDir), branch: branch, path: path, srcDir: srcDir, }) } return out, nil } // --- rm ------------------------------------------------------------------- func newRmCmd() *cobra.Command { var deleteBranch bool cmd := &cobra.Command{ Use: "rm ", Short: "Remove a managed worktree and refresh its source repo", Args: cobra.ExactArgs(1), SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { target := strings.TrimSpace(args[0]) wt, err := resolveWorktree(target) if err != nil { return err } return removeWorktree(cmd.OutOrStdout(), wt, deleteBranch) }, } cmd.Flags().BoolVar(&deleteBranch, "delete-branch", false, "Also delete the local branch after removing the worktree") return cmd } // resolveWorktree finds a managed worktree by exact path or by branch name. func resolveWorktree(target string) (managedWt, error) { managed, err := managedWorktrees() if err != nil { return managedWt{}, err } abs, _ := filepath.Abs(target) for _, w := range managed { if w.path == target || w.path == abs || w.branch == target { return w, nil } } return managedWt{}, fmt.Errorf("no managed worktree matching %q (try `agentws list`)", target) } func removeWorktree(out io.Writer, wt managedWt, deleteBranch bool) error { if err := agent.GitWorktreeRemove(wt.srcDir, wt.path, true); err != nil { return err } fmt.Fprintf(out, "removed worktree %s\n", wt.path) if deleteBranch { if err := agent.GitDeleteBranch(wt.srcDir, wt.branch, true); err != nil { return err } fmt.Fprintf(out, "deleted branch %s\n", wt.branch) } // Refresh the source repo's default branch, then prune. if err := agent.GitFetch(wt.srcDir, "origin", credentialHelperArgs()...); err != nil { return err } return agent.GitWorktreePrune(wt.srcDir) } // --- clean ---------------------------------------------------------------- func newCleanCmd() *cobra.Command { return &cobra.Command{ Use: "clean", Short: "Remove all managed worktrees and prune their source repos", SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { managed, err := managedWorktrees() if err != nil { return err } out := cmd.OutOrStdout() if len(managed) == 0 { fmt.Fprintln(out, "no managed worktrees") return nil } for _, w := range managed { if err := removeWorktree(out, w, false); err != nil { return err } } return nil }, } } // --- token ---------------------------------------------------------------- func newTokenCmd() *cobra.Command { return &cobra.Command{ Use: "token", Short: "Print a fresh unkin-agent Gitea token", SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { tok, err := agent.GiteaToken() if err != nil { return err } fmt.Fprintln(cmd.OutOrStdout(), tok) return nil }, } } // --- credential (git credential-helper protocol) -------------------------- func newCredentialCmd() *cobra.Command { cmd := &cobra.Command{ Use: "credential ", Short: "git credential-helper: emit unkin-agent creds for git.unkin.net", Args: cobra.ExactArgs(1), SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { switch args[0] { case "get": return credentialGet(cmd.InOrStdin(), cmd.OutOrStdout()) case "store", "erase": // Nothing to persist/erase for an ephemeral helper; git ignores // empty output and moves on. return nil default: return fmt.Errorf("unknown credential action %q", args[0]) } }, } return cmd } // credentialGet implements the `get` half of the git credential protocol: read // the key=value request on stdin and, for the configured Gitea host, emit a // username/password pair (unkin-agent + a fresh Vault-minted token). func credentialGet(stdin io.Reader, stdout io.Writer) error { req := map[string]string{} sc := bufio.NewScanner(stdin) for sc.Scan() { line := sc.Text() if line == "" { break } if k, v, ok := strings.Cut(line, "="); ok { req[k] = v } } if err := sc.Err(); err != nil { return err } // Only answer for the configured Gitea host to avoid handing the token to // any other remote git might ask about. if host := req["host"]; host != "" && host != giteaHost() { return nil } tok, err := agent.GiteaToken() if err != nil { return err } fmt.Fprintf(stdout, "username=%s\n", agent.AgentLogin()) fmt.Fprintf(stdout, "password=%s\n", tok) return nil } // giteaHost returns the host portion of the configured Gitea URL. func giteaHost() string { u := agent.GiteaURL() u = strings.TrimPrefix(u, "https://") u = strings.TrimPrefix(u, "http://") if i := strings.IndexByte(u, '/'); i >= 0 { u = u[:i] } return u } // --- version -------------------------------------------------------------- func newVersionCmd() *cobra.Command { return &cobra.Command{ Use: "version", Short: "Print the version", Run: func(cmd *cobra.Command, args []string) { fmt.Println(version) }, SilenceUsage: true, } }