cmd

package
v0.12.0 Latest Latest
Warning

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

Go to latest
Published: Oct 5, 2025 License: MIT Imports: 34 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// Default config file paths
	DefaultConfigPathYML  = ".config/binstaller.yml"
	DefaultConfigPathYAML = ".config/binstaller.yaml"
)

Variables

View Source
var CheckCommand = &cobra.Command{
	Use:   "check",
	Short: "Check and validate an InstallSpec config file",
	Long: `Checks an InstallSpec configuration file by:
- Validating the configuration format and required fields
- Generating asset filenames for all configured platforms
- Verifying if assets exist in the GitHub release (default: enabled)
- Validating checksums template configuration

This helps validate your configuration before generating installer scripts.

Asset Status Meanings:
  ✓ EXISTS       - Asset generated from config exists in GitHub release
  ✗ MISSING      - Asset generated from config not found in release
  ✗ NO MATCH     - Release asset exists but doesn't match any configured platform
  ⚠ NOT SUPPORTED - Feature not supported (e.g., per-asset checksums)
  -              - Ignored file (docs, signatures, package formats like .deb/.dmg)

The unified table shows:
1. Configured platforms and their generated filenames
2. Checksums file status (if configured)
3. Unmatched release assets that might need configuration

Exit Codes:
  0 - All checks passed (no MISSING or NO MATCH statuses)
  1 - Configuration issues detected (MISSING assets or NO MATCH files)`,
	Example: `  # Check the default config file
  binst check

  # Check a specific config file
  binst check -c myapp.binstaller.yml

  # Check without verifying GitHub assets
  binst check --check-assets=false

  # Check with a specific version
  binst check --version v1.2.3

  # Ignore additional file patterns
  binst check --ignore "\.AppImage$" --ignore ".*-musl.*"`,
	RunE: func(cmd *cobra.Command, args []string) error {
		log.Info("Running check command...")

		cfgFile, err := resolveConfigFile(configFile)
		if err != nil {
			log.WithError(err).Error("Config file detection failed")
			return err
		}
		if configFile == "" {
			log.Infof("Using default config file: %s", cfgFile)
		}
		log.Debugf("Using config file: %s", cfgFile)

		installSpec, err := loadInstallSpec(cfgFile)
		if err != nil {
			return err
		}

		installSpec.SetDefaults()

		if err := validateSpec(installSpec); err != nil {
			log.WithError(err).Error("InstallSpec validation failed")
			return fmt.Errorf("validation failed: %w", err)
		}

		if err := spec.Validate(installSpec); err != nil {
			log.WithError(err).Error("Security validation failed")
			return fmt.Errorf("security validation failed: %w", err)
		}

		log.Info("✓ InstallSpec validation passed")

		log.Info("Generating asset filenames for all supported platforms...")

		version := checkVersion
		if version == "" {
			version = spec.StringValue(installSpec.DefaultVersion)
		}

		if checkCheckAssets && (version == "" || version == "latest") {
			ctx := context.Background()
			repo := spec.StringValue(installSpec.Repo)
			if repo != "" {
				resolvedVersion, err := resolveLatestVersion(ctx, repo)
				if err != nil {
					log.WithError(err).Warn("Failed to resolve latest version, using default")
					version = "1.0.0"
				} else {
					log.Infof("Resolved latest version: %s", resolvedVersion)
					version = resolvedVersion
				}
			}
		} else if version == "" || version == "latest" {
			version = "1.0.0"
		}

		assetFilenames, err := generateAllAssetFilenames(installSpec, version)
		if err != nil {
			log.WithError(err).Error("Failed to generate asset filenames")
			return fmt.Errorf("failed to generate asset filenames: %w", err)
		}

		if checkCheckAssets {
			log.Info("Checking if assets exist in GitHub release...")
			ctx := context.Background()

			if len(installSpec.SupportedPlatforms) == 0 {
				err := checkAssetsExistWithDetection(ctx, installSpec, version)
				if err != nil {
					log.WithError(err).Error("Asset availability check failed")
					return fmt.Errorf("asset availability check failed: %w", err)
				}
			} else {
				err := checkAssetsExist(ctx, installSpec, version, assetFilenames)
				if err != nil {
					log.WithError(err).Error("Asset availability check failed")
					return fmt.Errorf("asset availability check failed: %w", err)
				}
			}

		} else {

			displayAssetFilenames(assetFilenames)
		}

		log.Info("✓ Check completed successfully")
		return nil
	},
}

