mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-14 03:30:26 +00:00
78570d4188
Removes ~600 lines of unreferenced code surfaced by deadcode + manual
audit (none of it reachable from production code paths or test setup):
- internal/models/pool.go: ProviderPool was never wired into kitsetup
or the agent; the global pool singleton had zero callers.
- internal/ui/debug_logger.go: CLIDebugLogger was unreachable; debug
routing goes through internal/tools/buffered_logger.go instead.
- internal/ui/tool_approval_input.go: tea.Model never instantiated;
approvals are handled inline in model.go.
- internal/ui/cli.go: DisplayAssistantMessage / DisplayCancellation /
GetDebugLogger had zero callers (the *WithModel variant is what
event_handler.go uses).
- internal/ui/style/enhanced.go: Style{Card,Header,Subheader,Muted,
Success,Error,Warning,Info} + Create{Separator,ProgressBar} — none
used. CreateBadge stays (used by model.go).
- internal/ui/style/themes.go: RefreshThemeRegistry — never called.
- internal/ui/block_renderer.go: With{FullWidth,MarginTop,Padding{Left,
Right},Background,Foreground,Width} — option helpers nobody calls.
- internal/ui/render/blocks.go: UserBlock, ToolBlock — replaced by
inline rendering elsewhere; the test for UserBlock was rewritten to
directly exercise HighlightFileTokens (which is what the test really
cared about).
- internal/ui/commands/commands.go: GetAllCommandNames — no callers.
- internal/ui/message_items.go: NewTextMessageItem,
NewSystemMessageItem + the entire SystemMessageItem type — model.go
uses NewStyledMessageItem instead.
- internal/prompts/loader.go: Deduplicate — the loader does dedup
internally; standalone helper was unused.
- internal/models/cache_options.go: mergeProviderOptions + its
test-only consumer.
- internal/extensions/installer.go: Installer.GetInstalledPackages —
intended for a 'kit ext list' command that was never built.
- internal/extensions/manifest.go: saveManifestToScope,
saveManifestToPath, GetGlobalManifest, GetProjectManifest,
addEntryToManifest, removeEntryFromManifest — package-level
duplicates of *Installer methods. Tests rewritten to exercise the
live Installer methods instead, which fixes a latent path-resolution
inconsistency between manifestPathForScope and Installer.manifestPath
(the former hard-coded paths, the latter respects projectGitRoot).
- internal/extensions/subagent.go: SpawnSubagent + helpers
(generateSubagentID, findKitBinary, subagentJSONOutput). The
subprocess-spawn implementation is unreachable; production code
routes through kit.Kit.Subagent (in-process). Types
(SubagentConfig/Result/Handle/etc.) and the SubagentHandle methods
remain because they are exposed to extensions via Yaegi symbols and
the Context.SpawnSubagent field.
- cmd/root.go: LoadConfigWithEnvSubstitution — one-line wrapper around
kit.LoadConfigWithEnvSubstitution with zero callers.
go test -race ./... passes.
317 lines
9.3 KiB
Go
317 lines
9.3 KiB
Go
package extensions
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/text/cases"
|
|
"golang.org/x/text/language"
|
|
)
|
|
|
|
// Manifest tracks installed git packages.
|
|
type Manifest struct {
|
|
Packages []ManifestEntry `json:"packages"`
|
|
}
|
|
|
|
// ManifestEntry represents a single installed package.
|
|
type ManifestEntry struct {
|
|
// Source is the canonical string representation (e.g., "git:github.com/user/repo@v1.0.0")
|
|
Source string `json:"source"`
|
|
// Repo is the clone URL
|
|
Repo string `json:"repo"`
|
|
// Host is the git host (e.g., github.com)
|
|
Host string `json:"host"`
|
|
// Path is the path on the host (e.g., user/repo)
|
|
Path string `json:"path"`
|
|
// Ref is the optional pinned ref (tag/branch/commit)
|
|
Ref string `json:"ref,omitempty"`
|
|
// Pinned indicates if the ref is pinned
|
|
Pinned bool `json:"pinned"`
|
|
// Scope is where the package is installed (global or project)
|
|
Scope InstallScope `json:"scope"`
|
|
// Installed is when the package was first installed
|
|
Installed time.Time `json:"installed"`
|
|
// Updated is when the package was last updated (only for unpinned, zero time means never updated)
|
|
Updated time.Time `json:"updated,omitzero"`
|
|
// Include is a list of relative paths to extensions that should be loaded.
|
|
// If empty, all extensions in the package are loaded.
|
|
// Paths are relative to the package root (e.g., "./git/main.go", "./weather.go")
|
|
Include []string `json:"include,omitempty"`
|
|
}
|
|
|
|
// Identity returns the normalized identity for deduplication.
|
|
func (e ManifestEntry) Identity() string {
|
|
return fmt.Sprintf("%s/%s", e.Host, e.Path)
|
|
}
|
|
|
|
// loadManifest loads the manifest from the given scope.
|
|
func loadManifestFromScope(scope InstallScope) (*Manifest, error) {
|
|
path := manifestPathForScope(scope)
|
|
return loadManifestFromPath(path)
|
|
}
|
|
|
|
// loadManifestFromPath loads a manifest from a specific file path.
|
|
func loadManifestFromPath(path string) (*Manifest, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return &Manifest{Packages: []ManifestEntry{}}, nil
|
|
}
|
|
return nil, fmt.Errorf("reading manifest: %w", err)
|
|
}
|
|
|
|
var manifest Manifest
|
|
if err := json.Unmarshal(data, &manifest); err != nil {
|
|
return nil, fmt.Errorf("parsing manifest: %w", err)
|
|
}
|
|
|
|
return &manifest, nil
|
|
}
|
|
|
|
// manifestPathForScope returns the manifest file path for a scope.
|
|
func manifestPathForScope(scope InstallScope) string {
|
|
if scope == ScopeProject {
|
|
return filepath.Join(".kit", "git", "packages.json")
|
|
}
|
|
|
|
base := os.Getenv("XDG_DATA_HOME")
|
|
if base == "" {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
base = filepath.Join(home, ".local", "share")
|
|
}
|
|
return filepath.Join(base, "kit", "git", "packages.json")
|
|
}
|
|
|
|
// FindInManifest finds an entry by identity in either global or project manifest.
|
|
// Returns the entry and its scope, or nil if not found.
|
|
func FindInManifest(identity string) (*ManifestEntry, InstallScope, error) {
|
|
global, err := loadManifestFromScope(ScopeGlobal)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("loading global manifest: %w", err)
|
|
}
|
|
for _, p := range global.Packages {
|
|
if p.Identity() == identity {
|
|
return &p, ScopeGlobal, nil
|
|
}
|
|
}
|
|
|
|
project, err := loadManifestFromScope(ScopeProject)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("loading project manifest: %w", err)
|
|
}
|
|
for _, p := range project.Packages {
|
|
if p.Identity() == identity {
|
|
return &p, ScopeProject, nil
|
|
}
|
|
}
|
|
|
|
return nil, "", nil
|
|
}
|
|
|
|
// ExtensionPreview represents a discovered extension in a package before installation.
|
|
type ExtensionPreview struct {
|
|
// Path is the relative path from the package root (e.g., "./git/main.go")
|
|
Path string `json:"path"`
|
|
// Name is a display name for the extension (derived from path or metadata)
|
|
Name string `json:"name"`
|
|
// Description is an optional description (could be extracted from comments)
|
|
Description string `json:"description,omitempty"`
|
|
// IsMain indicates if this is a main.go in a subdirectory
|
|
IsMain bool `json:"is_main"`
|
|
}
|
|
|
|
// ScanForExtensions discovers all extensions in a directory using opinionated conventions.
|
|
// Extensions are ONLY recognized in these specific locations:
|
|
// 1. Root-level *.go files
|
|
// 2. Files in examples/extensions/ or examples/ext/ subdirectories
|
|
// 3. Files in any top-level ext/ directory
|
|
// 4. Files in any subdirectory that ends in -ext/ or -extensions/
|
|
//
|
|
// Everything else (cmd/, internal/, pkg/, etc.) is ignored.
|
|
func ScanForExtensions(dir string) ([]ExtensionPreview, error) {
|
|
info, err := os.Stat(dir)
|
|
if err != nil || !info.IsDir() {
|
|
return nil, fmt.Errorf("not a directory: %s", dir)
|
|
}
|
|
|
|
var previews []ExtensionPreview
|
|
multiFileDirs := make(map[string]bool)
|
|
|
|
err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
relPath, _ := filepath.Rel(dir, path)
|
|
relPath = filepath.ToSlash(relPath)
|
|
|
|
// Skip directories we know don't contain extensions
|
|
if info.IsDir() {
|
|
// Never scan these directories
|
|
switch info.Name() {
|
|
case ".git", ".github", "node_modules", "vendor", "dist", "build":
|
|
return filepath.SkipDir
|
|
}
|
|
|
|
// Skip internal code directories
|
|
if strings.HasPrefix(relPath, "internal/") ||
|
|
strings.HasPrefix(relPath, "cmd/") ||
|
|
strings.HasPrefix(relPath, "pkg/") ||
|
|
strings.HasPrefix(relPath, "test/") ||
|
|
strings.HasPrefix(relPath, "tests/") {
|
|
return filepath.SkipDir
|
|
}
|
|
|
|
// Root directory - scan it
|
|
if relPath == "." {
|
|
return nil
|
|
}
|
|
|
|
// Check if this directory is an extension location by name
|
|
// Pattern: must be named "extensions", "ext", or end with those
|
|
base := info.Name()
|
|
isExtDir := base == "extensions" || base == "ext" ||
|
|
strings.HasSuffix(base, "-extensions") || strings.HasSuffix(base, "-ext")
|
|
|
|
// Allow walking into examples/ so we can reach examples/extensions/ etc,
|
|
// but don't treat examples/ itself or non-extension subdirs as extension locations.
|
|
if relPath == "examples" {
|
|
return nil
|
|
}
|
|
|
|
if !isExtDir {
|
|
// Check for main.go before skipping
|
|
mainPath := filepath.Join(path, "main.go")
|
|
if _, err := os.Stat(mainPath); err == nil {
|
|
// This is a package with main.go at root level
|
|
if relPath == base { // Top-level directory
|
|
if !multiFileDirs[relPath] {
|
|
multiFileDirs[relPath] = true
|
|
previews = append(previews, ExtensionPreview{
|
|
Path: "./" + relPath + "/main.go",
|
|
Name: deriveExtensionName(relPath+"/main.go", true),
|
|
IsMain: true,
|
|
})
|
|
}
|
|
return filepath.SkipDir
|
|
}
|
|
}
|
|
|
|
// Not an extension location
|
|
return filepath.SkipDir
|
|
}
|
|
|
|
// Check for main.go in this directory
|
|
mainPath := filepath.Join(path, "main.go")
|
|
if _, err := os.Stat(mainPath); err == nil {
|
|
if !multiFileDirs[relPath] {
|
|
multiFileDirs[relPath] = true
|
|
previews = append(previews, ExtensionPreview{
|
|
Path: "./" + relPath + "/main.go",
|
|
Name: deriveExtensionName(relPath+"/main.go", true),
|
|
IsMain: true,
|
|
})
|
|
}
|
|
return filepath.SkipDir
|
|
}
|
|
|
|
// Scan this extensions directory
|
|
return nil
|
|
}
|
|
|
|
// It's a file - check if it's a valid extension
|
|
if !strings.HasSuffix(info.Name(), ".go") || strings.HasSuffix(info.Name(), "_test.go") {
|
|
return nil
|
|
}
|
|
|
|
if info.Name() == "main.go" {
|
|
return nil // Already handled above
|
|
}
|
|
|
|
// Check if parent is a valid extension location
|
|
parentDir := filepath.Dir(relPath)
|
|
if parentDir == "." {
|
|
// Root-level .go file - valid extension
|
|
previews = append(previews, ExtensionPreview{
|
|
Path: "./" + relPath,
|
|
Name: deriveExtensionName(relPath, false),
|
|
IsMain: false,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// Check if we're in a valid extension directory
|
|
// Valid locations are:
|
|
// - examples/extensions/*
|
|
// - examples/ext/*
|
|
// - ext/* (top-level)
|
|
// - Any *-extensions/* or *-ext/* directory
|
|
isValidExtDir := false
|
|
if strings.HasPrefix(parentDir, "examples/extensions/") ||
|
|
parentDir == "examples/extensions" {
|
|
isValidExtDir = true
|
|
} else if strings.HasPrefix(parentDir, "examples/ext/") ||
|
|
parentDir == "examples/ext" {
|
|
isValidExtDir = true
|
|
} else if strings.HasPrefix(parentDir, "ext/") ||
|
|
parentDir == "ext" {
|
|
isValidExtDir = true
|
|
} else if strings.Contains(parentDir, "-extensions/") ||
|
|
strings.HasSuffix(parentDir, "-extensions") {
|
|
isValidExtDir = true
|
|
} else if strings.Contains(parentDir, "-ext/") ||
|
|
strings.HasSuffix(parentDir, "-ext") {
|
|
isValidExtDir = true
|
|
}
|
|
|
|
if !isValidExtDir {
|
|
return nil
|
|
}
|
|
|
|
previews = append(previews, ExtensionPreview{
|
|
Path: "./" + relPath,
|
|
Name: deriveExtensionName(relPath, false),
|
|
IsMain: false,
|
|
})
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return previews, nil
|
|
}
|
|
|
|
// deriveExtensionName creates a display name from a file path.
|
|
func deriveExtensionName(relPath string, isMain bool) string {
|
|
// Convert path to a readable name
|
|
// e.g., "git/main.go" -> "Git Extension"
|
|
// e.g., "weather.go" -> "Weather"
|
|
|
|
dir := filepath.Dir(relPath)
|
|
base := filepath.Base(relPath)
|
|
|
|
if isMain && dir != "." {
|
|
// Use immediate parent directory name for main.go files
|
|
name := filepath.Base(dir)
|
|
name = strings.ReplaceAll(name, "_", " ")
|
|
name = strings.ReplaceAll(name, "-", " ")
|
|
return cases.Title(language.English).String(name) + " Extension"
|
|
}
|
|
|
|
// Use filename without extension
|
|
name := strings.TrimSuffix(base, ".go")
|
|
name = strings.ReplaceAll(name, "_", " ")
|
|
name = strings.ReplaceAll(name, "-", " ")
|
|
return cases.Title(language.English).String(name)
|
|
}
|