cmd

package
v0.0.0-...-e7b98f1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 2, 2025 License: MIT Imports: 36 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var CleanupCmd = &cobra.Command{
	Use:   "cleanup",
	Short: "Clean up old extension images and containers to free disk space",
	Long: `Remove old versions of extension Docker images and stopped containers to reclaim disk space.

By default, keeps only the most recent version of each extension image.
Use --keep to retain more versions, or --all to also clean non-extension images.
Use --containers to also clean up stopped containers.`,
	Example: `  # Remove all but the latest version of each extension
  r2r cleanup

  # Keep 3 versions of each extension
  r2r cleanup --keep 3

  # Also clean up stopped containers
  r2r cleanup --containers

  # Clean only extension containers
  r2r cleanup --containers --extensions-only

  # Clean containers older than 24 hours
  r2r cleanup --containers --older-than 24h

  # Show what would be removed without actually removing
  r2r cleanup --dry-run

  # Clean all Docker images (not just extensions)
  r2r cleanup --all`,
	Run: func(cmd *cobra.Command, args []string) {
		conf.InitConfig()

		if cleanupContainers {
			cleanupDockerContainers()
			logging.Info("")
		}

		if cleanupAll {
			cleanAllDockerImages()
		} else {
			cleanExtensionImages()
		}
	},
}
View Source
var InitCmd = &cobra.Command{
	Use:   "init",
	Short: "Initialize a new r2r-cli configuration file",
	Long:  `Creates a minimal .r2r/r2r-cli.yml configuration file in the repository.`,
	Run: func(cmd *cobra.Command, args []string) {
		createConfigFile(cmd)
	},
}
View Source
var InstallCmd = &cobra.Command{
	Use:   "install [extension-name]",
	Short: "Install configured extensions or add and install new ones",
	Long: `Install extensions by pulling their Docker images.

When no extension name is provided, installs all configured extensions.
When an extension name is provided, adds it to the configuration with the latest SHA tag and installs it.

Examples:
  # Install all configured extensions
  r2r install
  
  # Add and install a specific extension
  r2r install pwsh
  r2r install python
  r2r install go
  
  # Install with local development images
  r2r install pwsh --load-local`,
	Run: func(cmd *cobra.Command, args []string) {

		if len(args) > 0 {
			extensionName := args[0]

			if err := addExtensionToConfig(extensionName); err != nil {
				logging.Errorf("Failed to add extension to config: %v", err)
				os.Exit(1)
			}
			logging.Infof("✅ Added %s to configuration", extensionName)
		} else {

			repoRoot, err := conf.FindRepositoryRoot()
			if err != nil {
				logging.Errorf("Failed to find repository root: %v", err)
				os.Exit(1)
			}

			configPaths := []string{
				filepath.Join(repoRoot, ".r2r", "r2r-cli.yml"),
				filepath.Join(repoRoot, ".r2r", "r2r-cli.yaml"),
			}
			configFound := false
			for _, cp := range configPaths {
				if _, err := os.Stat(cp); err == nil {
					configFound = true
					break
				}
			}
			if !configFound {
				configPath := filepath.Join(repoRoot, ".r2r", "r2r-cli.yml")
				logging.Error("❌ No configuration file found.")
				logging.Infof("To install all configured extensions, you need a configuration file at: %s", configPath)
				logging.Info("\nTo get started:")
				logging.Info("  • Run 'r2r init' to create a configuration file")
				logging.Info("  • Or install a specific extension: 'r2r install <extension-name>'")
				logging.Info("\nExamples:")
				logging.Info("  r2r install pwsh")
				logging.Info("  r2r install python")
				os.Exit(1)
			}
		}

		conf.InitConfig()

		loadLocal, _ := cmd.Flags().GetBool("load-local")
		var originalLoadLocal bool
		if loadLocal {
			originalLoadLocal = conf.Global.LoadLocal
			conf.Global.LoadLocal = true
			logging.Debugf("Temporarily overriding load_local setting from --load-local flag: load_local=%v", true)
		}
		defer func() {
			if loadLocal {
				conf.Global.LoadLocal = originalLoadLocal
				logging.Debugf("Restored original load_local setting: load_local=%v", originalLoadLocal)
			}
		}()

		installer, err := extensions.NewInstaller()
		if err != nil {
			logging.Errorf("Failed to create extension installer: %v", err)
			os.Exit(1)
		}
		defer installer.Close()

		// Determine which extensions to install
		var extsToInstall []conf.Extension
		if len(args) > 0 {

			extensionName := args[0]

			found := false
			for _, ext := range conf.Global.Extensions {
				if ext.Name == extensionName {
					extsToInstall = append(extsToInstall, ext)
					found = true
					break
				}
			}

			if !found {
				logging.Errorf("❌ Extension %s not found in configuration", extensionName)
				os.Exit(1)
			}
		} else {

			extsToInstall = conf.Global.Extensions
			if len(extsToInstall) == 0 {
				logging.Error("❌ No extensions configured. Add an extension with:")
				logging.Info("  r2r install <extension-name>")
				logging.Info("\nExamples:")
				logging.Info("  r2r install pwsh")
				logging.Info("  r2r install python")
				os.Exit(1)
			}
		}

		logging.Infof("📦 Installing %d extension(s)...", len(extsToInstall))

		successCount := 0
		for _, ext := range extsToInstall {
			logging.Infof("\n🔧 Installing %s...", ext.Name)

			pulled, err := installer.EnsureExtensionImage(ext.Name)
			if err != nil {
				logging.Errorf("Failed to install extension: extension=%s error=%v", ext.Name, err)
				logging.Errorf("❌ Failed to install %s: %v", ext.Name, err)
			} else {
				if pulled {
					logging.Infof("✅ %s installed (new image pulled)", ext.Name)
				} else {
					logging.Infof("✅ %s already up to date", ext.Name)
				}
				successCount++
			}
		}

		if successCount == len(extsToInstall) {
			logging.Info("\n✅ All extensions installed successfully")
		} else {
			logging.Warnf("\n⚠️  %d of %d extensions installed successfully", successCount, len(extsToInstall))
			os.Exit(1)
		}
	},
}
View Source
var InteractiveCmd = &cobra.Command{
	Use:   "interactive <extension>",
	Short: "Start an extension container in interactive mode",
	Long:  `Start an extension container in interactive mode with shell access.`,
	Args:  cobra.ExactArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		conf.InitConfig()

		host, err := docker.NewContainerHost()
		if err != nil {
			cmd.PrintErrln(err)
			os.Exit(1)
		}
		defer host.Close()

		if err := host.ValidateExtensions(); err != nil {
			cmd.PrintErrln(err)
			os.Exit(1)
		}

		cmd.Println("Root directory found:", host.GetRootDir())

		extensionName := args[0]
		ext, err := host.FindExtension(extensionName)
		if err != nil {
			cmd.PrintErrln(err)
			os.Exit(1)
		}
		cmd.Println("Loading extension image:", ext.Image)

		if err := host.EnsureImageExists(ext.Image, ext.ImagePullPolicy, ext.LoadLocal); err != nil {
			cmd.PrintErrf("Error ensuring image exists: %v\n", err)
			os.Exit(1)
		}

		imageInspect, err := host.InspectImage(ext.Image)
		if err != nil {
			cmd.PrintErrln(err)
			os.Exit(1)
		}

		// Get extension metadata for volume mounts and env vars
		var volumeRequests []cache.VolumeRequest
		extMeta, err := host.GetExtensionMetadata(ext)
		if err == nil && extMeta != nil {
			if len(extMeta.Volumes) > 0 {
				volumeRequests = extMeta.Volumes
			}

			docker.MergeMetadataEnv(ext, extMeta)
		}

		containerConfig := host.CreateContainerConfig(ext, docker.ModeInteractive, nil, imageInspect)
		hostConfig := host.CreateHostConfig(ext, volumeRequests)

		containerID, err := host.CreateContainer(containerConfig, hostConfig)
		if err != nil {
			cmd.PrintErrln(err)
			os.Exit(1)
		}

		if err := host.StartContainer(containerID); err != nil {
			cmd.PrintErrln(err)
			os.Exit(1)
		}

		cmd.Printf("Starting interactive session for extension '%s'...\n", extensionName)
		cmd.Println("Type 'exit' to quit the interactive session.")

		signalChan := make(chan os.Signal, 1)
		signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)

		go func() {
			sig := <-signalChan

			logging.Debugf("Received interrupt signal, stopping container gracefully: signal=%s container_id=%s", sig.String(), containerID)

			stopCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
			defer cancel()

			if err := host.StopContainerWithContext(stopCtx, containerID); err != nil {
				logging.Warnf("Failed to stop container gracefully: %v, forcing termination", err)

				host.StopContainer(containerID)
			} else {
				logging.Debug("Container stopped gracefully")
			}
			os.Exit(0)
		}()

		if len(imageInspect.Config.Entrypoint) > 0 {

			execCmd := exec.Command("docker", "attach", containerID)
			execCmd.Stdin = os.Stdin
			execCmd.Stdout = os.Stdout
			execCmd.Stderr = os.Stderr

			if err := execCmd.Run(); err != nil {
				cmd.PrintErrln("Error attaching to container:", err)
				os.Exit(1)
			}
		} else {

			execCmd := exec.Command("docker", "exec", "-it", containerID, "/bin/sh")
			execCmd.Stdin = os.Stdin
			execCmd.Stdout = os.Stdout
			execCmd.Stderr = os.Stderr

			if err := execCmd.Run(); err != nil {
				cmd.PrintErrln("Error running interactive session:", err)
				os.Exit(1)
			}
		}

		cmd.Println("Interactive session ended.")
	},
}
View Source
var MetadataCmd = &cobra.Command{
	Use:   "metadata <extension>",
	Short: "Retrieve metadata from an extension",
	Long:  `Retrieve metadata from an extension by executing its extension-meta command.`,
	Args:  cobra.ExactArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		conf.InitConfig()

		host, err := docker.NewContainerHost()
		if err != nil {
			cmd.PrintErrf("Error creating container host: %v\n", err)
			os.Exit(1)
		}
		defer host.Close()

		if err := host.ValidateExtensions(); err != nil {
			cmd.PrintErrf("Error: %v\n", err)
			os.Exit(1)
		}

		extensionName := args[0]
		ext, err := host.FindExtension(extensionName)
		if err != nil {
			cmd.PrintErrf("Error: %v\n", err)
			os.Exit(1)
		}

		cmd.PrintErrln("Retrieving metadata from extension:", ext.Name)
		cmd.PrintErrln("Image:", ext.Image)

		if err := host.EnsureImageExists(ext.Image, ext.ImagePullPolicy, ext.LoadLocal); err != nil {
			cmd.PrintErrf("Error ensuring image exists: %v\n", err)
			os.Exit(1)
		}

		output, err := host.ExecuteMetadataCommand(ext)
		if err != nil {
			cmd.PrintErrf("Error retrieving metadata: %v\n", err)
			os.Exit(1)
		}

		fmt.Fprint(cmd.OutOrStdout(), output)
	},
}
View Source
var RootCmd = &cobra.Command{
	Use:   "r2r",
	Short: "Ready to Release - Enterprise-grade automation framework",
	Long:  `r2r CLI standardizes and containerizes development workflows through a portable, scalable automation framework.`,
	PersistentPreRunE: func(cmd *cobra.Command, args []string) error {

		parsedCmd, err := GetParsedCommand()
		if err != nil {
			return fmt.Errorf("command parsing failed: %w", err)
		}

		if logging.IsDebugEnabled() {
			logging.Debugf("Parsed command: subcommand=%s, extension=%s, viper_args=%v, container_args=%v",
				parsedCmd.Subcommand, parsedCmd.ExtensionName, parsedCmd.ViperArgs, parsedCmd.ContainerArgs)
		}

		debug, err := cmd.Flags().GetBool("r2r-debug")
		if err != nil {
			return fmt.Errorf("failed to get r2r-debug flag: %w", err)
		}
		quiet, err := cmd.Flags().GetBool("r2r-quiet")
		if err != nil {
			return fmt.Errorf("failed to get r2r-quiet flag: %w", err)
		}
		if debug && quiet {
			return fmt.Errorf("cannot use both debug and quiet flags")
		}

		logLevel := "info"

		if envLogLevel := os.Getenv("R2R_LOG_LEVEL"); envLogLevel != "" {
			logLevel = envLogLevel
		}

		if debug {
			logLevel = "debug"
		} else if quiet {
			logLevel = "error"
		}

		if err := logging.SetLevel(logLevel); err != nil {
			return fmt.Errorf("failed to set log level: %w", err)
		}

		if os.Getenv("R2R_FIXED_REDIRECT") == "true" {
			logging.Warnf("Fixed bash redirect pollution in arguments (removed spurious '2' from '2>&1'): original=%s filtered=%s",
				os.Getenv("R2R_ORIGINAL_ARGS"), os.Getenv("R2R_FILTERED_ARGS"))

			os.Unsetenv("R2R_FIXED_REDIRECT")
			os.Unsetenv("R2R_ORIGINAL_ARGS")
			os.Unsetenv("R2R_FILTERED_ARGS")
		}

		logging.Debugf("Executing command: cmd=%s args=%v version=%s os=%s arch=%s",
			cmd.Name(), args, version.Version, runtime.GOOS, runtime.GOARCH)

		return nil
	},
	Run: func(cmd *cobra.Command, args []string) {
		if err := cmd.Help(); err != nil {
			logging.Errorf("Failed to show help: %v", err)
		}
	},
}

