Fix vm run guest repo path and add vm acp bridge
Normalize repo-backed guest checkouts to /root/repo so vm run, attach, and follow-on guest tooling stop depending on the source repository name. Add `banger vm acp [--cwd] <vm>` as an SSH stdio bridge to guest `opencode acp`, defaulting to /root/repo when that checkout exists and falling back to /root. Update the README and CLI coverage around the fixed guest path and ACP command. Validation: go test ./internal/cli, go test ./..., make build.
This commit is contained in:
parent
dbc70643c3
commit
5f89c07fc0
3 changed files with 210 additions and 12 deletions
|
|
@ -74,6 +74,9 @@ var (
|
|||
vmHealthFunc = func(ctx context.Context, socketPath, idOrName string) (api.VMHealthResult, error) {
|
||||
return rpc.Call[api.VMHealthResult](ctx, socketPath, "vm.health", api.VMRefParams{IDOrName: idOrName})
|
||||
}
|
||||
vmSSHFunc = func(ctx context.Context, socketPath, idOrName string) (api.VMSSHResult, error) {
|
||||
return rpc.Call[api.VMSSHResult](ctx, socketPath, "vm.ssh", api.VMRefParams{IDOrName: idOrName})
|
||||
}
|
||||
daemonPingFunc = func(ctx context.Context, socketPath string) (api.PingResult, error) {
|
||||
return rpc.Call[api.PingResult](ctx, socketPath, "ping", api.Empty{})
|
||||
}
|
||||
|
|
@ -460,6 +463,7 @@ func newVMCommand() *cobra.Command {
|
|||
newVMActionCommand("delete", "Delete a VM", "vm.delete"),
|
||||
newVMSetCommand(),
|
||||
newVMSSHCommand(),
|
||||
newVMACPCommand(),
|
||||
newVMLogsCommand(),
|
||||
newVMStatsCommand(),
|
||||
newVMPortsCommand(),
|
||||
|
|
@ -814,7 +818,7 @@ func newVMSSHCommand() *cobra.Command {
|
|||
if err := validateSSHPrereqs(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
result, err := rpc.Call[api.VMSSHResult](cmd.Context(), layout.SocketPath, "vm.ssh", api.VMRefParams{IDOrName: args[0]})
|
||||
result, err := vmSSHFunc(cmd.Context(), layout.SocketPath, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -827,6 +831,27 @@ func newVMSSHCommand() *cobra.Command {
|
|||
}
|
||||
}
|
||||
|
||||
func newVMACPCommand() *cobra.Command {
|
||||
var cwd string
|
||||
cmd := &cobra.Command{
|
||||
Use: "acp <id-or-name>",
|
||||
Short: "Bridge ACP to a running VM over SSH",
|
||||
Args: exactArgsUsage(1, "usage: banger vm acp [--cwd PATH] <id-or-name>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
layout, cfg, err := ensureDaemon(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateSSHPrereqs(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
return runVMACP(cmd.Context(), layout.SocketPath, cfg, cmd.InOrStdin(), cmd.OutOrStdout(), cmd.ErrOrStderr(), args[0], cwd)
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&cwd, "cwd", "", "guest working directory for opencode acp")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newVMLogsCommand() *cobra.Command {
|
||||
var follow bool
|
||||
cmd := &cobra.Command{
|
||||
|
|
@ -1424,6 +1449,18 @@ func runSSHSession(ctx context.Context, socketPath, vmRef string, stdin io.Reade
|
|||
return sshErr
|
||||
}
|
||||
|
||||
func runVMACP(ctx context.Context, socketPath string, cfg model.DaemonConfig, stdin io.Reader, stdout, stderr io.Writer, idOrName, cwd string) error {
|
||||
result, err := vmSSHFunc(ctx, socketPath, idOrName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sshArgs, err := sshACPCommandArgs(cfg, result.GuestIP, vmACPRemoteCommand(cwd))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sshExecFunc(ctx, stdin, stdout, stderr, sshArgs)
|
||||
}
|
||||
|
||||
func shouldCheckSSHReminder(err error) bool {
|
||||
if err == nil {
|
||||
return true
|
||||
|
|
@ -1459,6 +1496,30 @@ func sshCommandArgs(cfg model.DaemonConfig, guestIP string, extra []string) ([]s
|
|||
return args, nil
|
||||
}
|
||||
|
||||
func sshACPCommandArgs(cfg model.DaemonConfig, guestIP, remoteCommand string) ([]string, error) {
|
||||
if guestIP == "" {
|
||||
return nil, errors.New("vm has no guest IP")
|
||||
}
|
||||
args := []string{"-T", "-F", "/dev/null"}
|
||||
if cfg.SSHKeyPath != "" {
|
||||
args = append(args, "-i", cfg.SSHKeyPath)
|
||||
}
|
||||
args = append(
|
||||
args,
|
||||
"-o", "IdentitiesOnly=yes",
|
||||
"-o", "BatchMode=yes",
|
||||
"-o", "PreferredAuthentications=publickey",
|
||||
"-o", "PasswordAuthentication=no",
|
||||
"-o", "KbdInteractiveAuthentication=no",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "LogLevel=ERROR",
|
||||
"root@"+guestIP,
|
||||
"bash", "-lc", remoteCommand,
|
||||
)
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func validateSSHPrereqs(cfg model.DaemonConfig) error {
|
||||
checks := system.NewPreflight()
|
||||
checks.RequireCommand("ssh", "install openssh-client")
|
||||
|
|
@ -1688,7 +1749,7 @@ func runVMRun(ctx context.Context, socketPath string, cfg model.DaemonConfig, st
|
|||
if err := startVMRunToolingHarness(ctx, client, spec, progress); err != nil {
|
||||
printVMRunWarning(stderr, fmt.Sprintf("tooling harness start failed: %v", err))
|
||||
}
|
||||
if err := runVMRunAttach(ctx, socketPath, vmRef, cfg, stdin, stdout, stderr, vm.Runtime.GuestIP, vmRunGuestDir(spec.RepoName), progress); err != nil {
|
||||
if err := runVMRunAttach(ctx, socketPath, vmRef, cfg, stdin, stdout, stderr, vm.Runtime.GuestIP, vmRunGuestDir(), progress); err != nil {
|
||||
return fmt.Errorf("vm %q is running but opencode attach failed: %w", vmRef, err)
|
||||
}
|
||||
return nil
|
||||
|
|
@ -1707,7 +1768,7 @@ func importVMRunRepoToGuest(ctx context.Context, client vmRunGuestClient, spec v
|
|||
progress.render("copying repo metadata to guest")
|
||||
}
|
||||
var copyLog bytes.Buffer
|
||||
remoteCommand := fmt.Sprintf("rm -rf %s && mkdir -p %s && tar -o -C %s --strip-components=1 -xf -", shellQuote(vmRunGuestDir(spec.RepoName)), shellQuote(vmRunGuestDir(spec.RepoName)), shellQuote(vmRunGuestDir(spec.RepoName)))
|
||||
remoteCommand := fmt.Sprintf("rm -rf %s && mkdir -p %s && tar -o -C %s --strip-components=1 -xf -", shellQuote(vmRunGuestDir()), shellQuote(vmRunGuestDir()), shellQuote(vmRunGuestDir()))
|
||||
if err := client.StreamTar(ctx, repoCopyDir, remoteCommand, ©Log); err != nil {
|
||||
return formatVMRunStepError("copy guest git metadata", err, copyLog.String())
|
||||
}
|
||||
|
|
@ -1722,7 +1783,7 @@ func importVMRunRepoToGuest(ctx context.Context, client vmRunGuestClient, spec v
|
|||
progress.render("overlaying host working tree")
|
||||
}
|
||||
var overlayLog bytes.Buffer
|
||||
remoteCommand = fmt.Sprintf("tar -o -C %s --strip-components=1 -xf -", shellQuote(vmRunGuestDir(spec.RepoName)))
|
||||
remoteCommand = fmt.Sprintf("tar -o -C %s --strip-components=1 -xf -", shellQuote(vmRunGuestDir()))
|
||||
if err := client.StreamTarEntries(ctx, spec.RepoRoot, spec.OverlayPaths, remoteCommand, &overlayLog); err != nil {
|
||||
return formatVMRunStepError("overlay host working tree", err, overlayLog.String())
|
||||
}
|
||||
|
|
@ -1785,7 +1846,7 @@ func runHostCommand(ctx context.Context, name string, args ...string) error {
|
|||
}
|
||||
|
||||
func vmRunCheckoutScript(spec vmRunRepoSpec) string {
|
||||
guestDir := vmRunGuestDir(spec.RepoName)
|
||||
guestDir := vmRunGuestDir()
|
||||
var script strings.Builder
|
||||
script.WriteString("set -euo pipefail\n")
|
||||
fmt.Fprintf(&script, "DIR=%s\n", shellQuote(guestDir))
|
||||
|
|
@ -1806,11 +1867,12 @@ func vmRunCheckoutScript(spec vmRunRepoSpec) string {
|
|||
return script.String()
|
||||
}
|
||||
|
||||
func vmRunGuestDir(repoName string) string {
|
||||
return filepath.ToSlash(filepath.Join("/root", repoName))
|
||||
func vmRunGuestDir() string {
|
||||
return "/root/repo"
|
||||
}
|
||||
|
||||
func vmRunToolingHarnessPath(repoName string) string {
|
||||
|
||||
return filepath.ToSlash(filepath.Join("/tmp", "banger-vm-run-tooling-"+repoName+".sh"))
|
||||
}
|
||||
|
||||
|
|
@ -1870,7 +1932,7 @@ func vmRunToolingHarnessPromptData(plan toolingplan.Plan) string {
|
|||
func vmRunToolingHarnessScript(spec vmRunRepoSpec, plan toolingplan.Plan) string {
|
||||
var script strings.Builder
|
||||
script.WriteString("set -uo pipefail\n")
|
||||
fmt.Fprintf(&script, "DIR=%s\n", shellQuote(vmRunGuestDir(spec.RepoName)))
|
||||
fmt.Fprintf(&script, "DIR=%s\n", shellQuote(vmRunGuestDir()))
|
||||
script.WriteString("export PATH=/usr/local/bin:/root/.local/share/mise/shims:$PATH\n")
|
||||
script.WriteString("if [ -f /etc/profile.d/mise.sh ]; then . /etc/profile.d/mise.sh || true; fi\n")
|
||||
script.WriteString("log() { printf '%s\\n' \"$*\"; }\n")
|
||||
|
|
@ -2051,6 +2113,24 @@ func printVMRunWarning(out io.Writer, detail string) {
|
|||
_, _ = fmt.Fprintln(out, "[vm run] warning: "+detail)
|
||||
}
|
||||
|
||||
func vmACPRemoteCommand(cwd string) string {
|
||||
var script strings.Builder
|
||||
script.WriteString("set -euo pipefail\n")
|
||||
if strings.TrimSpace(cwd) != "" {
|
||||
fmt.Fprintf(&script, "DIR=%s\n", shellQuote(cwd))
|
||||
} else {
|
||||
fmt.Fprintf(&script, "REPO_DIR=%s\n", shellQuote(vmRunGuestDir()))
|
||||
fmt.Fprintf(&script, "DEFAULT_DIR=%s\n", shellQuote("/root"))
|
||||
script.WriteString(`if [ -d "$REPO_DIR" ]; then DIR="$REPO_DIR"; else DIR="$DEFAULT_DIR"; fi
|
||||
`)
|
||||
}
|
||||
script.WriteString(`cd "$DIR"
|
||||
`)
|
||||
script.WriteString(`exec opencode acp --cwd "$DIR"
|
||||
`)
|
||||
return script.String()
|
||||
}
|
||||
|
||||
func shellQuote(value string) string {
|
||||
return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue