Files
Ed Zynda 78570d4188 remove dead code identified by audit
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.
2026-05-07 12:20:08 +03:00

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)
}