132 lines · 4.1 KB · Go
RawDownload
package cli import (	"context"	"flag"	"fmt"	"os" 	"github.com/davis7dotsh/tx9/internal/box"	"github.com/davis7dotsh/tx9/internal/docker"	"github.com/davis7dotsh/tx9/internal/lock"	"github.com/davis7dotsh/tx9/internal/names"	"github.com/davis7dotsh/tx9/internal/state"	"github.com/davis7dotsh/tx9/internal/token"	"github.com/davis7dotsh/tx9/internal/version") // cmdCreate implements `tx9 create [name]` (command surface: generate a// friendly name if absent, build tx9-box:<version> if missing, create// network + volumes + both containers, mint token, wire MCP, run doctor).func cmdCreate(args []string) error {	fs := flag.NewFlagSet("create", flag.ContinueOnError)	executorFlags := addExecutorConfigFlags(fs)	resourceFlags := addResourceConfigFlags(fs)	if err := parseFlagsAnywhere(fs, args); err != nil {		return err	}	if fs.NArg() > 1 {		return fmt.Errorf("create: expected at most one box name (usage: tx9 create [box])")	}	requested := fs.Arg(0) 	ctx := context.Background()	cli, err := docker.NewClient(ctx)	if err != nil {		return err	}	defer cli.Close() 	name := requested	if name == "" {		existing, err := box.List(ctx, cli)		if err != nil {			return fmt.Errorf("create: %w", err)		}		taken := make(map[string]bool, len(existing))		for _, b := range existing {			taken[b.Name] = true		}		name, err = names.Generate(taken)		if err != nil {			return fmt.Errorf("create: %w", err)		}	} else if err := names.Validate(name); err != nil {		return fmt.Errorf("create: %w", err)	}	executorConfig, err := executorFlags.loadFresh(name)	if err != nil {		return fmt.Errorf("create %s: %w", name, err)	}	resources, err := resourceFlags.apply(box.DefaultResources())	if err != nil {		return fmt.Errorf("create %s: %w", name, err)	} 	lockPath, err := state.LockPath(name)	if err != nil {		return fmt.Errorf("create %s: %w", name, err)	}	release, err := lock.Acquire(lockPath)	if err != nil {		return fmt.Errorf("create %s: %w", name, err)	}	defer release() 	exists, err := box.Exists(ctx, cli, name)	if err != nil {		return fmt.Errorf("create %s: %w", name, err)	}	if exists {		return fmt.Errorf("create %s: box already exists", name)	} 	imageTag := fmt.Sprintf("tx9-box:%s", version.Version)	if err := ensureBoxImage(ctx, cli, imageTag); err != nil {		return fmt.Errorf("create %s: %w", name, err)	} 	tok, err := token.Mint()	if err != nil {		return fmt.Errorf("create %s: %w", name, err)	}	if err := box.SaveExecutorConfig(name, tok, executorConfig); err != nil {		return fmt.Errorf("create %s: %w", name, err)	}	// Host mounts are machine-local desired state, not part of a portable	// box. Never inherit them from a stale same-named env file.	if err := box.SaveAgentMounts(name, nil); err != nil {		return fmt.Errorf("create %s: %w", name, err)	}	if err := box.SaveResources(name, resources); err != nil {		return fmt.Errorf("create %s: %w", name, err)	} 	fmt.Printf("tx9: creating box %q (agent + executor containers, private network)\n", name)	b, err := box.Create(ctx, cli, box.CreateOpts{		Name: name, Image: imageTag, Token: tok, Executor: executorConfig, Resources: resources,	})	if err != nil {		box.Destroy(ctx, cli, name)		return fmt.Errorf("create %s: %w", name, err)	} 	fmt.Println("tx9: preparing runtime (executor reachability, MCP wiring, doctor)")	if err := box.PrepareRuntime(ctx, cli, b, tok, os.Stdout); err != nil {		box.Destroy(ctx, cli, name)		return fmt.Errorf("create %s: runtime failed, box removed: %w", name, err)	} 	port, err := box.HostPort(ctx, cli, b)	if err != nil {		return fmt.Errorf("create %s: box is up but %w", name, err)	} 	fmt.Printf("\nBox %q is ready. Next steps:\n", name)	fmt.Printf("  1. tx9 enter %s\n", name)	fmt.Println("  2. inside the box, authenticate claude/codex and run: hermes gateway setup")	fmt.Println("  3. leave the foreground setup process, then enable TX9's durable gateway from the host:")	fmt.Printf("       tx9 gateway enable %s --confirm-single-writer\n", name)	fmt.Printf("  4. dashboard: %s (run `tx9 open %s` for an authenticated one-shot URL)\n", box.DashboardURL(port, b.ExecutorWebBaseURL), name)	return nil}