RootCmd is the base command for the r2r CLI when called without any subcommands

View Source
var RunCmd = &cobra.Command{
	Use:                "run <extension> [args...]",
	Short:              "Run an extension from the config",
	Long:               `Run an extension using its configured Docker image.`,
	DisableFlagParsing: true,
	Run: func(cmd *cobra.Command, args []string) {

		if len(args) > 0 && (args[0] == "--help" || args[0] == "-h") {
			cmd.Help()
			return
		}

		if len(args) == 0 {
			cmd.Help()
			return
		}

		parsedCmd, _ := GetParsedCommand()

		extensionName := args[0]
		containerArgs := args[1:]

		if parsedCmd != nil && parsedCmd.Subcommand == "run" {
			if parsedCmd.ExtensionName != "" {
				extensionName = parsedCmd.ExtensionName
			}
			if len(parsedCmd.ContainerArgs) > 0 || parsedCmd.ArgumentBoundary > 0 {

				containerArgs = parsedCmd.ContainerArgs
			}
		}

		logging.Debugf("Running extension: extension=%s args=%v parsed_boundary=%d", extensionName, containerArgs, parsedCmd.ArgumentBoundary)

		if len(containerArgs) == 0 {
			logging.Debug("No arguments provided, switching to interactive mode")

			InteractiveCmd.Run(cmd, []string{extensionName})
			return
		}

		conf.InitConfig()

		logging.Debug("Creating extension installer")
		installer, err := extensions.NewInstaller()
		if err != nil {
			logging.Errorf("Failed to create extension installer: %v", err)
			os.Exit(1)
		}
		defer installer.Close()

		host := installer.GetContainerHost()

		logging.Debug("Validating extensions")
		if err := host.ValidateExtensions(); err != nil {
			logging.Errorf("Extension validation failed: %v", err)
			os.Exit(1)
		}

		logging.Debugf("Root directory found: root_dir=%s", host.GetRootDir())

		logging.Debugf("Available extensions in config: extension_count=%d", len(conf.Global.Extensions))
		for _, ext := range conf.Global.Extensions {
			logging.Debugf("Extension found in config: name=%s image=%s", ext.Name, ext.Image)
		}

		logging.Debugf("Finding extension: extension=%v", extensionName)
		ext, err := host.FindExtension(extensionName)
		if err != nil {
			logging.Errorf("Extension '%s' not found", extensionName)

			os.Stdout.Sync()
			os.Stderr.Sync()
			os.Exit(1)
		}
		logging.Debugf("Loading extension image: image=%s", ext.Image)

		beforeSnapshot, err := host.GetContainerSnapshot()
		if err != nil {
			logging.Debugf("Failed to take container snapshot before run: error=%v", err)
			beforeSnapshot = make(map[string]string)
		}

		logging.Debugf("Ensuring image exists: image=%s pull_policy=%s", ext.Image, ext.ImagePullPolicy)
		if _, err := installer.EnsureExtensionImage(extensionName); err != nil {
			logging.Errorf("Error ensuring image exists: %v", err)
			os.Exit(1)
		}

		logging.Debugf("Inspecting image: image=%v", ext.Image)
		imageInspect, err := host.InspectImage(ext.Image)
		if err != nil {
			logging.Errorf("Failed to inspect image '%s': %v", ext.Image, err)
			os.Exit(1)
		}

		// Get extension metadata for volume mounts and env vars
		var volumeRequests []cache.VolumeRequest
		extMeta, err := host.GetExtensionMetadata(ext)
		if err != nil {
			logging.Debugf("Failed to get extension metadata, continuing without metadata")
		} else if extMeta != nil {
			if len(extMeta.Volumes) > 0 {
				volumeRequests = extMeta.Volumes
				logging.Debugf("Loaded volume requests from extension metadata: extension=%s volumes=%d", ext.Name, len(volumeRequests))
			}

			docker.MergeMetadataEnv(ext, extMeta)
		}

		containerConfig := host.CreateContainerConfig(ext, docker.ModeRun, containerArgs, imageInspect)
		hostConfig := host.CreateHostConfig(ext, volumeRequests)

		logging.Debug("Creating container")
		containerID, err := host.CreateContainer(containerConfig, hostConfig)
		if err != nil {
			logging.Errorf("Failed to create container: %v", err)
			os.Exit(1)
		}
		logging.Debugf("Container created: container_id=%v", containerID)

		logging.Debugf("Attaching to container: container_id=%v", containerID)
		attachResp, err := host.AttachToContainer(containerID)
		if err != nil {
			logging.Errorf("Failed to attach to container %s: %v", containerID, err)
			os.Exit(1)
		}
		defer attachResp.Close()

		logging.Debugf("Setting up container wait: container_id=%v", containerID)
		statusCh, errCh := host.WaitForContainer(containerID)

		logging.Debugf("Starting container: container_id=%v", containerID)
		if err := host.StartContainer(containerID); err != nil {
			logging.Errorf("Failed to start container %s: %v", containerID, err)
			os.Exit(1)
		}

		signalChan := make(chan os.Signal, 1)
		signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)

		shuttingDown := false

		go func() {
			sig := <-signalChan
			shuttingDown = true

			logging.Debugf("Received interrupt signal, stopping container gracefully: signal=%s container_id=%s", sig.String(), containerID)

			go func() {

				if docker.IsRunningInContainer() {
					logging.Debug("Detected Docker-in-Docker, cleaning up child containers")
					if err := host.CleanupChildContainers(); err != nil {
						logging.Warnf("Failed to clean up some child containers: error=%v", err)
					}
				}

				stopCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
				defer cancel()

				if err := host.StopContainerWithContext(stopCtx, containerID); err != nil {
					logging.Warnf("Failed to stop container gracefully, forcing termination: container_id=%s error=%v", containerID, err)

					if err := host.StopContainer(containerID); err != nil {
						logging.Errorf("Failed to force stop container: %v", err)
					}
				} else {
					logging.Debugf("Container stopped gracefully: container_id=%s", containerID)
				}
			}()

			time.Sleep(100 * time.Millisecond)
			os.Exit(130)
		}()

		done := make(chan error, 1)
		if containerConfig.Tty {

			go func() {

				ansiFilter := docker.NewAnsiFilter(os.Stdout)
				_, err := io.Copy(ansiFilter, attachResp.Reader)
				done <- err
			}()
		} else {

			go func() {
				_, err := stdcopy.StdCopy(os.Stdout, os.Stderr, attachResp.Reader)
				done <- err
			}()
		}

		if containerConfig.OpenStdin {
			go func() {
				defer func() {

					if conn, ok := attachResp.Conn.(interface {
						CloseWrite() error
					}); ok {
						conn.CloseWrite()
					}
				}()

				_, err := io.Copy(attachResp.Conn, os.Stdin)
				if err != nil && err != io.EOF {
					logging.Debugf("stdin copy error: %v", err)
				}
			}()
		}

		logging.Debugf("Waiting for container to finish: container_id=%v", containerID)

		// Wait for container completion
		var containerExitCode int64

		select {
		case status := <-statusCh:
			logging.Debugf("Container finished: container_id=%s status_code=%d", containerID, status.StatusCode)
			containerExitCode = status.StatusCode
		case err := <-errCh:
			if err != nil {
				errStr := err.Error()
				if !strings.Contains(errStr, "No such container") && errStr != "" {
					logging.Errorf("Error waiting for container: container_id=%s error=%s", containerID, errStr)
					os.Exit(1)
				}
			}
		}

		if shuttingDown {
			os.Exit(0)
		}

		ioErr := <-done
		if ioErr != nil && ioErr != io.EOF {
			logging.Debugf("I/O error: error=%v", ioErr)
		}
		logging.Debug("I/O copy completed")

		afterSnapshot, err := host.GetContainerSnapshot()
		if err != nil {
			logging.Debugf("Failed to take container snapshot after run: error=%v", err)
		} else {
			// Get expected host images from extension metadata (for serve commands, etc.)
			var expectedHostImages []string
			if extMeta != nil {
				expectedHostImages = extMeta.ExpectedHostImages
			}
			host.WarnAboutNewContainers(beforeSnapshot, afterSnapshot, ext.Image, ext.AutoRemoveChildren, expectedHostImages)
		}

		if docker.IsRunningInContainer() {
			logging.Debug("Cleaning up any remaining child containers before exit")
			if err := host.CleanupChildContainers(); err != nil {
				logging.Warnf("Failed to clean up some child containers: error=%v", err)
			}
		}

		if !shuttingDown && containerExitCode != 0 {
			os.Exit(int(containerExitCode))
		}

	},
}
View Source
var VerifyCmd = &cobra.Command{
	Use:   "verify",
	Short: "Verify system prerequisites",
	Long:  `Verifies that GitHub authentication and Docker service are working properly.`,
	Run: func(cmd *cobra.Command, args []string) {
		verifySystem(cmd)
	},
}
View Source
var Version string

