mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-08-17 21:25:49 +02:00
Org (tenant) layer - New Org model with org_uuid denormalized onto workspaces, assets, vulnerabilities and runs so cross-workspace queries need no join - Automatic attribution via BeforeAppendModel hooks; importers stay org-unaware - Read semantics: empty org means no filter (backward compatible) Write semantics: empty org coerced to the default org - Migration backfills every pre-existing row into the default org - CLI: osmedeus org create/show/assign/use/rename/delete - API: /osm/api/orgs CRUD plus ?org= on assets, vulns, runs and workspaces npm distribution - npm install -g @j3ssie/osmedeus ships the Go binary through npm - One npm name with version-suffixed platform builds pulled in as aliased optionalDependencies, so an install downloads exactly one binary - Binary ships gzipped and is decompressed on first run into a version-scoped cache, so an upgrade can never exec a stale binary - make bump-version is the single source of truth for the version constant Bundled agent skills - public/skills/ embedded in the binary, installed via osmedeus skills install - Filesystem-driven discovery: a new bundle needs no code change - make sync-skills mirrors bundles out to the standalone skills repo Platform sub-projects - Vendor dashboard, registry and workflow under platform/ so they version with the engine they talk to; make sync-platform publishes them out - Rebuild the embedded UI in public/ui/
1901 lines
59 KiB
Go
1901 lines
59 KiB
Go
package cli
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/j3ssie/osmedeus/v5/internal/config"
|
|
"github.com/j3ssie/osmedeus/v5/internal/core"
|
|
"github.com/j3ssie/osmedeus/v5/internal/installer"
|
|
"github.com/j3ssie/osmedeus/v5/internal/parser"
|
|
"github.com/j3ssie/osmedeus/v5/internal/terminal"
|
|
"github.com/j3ssie/osmedeus/v5/public"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
registryPath string
|
|
installAll bool
|
|
binaryNames []string
|
|
checkOnly bool
|
|
customHeaders []string
|
|
nixBuildInstall bool
|
|
nixInstallation bool
|
|
nixPkgs []string
|
|
installOptional bool
|
|
hideBinaryTags bool
|
|
baseSample bool
|
|
basePreset bool
|
|
workflowPreset bool
|
|
validateSample bool
|
|
validatePreset bool
|
|
installEnvAll bool
|
|
keepSetting bool
|
|
goGetterSources []string
|
|
goGetterDest string
|
|
listRegistryNixBuild bool
|
|
listRegistryDirectFetch bool
|
|
)
|
|
|
|
// installCmd represents the install parent command
|
|
var installCmd = &cobra.Command{
|
|
Use: "install",
|
|
Short: "Install workflows, base folder, or binaries",
|
|
Long: UsageInstall(),
|
|
}
|
|
|
|
// installWorkflowCmd installs workflows from a source
|
|
var installWorkflowCmd = &cobra.Command{
|
|
Use: "workflow [source]",
|
|
Short: "Install workflows from git URL, zip URL, local zip file, or local folder",
|
|
Args: func(cmd *cobra.Command, args []string) error {
|
|
if workflowPreset {
|
|
if len(args) != 0 {
|
|
return fmt.Errorf("no source argument is allowed with --preset")
|
|
}
|
|
return nil
|
|
}
|
|
return cobra.ExactArgs(1)(cmd, args)
|
|
},
|
|
RunE: RunInstallWorkflow,
|
|
}
|
|
|
|
// installBaseCmd installs the base folder from a source
|
|
var installBaseCmd = &cobra.Command{
|
|
Use: "base [source]",
|
|
Short: "Install base folder from git URL, zip URL, local zip file, or local folder",
|
|
Args: func(cmd *cobra.Command, args []string) error {
|
|
if baseSample || basePreset {
|
|
if len(args) != 0 {
|
|
return fmt.Errorf("no source argument is allowed with --sample or --preset")
|
|
}
|
|
return nil
|
|
}
|
|
return cobra.ExactArgs(1)(cmd, args)
|
|
},
|
|
RunE: runInstallBase,
|
|
}
|
|
|
|
// installBinaryCmd installs binaries from a registry
|
|
var installBinaryCmd = &cobra.Command{
|
|
Use: "binary",
|
|
Aliases: []string{"bin", "binray", "binr", "birary"},
|
|
Short: "Install binary tools from registry",
|
|
Long: `Install one or more binary tools from the registry. Skips binaries already available in PATH.`,
|
|
Example: ` # List binaries in registry
|
|
osmedeus install binary --list-registry-direct-fetch
|
|
osmedeus install binary --list-registry-nix-build
|
|
|
|
# Install specific binaries
|
|
osmedeus install binary --name nuclei
|
|
osmedeus install binary --name nuclei --name ffuf --name httpx
|
|
osmedeus install binary -n nuclei -n ffuf
|
|
|
|
# Install all required binaries
|
|
osmedeus install binary --all
|
|
|
|
# Install all binaries including optional
|
|
osmedeus install binary --all --install-optional
|
|
|
|
# Check binary installation status
|
|
osmedeus install binary --name amass --check
|
|
osmedeus install binary --all --check
|
|
|
|
# Install via go install
|
|
osmedeus install binary --name nuclei --go-install
|
|
osmedeus install binary --all --go-install
|
|
osmedeus install binary --go-install-pkg github.com/tomnomnom/waybackurls@latest
|
|
|
|
# Install via Nix
|
|
osmedeus install binary --nix-installation
|
|
osmedeus install binary --name nuclei --nix-build-install
|
|
osmedeus install binary --all --nix-build-install`,
|
|
RunE: runInstallBinary,
|
|
}
|
|
|
|
// installEnvCmd adds binaries path to shell configuration
|
|
var installEnvCmd = &cobra.Command{
|
|
Use: "env",
|
|
Short: "Add binaries path to shell configuration",
|
|
Long: `Add the osmedeus binaries folder to your PATH in shell configuration files (~/.bashrc, ~/.zshrc, ~/.profile).`,
|
|
Example: ` osmedeus install env`,
|
|
RunE: runInstallEnv,
|
|
}
|
|
|
|
// installValidateCmd checks environment health
|
|
var installValidateCmd = &cobra.Command{
|
|
Use: "validate",
|
|
Aliases: []string{"val"},
|
|
Short: "Check and fix environment health (folders, config, workflows)",
|
|
Long: `Validate the osmedeus installation by checking:
|
|
- Required folders exist (base, workspaces, workflows, binaries, data)
|
|
- Configuration files are valid
|
|
- Workflows can be loaded and parsed correctly
|
|
|
|
This is the primary command for health checks. 'osmedeus health' is an alias for this command.`,
|
|
Example: ` osmedeus install validate`,
|
|
RunE: runInstallValidate,
|
|
}
|
|
|
|
// installSkillsCmd is an alias for `osmedeus skills install`, registered here so
|
|
// skills are discoverable alongside the other install targets. A cobra command
|
|
// has a single parent, so the alias is a thin second command sharing the handler
|
|
// and flag variables — the same pattern as `workflow install`.
|
|
var installSkillsCmd = &cobra.Command{
|
|
Use: "skills [name...]",
|
|
Aliases: []string{"skill"},
|
|
Short: "Install coding-agent skills into a skills directory",
|
|
Long: `Install the skill bundles embedded in this binary into a coding agent's
|
|
skills directory. Alias for 'osmedeus skills install'.`,
|
|
Example: ` osmedeus install skills
|
|
osmedeus install skills --scope global
|
|
osmedeus install skills --agent codex`,
|
|
Args: cobra.ArbitraryArgs,
|
|
RunE: RunSkillsInstall,
|
|
}
|
|
|
|
// RunInstallWorkflow installs workflows from a source (exported for use by workflow install alias)
|
|
func RunInstallWorkflow(cmd *cobra.Command, args []string) error {
|
|
cfg := config.Get()
|
|
if cfg == nil {
|
|
return fmt.Errorf("configuration not loaded")
|
|
}
|
|
|
|
headers := parseCustomHeaders(customHeaders)
|
|
inst := installer.NewInstaller(
|
|
cfg.BaseFolder,
|
|
cfg.WorkflowsPath,
|
|
cfg.BinariesPath,
|
|
headers,
|
|
)
|
|
|
|
printer := terminal.NewPrinter()
|
|
|
|
// Check if workflow folder exists and prompt for confirmation
|
|
if _, err := os.Stat(inst.WorkflowFolder); err == nil {
|
|
if !globalForce {
|
|
printer.Warning("Existing workflow folder detected!")
|
|
printer.Warning("Path: %s", inst.WorkflowFolder)
|
|
printer.Warning("This operation will REMOVE the existing workflow folder and all its contents.")
|
|
fmt.Println()
|
|
|
|
if !confirmPrompt("Do you want to continue?") {
|
|
printer.Info("Operation cancelled. Use --force to skip this confirmation.")
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
if workflowPreset {
|
|
workflowURL := os.Getenv("OSM_WORKFLOW_URL")
|
|
if workflowURL != "" {
|
|
printer.Info("Using workflow URL from OSM_WORKFLOW_URL environment variable")
|
|
printer.Println(" %s %s", terminal.SymbolBullet, terminal.Gray(workflowURL))
|
|
} else {
|
|
workflowURL = core.DEFAULT_WORKFLOW_REPO
|
|
printer.Info("Using default workflow URL")
|
|
printer.Println(" %s %s", terminal.SymbolBullet, terminal.Gray(workflowURL))
|
|
}
|
|
|
|
if err := inst.InstallWorkflow(workflowURL); err != nil {
|
|
return err
|
|
}
|
|
printWorkflowSummary(printer, cfg.WorkflowsPath)
|
|
return nil
|
|
}
|
|
|
|
source := args[0]
|
|
if err := inst.InstallWorkflow(source); err != nil {
|
|
return err
|
|
}
|
|
printWorkflowSummary(printer, cfg.WorkflowsPath)
|
|
return nil
|
|
}
|
|
|
|
// printWorkflowSummary counts and prints the number of workflows loaded
|
|
func printWorkflowSummary(printer *terminal.Printer, workflowsPath string) {
|
|
loader := parser.NewLoader(workflowsPath)
|
|
|
|
flows, flowErr := loader.ListFlows()
|
|
modules, modErr := loader.ListModules()
|
|
|
|
if flowErr == nil && modErr == nil {
|
|
total := len(flows) + len(modules)
|
|
if total > 0 {
|
|
printer.Info("Loaded %s workflows (%s flows, %s modules)",
|
|
terminal.Green(fmt.Sprintf("%d", total)),
|
|
terminal.Cyan(fmt.Sprintf("%d", len(flows))),
|
|
terminal.Yellow(fmt.Sprintf("%d", len(modules))))
|
|
printer.Println(" %s Run %s to see workflow details",
|
|
terminal.Gray(terminal.SymbolLightning),
|
|
terminal.Cyan("osmedeus workflow ls"))
|
|
}
|
|
}
|
|
}
|
|
|
|
func runInstallBase(cmd *cobra.Command, args []string) error {
|
|
cfg := config.Get()
|
|
if cfg == nil {
|
|
return fmt.Errorf("configuration not loaded")
|
|
}
|
|
|
|
printer := terminal.NewPrinter()
|
|
|
|
// Determine binaries folder for PATH setup
|
|
binariesFolder := cfg.BinariesPath
|
|
if binariesFolder == "" {
|
|
binariesFolder = filepath.Join(cfg.BaseFolder, "external-binaries")
|
|
}
|
|
|
|
if baseSample {
|
|
if err := replaceBaseFolderWithEmbeddedSample(cfg.BaseFolder); err != nil {
|
|
return err
|
|
}
|
|
reloaded, err := config.Load(cfg.BaseFolder)
|
|
if err == nil {
|
|
config.Set(reloaded)
|
|
// Update binariesFolder from reloaded config
|
|
if reloaded.BinariesPath != "" {
|
|
binariesFolder = reloaded.BinariesPath
|
|
}
|
|
}
|
|
ensureBinariesPathInEnv(printer, binariesFolder, true)
|
|
return nil
|
|
}
|
|
|
|
if basePreset {
|
|
// Get preset URL from environment or use default
|
|
presetURL := os.Getenv("OSM_PRESET_URL")
|
|
if presetURL != "" {
|
|
printer.Info("Using preset URL from OSM_PRESET_URL environment variable")
|
|
printer.Println(" %s %s", terminal.SymbolBullet, terminal.Gray(presetURL))
|
|
} else {
|
|
presetURL = core.DEFAULT_BASE_REPO
|
|
printer.Info("Using default preset URL")
|
|
printer.Println(" %s %s", terminal.SymbolBullet, terminal.Gray(presetURL))
|
|
}
|
|
|
|
headers := parseCustomHeaders(customHeaders)
|
|
inst := installer.NewInstaller(
|
|
cfg.BaseFolder,
|
|
cfg.WorkflowsPath,
|
|
cfg.BinariesPath,
|
|
headers,
|
|
)
|
|
inst.KeepSetting = keepSetting
|
|
if err := inst.InstallBase(presetURL); err != nil {
|
|
return err
|
|
}
|
|
printer.Success("Base installed from: %s", terminal.Cyan(presetURL))
|
|
printer.Newline()
|
|
|
|
// Install workflows from OSM_WORKFLOW_URL or DEFAULT_WORKFLOW_REPO
|
|
workflowURL := os.Getenv("OSM_WORKFLOW_URL")
|
|
if workflowURL != "" {
|
|
printer.Info("Using workflow URL from OSM_WORKFLOW_URL environment variable")
|
|
printer.Println(" %s %s", terminal.SymbolBullet, terminal.Gray(workflowURL))
|
|
} else {
|
|
workflowURL = core.DEFAULT_WORKFLOW_REPO
|
|
printer.Info("Using default workflow URL")
|
|
printer.Println(" %s %s", terminal.SymbolBullet, terminal.Gray(workflowURL))
|
|
}
|
|
|
|
if err := inst.InstallWorkflow(workflowURL); err != nil {
|
|
printer.Warning("Failed to install workflows: %s", err)
|
|
// Continue - workflow installation failure shouldn't block base setup
|
|
} else {
|
|
printer.Success("Workflows installed from: %s", terminal.Cyan(workflowURL))
|
|
printWorkflowSummary(printer, cfg.WorkflowsPath)
|
|
}
|
|
printer.Newline()
|
|
|
|
// Reload config after base and workflow installation
|
|
reloaded, err := config.Load(cfg.BaseFolder)
|
|
if err == nil {
|
|
config.Set(reloaded)
|
|
cfg = reloaded
|
|
if reloaded.BinariesPath != "" {
|
|
binariesFolder = reloaded.BinariesPath
|
|
}
|
|
}
|
|
|
|
// Check if first-time setup is needed (install binaries if so)
|
|
if isFirstTimeSetupNeeded(cfg.BaseFolder) {
|
|
printer.Println("%s %s", terminal.BoldBlue(terminal.SymbolLightning), terminal.HiBlue("First-time setup detected. Installing binaries..."))
|
|
printer.Newline()
|
|
|
|
// Install required binaries
|
|
installRequiredBinaries(cfg, printer)
|
|
|
|
// Create initialization marker
|
|
_ = createInitializationMarker(printer)
|
|
|
|
printer.Newline()
|
|
printer.Println("%s %s", terminal.Green(terminal.SymbolSuccess), terminal.BoldGreen("First-time setup complete!"))
|
|
}
|
|
|
|
ensureBinariesPathInEnv(printer, binariesFolder, true)
|
|
return nil
|
|
}
|
|
|
|
source := args[0]
|
|
headers := parseCustomHeaders(customHeaders)
|
|
inst := installer.NewInstaller(
|
|
cfg.BaseFolder,
|
|
cfg.WorkflowsPath,
|
|
cfg.BinariesPath,
|
|
headers,
|
|
)
|
|
inst.KeepSetting = keepSetting
|
|
|
|
if err := inst.InstallBase(source); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Reload config to get updated paths after base installation
|
|
reloaded, err := config.Load(cfg.BaseFolder)
|
|
if err == nil {
|
|
config.Set(reloaded)
|
|
cfg = reloaded
|
|
if reloaded.BinariesPath != "" {
|
|
binariesFolder = reloaded.BinariesPath
|
|
}
|
|
}
|
|
|
|
// Check if workflows folder exists under the base folder and print stats
|
|
printWorkflowSummary(printer, cfg.WorkflowsPath)
|
|
|
|
ensureBinariesPathInEnv(printer, binariesFolder, true)
|
|
return nil
|
|
}
|
|
|
|
func runInstallBinary(cmd *cobra.Command, args []string) error {
|
|
cfg := config.Get()
|
|
if cfg == nil {
|
|
return fmt.Errorf("configuration not loaded")
|
|
}
|
|
|
|
printer := terminal.NewPrinter()
|
|
|
|
// Auto-detect Nix installation and add to PATH if not already
|
|
// Check if /nix/var/nix/profiles/default/bin is not empty
|
|
nixBinDir := "/nix/var/nix/profiles/default/bin"
|
|
if entries, err := os.ReadDir(nixBinDir); err == nil && len(entries) > 0 {
|
|
ensureNixProfileBinInProcess(nil) // Silent - no printer output for auto-detection
|
|
}
|
|
|
|
// Handle --nix-installation flag first
|
|
if nixInstallation {
|
|
silent = true // Nix installation produces verbose output, enable silent by default
|
|
hasFollowupAction := nixBuildInstall || len(nixPkgs) > 0 || installAll || len(binaryNames) > 0 || checkOnly || listRegistryNixBuild || listRegistryDirectFetch
|
|
|
|
if installer.IsNixInstalled() {
|
|
printer.Info("Nix already available in PATH")
|
|
} else {
|
|
printer.Println("%s Running Nix install command:", terminal.BoldCyan("◆"))
|
|
printer.GrayOutput(installer.NixInstallCommandPrettyForHost())
|
|
printer.Newline()
|
|
}
|
|
|
|
printer.Info("Installing Nix package manager...")
|
|
if err := installer.InstallNix(); err != nil {
|
|
printer.Error("Failed to install Nix: %s", err)
|
|
return err
|
|
}
|
|
printer.Success("Nix installed successfully!")
|
|
ensureNixProfileBinInProcess(printer)
|
|
ensureNixProfileBinInShell(printer)
|
|
if !hasFollowupAction {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if nixBuildInstall {
|
|
ensureNixProfileBinInProcess(printer)
|
|
}
|
|
|
|
// Check if Nix build install is requested but Nix isn't installed
|
|
if (nixBuildInstall || len(nixPkgs) > 0) && !installer.IsNixInstalled() {
|
|
printer.Error("Nix is not installed")
|
|
printer.Println("%s Install Nix with: %s", terminal.BoldCyan("◆"), terminal.Gray("osmedeus install binary --nix-installation"))
|
|
printer.Info("Or manually run:")
|
|
printer.GrayOutput(installer.NixInstallCommandPrettyForHost())
|
|
printer.Newline()
|
|
printer.Println("%s Ensure Nix is in PATH:", terminal.BoldCyan("◆"))
|
|
printer.Println(" %s", terminal.Gray("export PATH=\"$PATH:/nix/var/nix/profiles/default/bin\""))
|
|
return fmt.Errorf("nix not installed")
|
|
}
|
|
|
|
// Handle --list-registry-nix-build flag
|
|
if listRegistryNixBuild {
|
|
return printNixBinaries(printer, globalWidth, !hideBinaryTags)
|
|
}
|
|
|
|
// Handle --list-registry-direct-fetch flag
|
|
if listRegistryDirectFetch {
|
|
headers := parseCustomHeaders(customHeaders)
|
|
return printRegistryBinaries(registryPath, headers, printer, globalWidth, !hideBinaryTags)
|
|
}
|
|
|
|
// Handle --go-getter flag (download/clone via go-getter)
|
|
if len(goGetterSources) > 0 {
|
|
if checkOnly {
|
|
return fmt.Errorf("cannot use --check with --go-getter")
|
|
}
|
|
if installAll || len(binaryNames) > 0 {
|
|
return fmt.Errorf("cannot combine --go-getter with --all or --name")
|
|
}
|
|
if nixBuildInstall {
|
|
return fmt.Errorf("cannot combine --go-getter with --nix-build-install")
|
|
}
|
|
|
|
var lastErr error
|
|
var failed []string
|
|
for _, srcArg := range goGetterSources {
|
|
srcArg = strings.TrimSpace(srcArg)
|
|
if srcArg == "" {
|
|
continue
|
|
}
|
|
|
|
// Parse inline destination if present (format: "source destination")
|
|
src := srcArg
|
|
dest := goGetterDest
|
|
if strings.Contains(srcArg, " ") {
|
|
parts := strings.SplitN(srcArg, " ", 2)
|
|
src = strings.TrimSpace(parts[0])
|
|
dest = strings.TrimSpace(parts[1])
|
|
}
|
|
|
|
// Use default destination if not specified
|
|
if dest == "" {
|
|
homeDir, _ := os.UserHomeDir()
|
|
// Extract repo name from source URL for default destination
|
|
repoName := extractRepoNameFromURL(src)
|
|
if repoName != "" {
|
|
dest = filepath.Join(homeDir, repoName)
|
|
} else {
|
|
dest = homeDir
|
|
}
|
|
} else {
|
|
// Expand ~ and $HOME in destination
|
|
dest = installer.ExpandPath(dest)
|
|
}
|
|
|
|
printer.Println("%s Destination: %s", terminal.BoldCyan("◆"), terminal.Gray(dest))
|
|
printer.Installing(src)
|
|
if err := installer.GetViaGoGetter(src, dest); err != nil {
|
|
printer.Error("Failed to download %s: %s", src, err)
|
|
lastErr = err
|
|
failed = append(failed, src)
|
|
} else {
|
|
printer.Success("Downloaded '%s' to %s", src, dest)
|
|
}
|
|
}
|
|
printFailedBinariesSummary(printer, failed)
|
|
return lastErr
|
|
}
|
|
|
|
if len(nixPkgs) > 0 {
|
|
if checkOnly {
|
|
return fmt.Errorf("cannot use --check with --nix-pkgs")
|
|
}
|
|
if installAll || len(binaryNames) > 0 {
|
|
return fmt.Errorf("cannot combine --nix-pkgs with --all or --name")
|
|
}
|
|
}
|
|
|
|
if shouldInitializeBaseForBinary(cfg.BaseFolder) {
|
|
printer.Info("Base folder not initialized, running: %s", terminal.Gray("osmedeus install base --sample"))
|
|
if err := replaceBaseFolderWithEmbeddedSample(cfg.BaseFolder); err != nil {
|
|
return err
|
|
}
|
|
reloaded, err := config.Load(cfg.BaseFolder)
|
|
if err == nil {
|
|
config.Set(reloaded)
|
|
cfg = reloaded
|
|
}
|
|
}
|
|
|
|
// Validate: either --all, --name, or --nix-pkgs must be specified
|
|
if !installAll && len(binaryNames) == 0 && len(nixPkgs) == 0 {
|
|
printer.Error("Please specify binary name(s) with --name, Nix packages with --nix-pkgs, or use --all")
|
|
return fmt.Errorf("either --all, --name, or --nix-pkgs flag is required")
|
|
}
|
|
|
|
// Determine binaries folder
|
|
binariesFolder := cfg.BinariesPath
|
|
if binariesFolder == "" {
|
|
binariesFolder = filepath.Join(cfg.BaseFolder, "binaries")
|
|
}
|
|
|
|
headers := parseCustomHeaders(customHeaders)
|
|
|
|
// Load registry once for all operations
|
|
registry, err := installer.LoadRegistry(registryPath, headers)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load registry: %w", err)
|
|
}
|
|
|
|
registrySource := ""
|
|
if registryPath == "" {
|
|
registrySource = "embedded preset: public/presets/registry-metadata-direct-fetch.json"
|
|
} else if installer.IsURL(registryPath) {
|
|
registrySource = registryPath
|
|
} else {
|
|
registrySource = registryPath
|
|
}
|
|
printer.Println("%s Registry metadata: %s", terminal.BoldCyan("◆"), terminal.Gray(registrySource))
|
|
|
|
// Show security warning before installation (skip for check-only mode)
|
|
if !checkOnly {
|
|
printer.SecurityWarning("This command will download and execute external binaries")
|
|
printer.BulletColored("github-release: Downloads binaries from URLs in the registry", terminal.Yellow)
|
|
printer.BulletColored("command-based: Executes commands like 'pip install' or 'brew install'", terminal.Yellow)
|
|
printer.Println("%s %s", terminal.BoldCyan("◆"), terminal.Gray("Only run with trusted registry sources. Review registry metadata before proceeding."))
|
|
printer.Newline()
|
|
}
|
|
|
|
// Handle --all flag
|
|
if installAll {
|
|
var allNames []string
|
|
var skippedOptional int
|
|
|
|
if nixBuildInstall {
|
|
flakeContent, err := public.GetFlakeNix()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read embedded flake.nix: %w", err)
|
|
}
|
|
categories, err := installer.ParseFlakeNixBinariesFromString(string(flakeContent))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse flake.nix: %w", err)
|
|
}
|
|
for _, name := range installer.GetAllFlakeBinaries(categories) {
|
|
if entry, ok := registry[name]; ok {
|
|
if !installOptional && containsTag(entry.Tags, "optional") {
|
|
skippedOptional++
|
|
continue
|
|
}
|
|
}
|
|
allNames = append(allNames, name)
|
|
}
|
|
} else {
|
|
for name, entry := range registry {
|
|
if !installOptional && containsTag(entry.Tags, "optional") {
|
|
skippedOptional++
|
|
continue
|
|
}
|
|
allNames = append(allNames, name)
|
|
}
|
|
sort.Strings(allNames)
|
|
}
|
|
if skippedOptional > 0 {
|
|
printOptionalBinarySkipMessage(printer, skippedOptional, nixBuildInstall)
|
|
}
|
|
if installOptional {
|
|
printer.Println("%s %s", terminal.BoldCyan("◆"), terminal.Gray("Optional binaries may require extra tools in your $PATH (e.g., go, pip)."))
|
|
}
|
|
|
|
if checkOnly {
|
|
// Check-only mode: just verify binary availability for all binaries
|
|
return checkBinaries(allNames, registry, printer)
|
|
}
|
|
if nixBuildInstall {
|
|
failed, err := installBinariesViaNix(allNames, registry, binariesFolder, printer)
|
|
printFailedBinariesSummary(printer, failed)
|
|
ensureBinariesPathInEnv(printer, binariesFolder, true)
|
|
return err
|
|
}
|
|
|
|
// Install binaries in parallel (suppress spinner display)
|
|
silent = true
|
|
printer.Println(" %s Installing %s binaries to: %s",
|
|
terminal.SymbolBullet,
|
|
terminal.Green(fmt.Sprintf("%d", len(allNames))),
|
|
terminal.Cyan(binariesFolder))
|
|
printer.Newline()
|
|
|
|
failed := installBinariesParallel(allNames, registry, binariesFolder, headers, printer, silent)
|
|
printer.Newline()
|
|
printFailedBinariesSummary(printer, failed)
|
|
ensureBinariesPathInEnv(printer, binariesFolder, true)
|
|
if len(failed) > 0 {
|
|
return fmt.Errorf("%d binaries failed to install", len(failed))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if len(nixPkgs) > 0 {
|
|
silent = true // Nix profile add produces verbose output, enable silent by default
|
|
ensureNixProfileBinInProcess(printer)
|
|
ensureNixProfileBinInShell(printer)
|
|
|
|
var lastErr error
|
|
var failed []string
|
|
for _, pkg := range nixPkgs {
|
|
pkg = strings.TrimSpace(pkg)
|
|
if pkg == "" {
|
|
continue
|
|
}
|
|
installable := pkg
|
|
if !strings.Contains(installable, "#") {
|
|
printer.Warning("Package '%s' doesn't contain '#'. Using '%s' format.", pkg, "nixpkgs#"+pkg)
|
|
printer.Info("Tip: Use format 'nixpkgs#%s' or 'flake#package' directly", pkg)
|
|
installable = "nixpkgs#" + pkg
|
|
}
|
|
|
|
printer.Installing(installable)
|
|
if !silent {
|
|
printer.GrayOutput("nix profile add " + installable)
|
|
}
|
|
if err := installer.InstallBinaryViaNix(pkg, pkg, ""); err != nil {
|
|
printer.Error("Failed to add %s: %s", pkg, err)
|
|
lastErr = err
|
|
failed = append(failed, pkg)
|
|
} else {
|
|
if !silent {
|
|
if installer.NixInstallOutput != "" {
|
|
printer.GrayOutput(installer.NixInstallOutput)
|
|
}
|
|
}
|
|
printer.Success("Nix package '%s' added", pkg)
|
|
}
|
|
}
|
|
printFailedBinariesSummary(printer, failed)
|
|
return lastErr
|
|
}
|
|
|
|
// Check-only mode: just verify binary availability
|
|
if checkOnly {
|
|
return checkBinaries(binaryNames, registry, printer)
|
|
}
|
|
|
|
// Install via Nix if --nix-build-install is set
|
|
if nixBuildInstall {
|
|
failed, err := installBinariesViaNix(binaryNames, registry, binariesFolder, printer)
|
|
printFailedBinariesSummary(printer, failed)
|
|
ensureBinariesPathInEnv(printer, binariesFolder, true)
|
|
return err
|
|
}
|
|
|
|
// Install each specified binary
|
|
var lastErr error
|
|
var failed []string
|
|
for _, name := range binaryNames {
|
|
printer.Info("Processing binary: %s", name)
|
|
if err := installer.InstallBinary(name, registry, binariesFolder, headers); err != nil {
|
|
printer.Error("Failed to install %s: %s", name, err)
|
|
lastErr = err
|
|
failed = append(failed, name)
|
|
} else {
|
|
printer.Success("Binary '%s' ready", terminal.HiBlue(name))
|
|
}
|
|
}
|
|
printFailedBinariesSummary(printer, failed)
|
|
ensureBinariesPathInEnv(printer, binariesFolder, true)
|
|
|
|
return lastErr
|
|
}
|
|
|
|
func shouldInitializeBaseForBinary(baseFolder string) bool {
|
|
if baseFolder == "" {
|
|
return false
|
|
}
|
|
info, err := os.Stat(baseFolder)
|
|
if err != nil {
|
|
return os.IsNotExist(err)
|
|
}
|
|
if !info.IsDir() {
|
|
return false
|
|
}
|
|
entries, err := os.ReadDir(baseFolder)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
for _, entry := range entries {
|
|
name := entry.Name()
|
|
if name == "osm-settings.yaml" || name == ".DS_Store" {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// installBinariesViaNix installs binaries using Nix and copies them to binariesFolder
|
|
func installBinariesViaNix(names []string, registry installer.BinaryRegistry, binariesFolder string, printer *terminal.Printer) ([]string, error) {
|
|
ensureNixProfileBinInProcess(printer)
|
|
ensureNixProfileBinInShell(printer)
|
|
|
|
printer.Println("%s %s", terminal.BoldCyan(terminal.SymbolLightning), terminal.HiWhite("Nix builds packages from source, ensuring reproducibility. It might take longer, but the results are well worth the wait"))
|
|
printer.Newline()
|
|
|
|
var lastErr error
|
|
var failed []string
|
|
for _, name := range names {
|
|
// Always show the binary name being processed
|
|
printer.Info("Processing binary: %s", terminal.HiBlue(name))
|
|
|
|
// Check if already in PATH
|
|
if installer.IsBinaryInPath(name) {
|
|
if !installer.IsCoreUnixTool(name) {
|
|
entry, ok := registry[name]
|
|
if ok {
|
|
_ = installer.SymlinkInstalledBinaryToFolder(name, &entry, binariesFolder)
|
|
}
|
|
}
|
|
printer.Info("Binary '%s' already available in PATH, skipping", terminal.HiBlue(name))
|
|
continue
|
|
}
|
|
|
|
entry, ok := registry[name]
|
|
if !ok {
|
|
printer.Warning("Binary '%s' not found in registry, trying direct Nix install", name)
|
|
}
|
|
|
|
nixPkg := installer.GetNixPackageName(entry, name)
|
|
|
|
// Show installing message with package name
|
|
printer.Installing(fmt.Sprintf("nixpkgs#%s", nixPkg))
|
|
if !silent {
|
|
printer.GrayOutput(fmt.Sprintf("nix profile add nixpkgs#%s", nixPkg))
|
|
}
|
|
|
|
if err := installer.InstallBinaryViaNix(name, nixPkg, binariesFolder); err != nil {
|
|
printer.Error("Failed to install %s: %s", name, err)
|
|
lastErr = err
|
|
failed = append(failed, name)
|
|
} else {
|
|
if !silent {
|
|
if installer.NixInstallOutput != "" {
|
|
printer.GrayOutput(installer.NixInstallOutput)
|
|
}
|
|
}
|
|
printer.Success("Binary '%s' installed via Nix and copied to %s", terminal.HiBlue(name), binariesFolder)
|
|
}
|
|
}
|
|
return failed, lastErr
|
|
}
|
|
|
|
func printOptionalBinarySkipMessage(printer *terminal.Printer, skippedOptional int, nixMode bool) {
|
|
flag := terminal.Gray("--install-optional")
|
|
printer.Println("%s Skipping %d optional binaries (use %s to include them)", terminal.BoldCyan("◆"), skippedOptional, flag)
|
|
printer.Println(" %s", terminal.Gray("Optional binaries may require extra tools in your $PATH (e.g., go, pip)."))
|
|
|
|
cmd := "osmedeus install binary --all --install-optional"
|
|
if nixMode {
|
|
cmd = "osmedeus install binary --all --nix-build-install --install-optional"
|
|
}
|
|
printer.Println(" %s", terminal.Gray("Example: "+cmd))
|
|
}
|
|
|
|
func printFailedBinariesSummary(printer *terminal.Printer, failed []string) {
|
|
if len(failed) == 0 {
|
|
return
|
|
}
|
|
|
|
unique := make(map[string]struct{}, len(failed))
|
|
for _, name := range failed {
|
|
unique[name] = struct{}{}
|
|
}
|
|
|
|
list := make([]string, 0, len(unique))
|
|
for name := range unique {
|
|
list = append(list, name)
|
|
}
|
|
sort.Strings(list)
|
|
|
|
printer.Newline()
|
|
printer.Section("Failed Binaries")
|
|
for _, name := range list {
|
|
printer.Bullet(name)
|
|
}
|
|
printer.Newline()
|
|
|
|
printer.Println("%s %s", terminal.BoldCyan(terminal.SymbolLightning), terminal.Gray("Please note that your workflow may not require all binaries listed in the registry, so the scan can "+terminal.Green("still function properly")+terminal.Gray(" even if some tools are absent.")))
|
|
printer.Println("%s %s", terminal.BoldCyan(terminal.SymbolLightning), terminal.Gray("The registry is intended to provide all external tools that are worthwhile to include in your workflow and is not mandatory."))
|
|
}
|
|
|
|
// printNixBinaries prints the list of binaries available in Nix flake
|
|
func printNixBinaries(printer *terminal.Printer, descWidth int, showTags bool) error {
|
|
printer.Println(terminal.BoldCyan("Available binaries in Nix flake"))
|
|
// Get embedded flake.nix content
|
|
flakeContent, err := public.GetFlakeNix()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read embedded flake.nix: %w", err)
|
|
}
|
|
|
|
// Parse the flake to extract tool categories
|
|
categories, err := installer.ParseFlakeNixBinariesFromString(string(flakeContent))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse flake.nix: %w", err)
|
|
}
|
|
|
|
// Load registry for descriptions and tags
|
|
registry, _ := installer.LoadRegistry("", nil) // Ignore error, descriptions are optional
|
|
|
|
// Column widths
|
|
nameWidth := 18
|
|
statusWidth := 10
|
|
tagsWidth := 30
|
|
|
|
// Print header
|
|
printer.Println(terminal.BoldCyan("## Nix Flake Binaries (nix-build mode)"))
|
|
printer.Println(terminal.Gray("Source: public/presets/flake.nix"))
|
|
printer.Newline()
|
|
|
|
totalTools := 0
|
|
for _, cat := range categories {
|
|
printer.Println(terminal.BoldGreen("### %s (%d)"), cat.Name, len(cat.Tools))
|
|
printer.Newline()
|
|
|
|
// Print table header
|
|
if showTags {
|
|
printer.Println("| %s | %s | %s | %s |",
|
|
padRight("Name", nameWidth),
|
|
padRight("Status", statusWidth),
|
|
padRight("Description", descWidth),
|
|
padRight("Tags", tagsWidth))
|
|
printer.Println("|%s|%s|%s|%s|",
|
|
strings.Repeat("-", nameWidth+2),
|
|
strings.Repeat("-", statusWidth+2),
|
|
strings.Repeat("-", descWidth+2),
|
|
strings.Repeat("-", tagsWidth+2))
|
|
} else {
|
|
printer.Println("| %s | %s | %s |",
|
|
padRight("Name", nameWidth),
|
|
padRight("Status", statusWidth),
|
|
padRight("Description", descWidth))
|
|
printer.Println("|%s|%s|%s|",
|
|
strings.Repeat("-", nameWidth+2),
|
|
strings.Repeat("-", statusWidth+2),
|
|
strings.Repeat("-", descWidth+2))
|
|
}
|
|
|
|
for _, tool := range cat.Tools {
|
|
statusText := "missing"
|
|
statusColor := terminal.Red
|
|
|
|
// Get registry entry for validation command and metadata
|
|
var entryPtr *installer.BinaryEntry
|
|
desc := ""
|
|
tags := ""
|
|
if registry != nil {
|
|
if entry, ok := registry[tool]; ok {
|
|
entryPtr = &entry
|
|
desc = entry.Desc
|
|
tags = strings.Join(entry.Tags, ", ")
|
|
}
|
|
}
|
|
|
|
if installer.IsBinaryInstalled(tool, entryPtr) {
|
|
statusText = "installed"
|
|
statusColor = terminal.Green
|
|
}
|
|
desc = truncatePad(desc, descWidth)
|
|
tags = truncatePad(tags, tagsWidth)
|
|
|
|
if showTags {
|
|
printer.Println("| %s | %s | %s | %s |",
|
|
colorPadRight(tool, nameWidth, terminal.Cyan),
|
|
colorPadRight(statusText, statusWidth, statusColor),
|
|
desc,
|
|
tags)
|
|
} else {
|
|
printer.Println("| %s | %s | %s |",
|
|
colorPadRight(tool, nameWidth, terminal.Cyan),
|
|
colorPadRight(statusText, statusWidth, statusColor),
|
|
desc)
|
|
}
|
|
}
|
|
totalTools += len(cat.Tools)
|
|
printer.Newline()
|
|
}
|
|
|
|
// Print usage examples
|
|
printer.Println(terminal.BoldCyan("### Usage Examples"))
|
|
printer.Println(" " + terminal.Green("# Enter Nix development shell with all tools"))
|
|
printer.Println(" nix develop")
|
|
printer.Newline()
|
|
printer.Println(" " + terminal.Green("# Install specific binary via Nix"))
|
|
printer.Println(" osmedeus install binary --name nuclei --nix-build-install")
|
|
printer.Newline()
|
|
printer.Println(" " + terminal.Green("# Install all binaries via Nix"))
|
|
printer.Println(" osmedeus install binary --all --nix-build-install")
|
|
|
|
printer.Newline()
|
|
printer.Println(terminal.Gray("Total: %d binaries in %d categories"), totalTools, len(categories))
|
|
return nil
|
|
}
|
|
|
|
// printRegistryBinaries prints the list of binaries available in registry JSON
|
|
func printRegistryBinaries(registryPath string, headers map[string]string, printer *terminal.Printer, descWidth int, showTags bool) error {
|
|
printer.Println(terminal.BoldCyan("Available binaries in registry"))
|
|
registry, err := installer.LoadRegistry(registryPath, headers)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load registry: %w", err)
|
|
}
|
|
|
|
// Separate required and optional binaries
|
|
var required, optional []string
|
|
for name, entry := range registry {
|
|
if containsTag(entry.Tags, "optional") {
|
|
optional = append(optional, name)
|
|
} else {
|
|
required = append(required, name)
|
|
}
|
|
}
|
|
sort.Strings(required)
|
|
sort.Strings(optional)
|
|
|
|
// Column widths
|
|
nameWidth := 18
|
|
statusWidth := 10
|
|
tagsWidth := 30
|
|
|
|
// Print header
|
|
printer.Println(terminal.BoldCyan("## Registry Binaries (direct-fetch mode)"))
|
|
printer.Newline()
|
|
|
|
// Print required binaries as markdown table
|
|
printer.Println(terminal.BoldGreen("### Required Binaries (%d)"), len(required))
|
|
printer.Newline()
|
|
|
|
// Print table header
|
|
if showTags {
|
|
printer.Println("| %s | %s | %s | %s |",
|
|
padRight("Name", nameWidth),
|
|
padRight("Status", statusWidth),
|
|
padRight("Description", descWidth),
|
|
padRight("Tags", tagsWidth))
|
|
printer.Println("|%s|%s|%s|%s|",
|
|
strings.Repeat("-", nameWidth+2),
|
|
strings.Repeat("-", statusWidth+2),
|
|
strings.Repeat("-", descWidth+2),
|
|
strings.Repeat("-", tagsWidth+2))
|
|
} else {
|
|
printer.Println("| %s | %s | %s |",
|
|
padRight("Name", nameWidth),
|
|
padRight("Status", statusWidth),
|
|
padRight("Description", descWidth))
|
|
printer.Println("|%s|%s|%s|",
|
|
strings.Repeat("-", nameWidth+2),
|
|
strings.Repeat("-", statusWidth+2),
|
|
strings.Repeat("-", descWidth+2))
|
|
}
|
|
|
|
// Print required binaries
|
|
for _, name := range required {
|
|
entry := registry[name]
|
|
statusText := "missing"
|
|
statusColor := terminal.Red
|
|
if installer.IsBinaryInstalled(name, &entry) {
|
|
statusText = "installed"
|
|
statusColor = terminal.Green
|
|
}
|
|
desc := truncatePad(entry.Desc, descWidth)
|
|
|
|
if showTags {
|
|
tags := truncatePad(strings.Join(entry.Tags, ", "), tagsWidth)
|
|
printer.Println("| %s | %s | %s | %s |",
|
|
colorPadRight(name, nameWidth, terminal.Cyan),
|
|
colorPadRight(statusText, statusWidth, statusColor),
|
|
desc,
|
|
tags)
|
|
} else {
|
|
printer.Println("| %s | %s | %s |",
|
|
colorPadRight(name, nameWidth, terminal.Cyan),
|
|
colorPadRight(statusText, statusWidth, statusColor),
|
|
desc)
|
|
}
|
|
}
|
|
|
|
// Print optional binaries section
|
|
printer.Newline()
|
|
printer.Println(terminal.BoldYellow("### Optional Binaries (%d)"), len(optional))
|
|
printer.Println(terminal.Gray("(use --install-optional to include these)"))
|
|
printer.Newline()
|
|
|
|
// Print table header for optional
|
|
if showTags {
|
|
printer.Println("| %s | %s | %s | %s |",
|
|
padRight("Name", nameWidth),
|
|
padRight("Status", statusWidth),
|
|
padRight("Description", descWidth),
|
|
padRight("Tags", tagsWidth))
|
|
printer.Println("|%s|%s|%s|%s|",
|
|
strings.Repeat("-", nameWidth+2),
|
|
strings.Repeat("-", statusWidth+2),
|
|
strings.Repeat("-", descWidth+2),
|
|
strings.Repeat("-", tagsWidth+2))
|
|
} else {
|
|
printer.Println("| %s | %s | %s |",
|
|
padRight("Name", nameWidth),
|
|
padRight("Status", statusWidth),
|
|
padRight("Description", descWidth))
|
|
printer.Println("|%s|%s|%s|",
|
|
strings.Repeat("-", nameWidth+2),
|
|
strings.Repeat("-", statusWidth+2),
|
|
strings.Repeat("-", descWidth+2))
|
|
}
|
|
|
|
// Print optional binaries
|
|
for _, name := range optional {
|
|
entry := registry[name]
|
|
statusText := "missing"
|
|
statusColor := terminal.Red
|
|
if installer.IsBinaryInstalled(name, &entry) {
|
|
statusText = "installed"
|
|
statusColor = terminal.Green
|
|
}
|
|
desc := truncatePad(entry.Desc, descWidth)
|
|
|
|
if showTags {
|
|
tags := truncatePad(strings.Join(entry.Tags, ", "), tagsWidth)
|
|
printer.Println("| %s | %s | %s | %s |",
|
|
colorPadRight(name, nameWidth, terminal.Yellow),
|
|
colorPadRight(statusText, statusWidth, statusColor),
|
|
desc,
|
|
tags)
|
|
} else {
|
|
printer.Println("| %s | %s | %s |",
|
|
colorPadRight(name, nameWidth, terminal.Yellow),
|
|
colorPadRight(statusText, statusWidth, statusColor),
|
|
desc)
|
|
}
|
|
}
|
|
|
|
// Print usage examples
|
|
printer.Newline()
|
|
printer.Println(terminal.BoldCyan("### Usage Examples"))
|
|
printer.Println(" " + terminal.Green("# Install specific binaries"))
|
|
printer.Println(" osmedeus install binary --name nuclei --name httpx")
|
|
printer.Newline()
|
|
printer.Println(" " + terminal.Green("# Install all required binaries"))
|
|
printer.Println(" osmedeus install binary --all")
|
|
printer.Newline()
|
|
printer.Println(" " + terminal.Green("# Install all binaries including optional"))
|
|
printer.Println(" osmedeus install binary --all --install-optional")
|
|
|
|
printer.Newline()
|
|
printer.Println(terminal.Gray("Total: %d required, %d optional"), len(required), len(optional))
|
|
return nil
|
|
}
|
|
|
|
// checkBinaries checks if binaries are installed without downloading
|
|
func checkBinaries(names []string, registry installer.BinaryRegistry, printer *terminal.Printer) error {
|
|
ready := 0
|
|
missing := 0
|
|
|
|
for _, name := range names {
|
|
entry, exists := registry[name]
|
|
if !exists {
|
|
printer.Warning("%s - not found in registry", name)
|
|
continue
|
|
}
|
|
|
|
if installer.IsBinaryInstalled(name, &entry) {
|
|
printer.Success("%s - ready", name)
|
|
ready++
|
|
} else {
|
|
printer.Error("%s - not installed", name)
|
|
missing++
|
|
}
|
|
}
|
|
|
|
printer.Info("")
|
|
printer.Info("Summary: %d/%d binaries ready", ready, ready+missing)
|
|
|
|
if missing > 0 {
|
|
return fmt.Errorf("%d binaries not installed", missing)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func runInstallValidate(cmd *cobra.Command, args []string) error {
|
|
cfg := config.Get()
|
|
if cfg == nil {
|
|
return fmt.Errorf("configuration not loaded")
|
|
}
|
|
|
|
if validateSample && validatePreset {
|
|
return fmt.Errorf("cannot use --sample and --preset together")
|
|
}
|
|
|
|
if validateSample {
|
|
if err := replaceBaseFolderWithEmbeddedSample(cfg.BaseFolder); err != nil {
|
|
return err
|
|
}
|
|
reloaded, err := config.Load(cfg.BaseFolder)
|
|
if err == nil {
|
|
config.Set(reloaded)
|
|
}
|
|
}
|
|
|
|
if validatePreset {
|
|
printer := terminal.NewPrinter()
|
|
|
|
// Get preset URL from environment or use default
|
|
presetURL := os.Getenv("OSM_PRESET_URL")
|
|
if presetURL != "" {
|
|
printer.Info("Using preset URL from OSM_PRESET_URL environment variable")
|
|
printer.Println(" %s %s", terminal.SymbolBullet, terminal.Gray(presetURL))
|
|
} else {
|
|
presetURL = core.DEFAULT_BASE_REPO
|
|
printer.Info("Using default preset URL")
|
|
printer.Println(" %s %s", terminal.SymbolBullet, terminal.Gray(presetURL))
|
|
}
|
|
|
|
headers := parseCustomHeaders(customHeaders)
|
|
inst := installer.NewInstaller(
|
|
cfg.BaseFolder,
|
|
cfg.WorkflowsPath,
|
|
cfg.BinariesPath,
|
|
headers,
|
|
)
|
|
inst.KeepSetting = keepSetting
|
|
if err := inst.InstallBase(presetURL); err != nil {
|
|
return err
|
|
}
|
|
reloaded, err := config.Load(cfg.BaseFolder)
|
|
if err == nil {
|
|
config.Set(reloaded)
|
|
}
|
|
}
|
|
|
|
return runHealth(cmd, args)
|
|
}
|
|
|
|
func replaceBaseFolderWithEmbeddedSample(baseFolder string) error {
|
|
if baseFolder == "" {
|
|
return fmt.Errorf("base folder is not configured")
|
|
}
|
|
if _, err := os.Stat(baseFolder); err == nil {
|
|
if err := os.RemoveAll(baseFolder); err != nil {
|
|
return fmt.Errorf("failed to remove existing base folder: %w", err)
|
|
}
|
|
}
|
|
return copyEmbeddedAssets(baseFolder)
|
|
}
|
|
|
|
func runInstallEnv(cmd *cobra.Command, args []string) error {
|
|
cfg := config.Get()
|
|
if cfg == nil {
|
|
return fmt.Errorf("configuration not loaded")
|
|
}
|
|
|
|
printer := terminal.NewPrinter()
|
|
|
|
// Determine binaries folder
|
|
binariesFolder := cfg.BinariesPath
|
|
if binariesFolder == "" {
|
|
binariesFolder = filepath.Join(cfg.BaseFolder, "external-binaries")
|
|
}
|
|
|
|
// Get home directory
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get home directory: %w", err)
|
|
}
|
|
|
|
shell := detectShell()
|
|
primaryConfig := shellConfigForShell(homeDir, shell)
|
|
|
|
// The export line to add
|
|
exportLine := fmt.Sprintf(`export PATH="%s:$PATH"`, binariesFolder)
|
|
comment := "# Added by osmedeus"
|
|
|
|
printer.Println("%s Binaries folder: %s", terminal.BoldCyan("◆"), terminal.White(binariesFolder))
|
|
printer.Println("%s Detected shell: %s", terminal.BoldCyan("◆"), terminal.White(shellDisplayName(shell)))
|
|
|
|
configs := []string{primaryConfig}
|
|
if installEnvAll {
|
|
configs = allShellConfigs(homeDir, primaryConfig)
|
|
}
|
|
|
|
for _, configFile := range configs {
|
|
updated, err := addOrUpdateShellConfigBlock(configFile, exportLine, comment)
|
|
if err != nil {
|
|
printer.Error("Failed to update %s: %s", terminal.White(configFile), err)
|
|
continue
|
|
}
|
|
if updated {
|
|
printer.Success("Added PATH to %s", terminal.White(configFile))
|
|
} else {
|
|
printer.Info("PATH already configured in %s", terminal.White(configFile))
|
|
}
|
|
}
|
|
|
|
// Update current process PATH immediately
|
|
currentPath := os.Getenv("PATH")
|
|
if !pathContainsDir(currentPath, binariesFolder) {
|
|
newPath := binariesFolder + string(os.PathListSeparator) + currentPath
|
|
_ = os.Setenv("PATH", newPath)
|
|
printer.Success("Updated PATH in current process")
|
|
}
|
|
|
|
// Always show the export command for the user's terminal session
|
|
// (we can't modify the parent shell's environment, only our own process)
|
|
printer.Println("%s For this terminal session, run: %s", terminal.BoldCyan("◆"),
|
|
terminal.Gray(fmt.Sprintf(`export PATH="%s:$PATH"`, binariesFolder)))
|
|
|
|
return nil
|
|
}
|
|
|
|
func detectShell() string {
|
|
return strings.TrimSpace(os.Getenv("SHELL"))
|
|
}
|
|
|
|
func shellDisplayName(shell string) string {
|
|
base := filepath.Base(shell)
|
|
if base == "." || base == "/" {
|
|
base = shell
|
|
}
|
|
base = strings.TrimSpace(base)
|
|
if base == "" {
|
|
return "unknown"
|
|
}
|
|
return base
|
|
}
|
|
|
|
func shellConfigForShell(homeDir, shell string) string {
|
|
s := strings.ToLower(shell)
|
|
zshrc := filepath.Join(homeDir, ".zshrc")
|
|
bashrc := filepath.Join(homeDir, ".bashrc")
|
|
bashProfile := filepath.Join(homeDir, ".bash_profile")
|
|
profile := filepath.Join(homeDir, ".profile")
|
|
|
|
if strings.Contains(s, "zsh") {
|
|
return zshrc
|
|
}
|
|
if strings.Contains(s, "bash") {
|
|
if _, err := os.Stat(bashrc); err == nil {
|
|
return bashrc
|
|
}
|
|
if _, err := os.Stat(bashProfile); err == nil {
|
|
return bashProfile
|
|
}
|
|
return bashrc
|
|
}
|
|
|
|
// Fallback when $SHELL is not set: prefer bashrc over zshrc
|
|
// (bash is more common default, especially in Docker containers)
|
|
if _, err := os.Stat(bashrc); err == nil {
|
|
return bashrc
|
|
}
|
|
if _, err := os.Stat(bashProfile); err == nil {
|
|
return bashProfile
|
|
}
|
|
if _, err := os.Stat(zshrc); err == nil {
|
|
return zshrc
|
|
}
|
|
return profile
|
|
}
|
|
|
|
func allShellConfigs(homeDir, primary string) []string {
|
|
paths := []string{}
|
|
seen := map[string]struct{}{}
|
|
|
|
add := func(p string) {
|
|
p = strings.TrimSpace(p)
|
|
if p == "" {
|
|
return
|
|
}
|
|
if _, ok := seen[p]; ok {
|
|
return
|
|
}
|
|
seen[p] = struct{}{}
|
|
paths = append(paths, p)
|
|
}
|
|
|
|
add(primary)
|
|
add(filepath.Join(homeDir, ".bashrc"))
|
|
add(filepath.Join(homeDir, ".bash_profile"))
|
|
add(filepath.Join(homeDir, ".zshrc"))
|
|
add(filepath.Join(homeDir, ".profile"))
|
|
|
|
return paths
|
|
}
|
|
|
|
func pathContainsDir(pathEnv, dir string) bool {
|
|
if dir == "" {
|
|
return false
|
|
}
|
|
dir = filepath.Clean(dir)
|
|
for _, p := range strings.Split(pathEnv, string(os.PathListSeparator)) {
|
|
if filepath.Clean(strings.TrimSpace(p)) == dir {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// extractRepoNameFromURL extracts the repository name from a go-getter URL
|
|
// Examples:
|
|
// - "github.com/user/repo.git?ref=main" -> "repo"
|
|
// - "https://github.com/user/repo.git//subfolder" -> "repo"
|
|
// - "github.com/projectdiscovery/nuclei-templates.git?ref=main&depth=1" -> "nuclei-templates"
|
|
func extractRepoNameFromURL(url string) string {
|
|
// Remove query parameters
|
|
if idx := strings.Index(url, "?"); idx != -1 {
|
|
url = url[:idx]
|
|
}
|
|
// Remove subfolder reference
|
|
if idx := strings.Index(url, "//"); idx != -1 {
|
|
url = url[:idx]
|
|
}
|
|
// Remove .git suffix
|
|
url = strings.TrimSuffix(url, ".git")
|
|
// Get the last path segment
|
|
parts := strings.Split(url, "/")
|
|
if len(parts) > 0 {
|
|
return parts[len(parts)-1]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// ensureBinariesPathInEnv ensures the binaries folder is in PATH for both
|
|
// the current process and shell config. Returns true if PATH was already
|
|
// configured or successfully updated.
|
|
func ensureBinariesPathInEnv(printer *terminal.Printer, binariesFolder string, showExportHint bool) bool {
|
|
if binariesFolder == "" {
|
|
return true
|
|
}
|
|
|
|
// Skip shell config modification in test environments
|
|
if os.Getenv("OSM_SKIP_PATH_SETUP") == "1" {
|
|
// Still add to current process PATH
|
|
if !pathContainsDir(os.Getenv("PATH"), binariesFolder) {
|
|
currentPath := os.Getenv("PATH")
|
|
newPath := binariesFolder + string(os.PathListSeparator) + currentPath
|
|
_ = os.Setenv("PATH", newPath)
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Check if already in PATH
|
|
if pathContainsDir(os.Getenv("PATH"), binariesFolder) {
|
|
return true
|
|
}
|
|
|
|
// Add to current process PATH immediately
|
|
currentPath := os.Getenv("PATH")
|
|
newPath := binariesFolder + string(os.PathListSeparator) + currentPath
|
|
_ = os.Setenv("PATH", newPath)
|
|
|
|
// Add to shell config for persistence
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
if showExportHint {
|
|
printer.Println("%s For this session: %s", terminal.BoldCyan("◆"),
|
|
terminal.Gray(fmt.Sprintf(`export PATH="%s:$PATH"`, binariesFolder)))
|
|
}
|
|
return false
|
|
}
|
|
|
|
shell := detectShell()
|
|
configFile := shellConfigForShell(homeDir, shell)
|
|
exportLine := fmt.Sprintf(`export PATH="%s:$PATH"`, binariesFolder)
|
|
comment := "# Added by osmedeus"
|
|
|
|
updated, _ := addOrUpdateShellConfigBlock(configFile, exportLine, comment)
|
|
if updated {
|
|
printer.Success("Added PATH to %s", terminal.White(configFile))
|
|
}
|
|
|
|
if showExportHint {
|
|
printer.Println("%s For this session: %s", terminal.BoldCyan("◆"),
|
|
terminal.Gray(fmt.Sprintf(`export PATH="%s:$PATH"`, binariesFolder)))
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func ensureNixProfileBinInProcess(printer *terminal.Printer) {
|
|
nixBinDir := "/nix/var/nix/profiles/default/bin"
|
|
if pathContainsDir(os.Getenv("PATH"), nixBinDir) {
|
|
return
|
|
}
|
|
if _, err := os.Stat(filepath.Join(nixBinDir, "nix")); err != nil {
|
|
return
|
|
}
|
|
_ = os.Setenv("PATH", os.Getenv("PATH")+string(os.PathListSeparator)+nixBinDir)
|
|
if printer != nil {
|
|
printer.Success("Added Nix binary path to current PATH: %s", terminal.White(nixBinDir))
|
|
}
|
|
}
|
|
|
|
func ensureNixProfileBinInShell(printer *terminal.Printer) {
|
|
// Skip in test environments
|
|
if os.Getenv("OSM_SKIP_PATH_SETUP") == "1" {
|
|
return
|
|
}
|
|
|
|
nixBinDir := "/nix/var/nix/profiles/default/bin"
|
|
if _, err := os.Stat(filepath.Join(nixBinDir, "nix")); err != nil {
|
|
return
|
|
}
|
|
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
shell := detectShell()
|
|
primaryConfig := shellConfigForShell(homeDir, shell)
|
|
exportLine := fmt.Sprintf(`export PATH="$PATH:%s"`, nixBinDir)
|
|
comment := "# Added by osmedeus (nix)"
|
|
|
|
updated, err := addToShellConfig(primaryConfig, exportLine, comment)
|
|
if err != nil {
|
|
if printer != nil {
|
|
printer.Warning("Failed to update %s: %s", terminal.White(primaryConfig), err)
|
|
}
|
|
return
|
|
}
|
|
if updated {
|
|
if printer != nil {
|
|
printer.Success("Added Nix PATH to %s", terminal.White(primaryConfig))
|
|
}
|
|
}
|
|
}
|
|
|
|
// addToShellConfig adds the export line to a shell config file if not already present
|
|
func addToShellConfig(configFile, exportLine, comment string) (bool, error) {
|
|
// Check if file exists
|
|
content := ""
|
|
if data, err := os.ReadFile(configFile); err == nil {
|
|
content = string(data)
|
|
}
|
|
|
|
// Check if the export line already exists
|
|
if strings.Contains(content, exportLine) {
|
|
return false, nil
|
|
}
|
|
|
|
// Open file for appending (create if doesn't exist)
|
|
file, err := os.OpenFile(configFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
|
|
// Write the export line with a comment
|
|
writer := bufio.NewWriter(file)
|
|
_, err = fmt.Fprintf(writer, "\n%s\n%s\n", comment, exportLine)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return true, writer.Flush()
|
|
}
|
|
|
|
func addOrUpdateShellConfigBlock(configFile, exportLine, comment string) (bool, error) {
|
|
data, err := os.ReadFile(configFile)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return false, err
|
|
}
|
|
content := string(data)
|
|
if strings.Contains(content, exportLine) {
|
|
return false, nil
|
|
}
|
|
|
|
legacyComments := []string{"# Added by osmedeus", "# Added by osmedeus install env"}
|
|
lines := strings.Split(content, "\n")
|
|
|
|
for i := 0; i < len(lines); i++ {
|
|
lineTrim := strings.TrimSpace(lines[i])
|
|
matched := false
|
|
for _, legacy := range legacyComments {
|
|
if lineTrim == legacy {
|
|
matched = true
|
|
break
|
|
}
|
|
}
|
|
if !matched {
|
|
continue
|
|
}
|
|
|
|
lines[i] = comment
|
|
if i+1 >= len(lines) {
|
|
lines = append(lines, exportLine)
|
|
} else {
|
|
nextTrim := strings.TrimSpace(lines[i+1])
|
|
if strings.HasPrefix(nextTrim, "export PATH=") {
|
|
lines[i+1] = exportLine
|
|
} else {
|
|
lines = append(lines[:i+1], append([]string{exportLine}, lines[i+1:]...)...)
|
|
}
|
|
}
|
|
|
|
mode := os.FileMode(0644)
|
|
if st, statErr := os.Stat(configFile); statErr == nil {
|
|
mode = st.Mode()
|
|
}
|
|
return true, os.WriteFile(configFile, []byte(strings.Join(lines, "\n")), mode)
|
|
}
|
|
|
|
return addToShellConfig(configFile, exportLine, comment)
|
|
}
|
|
|
|
func printInstallBinaryHelp(cmd *cobra.Command) {
|
|
fmt.Print(terminal.Banner())
|
|
|
|
fmt.Println(terminal.BoldCyan("◆ Usage"))
|
|
fmt.Printf(" %s %s\n\n", terminal.Yellow("osmedeus install binary"), terminal.Gray("[flags]"))
|
|
|
|
fmt.Println(terminal.BoldCyan("◆ Modes"))
|
|
fmt.Printf(" %s %s\n", terminal.Yellow("direct-fetch"), terminal.Gray("download from registry metadata (github-release)"))
|
|
fmt.Printf(" %s %s\n", terminal.Yellow("go-getter"), terminal.Gray("clone repos or download archives via go-getter"))
|
|
fmt.Printf(" %s %s\n\n", terminal.Yellow("nix"), terminal.Gray("install with 'nix profile add'"))
|
|
|
|
fmt.Println(terminal.BoldCyan("◆ Examples"))
|
|
fmt.Printf(" %s\n", terminal.Green("# List available binaries"))
|
|
fmt.Printf(" %s\n", terminal.Gray("osmedeus install binary --list-registry-direct-fetch"))
|
|
fmt.Printf(" %s\n\n", terminal.Gray("osmedeus install binary --list-registry-nix-build"))
|
|
|
|
fmt.Printf(" %s\n", terminal.Green("# Install specific binaries (auto-detects method from registry)"))
|
|
fmt.Printf(" %s\n", terminal.Gray("osmedeus install binary --name nuclei"))
|
|
fmt.Printf(" %s\n", terminal.Gray("osmedeus install binary --name nuclei --name httpx --name ffuf"))
|
|
fmt.Printf(" %s\n\n", terminal.Gray("osmedeus install binary -n dalfox -n gau -n interactsh"))
|
|
|
|
fmt.Printf(" %s\n", terminal.Green("# Install all required binaries"))
|
|
fmt.Printf(" %s\n", terminal.Gray("osmedeus install binary --all"))
|
|
fmt.Printf(" %s\n\n", terminal.Gray("osmedeus install binary --all --install-optional"))
|
|
|
|
fmt.Printf(" %s\n", terminal.Green("# Check binary installation status"))
|
|
fmt.Printf(" %s\n", terminal.Gray("osmedeus install binary --name nuclei --check"))
|
|
fmt.Printf(" %s\n\n", terminal.Gray("osmedeus install binary --all --check"))
|
|
|
|
fmt.Printf(" %s\n", terminal.Green("# Download via go-getter (git clone, archives, etc.)"))
|
|
fmt.Printf(" %s\n", terminal.Gray("osmedeus install binary --go-getter github.com/user/repo.git"))
|
|
fmt.Printf(" %s\n", terminal.Gray("osmedeus install binary --go-getter github.com/user/repo.git?ref=main&depth=1"))
|
|
fmt.Printf(" %s\n", terminal.Gray("osmedeus install binary --go-getter https://github.com/user/repo.git//subfolder"))
|
|
fmt.Printf(" %s\n\n", terminal.Gray("osmedeus install binary --go-getter github.com/user/repo.git --go-getter-dest /opt/tools"))
|
|
|
|
fmt.Printf(" %s\n", terminal.Green("# Install via Nix (requires Nix)"))
|
|
fmt.Printf(" %s\n", terminal.Gray("osmedeus install binary --nix-installation"))
|
|
fmt.Printf(" %s\n", terminal.Gray("osmedeus install binary --name nuclei --nix-build-install"))
|
|
fmt.Printf(" %s\n\n", terminal.Gray("osmedeus install binary --nix-pkgs nixpkgs#redis --nix-pkgs nixpkgs#jq"))
|
|
|
|
if cmd == nil {
|
|
return
|
|
}
|
|
|
|
fmt.Println()
|
|
fmt.Println("Usage:")
|
|
fmt.Printf(" %s\n\n", cmd.UseLine())
|
|
|
|
// Only show local flags (not inherited from parent commands)
|
|
fmt.Println("Flags:")
|
|
fmt.Print(cmd.LocalFlags().FlagUsages())
|
|
|
|
if cmd.HasAvailableInheritedFlags() {
|
|
fmt.Println("\nGlobal Flags:")
|
|
fmt.Print(cmd.InheritedFlags().FlagUsages())
|
|
}
|
|
}
|
|
|
|
func init() {
|
|
// Persistent flag for custom headers (inherited by all subcommands)
|
|
installCmd.PersistentFlags().StringArrayVar(&customHeaders, "custom-header", []string{}, "custom HTTP header(s) for downloads (format: 'Key: Value', can be repeated)")
|
|
|
|
installBaseCmd.Flags().BoolVar(&baseSample, "sample", false, "initialize base folder from embedded sample (replaces existing base folder)")
|
|
installBaseCmd.Flags().BoolVar(&basePreset, "preset", false, "install from OSM_PRESET_URL environment variable (default: DEFAULT_BASE_REPO)")
|
|
installBaseCmd.Flags().BoolVar(&keepSetting, "keep-setting", false, "restore previous osm-settings.yaml after base installation")
|
|
installWorkflowCmd.Flags().BoolVar(&workflowPreset, "preset", false, "install from OSM_WORKFLOW_URL environment variable (default: DEFAULT_WORKFLOW_REPO)")
|
|
// Note: --force flag is now global (defined in root.go)
|
|
|
|
// Binary command flags
|
|
installBinaryCmd.Flags().StringSliceVarP(&binaryNames, "name", "n", []string{}, "binary name(s) to install (can be repeated)")
|
|
installBinaryCmd.Flags().StringSliceVar(&nixPkgs, "nix-pkgs", []string{}, "Nix package(s) to add to profile (repeatable)")
|
|
installBinaryCmd.Flags().StringVarP(®istryPath, "registry", "r", "", "path or URL to binary registry JSON (default: embedded registry)")
|
|
installBinaryCmd.Flags().BoolVar(&installAll, "all", false, "install all binaries from the registry")
|
|
installBinaryCmd.Flags().BoolVar(&checkOnly, "check", false, "check if binaries are installed without downloading")
|
|
installBinaryCmd.Flags().BoolVar(&nixBuildInstall, "nix-build-install", false, "use Nix to install binaries instead of direct downloads")
|
|
installBinaryCmd.Flags().BoolVar(&nixInstallation, "nix-installation", false, "install Nix package manager (Determinate Systems installer)")
|
|
installBinaryCmd.Flags().BoolVar(&installOptional, "install-optional", false, "include optional binaries in installation")
|
|
// Note: --width flag is now global (defined in root.go), --max-width removed in favor of --width
|
|
installBinaryCmd.Flags().BoolVar(&hideBinaryTags, "disable-tags", false, "hide tags column in list output")
|
|
installBinaryCmd.Flags().StringSliceVar(&goGetterSources, "go-getter", []string{}, "source URL(s) to download via go-getter (supports git repos, archives, etc.)")
|
|
installBinaryCmd.Flags().StringVar(&goGetterDest, "go-getter-dest", "", "destination directory for go-getter downloads (default: $HOME)")
|
|
installBinaryCmd.Flags().BoolVar(&listRegistryNixBuild, "list-registry-nix-build", false, "list binaries available in Nix flake")
|
|
installBinaryCmd.Flags().BoolVar(&listRegistryNixBuild, "list-binary-nix", false, "list binaries available in Nix flake (alias)")
|
|
installBinaryCmd.Flags().BoolVar(&listRegistryDirectFetch, "list-registry-direct-fetch", false, "list binaries available in registry JSON")
|
|
installBinaryCmd.Flags().BoolVar(&listRegistryDirectFetch, "list-binary-registry", false, "list binaries available in registry JSON (alias)")
|
|
installEnvCmd.Flags().BoolVar(&installEnvAll, "all", false, "add binaries path to all supported shell config files")
|
|
|
|
installBinaryCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
|
|
printInstallBinaryHelp(cmd)
|
|
})
|
|
|
|
// Add subcommands
|
|
installCmd.AddCommand(installWorkflowCmd)
|
|
installCmd.AddCommand(installBaseCmd)
|
|
installCmd.AddCommand(installBinaryCmd)
|
|
installCmd.AddCommand(installEnvCmd)
|
|
installCmd.AddCommand(installValidateCmd)
|
|
installCmd.AddCommand(installSkillsCmd)
|
|
addSkillsInstallFlags(installSkillsCmd)
|
|
|
|
installValidateCmd.Flags().BoolVar(&validateSample, "sample", false, "initialize base folder from embedded sample (replaces existing base folder)")
|
|
installValidateCmd.Flags().BoolVar(&validatePreset, "preset", false, "install ready-to-use base from default repository")
|
|
installValidateCmd.Flags().BoolVar(&keepSetting, "keep-setting", false, "restore previous osm-settings.yaml after base installation")
|
|
}
|
|
|
|
// parseCustomHeaders converts the string slice of "Key: Value" pairs to a map
|
|
func parseCustomHeaders(headers []string) map[string]string {
|
|
result := make(map[string]string)
|
|
for _, h := range headers {
|
|
// Split on first ": " to handle values that contain colons
|
|
idx := strings.Index(h, ": ")
|
|
if idx == -1 {
|
|
// Try just ":" without space
|
|
idx = strings.Index(h, ":")
|
|
if idx == -1 {
|
|
continue
|
|
}
|
|
key := strings.TrimSpace(h[:idx])
|
|
value := strings.TrimSpace(h[idx+1:])
|
|
if key != "" {
|
|
result[key] = value
|
|
}
|
|
} else {
|
|
key := strings.TrimSpace(h[:idx])
|
|
value := strings.TrimSpace(h[idx+2:])
|
|
if key != "" {
|
|
result[key] = value
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// containsTag checks if a slice of tags contains a specific tag
|
|
func containsTag(tags []string, target string) bool {
|
|
for _, tag := range tags {
|
|
if tag == target {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// padRight pads a string to the specified width with spaces on the right
|
|
// This ensures ANSI codes don't affect alignment when applied after padding
|
|
func padRight(s string, width int) string {
|
|
if len(s) >= width {
|
|
return s[:width]
|
|
}
|
|
return s + strings.Repeat(" ", width-len(s))
|
|
}
|
|
|
|
// colorPadRight pads string to width, then applies color function
|
|
func colorPadRight(s string, width int, colorFn func(string) string) string {
|
|
padded := padRight(s, width)
|
|
return colorFn(padded)
|
|
}
|
|
|
|
// truncatePad truncates then pads to exact width
|
|
func truncatePad(s string, width int) string {
|
|
if len(s) > width {
|
|
if width > 3 {
|
|
return s[:width-3] + "..."
|
|
}
|
|
return s[:width]
|
|
}
|
|
return padRight(s, width)
|
|
}
|
|
|
|
// defaultParallelWorkers is the number of concurrent binary installation workers
|
|
const defaultParallelWorkers = 3
|
|
|
|
// binariesPerRow is the number of binary names displayed per row
|
|
const binariesPerRow = 6
|
|
|
|
// splitIntoRows splits binary names into rows of specified size
|
|
func splitIntoRows(names []string, perRow int) [][]string {
|
|
var rows [][]string
|
|
for i := 0; i < len(names); i += perRow {
|
|
end := i + perRow
|
|
if end > len(names) {
|
|
end = len(names)
|
|
}
|
|
rows = append(rows, names[i:end])
|
|
}
|
|
return rows
|
|
}
|
|
|
|
// renderMultiRowDisplay renders rows of binaries with spinners
|
|
// Each row gets its own spinner if any binary in that row is pending
|
|
func renderMultiRowDisplay(rows [][]string, status map[string]string, spinnerFrames []string,
|
|
spinnerIdx int, mu *sync.Mutex, showSpinner bool, isFirstRender *bool) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
// Only move cursor up if not the first render (avoids race condition with initial output)
|
|
lineCount := len(rows)
|
|
if !*isFirstRender {
|
|
fmt.Printf("\033[%dA\r", lineCount)
|
|
}
|
|
*isFirstRender = false
|
|
|
|
for _, row := range rows {
|
|
fmt.Print("\033[K") // Clear line
|
|
|
|
// Check if any binary in this row is pending
|
|
hasPending := false
|
|
for _, name := range row {
|
|
if status[name] == "pending" {
|
|
hasPending = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if showSpinner && hasPending {
|
|
fmt.Printf(" %s %s ", terminal.Cyan(spinnerFrames[spinnerIdx]), terminal.SymbolBullet)
|
|
} else {
|
|
fmt.Printf(" %s ", terminal.SymbolBullet)
|
|
}
|
|
|
|
for i, name := range row {
|
|
if i > 0 {
|
|
fmt.Print(" ")
|
|
}
|
|
switch status[name] {
|
|
case "installed":
|
|
fmt.Print(terminal.HiGreen(name))
|
|
case "failed":
|
|
fmt.Print(terminal.Red(name))
|
|
default:
|
|
fmt.Print(terminal.Gray(name))
|
|
}
|
|
}
|
|
fmt.Println()
|
|
}
|
|
}
|
|
|
|
// installBinariesParallel installs binaries using parallel workers with multi-row spinner
|
|
func installBinariesParallel(names []string, registry installer.BinaryRegistry,
|
|
binariesFolder string, headers map[string]string, printer *terminal.Printer, silent bool) (failed []string) {
|
|
|
|
if len(names) == 0 {
|
|
return nil
|
|
}
|
|
|
|
rows := splitIntoRows(names, binariesPerRow)
|
|
|
|
// Initialize status tracking
|
|
status := make(map[string]string)
|
|
var statusMu sync.Mutex
|
|
for _, name := range names {
|
|
status[name] = "pending"
|
|
}
|
|
|
|
// Spinner configuration
|
|
spinnerFrames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
|
|
|
// Pre-check which binaries are already installed
|
|
var toInstall []string
|
|
for _, name := range names {
|
|
if installer.IsBinaryInPath(name) {
|
|
if !installer.IsCoreUnixTool(name) {
|
|
entry, ok := registry[name]
|
|
if ok {
|
|
_ = installer.SymlinkInstalledBinaryToFolder(name, &entry, binariesFolder)
|
|
}
|
|
}
|
|
status[name] = "installed"
|
|
} else {
|
|
toInstall = append(toInstall, name)
|
|
}
|
|
}
|
|
|
|
// If all binaries are already installed, just show final state and return
|
|
if len(toInstall) == 0 {
|
|
if !silent {
|
|
isFirst := true
|
|
renderMultiRowDisplay(rows, status, spinnerFrames, 0, &statusMu, false, &isFirst)
|
|
}
|
|
return nil
|
|
}
|
|
spinnerIdx := 0
|
|
spinnerDone := make(chan struct{})
|
|
|
|
// Start spinner display (skip in silent mode)
|
|
if !silent {
|
|
isFirstRender := true
|
|
|
|
// Start spinner goroutine (only in non-silent mode)
|
|
go func() {
|
|
// Do immediate first render to avoid race condition
|
|
renderMultiRowDisplay(rows, status, spinnerFrames, spinnerIdx, &statusMu, true, &isFirstRender)
|
|
|
|
ticker := time.NewTicker(80 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-spinnerDone:
|
|
return
|
|
case <-ticker.C:
|
|
spinnerIdx = (spinnerIdx + 1) % len(spinnerFrames)
|
|
renderMultiRowDisplay(rows, status, spinnerFrames, spinnerIdx, &statusMu, true, &isFirstRender)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Create work channel and results
|
|
workCh := make(chan string, len(toInstall))
|
|
var wg sync.WaitGroup
|
|
var failedMu sync.Mutex
|
|
|
|
// Start workers
|
|
for i := 0; i < defaultParallelWorkers; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for name := range workCh {
|
|
if silent {
|
|
fmt.Printf(" %s Installing: %s\n", terminal.SymbolBullet, terminal.Cyan(name))
|
|
}
|
|
err := installer.InstallBinary(name, registry, binariesFolder, headers)
|
|
statusMu.Lock()
|
|
if err != nil {
|
|
status[name] = "failed"
|
|
failedMu.Lock()
|
|
failed = append(failed, name)
|
|
failedMu.Unlock()
|
|
} else {
|
|
status[name] = "installed"
|
|
}
|
|
statusMu.Unlock()
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Send work (only binaries that need installation)
|
|
for _, name := range toInstall {
|
|
workCh <- name
|
|
}
|
|
close(workCh)
|
|
|
|
// Wait for completion
|
|
wg.Wait()
|
|
close(spinnerDone)
|
|
|
|
if !silent {
|
|
time.Sleep(100 * time.Millisecond)
|
|
// Final render without spinners
|
|
isFirstFinal := false
|
|
renderMultiRowDisplay(rows, status, spinnerFrames, 0, &statusMu, false, &isFirstFinal)
|
|
}
|
|
|
|
return failed
|
|
}
|