package cmd import ( "context" "fmt" "os" "sort" "text/tabwriter" "mydev/internal/checks" "mydev/internal/config" "mydev/internal/run" "github.com/spf13/cobra" ) // devCompose loads config and returns the compose file path, exiting on error. func devCompose() string { cfg, err := config.Load() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } file, err := cfg.ComposeFile() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } return file } // compose runs `docker compose --file ` streamed to terminal. func compose(file string, args ...string) { full := append([]string{"compose", "--file", file}, args...) if err := run.Stream("", "docker", full...); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } // ensureAuthed re-auths first if the AWS session is gone (mirrors `myd auth`). func ensureAuthed() { cfg, err := config.Load() if err != nil { return } awsmfa, err := cfg.AWSMFA() if err != nil { return } if s := checks.AWSAuth(context.Background(), awsmfa); !s.Authed { fmt.Println("AWS session expired — re-authenticating…") _ = run.Stream("", awsmfa, "auth") } } var devCmd = &cobra.Command{ Use: "dev", Short: "Control the dev docker-compose stack", } var devStartCmd = &cobra.Command{ Use: "start", Short: "Auth, then start the stack (up -d --force-recreate)", Run: func(cmd *cobra.Command, args []string) { ensureAuthed() compose(devCompose(), "up", "-d", "--force-recreate") }, } var devStopCmd = &cobra.Command{ Use: "stop", Short: "Stop the stack", Run: func(cmd *cobra.Command, args []string) { compose(devCompose(), "stop") }, } var devRestartCmd = &cobra.Command{ Use: "restart", Short: "Stop, auth, then start again", Run: func(cmd *cobra.Command, args []string) { file := devCompose() compose(file, "stop") ensureAuthed() compose(file, "up", "-d", "--force-recreate") }, } var devRemoveCmd = &cobra.Command{ Use: "remove", Short: "Tear down the stack (down)", Run: func(cmd *cobra.Command, args []string) { compose(devCompose(), "down") }, } var devStatusFetch bool var devStatusCmd = &cobra.Command{ Use: "status", Short: "Check if running containers match the current dev repo", Run: func(cmd *cobra.Command, args []string) { ctx := context.Background() // Git freshness: is the dev repo itself behind origin? cfg, err := config.Load() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } if cfg.DevDir != "" { if devStatusFetch { fmt.Println("fetching dev repo…") } fr := checks.GitFreshness(ctx, cfg.DevDir, devStatusFetch) switch { case fr.Err != "": fmt.Printf("dev repo: ? (%s)\n\n", fr.Err) case fr.NoUpstream: fmt.Printf("dev repo: %s (no upstream)\n\n", fr.Branch) case fr.Behind > 0: fmt.Printf("dev repo: %s ✗ behind origin by %d — git pull to get latest defs\n\n", fr.Branch, fr.Behind) default: fmt.Printf("dev repo: %s ✓ up to date with origin\n\n", fr.Branch) } } file := devCompose() drift, errMsg := checks.ComposeDrift(ctx, file) if errMsg != "" { fmt.Fprintln(os.Stderr, errMsg) os.Exit(1) } sort.Slice(drift, func(i, j int) bool { return drift[i].Service < drift[j].Service }) stale := 0 w := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0) for _, d := range drift { mark := "✓" if !d.UpToDate() { mark = "✗" stale++ } fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", mark, d.Service, d.State, d.Reason()) } w.Flush() if stale > 0 { fmt.Printf("\n%d service(s) out of date — run 'mydev dev restart' to recreate\n", stale) os.Exit(1) } fmt.Println("\nall services up to date") }, } func init() { devStatusCmd.Flags().BoolVar(&devStatusFetch, "fetch", true, "git fetch the dev repo first (set --fetch=false to skip network)") devCmd.AddCommand(devStartCmd, devStopCmd, devRestartCmd, devRemoveCmd, devStatusCmd) rootCmd.AddCommand(devCmd) }