CheckCommand represents the check command

View Source
var EmbedChecksumsCommand = &cobra.Command{
	Use:   "embed-checksums",
	Short: "Embed checksums for release assets into a binstaller configuration",
	Long: `Reads an InstallSpec configuration file and embeds checksums for the assets.
This command supports three modes of operation:
- download: Fetches the checksum file from GitHub releases
- checksum-file: Uses a local checksum file
- calculate: Downloads the assets and calculates checksums directly`,
	Example: `  # Embed checksums by downloading checksum file from GitHub
  binst embed-checksums --version v1.0.0 --mode download

  # Embed checksums from a local checksum file
  binst embed-checksums --version v1.0.0 --mode checksum-file --file checksums.txt

  # Calculate checksums by downloading assets (GITHUB_TOKEN recommended)
  export GITHUB_TOKEN=$(gh auth token)
  binst embed-checksums --version v1.0.0 --mode calculate

  # Embed checksums for latest version
  binst embed-checksums --version latest --mode download

  # Embed checksums with custom config and output
  binst embed-checksums --config myapp.yml --version v2.0.0 --mode download -o myapp-checksums.yml

  # Typical workflow with embed-checksums
  binst init --source=github --repo=owner/repo
  binst embed-checksums --version v1.0.0 --mode download
  binst gen -o install.sh`,
	RunE: func(cmd *cobra.Command, args []string) error {
		log.Info("Running embed-checksums command...")

		cfgFile, err := resolveConfigFile(configFile)
		if err != nil {
			log.WithError(err).Error("Config file detection failed")
			return err
		}
		if configFile == "" {
			log.Infof("Using default config file: %s", cfgFile)
		}
		log.Debugf("Using config file: %s", cfgFile)

		log.Debugf("Reading InstallSpec from: %s", cfgFile)

		ast, err := parser.ParseFile(cfgFile, parser.ParseComments)
		if err != nil {
			return err
		}

		yamlData, err := os.ReadFile(cfgFile)
		if err != nil {
			log.WithError(err).Errorf("Failed to read install spec file: %s", cfgFile)
			return fmt.Errorf("failed to read install spec file %s: %w", cfgFile, err)
		}

		log.Debug("Unmarshalling InstallSpec YAML")
		var installSpec spec.InstallSpec
		err = yaml.UnmarshalWithOptions(yamlData, &installSpec, yaml.UseOrderedMap())
		if err != nil {
			log.WithError(err).Errorf("Failed to unmarshal install spec YAML from: %s", cfgFile)
			return fmt.Errorf("failed to unmarshal install spec YAML from %s: %w", cfgFile, err)
		}

		// Create the embedder
		var mode checksums.EmbedMode
		switch embedMode {
		case "download":
			mode = checksums.EmbedModeDownload
		case "checksum-file":
			mode = checksums.EmbedModeChecksumFile
		case "calculate":
			mode = checksums.EmbedModeCalculate
		default:
			return fmt.Errorf("invalid mode: %s. Must be one of: download, checksum-file, calculate", embedMode)
		}

		if mode == checksums.EmbedModeChecksumFile && embedFile == "" {
			log.Error("--file flag is required for checksum-file mode")
			return fmt.Errorf("--file flag is required for checksum-file mode")
		}

		embedder := &checksums.Embedder{
			Mode:         mode,
			Version:      embedVersion,
			Spec:         &installSpec,
			SpecAST:      ast,
			ChecksumFile: embedFile,
		}

		log.Infof("Embedding checksums using %s mode for version: %s", mode, embedVersion)
		if err := embedder.Embed(); err != nil {
			log.WithError(err).Error("Failed to embed checksums")
			return fmt.Errorf("failed to embed checksums: %w", err)
		}

		outputFile := embedOutput
		if outputFile == "" {
			outputFile = cfgFile
			log.Infof("No output specified, overwriting input file: %s", outputFile)
		}

		log.Infof("Writing updated InstallSpec to file: %s", outputFile)

		outputDir := filepath.Dir(outputFile)
		if err := os.MkdirAll(outputDir, 0755); err != nil {
			log.WithError(err).Errorf("Failed to create output directory: %s", outputDir)
			return fmt.Errorf("failed to create output directory %s: %w", outputDir, err)
		}

		if err := os.WriteFile(outputFile, []byte(ast.String()), 0644); err != nil {
			log.WithError(err).Errorf("Failed to write InstallSpec to file: %s", outputFile)
			return fmt.Errorf("failed to write InstallSpec to file %s: %w", outputFile, err)
		}
		log.Infof("InstallSpec successfully updated with embedded checksums")

		return nil
	},
}

EmbedChecksumsCommand represents the embed-checksums command

View Source
var GenCommand = &cobra.Command{
	Use:   "gen",
	Short: "Generate an installer script from an InstallSpec config file",
	Long: `Reads an InstallSpec configuration file (e.g., .binstaller.yml) and
generates a POSIX-compatible shell installer script.`,
	Example: `  # Generate installer script using default config
  binst gen

  # Generate installer with custom output file
  binst gen -o install.sh

  # Generate runner script (runs binary without installing)
  binst gen --type=runner -o run.sh

  # Generate runner for specific binary (when multiple binaries exist)
  binst gen --type=runner --binary=mytool-helper -o run-helper.sh

  # Run binary directly using runner script (all arguments pass to binary)
  ./run.sh --help
  ./run.sh --version

  # Control runner script with environment variables
  BINSTALLER_TARGET_TAG=v1.2.3 ./run.sh --help  # Use specific version
  BINSTALLER_SHOW_HELP=1 ./run.sh                # Show script help

  # Generate installer from specific config file
  binst gen --config myapp.binstaller.yml -o myapp-install.sh

  # Generate installer from stdin
  cat myapp.binstaller.yml | binst gen --config - -o install.sh

  # Generate installer for a specific version only
  binst gen --target-version v1.2.3 -o install-v1.2.3.sh

  # Generate runner for specific version
  binst gen --type=runner --target-version v1.2.3 -o run-v1.2.3.sh

  # Typical workflow with init and gen
  binst init --source=github --repo=owner/repo
  binst gen -o install.sh

  # Generate and execute installer script directly
  binst gen | sh

  # View generated script's help
  binst gen | sh -s -- -h

  # Install to custom directory
  binst gen | sh -s -- -b /usr/local/bin

  # Install specific version
  binst gen | sh -s -- v1.2.3

  # Run generated runner script with environment control
  binst gen --type=runner | BINSTALLER_TARGET_TAG=v1.2.3 sh

  # Test installer with dry run mode
  binst gen | sh -s -- -n`,
	RunE: func(cmd *cobra.Command, args []string) error {
		log.Info("Running gen command...")

		if err := validateScriptType(genScriptType); err != nil {
			log.WithError(err).Error("Invalid script type")
			return err
		}
		if genScriptType == "" {
			genScriptType = "installer"
		}

		cfgFile, err := resolveConfigFile(configFile)
		if err != nil {
			log.WithError(err).Error("Config file detection failed")
			return err
		}
		if configFile == "" {
			log.Infof("Using default config file: %s", cfgFile)
		}
		log.Debugf("Using config file: %s", cfgFile)

		installSpec, err := loadInstallSpec(cfgFile)
		if err != nil {
			return err
		}

		if err := handleRunnerBinarySelection(installSpec, genScriptType, genBinaryName); err != nil {
			return err
		}

		log.Infof("Generating %s script...", genScriptType)
		scriptBytes, err := shell.GenerateWithScriptType(installSpec, genTargetVersion, genScriptType)
		if err != nil {
			log.WithError(err).Errorf("Failed to generate %s script", genScriptType)
			return fmt.Errorf("failed to generate %s script: %w", genScriptType, err)
		}
		log.Debugf("%s script generated successfully", genScriptType)

		return writeScript(scriptBytes, genOutputFile, genScriptType)
	},
}

GenCommand represents the gen command

View Source
var HelpfulCommand = &cobra.Command{
	Use:   "helpful",
	Short: "Display comprehensive help for all commands",
	Long: `Displays help information for all binstaller commands in a single, styled output.
This is especially useful for getting a complete overview of the tool's capabilities,
including for LLMs or automated documentation tools.`,
	RunE: func(cmd *cobra.Command, args []string) error {
		config := &HelpfulConfig{
			SkipFunc: func(c *cobra.Command) bool {

				if c == cmd {
					return true
				}
				return defaultSkipFunc(c)
			},
			Output: os.Stdout,
		}
		return RunHelpful(cmd, config)
	},
}

HelpfulCommand represents the helpful command

View Source
var InitCommand = &cobra.Command{
	Use:   "init",
	Short: "Generate an InstallSpec config file from various sources",
	Long: `Initializes a binstaller configuration file (.config/binstaller.yml) by detecting
settings from a source like a GoReleaser config file or a GitHub repository.`,
	Example: `  # Initialize from GitHub releases
  binst init --source=github --repo=junegunn/fzf

  # Initialize from local GoReleaser config
  binst init --source=goreleaser --file=.goreleaser.yml

  # Initialize from GoReleaser config in a GitHub repo
  binst init --source=goreleaser --repo=owner/repo

  # Initialize from GoReleaser with specific commit SHA
  binst init --source=goreleaser --repo=owner/repo --sha=abc123

  # Initialize from Aqua registry for a specific package
  binst init --source=aqua --repo=junegunn/fzf

  # Initialize from Aqua registry with custom output file
  binst init --source=aqua --repo=junegunn/fzf -o fzf.binstaller.yml

  # Initialize from local Aqua registry file
  binst init --source=aqua --file=path/to/registry.yaml

  # Initialize from Aqua registry via stdin
  cat registry.yaml | binst init --source=aqua --file=-

  # Initialize and overwrite existing config without confirmation
  binst init --source=github --repo=junegunn/fzf --force`,
	RunE: func(cmd *cobra.Command, args []string) error {
		log.Infof("Running init command...")

		var adapter datasource.SourceAdapter

		switch initSource {
		case "goreleaser":
			adapter = datasource.NewGoReleaserAdapter(
				initRepo,
				initSourceFile,
				initCommitSHA,
				initName,
			)
		case "github":
			adapter = datasource.NewGitHubAdapter(initRepo)
		case "aqua":

			switch initSourceFile {
			case "":

				if initRepo == "" {
					return fmt.Errorf("--repo is required for aqua source when --file is not specified")
				}
				adapter = datasource.NewAquaRegistryAdapterFromRepo(initRepo, initCommitSHA)
			case "-":

				adapter = datasource.NewAquaRegistryAdapterFromReader(os.Stdin)
			default:

				f, err := os.Open(initSourceFile)
				if err != nil {
					return fmt.Errorf("failed to open aqua registry file: %w", err)
				}
				defer f.Close()
				adapter = datasource.NewAquaRegistryAdapterFromReader(f)
			}
		default:
			err := fmt.Errorf("unknown source specified: %s. Valid sources are: goreleaser, github, aqua", initSource)
			log.WithError(err).Error("invalid source")
			return err
		}

		ctx := context.Background()

		log.Infof("Generating InstallSpec using source: %s", initSource)
		installSpec, err := adapter.GenerateInstallSpec(ctx)
		if err != nil {
			log.WithError(err).Error("Failed to detect install spec")
			return fmt.Errorf("failed to detect install spec: %w", err)
		}
		if spec.StringValue(installSpec.Schema) == "" {
			installSpec.Schema = spec.StringPtr("v1")
		}
		log.Info("Successfully detected InstallSpec")

		log.Debug("Marshalling InstallSpec to YAML")
		yamlData, err := yaml.Marshal(installSpec)
		if err != nil {
			log.WithError(err).Error("Failed to marshal InstallSpec to YAML")
			return fmt.Errorf("failed to marshal install spec to YAML: %w", err)
		}

		schemaComment := "# yaml-language-server: $schema=https://raw.githubusercontent.com/binary-install/binstaller/main/schema/InstallSpec.json\n"
		yamlData = append([]byte(schemaComment), yamlData...)

		if initOutputFile == "" || initOutputFile == "-" {

			log.Debug("Writing InstallSpec YAML to stdout")
			fmt.Println(string(yamlData))
			log.Info("InstallSpec YAML written to stdout")
		} else {

			log.Infof("Writing InstallSpec YAML to file: %s", initOutputFile)

			if _, err := os.Stat(initOutputFile); err == nil {

				if !initForce {
					message := fmt.Sprintf("File %s already exists. Overwrite?", initOutputFile)
					if !promptForConfirmation(message) {
						log.Info("Operation cancelled by user")
						return fmt.Errorf("operation cancelled: file %s already exists", initOutputFile)
					}
				}
				log.Infof("Overwriting existing file: %s", initOutputFile)
			}

			outputDir := filepath.Dir(initOutputFile)
			if err := os.MkdirAll(outputDir, 0755); err != nil {
				log.WithError(err).Errorf("Failed to create output directory: %s", outputDir)
				return fmt.Errorf("failed to create output directory %s: %w", outputDir, err)
			}

			err = os.WriteFile(initOutputFile, yamlData, 0644)
			if err != nil {
				log.WithError(err).Errorf("Failed to write InstallSpec to file: %s", initOutputFile)
				return fmt.Errorf("failed to write install spec to file %s: %w", initOutputFile, err)
			}
			log.Infof("InstallSpec successfully written to %s", initOutputFile)
		}

		return nil
	},
}

InitCommand represents the init command

View Source
var InstallCommand = &cobra.Command{
	Use:   "install [VERSION]",
	Short: "Install a binary directly from GitHub releases",
	Long: `Install a binary directly from GitHub releases, achieving script-parity with the generated shell installers.

This command provides a native Go implementation of the installation process, supporting version resolution, checksum verification, and cross-platform binary installation.`,
	Example: `  # Install latest version
  binst install

  # Install specific version
  binst install v1.2.3

  # Install to custom directory
  binst install --bin-dir=/usr/local/bin

  # Dry run mode (verify URLs/versions without installing)
  binst install --dry-run`,
	Args: cobra.MaximumNArgs(1),
	RunE: runInstall,
}

InstallCommand represents the install command

View Source
var RootCmd = &cobra.Command{
	Use:   "binst",
	Short: "Config-driven secure shell-script installer generator",
	Long: `binstaller (binst) is a config-driven secure shell-script installer generator that
creates reproducible installation scripts for static binaries distributed via GitHub releases.

It works with Go binaries, Rust binaries, and any other static binaries - as long as they're
released on GitHub, binstaller can generate installation scripts for them.`,
	PersistentPreRun: func(cmd *cobra.Command, args []string) {
		log.SetHandler(cli.Default)
		if verbose {
			log.SetLevel(log.DebugLevel)
			log.Debugf("Verbose logging enabled")
		} else if quiet {
			log.SetLevel(log.ErrorLevel)
		} else {
			log.SetLevel(log.InfoLevel)
		}
		log.Debugf("Config file: %s", configFile)
	},
}

RootCmd represents the base command when called without any subcommands

View Source
var SchemaCommand = &cobra.Command{
	Use:   "schema",
	Short: "Display configuration schema",
	Long: `Display binstaller configuration schema directly from the CLI.

This command shows the binstaller configuration schema in various formats.
For filtering and processing, use yq or jq tools on the output.`,
	Example: `  # Display schema in YAML format (default)
  binst schema

  # Display schema in JSON format
  binst schema --format json

  # Display original TypeSpec source
  binst schema --format typespec

  # List all available schema types
  binst schema | yq '."$defs" | keys'

  # Filter specific type definitions
  binst schema | yq '."$defs".AssetConfig'

  # Get only the root schema (without type definitions)
  binst schema | yq 'del(."$defs")'

  # Use jq for JSON processing
  binst schema --format json | jq '."$defs".Platform'

  # Get list of supported platform os/arch combinations
  binst schema --format json | jq '.["$defs"].Platform.properties.os.anyOf[].const'
  binst schema --format json | jq '.["$defs"].Platform.properties.arch.anyOf[].const'`,
	RunE: func(cmd *cobra.Command, args []string) error {
		format, _ := cmd.Flags().GetString("format")
		return RunSchema(format, os.Stdout)
	},
}

SchemaCommand represents the schema command

Functions

func Execute

func Execute()

Execute adds all child commands to the root command and sets flags appropriately. This is called by main.main(). It only needs to happen once to the RootCmd.

func RunHelpful added in v0.3.1

func RunHelpful(cmd *cobra.Command, config *HelpfulConfig) error

RunHelpful executes the helpful command with the given configuration

func RunSchema added in v0.4.0

func RunSchema(format string, output interface{}) error

RunSchema executes the schema command with the given parameters

Types

type BinaryInfo added in v0.9.2

type BinaryInfo struct {
	Name string
	Path string
}

BinaryInfo holds information about a binary to install

type GitHubRelease added in v0.9.2

type GitHubRelease struct {
	TagName string `json:"tag_name"`
	Name    string `json:"name"`
}

GitHubRelease represents the GitHub API response for a release

type HelpfulConfig added in v0.3.1

type HelpfulConfig struct {
	// SkipFunc determines if a command should be skipped
	SkipFunc func(cmd *cobra.Command) bool
	// Output writer
	Output io.Writer
}

HelpfulConfig configures the helpful command behavior

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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