Version is set at build time via ldflags

Functions

func CreateExtensionAliases

func CreateExtensionAliases()

CreateExtensionAliases creates direct command aliases for configured extensions This allows users to run "r2r pwsh" instead of "r2r run pwsh"

func Execute

func Execute()

func GetContainerArgs

func GetContainerArgs() []string

GetContainerArgs returns the container arguments from the parsed command This is used by the run command to get arguments that should be passed to the container

func GetExtensionName

func GetExtensionName() string

GetExtensionName returns the extension name from the parsed command

func GetParsedCommand

func GetParsedCommand() (*commandparser.ParsedCommand, error)

GetParsedCommand returns the parsed command structure This is populated during the root command's PersistentPreRun

func GetViperArgs

func GetViperArgs() []string

GetViperArgs returns the arguments that should be processed by Viper/Cobra

func InitializeExtensionAliases

func InitializeExtensionAliases()

InitializeExtensionAliases should be called after config is loaded but before command execution

func IsRunCommand

func IsRunCommand() bool

IsRunCommand checks if the current command is a run command

Types

type Release

type Release struct {
	TagName string `json:"tag_name"`
	Assets  []struct {
		Name        string `json:"name"`
		DownloadURL string `json:"browser_download_url"`
		ID          int    `json:"id"`
		Size        int    `json:"size"`
	} `json:"assets"`
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL