From f8afb7d5889f5c8b6ea256fd078fa8426d21c7be Mon Sep 17 00:00:00 2001 From: James O'Doherty Date: Sun, 7 Jun 2026 22:57:34 -0400 Subject: feat(cli): introduce explicit run/exec subcommands to prevent typo-execution Prevent the ambiguity where a mistyped subcommand was interpreted as the target wrapped process. - Introduce `run` and `exec` (alias) subcommands for launching wrapped processes. - Promote internal test commands (`test-ns`, `test-args`, `test-lifecycle`) to explicit subcommands. - Update CLI routing to return an error for unknown subcommands instead of falling back to the default execution path. - Update `README.md` usage examples and all test suites to use the new subcommand structure. --- internal/cli/cli.go | 122 +++++++++++++++++++++++++++++----------------------- 1 file changed, 68 insertions(+), 54 deletions(-) (limited to 'internal/cli/cli.go') diff --git a/internal/cli/cli.go b/internal/cli/cli.go index d100d4f..5beb989 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -51,62 +51,40 @@ func (a *App) Route() error { } } - if len(a.Args) > 1 { - switch a.Args[1] { - case "show-config": - return a.showConfig() - case "profile": - return a.handleProfileCmd() - } + if len(a.Args) < 2 { + a.printUsage() + return fmt.Errorf("no command provided") + } + + switch a.Args[1] { + case "show-config": + return a.showConfig() + case "profile": + return a.handleProfileCmd() + case "run", "exec": + return a.executeWrapped(a.Args[2:]) + case "test-ns": + return a.testNS() + case "test-args": + return a.testArgs() + case "test-lifecycle": + return a.testLifecycle() + default: + a.printUsage() + return fmt.Errorf("unknown command: %s", a.Args[1]) } - - return a.Run() } -// Run executes the main logic of wg-wrap, including bootstrapping the namespace +// executeWrapped executes the main logic of wg-wrap, including bootstrapping the namespace // and launching the wrapped command. -func (a *App) Run() error { - if len(a.Args) > 1 { - switch a.Args[1] { - case "test-ns": - if !namespace.IsIsolated() { - if err := namespace.Bootstrap(); err != nil { - return fmt.Errorf("bootstrap failed: %w", err) - } - } - ok, msg := namespace.VerifyIsolation() - if !ok { - return fmt.Errorf("isolation check failed: %s", msg) - } - fmt.Println("Isolation Verified: OK") - return nil - case "test-args": - if !namespace.IsIsolated() { - if err := namespace.Bootstrap(); err != nil { - return fmt.Errorf("bootstrap failed: %w", err) - } - } - return namespace.VerifyArguments(a.Args) - case "test-lifecycle": - profile := "default" - for i := 0; i < len(a.Args)-1; i++ { - if a.Args[i] == "--profile" && i+1 < len(a.Args) { - profile = a.Args[i+1] - break - } - } - return a.getManager().VerifyLifecycle(profile) - } - } - +func (a *App) executeWrapped(args []string) error { cfg := &config.Config{} - fs := flag.NewFlagSet("wg-wrap", flag.ExitOnError) + fs := flag.NewFlagSet("wg-wrap exec", flag.ExitOnError) fs.Usage = a.printUsage fs.StringVar(&cfg.Profile, "profile", "", "WireGuard profile to use") fs.StringVar(&cfg.DNSServer, "dns-server", "", "Override DNS server to use") - args := a.Args[1:] sepIdx := -1 for i, arg := range args { if arg == "--" { @@ -155,20 +133,56 @@ func (a *App) Run() error { return nil } +func (a *App) testNS() error { + if !namespace.IsIsolated() { + if err := namespace.Bootstrap(); err != nil { + return fmt.Errorf("bootstrap failed: %w", err) + } + } + ok, msg := namespace.VerifyIsolation() + if !ok { + return fmt.Errorf("isolation check failed: %s", msg) + } + fmt.Println("Isolation Verified: OK") + return nil +} + +func (a *App) testArgs() error { + if !namespace.IsIsolated() { + if err := namespace.Bootstrap(); err != nil { + return fmt.Errorf("bootstrap failed: %w", err) + } + } + return namespace.VerifyArguments(a.Args) +} + +func (a *App) testLifecycle() error { + profile := "default" + for i := 0; i < len(a.Args)-1; i++ { + if a.Args[i] == "--profile" && i+1 < len(a.Args) { + profile = a.Args[i+1] + break + } + } + return a.getManager().VerifyLifecycle(profile) +} + func (a *App) isVerbose() bool { return os.Getenv("WG_WRAP_VERBOSE") == "1" } func (a *App) printUsage() { - fmt.Fprintf(os.Stderr, "Usage: wg-wrap [options] [-- command [args]]\n\n") - fmt.Fprintf(os.Stderr, "Options:\n") - fmt.Fprintf(os.Stderr, " -profile string\n\tWireGuard profile to use (default \"default\")\n") - fmt.Fprintf(os.Stderr, " -dns-server string\n\tOverride DNS server to use\n\n") + fmt.Fprintf(os.Stderr, "Usage: wg-wrap [args]\n\n") fmt.Fprintf(os.Stderr, "Commands:\n") - fmt.Fprintf(os.Stderr, " show-config\n\tDisplay the current configuration and environment details\n") - fmt.Fprintf(os.Stderr, " profile \n\tManage WireGuard profiles\n\t\t(list, import, configure, delete, stop)\n\n") - fmt.Fprintf(os.Stderr, "Run the wrapped command:\n") - fmt.Fprintf(os.Stderr, " wg-wrap [options] -- [args]\n") + fmt.Fprintf(os.Stderr, " run [options] [-- command] \tRun a command in the wrapped environment\n") + fmt.Fprintf(os.Stderr, " exec [options] [-- command] \tAlias for 'run'\n") + fmt.Fprintf(os.Stderr, " profile \t\tManage WireGuard profiles (list, import, configure, delete, stop)\n") + fmt.Fprintf(os.Stderr, " show-config \t\t\tDisplay the current configuration and environment details\n\n") + fmt.Fprintf(os.Stderr, "Run Options:\n") + fmt.Fprintf(os.Stderr, " -profile string \t\tWireGuard profile to use (default \"default\")\n") + fmt.Fprintf(os.Stderr, " -dns-server string \tOverride DNS server to use\n\n") + fmt.Fprintf(os.Stderr, "Internal/Test Commands:\n") + fmt.Fprintf(os.Stderr, " test-ns, test-args, test-lifecycle\n") } func (a *App) printProfileUsage() { -- cgit v1.2.3