mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-14 03:30:26 +00:00
54717e32bc
Remove --select flag. Multi-select now appears automatically when a repo contains more than one extension. Add --all flag to skip selection.
226 lines
7.0 KiB
Go
226 lines
7.0 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os/exec"
|
|
|
|
"github.com/charmbracelet/log"
|
|
"github.com/mark3labs/kit/internal/extensions"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
installLocalFlag bool
|
|
installUpdateFlag bool
|
|
installUninstallFlag bool
|
|
installAllFlag bool
|
|
)
|
|
|
|
var installCmd = &cobra.Command{
|
|
Use: "install <git-url>",
|
|
Short: "Install extensions from git repositories",
|
|
Long: `Install extensions from git repositories.
|
|
|
|
The install command downloads and installs Kit extensions from git repositories.
|
|
Extensions are stored in the global extensions directory by default, or in the
|
|
project's .kit/git/ directory when using the --local flag.
|
|
|
|
When a repo contains multiple extensions, an interactive multi-select is shown
|
|
so you can choose which to install. Use --all to skip selection and install everything.
|
|
|
|
Supported URL formats:
|
|
- github.com/user/repo (shorthand, defaults to HTTPS)
|
|
- git:github.com/user/repo
|
|
- https://github.com/user/repo
|
|
- ssh://git@github.com/user/repo
|
|
- git@github.com:user/repo
|
|
|
|
You can pin to a specific version, tag, or commit using @:
|
|
- github.com/user/repo@v1.0.0
|
|
- github.com/user/repo@main
|
|
- github.com/user/repo@abc1234
|
|
|
|
Examples:
|
|
kit install github.com/user/my-extension
|
|
kit install github.com/user/my-extension@v1.0.0
|
|
kit install github.com/user/my-extension --local
|
|
kit install github.com/user/collection --all`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runInstall,
|
|
}
|
|
|
|
func init() {
|
|
installCmd.Flags().BoolVarP(&installLocalFlag, "local", "l", false, "Install to project-local .kit/git/ directory")
|
|
installCmd.Flags().BoolVarP(&installUpdateFlag, "update", "u", false, "Update an already-installed package")
|
|
installCmd.Flags().BoolVar(&installUninstallFlag, "uninstall", false, "Remove an installed package")
|
|
installCmd.Flags().BoolVar(&installAllFlag, "all", false, "Install all extensions without prompting")
|
|
|
|
rootCmd.AddCommand(installCmd)
|
|
}
|
|
|
|
func runInstall(cmd *cobra.Command, args []string) error {
|
|
sourceStr := args[0]
|
|
|
|
// Check that git is available
|
|
if _, err := exec.LookPath("git"); err != nil {
|
|
return fmt.Errorf("git is not installed or not in PATH")
|
|
}
|
|
|
|
// Parse the source
|
|
source, err := extensions.ParseGitSource(sourceStr)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid source: %w", err)
|
|
}
|
|
|
|
// Determine scope
|
|
scope := extensions.ScopeGlobal
|
|
if installLocalFlag {
|
|
scope = extensions.ScopeProject
|
|
}
|
|
|
|
installer := extensions.NewInstaller(".")
|
|
|
|
// Handle uninstall
|
|
if installUninstallFlag {
|
|
return runUninstall(installer, source, scope)
|
|
}
|
|
|
|
// Handle update
|
|
if installUpdateFlag {
|
|
return runUpdate(installer, source, scope)
|
|
}
|
|
|
|
// Handle install
|
|
return runInstallPackage(installer, source, scope)
|
|
}
|
|
|
|
func runInstallPackage(installer *extensions.Installer, source *extensions.GitSource, scope extensions.InstallScope) error {
|
|
// Check if already installed
|
|
existingScope, installed := installer.IsInstalled(source)
|
|
if installed {
|
|
return fmt.Errorf("extension already installed (scope: %s). Use --update to update or --uninstall to remove", existingScope)
|
|
}
|
|
|
|
// Preview extensions to decide if we need multi-select
|
|
previews, tempDir, err := installer.PreviewExtensions(source)
|
|
if err != nil {
|
|
return fmt.Errorf("previewing extensions: %w", err)
|
|
}
|
|
defer extensions.CleanupTempDir(tempDir)
|
|
|
|
if len(previews) == 0 {
|
|
return fmt.Errorf("no extensions found in %s", source.String())
|
|
}
|
|
|
|
scopeStr := "globally"
|
|
if scope == extensions.ScopeProject {
|
|
scopeStr = "locally in .kit/git/"
|
|
}
|
|
|
|
// Single extension or --all flag: install everything directly
|
|
if len(previews) == 1 || installAllFlag {
|
|
if err := installer.Install(source, scope); err != nil {
|
|
return fmt.Errorf("install failed: %w", err)
|
|
}
|
|
|
|
if source.Pinned {
|
|
fmt.Printf("Installed %s at %s %s\n", source.String(), source.Ref, scopeStr)
|
|
} else {
|
|
fmt.Printf("Installed %d extension(s) from %s %s\n", len(previews), source.String(), scopeStr)
|
|
}
|
|
|
|
log.Info("extension installed", "source", source.String(), "scope", scope)
|
|
return nil
|
|
}
|
|
|
|
// Multiple extensions: show interactive selection
|
|
includePaths, err := multiSelectForInstall(previews)
|
|
if err != nil {
|
|
if err.Error() == "selection cancelled" || err.Error() == "no extensions selected" {
|
|
fmt.Println("Install cancelled.")
|
|
return nil
|
|
}
|
|
return fmt.Errorf("selection failed: %w", err)
|
|
}
|
|
|
|
if err := installer.InstallWithInclude(source, scope, includePaths); err != nil {
|
|
return fmt.Errorf("install failed: %w", err)
|
|
}
|
|
|
|
fmt.Printf("Installed %d extension(s) from %s %s\n", len(includePaths), source.String(), scopeStr)
|
|
for _, path := range includePaths {
|
|
fmt.Printf(" - %s\n", path)
|
|
}
|
|
|
|
log.Info("extension installed", "source", source.String(), "scope", scope, "selected", len(includePaths))
|
|
return nil
|
|
}
|
|
|
|
func runUpdate(installer *extensions.Installer, source *extensions.GitSource, scope extensions.InstallScope) error {
|
|
// Find the installed package
|
|
existingScope, installed := installer.IsInstalled(source)
|
|
if !installed {
|
|
// Try to find with wildcard (no version)
|
|
entry, foundScope, err := extensions.FindInManifest(source.Identity())
|
|
if err != nil || entry == nil {
|
|
return fmt.Errorf("extension not installed: %s", source.Identity())
|
|
}
|
|
// Parse the found entry's source
|
|
foundSource, err := extensions.ParseGitSource(entry.Source)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse installed source: %w", err)
|
|
}
|
|
existingScope = foundScope
|
|
source = foundSource
|
|
}
|
|
|
|
// Override scope if specified
|
|
if installLocalFlag && scope != existingScope {
|
|
return fmt.Errorf("extension installed in %s scope, cannot update with --local flag", existingScope)
|
|
}
|
|
scope = existingScope
|
|
|
|
// Check if pinned
|
|
if source.Pinned {
|
|
fmt.Printf("Skipping %s (pinned at %s)\n", source.Identity(), source.Ref)
|
|
return nil
|
|
}
|
|
|
|
// Update
|
|
if err := installer.Update(source, scope); err != nil {
|
|
return fmt.Errorf("update failed: %w", err)
|
|
}
|
|
|
|
fmt.Printf("Updated %s\n", source.Identity())
|
|
log.Info("extension updated", "source", source.Identity(), "scope", scope)
|
|
return nil
|
|
}
|
|
|
|
func runUninstall(installer *extensions.Installer, source *extensions.GitSource, scope extensions.InstallScope) error {
|
|
// Find where it's installed (ignore scope flag for uninstall - remove from wherever it exists)
|
|
existingScope, installed := installer.IsInstalled(source)
|
|
if !installed {
|
|
// Try to find in manifests
|
|
entry, foundScope, err := extensions.FindInManifest(source.Identity())
|
|
if err != nil || entry == nil {
|
|
return fmt.Errorf("extension not installed: %s", source.Identity())
|
|
}
|
|
existingScope = foundScope
|
|
// Parse the found entry's source
|
|
foundSource, err := extensions.ParseGitSource(entry.Source)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse installed source: %w", err)
|
|
}
|
|
source = foundSource
|
|
}
|
|
|
|
// Uninstall from the scope where it's installed
|
|
if err := installer.Uninstall(source, existingScope); err != nil {
|
|
return fmt.Errorf("uninstall failed: %w", err)
|
|
}
|
|
|
|
fmt.Printf("Uninstalled %s from %s scope\n", source.Identity(), existingScope)
|
|
log.Info("extension uninstalled", "source", source.Identity(), "scope", existingScope)
|
|
return nil
|
|
}
|