feat(core): add multi-repo management CLI commands
Add comprehensive CLI tooling for managing multiple repositories: Commands added: - core health: Quick health summary across all repos - core work: Status + Claude-assisted commits + push (replaces push-all.sh) - core commit: Claude-assisted commits only - core push: Push repos with unpushed commits - core pull: Pull repos that are behind - core impact <repo>: Dependency impact analysis - core issues: List GitHub issues across repos - core reviews: List PRs with review status - core ci: Check GitHub Actions status - core docs list/sync: Documentation management New packages: - pkg/repos: Registry parser for repos.yaml with dependency graph - pkg/git: Parallel git status operations Features: - Auto-discovers repos.yaml or scans current directory - Sequential GitHub API calls to avoid rate limits - Colored output with lipgloss - SSH passphrase gate preserved for push operations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
091c725036
commit
f3d5fd6668
21 changed files with 2950 additions and 117 deletions
|
|
@ -12,6 +12,6 @@ func AddAPICommands(parent *clir.Command) {
|
||||||
// Add the 'sync' command to 'api'
|
// Add the 'sync' command to 'api'
|
||||||
AddSyncCommand(apiCmd)
|
AddSyncCommand(apiCmd)
|
||||||
|
|
||||||
// Add the 'test-gen' command to 'api'
|
// TODO: Add the 'test-gen' command to 'api'
|
||||||
AddTestGenCommand(apiCmd)
|
// AddTestGenCommand(apiCmd)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
261
cmd/core/cmd/ci.go
Normal file
261
cmd/core/cmd/ci.go
Normal file
|
|
@ -0,0 +1,261 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Snider/Core/pkg/repos"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/leaanthony/clir"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ciSuccessStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("#22c55e")) // green-500
|
||||||
|
|
||||||
|
ciFailureStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("#ef4444")) // red-500
|
||||||
|
|
||||||
|
ciPendingStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#f59e0b")) // amber-500
|
||||||
|
|
||||||
|
ciSkippedStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#6b7280")) // gray-500
|
||||||
|
)
|
||||||
|
|
||||||
|
// WorkflowRun represents a GitHub Actions workflow run
|
||||||
|
type WorkflowRun struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Conclusion string `json:"conclusion"`
|
||||||
|
HeadBranch string `json:"headBranch"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
|
||||||
|
// Added by us
|
||||||
|
RepoName string `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCICommand adds the 'ci' command to the given parent command.
|
||||||
|
func AddCICommand(parent *clir.Cli) {
|
||||||
|
var registryPath string
|
||||||
|
var branch string
|
||||||
|
var failedOnly bool
|
||||||
|
|
||||||
|
ciCmd := parent.NewSubCommand("ci", "Check CI status across all repos")
|
||||||
|
ciCmd.LongDescription("Fetches GitHub Actions workflow status for all repos.\n" +
|
||||||
|
"Shows latest run status for each repo.\n" +
|
||||||
|
"Requires the 'gh' CLI to be installed and authenticated.")
|
||||||
|
|
||||||
|
ciCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", ®istryPath)
|
||||||
|
ciCmd.StringFlag("branch", "Filter by branch (default: main)", &branch)
|
||||||
|
ciCmd.BoolFlag("failed", "Show only failed runs", &failedOnly)
|
||||||
|
|
||||||
|
ciCmd.Action(func() error {
|
||||||
|
if branch == "" {
|
||||||
|
branch = "main"
|
||||||
|
}
|
||||||
|
return runCI(registryPath, branch, failedOnly)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCI(registryPath string, branch string, failedOnly bool) error {
|
||||||
|
// Check gh is available
|
||||||
|
if _, err := exec.LookPath("gh"); err != nil {
|
||||||
|
return fmt.Errorf("'gh' CLI not found. Install from https://cli.github.com/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find or use provided registry
|
||||||
|
var reg *repos.Registry
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if registryPath != "" {
|
||||||
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
registryPath, err = repos.FindRegistry()
|
||||||
|
if err == nil {
|
||||||
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
reg, err = repos.ScanDirectory(cwd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to scan directory: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch CI status sequentially
|
||||||
|
var allRuns []WorkflowRun
|
||||||
|
var fetchErrors []error
|
||||||
|
var noCI []string
|
||||||
|
|
||||||
|
repoList := reg.List()
|
||||||
|
for i, repo := range repoList {
|
||||||
|
repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name)
|
||||||
|
fmt.Printf("\033[2K\r%s %d/%d %s", dimStyle.Render("Checking"), i+1, len(repoList), repo.Name)
|
||||||
|
|
||||||
|
runs, err := fetchWorkflowRuns(repoFullName, repo.Name, branch)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "no workflows") {
|
||||||
|
noCI = append(noCI, repo.Name)
|
||||||
|
} else {
|
||||||
|
fetchErrors = append(fetchErrors, fmt.Errorf("%s: %w", repo.Name, err))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runs) > 0 {
|
||||||
|
// Just get the latest run
|
||||||
|
allRuns = append(allRuns, runs[0])
|
||||||
|
} else {
|
||||||
|
noCI = append(noCI, repo.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Print("\033[2K\r") // Clear progress line
|
||||||
|
|
||||||
|
// Count by status
|
||||||
|
var success, failed, pending, other int
|
||||||
|
for _, run := range allRuns {
|
||||||
|
switch run.Conclusion {
|
||||||
|
case "success":
|
||||||
|
success++
|
||||||
|
case "failure":
|
||||||
|
failed++
|
||||||
|
case "":
|
||||||
|
if run.Status == "in_progress" || run.Status == "queued" {
|
||||||
|
pending++
|
||||||
|
} else {
|
||||||
|
other++
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
other++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print summary
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("%d repos checked", len(repoList))
|
||||||
|
if success > 0 {
|
||||||
|
fmt.Printf(" · %s", ciSuccessStyle.Render(fmt.Sprintf("%d passing", success)))
|
||||||
|
}
|
||||||
|
if failed > 0 {
|
||||||
|
fmt.Printf(" · %s", ciFailureStyle.Render(fmt.Sprintf("%d failing", failed)))
|
||||||
|
}
|
||||||
|
if pending > 0 {
|
||||||
|
fmt.Printf(" · %s", ciPendingStyle.Render(fmt.Sprintf("%d pending", pending)))
|
||||||
|
}
|
||||||
|
if len(noCI) > 0 {
|
||||||
|
fmt.Printf(" · %s", ciSkippedStyle.Render(fmt.Sprintf("%d no CI", len(noCI))))
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Filter if needed
|
||||||
|
displayRuns := allRuns
|
||||||
|
if failedOnly {
|
||||||
|
displayRuns = nil
|
||||||
|
for _, run := range allRuns {
|
||||||
|
if run.Conclusion == "failure" {
|
||||||
|
displayRuns = append(displayRuns, run)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print details
|
||||||
|
for _, run := range displayRuns {
|
||||||
|
printWorkflowRun(run)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print errors
|
||||||
|
if len(fetchErrors) > 0 {
|
||||||
|
fmt.Println()
|
||||||
|
for _, err := range fetchErrors {
|
||||||
|
fmt.Printf("%s %s\n", errorStyle.Render("Error:"), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchWorkflowRuns(repoFullName, repoName string, branch string) ([]WorkflowRun, error) {
|
||||||
|
args := []string{
|
||||||
|
"run", "list",
|
||||||
|
"--repo", repoFullName,
|
||||||
|
"--branch", branch,
|
||||||
|
"--limit", "1",
|
||||||
|
"--json", "name,status,conclusion,headBranch,createdAt,updatedAt,url",
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("gh", args...)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
stderr := string(exitErr.Stderr)
|
||||||
|
return nil, fmt.Errorf("%s", strings.TrimSpace(stderr))
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var runs []WorkflowRun
|
||||||
|
if err := json.Unmarshal(output, &runs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag with repo name
|
||||||
|
for i := range runs {
|
||||||
|
runs[i].RepoName = repoName
|
||||||
|
}
|
||||||
|
|
||||||
|
return runs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printWorkflowRun(run WorkflowRun) {
|
||||||
|
// Status icon
|
||||||
|
var status string
|
||||||
|
switch run.Conclusion {
|
||||||
|
case "success":
|
||||||
|
status = ciSuccessStyle.Render("✓")
|
||||||
|
case "failure":
|
||||||
|
status = ciFailureStyle.Render("✗")
|
||||||
|
case "":
|
||||||
|
if run.Status == "in_progress" {
|
||||||
|
status = ciPendingStyle.Render("●")
|
||||||
|
} else if run.Status == "queued" {
|
||||||
|
status = ciPendingStyle.Render("○")
|
||||||
|
} else {
|
||||||
|
status = ciSkippedStyle.Render("—")
|
||||||
|
}
|
||||||
|
case "skipped":
|
||||||
|
status = ciSkippedStyle.Render("—")
|
||||||
|
case "cancelled":
|
||||||
|
status = ciSkippedStyle.Render("○")
|
||||||
|
default:
|
||||||
|
status = ciSkippedStyle.Render("?")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workflow name (truncated)
|
||||||
|
workflowName := truncate(run.Name, 20)
|
||||||
|
|
||||||
|
// Age
|
||||||
|
age := formatAge(run.UpdatedAt)
|
||||||
|
|
||||||
|
fmt.Printf(" %s %-18s %-22s %s\n",
|
||||||
|
status,
|
||||||
|
repoNameStyle.Render(run.RepoName),
|
||||||
|
dimStyle.Render(workflowName),
|
||||||
|
issueAgeStyle.Render(age),
|
||||||
|
)
|
||||||
|
}
|
||||||
169
cmd/core/cmd/commit.go
Normal file
169
cmd/core/cmd/commit.go
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/Snider/Core/pkg/git"
|
||||||
|
"github.com/Snider/Core/pkg/repos"
|
||||||
|
"github.com/leaanthony/clir"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddCommitCommand adds the 'commit' command to the given parent command.
|
||||||
|
func AddCommitCommand(parent *clir.Cli) {
|
||||||
|
var registryPath string
|
||||||
|
var all bool
|
||||||
|
|
||||||
|
commitCmd := parent.NewSubCommand("commit", "Claude-assisted commits across repos")
|
||||||
|
commitCmd.LongDescription("Uses Claude to create commits for dirty repos.\n" +
|
||||||
|
"Shows uncommitted changes and invokes Claude to generate commit messages.")
|
||||||
|
|
||||||
|
commitCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", ®istryPath)
|
||||||
|
commitCmd.BoolFlag("all", "Commit all dirty repos without prompting", &all)
|
||||||
|
|
||||||
|
commitCmd.Action(func() error {
|
||||||
|
return runCommit(registryPath, all)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCommit(registryPath string, all bool) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Find or use provided registry, fall back to directory scan
|
||||||
|
var reg *repos.Registry
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if registryPath != "" {
|
||||||
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
registryPath, err = repos.FindRegistry()
|
||||||
|
if err == nil {
|
||||||
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: scan current directory
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
reg, err = repos.ScanDirectory(cwd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to scan directory: %w", err)
|
||||||
|
}
|
||||||
|
registryPath = cwd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build paths and names for git operations
|
||||||
|
var paths []string
|
||||||
|
names := make(map[string]string)
|
||||||
|
|
||||||
|
for _, repo := range reg.List() {
|
||||||
|
if repo.IsGitRepo() {
|
||||||
|
paths = append(paths, repo.Path)
|
||||||
|
names[repo.Path] = repo.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(paths) == 0 {
|
||||||
|
fmt.Println("No git repositories found.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get status for all repos
|
||||||
|
statuses := git.Status(ctx, git.StatusOptions{
|
||||||
|
Paths: paths,
|
||||||
|
Names: names,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Find dirty repos
|
||||||
|
var dirtyRepos []git.RepoStatus
|
||||||
|
for _, s := range statuses {
|
||||||
|
if s.Error == nil && s.IsDirty() {
|
||||||
|
dirtyRepos = append(dirtyRepos, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(dirtyRepos) == 0 {
|
||||||
|
fmt.Println("No uncommitted changes found.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show dirty repos
|
||||||
|
fmt.Printf("\n%d repo(s) with uncommitted changes:\n\n", len(dirtyRepos))
|
||||||
|
for _, s := range dirtyRepos {
|
||||||
|
fmt.Printf(" %s: ", repoNameStyle.Render(s.Name))
|
||||||
|
if s.Modified > 0 {
|
||||||
|
fmt.Printf("%s ", dirtyStyle.Render(fmt.Sprintf("%d modified", s.Modified)))
|
||||||
|
}
|
||||||
|
if s.Untracked > 0 {
|
||||||
|
fmt.Printf("%s ", dirtyStyle.Render(fmt.Sprintf("%d untracked", s.Untracked)))
|
||||||
|
}
|
||||||
|
if s.Staged > 0 {
|
||||||
|
fmt.Printf("%s ", aheadStyle.Render(fmt.Sprintf("%d staged", s.Staged)))
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm unless --all
|
||||||
|
if !all {
|
||||||
|
fmt.Println()
|
||||||
|
if !confirm("Have Claude commit these repos?") {
|
||||||
|
fmt.Println("Aborted.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Commit each dirty repo
|
||||||
|
var succeeded, failed int
|
||||||
|
for _, s := range dirtyRepos {
|
||||||
|
fmt.Printf("%s %s\n", dimStyle.Render("Committing"), s.Name)
|
||||||
|
|
||||||
|
if err := claudeCommit(ctx, s.Path, s.Name, registryPath); err != nil {
|
||||||
|
fmt.Printf(" %s %s\n", errorStyle.Render("✗"), err)
|
||||||
|
failed++
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" %s committed\n", successStyle.Render("✓"))
|
||||||
|
succeeded++
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
fmt.Printf("%s %d succeeded", successStyle.Render("Done:"), succeeded)
|
||||||
|
if failed > 0 {
|
||||||
|
fmt.Printf(", %s", errorStyle.Render(fmt.Sprintf("%d failed", failed)))
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// claudeCommit is defined in work.go but we need it here too
|
||||||
|
// This version includes better output handling
|
||||||
|
func claudeCommitWithOutput(ctx context.Context, repoPath, repoName, registryPath string) error {
|
||||||
|
// Load AGENTS.md context if available
|
||||||
|
agentsPath := filepath.Join(filepath.Dir(registryPath), "AGENTS.md")
|
||||||
|
var agentContext string
|
||||||
|
if data, err := os.ReadFile(agentsPath); err == nil {
|
||||||
|
agentContext = string(data) + "\n\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt := agentContext + "Review the uncommitted changes and create an appropriate commit. " +
|
||||||
|
"Use Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>. Be concise."
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "claude", "-p", prompt, "--allowedTools", "Bash,Read,Glob,Grep")
|
||||||
|
cmd.Dir = repoPath
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
339
cmd/core/cmd/docs.go
Normal file
339
cmd/core/cmd/docs.go
Normal file
|
|
@ -0,0 +1,339 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Snider/Core/pkg/repos"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/leaanthony/clir"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
docsFoundStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#22c55e")) // green-500
|
||||||
|
|
||||||
|
docsMissingStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#6b7280")) // gray-500
|
||||||
|
|
||||||
|
docsFileStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#3b82f6")) // blue-500
|
||||||
|
)
|
||||||
|
|
||||||
|
// RepoDocInfo holds documentation info for a repo
|
||||||
|
type RepoDocInfo struct {
|
||||||
|
Name string
|
||||||
|
Path string
|
||||||
|
HasDocs bool
|
||||||
|
Readme string
|
||||||
|
ClaudeMd string
|
||||||
|
Changelog string
|
||||||
|
DocsDir []string // Files in docs/ directory
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddDocsCommand adds the 'docs' command to the given parent command.
|
||||||
|
func AddDocsCommand(parent *clir.Cli) {
|
||||||
|
docsCmd := parent.NewSubCommand("docs", "Documentation management")
|
||||||
|
docsCmd.LongDescription("Manage documentation across all repos.\n" +
|
||||||
|
"Scan for docs, check coverage, and sync to core.help.")
|
||||||
|
|
||||||
|
// Add subcommands
|
||||||
|
addDocsSyncCommand(docsCmd)
|
||||||
|
addDocsListCommand(docsCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addDocsSyncCommand(parent *clir.Command) {
|
||||||
|
var registryPath string
|
||||||
|
var dryRun bool
|
||||||
|
var outputDir string
|
||||||
|
|
||||||
|
syncCmd := parent.NewSubCommand("sync", "Sync documentation to output directory")
|
||||||
|
syncCmd.StringFlag("registry", "Path to repos.yaml", ®istryPath)
|
||||||
|
syncCmd.BoolFlag("dry-run", "Show what would be synced without copying", &dryRun)
|
||||||
|
syncCmd.StringFlag("output", "Output directory (default: ./docs-build)", &outputDir)
|
||||||
|
|
||||||
|
syncCmd.Action(func() error {
|
||||||
|
if outputDir == "" {
|
||||||
|
outputDir = "./docs-build"
|
||||||
|
}
|
||||||
|
return runDocsSync(registryPath, outputDir, dryRun)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func addDocsListCommand(parent *clir.Command) {
|
||||||
|
var registryPath string
|
||||||
|
|
||||||
|
listCmd := parent.NewSubCommand("list", "List documentation across repos")
|
||||||
|
listCmd.StringFlag("registry", "Path to repos.yaml", ®istryPath)
|
||||||
|
|
||||||
|
listCmd.Action(func() error {
|
||||||
|
return runDocsList(registryPath)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDocsSync(registryPath string, outputDir string, dryRun bool) error {
|
||||||
|
// Find or use provided registry
|
||||||
|
var reg *repos.Registry
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if registryPath != "" {
|
||||||
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
registryPath, err = repos.FindRegistry()
|
||||||
|
if err == nil {
|
||||||
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
reg, err = repos.ScanDirectory(cwd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to scan directory: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan all repos for docs
|
||||||
|
var docsInfo []RepoDocInfo
|
||||||
|
for _, repo := range reg.List() {
|
||||||
|
info := scanRepoDocs(repo)
|
||||||
|
if info.HasDocs {
|
||||||
|
docsInfo = append(docsInfo, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(docsInfo) == 0 {
|
||||||
|
fmt.Println("No documentation found in any repos.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n%s %d repo(s) with documentation\n\n", dimStyle.Render("Found"), len(docsInfo))
|
||||||
|
|
||||||
|
// Show what will be synced
|
||||||
|
var totalFiles int
|
||||||
|
for _, info := range docsInfo {
|
||||||
|
files := countDocFiles(info)
|
||||||
|
totalFiles += files
|
||||||
|
fmt.Printf(" %s %s\n", repoNameStyle.Render(info.Name), dimStyle.Render(fmt.Sprintf("(%d files)", files)))
|
||||||
|
|
||||||
|
if info.Readme != "" {
|
||||||
|
fmt.Printf(" %s\n", docsFileStyle.Render("README.md"))
|
||||||
|
}
|
||||||
|
if info.ClaudeMd != "" {
|
||||||
|
fmt.Printf(" %s\n", docsFileStyle.Render("CLAUDE.md"))
|
||||||
|
}
|
||||||
|
if info.Changelog != "" {
|
||||||
|
fmt.Printf(" %s\n", docsFileStyle.Render("CHANGELOG.md"))
|
||||||
|
}
|
||||||
|
for _, f := range info.DocsDir {
|
||||||
|
fmt.Printf(" %s\n", docsFileStyle.Render("docs/"+f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n%s %d files from %d repos\n", dimStyle.Render("Total:"), totalFiles, len(docsInfo))
|
||||||
|
|
||||||
|
if dryRun {
|
||||||
|
fmt.Printf("\n%s\n", dimStyle.Render("Dry run - no files copied"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm
|
||||||
|
fmt.Println()
|
||||||
|
if !confirm(fmt.Sprintf("Sync to %s?", outputDir)) {
|
||||||
|
fmt.Println("Aborted.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create output directory
|
||||||
|
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create output directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync docs
|
||||||
|
fmt.Println()
|
||||||
|
var synced int
|
||||||
|
for _, info := range docsInfo {
|
||||||
|
repoOutDir := filepath.Join(outputDir, "packages", info.Name)
|
||||||
|
if err := os.MkdirAll(repoOutDir, 0755); err != nil {
|
||||||
|
fmt.Printf(" %s %s: %s\n", errorStyle.Render("✗"), info.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy files
|
||||||
|
if info.Readme != "" {
|
||||||
|
copyFile(info.Readme, filepath.Join(repoOutDir, "index.md"))
|
||||||
|
}
|
||||||
|
if info.ClaudeMd != "" {
|
||||||
|
copyFile(info.ClaudeMd, filepath.Join(repoOutDir, "claude.md"))
|
||||||
|
}
|
||||||
|
if info.Changelog != "" {
|
||||||
|
copyFile(info.Changelog, filepath.Join(repoOutDir, "changelog.md"))
|
||||||
|
}
|
||||||
|
for _, f := range info.DocsDir {
|
||||||
|
src := filepath.Join(info.Path, "docs", f)
|
||||||
|
dst := filepath.Join(repoOutDir, f)
|
||||||
|
os.MkdirAll(filepath.Dir(dst), 0755)
|
||||||
|
copyFile(src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(" %s %s\n", successStyle.Render("✓"), info.Name)
|
||||||
|
synced++
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n%s Synced %d repos to %s\n", successStyle.Render("Done:"), synced, outputDir)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDocsList(registryPath string) error {
|
||||||
|
// Find or use provided registry
|
||||||
|
var reg *repos.Registry
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if registryPath != "" {
|
||||||
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
registryPath, err = repos.FindRegistry()
|
||||||
|
if err == nil {
|
||||||
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
reg, err = repos.ScanDirectory(cwd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to scan directory: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n%-20s %-8s %-8s %-10s %s\n",
|
||||||
|
headerStyle.Render("Repo"),
|
||||||
|
headerStyle.Render("README"),
|
||||||
|
headerStyle.Render("CLAUDE"),
|
||||||
|
headerStyle.Render("CHANGELOG"),
|
||||||
|
headerStyle.Render("docs/"),
|
||||||
|
)
|
||||||
|
fmt.Println(strings.Repeat("─", 70))
|
||||||
|
|
||||||
|
var withDocs, withoutDocs int
|
||||||
|
for _, repo := range reg.List() {
|
||||||
|
info := scanRepoDocs(repo)
|
||||||
|
|
||||||
|
readme := docsMissingStyle.Render("—")
|
||||||
|
if info.Readme != "" {
|
||||||
|
readme = docsFoundStyle.Render("✓")
|
||||||
|
}
|
||||||
|
|
||||||
|
claude := docsMissingStyle.Render("—")
|
||||||
|
if info.ClaudeMd != "" {
|
||||||
|
claude = docsFoundStyle.Render("✓")
|
||||||
|
}
|
||||||
|
|
||||||
|
changelog := docsMissingStyle.Render("—")
|
||||||
|
if info.Changelog != "" {
|
||||||
|
changelog = docsFoundStyle.Render("✓")
|
||||||
|
}
|
||||||
|
|
||||||
|
docsDir := docsMissingStyle.Render("—")
|
||||||
|
if len(info.DocsDir) > 0 {
|
||||||
|
docsDir = docsFoundStyle.Render(fmt.Sprintf("%d files", len(info.DocsDir)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%-20s %-8s %-8s %-10s %s\n",
|
||||||
|
repoNameStyle.Render(info.Name),
|
||||||
|
readme,
|
||||||
|
claude,
|
||||||
|
changelog,
|
||||||
|
docsDir,
|
||||||
|
)
|
||||||
|
|
||||||
|
if info.HasDocs {
|
||||||
|
withDocs++
|
||||||
|
} else {
|
||||||
|
withoutDocs++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("%s %d with docs, %d without\n",
|
||||||
|
dimStyle.Render("Coverage:"),
|
||||||
|
withDocs,
|
||||||
|
withoutDocs,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanRepoDocs(repo *repos.Repo) RepoDocInfo {
|
||||||
|
info := RepoDocInfo{
|
||||||
|
Name: repo.Name,
|
||||||
|
Path: repo.Path,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for README.md
|
||||||
|
readme := filepath.Join(repo.Path, "README.md")
|
||||||
|
if _, err := os.Stat(readme); err == nil {
|
||||||
|
info.Readme = readme
|
||||||
|
info.HasDocs = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for CLAUDE.md
|
||||||
|
claudeMd := filepath.Join(repo.Path, "CLAUDE.md")
|
||||||
|
if _, err := os.Stat(claudeMd); err == nil {
|
||||||
|
info.ClaudeMd = claudeMd
|
||||||
|
info.HasDocs = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for CHANGELOG.md
|
||||||
|
changelog := filepath.Join(repo.Path, "CHANGELOG.md")
|
||||||
|
if _, err := os.Stat(changelog); err == nil {
|
||||||
|
info.Changelog = changelog
|
||||||
|
info.HasDocs = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for docs/ directory
|
||||||
|
docsDir := filepath.Join(repo.Path, "docs")
|
||||||
|
if entries, err := os.ReadDir(docsDir); err == nil {
|
||||||
|
for _, e := range entries {
|
||||||
|
if !e.IsDir() && strings.HasSuffix(e.Name(), ".md") {
|
||||||
|
info.DocsDir = append(info.DocsDir, e.Name())
|
||||||
|
info.HasDocs = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func countDocFiles(info RepoDocInfo) int {
|
||||||
|
count := len(info.DocsDir)
|
||||||
|
if info.Readme != "" {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
if info.ClaudeMd != "" {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
if info.Changelog != "" {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFile(src, dst string) error {
|
||||||
|
data, err := os.ReadFile(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(dst, data, 0644)
|
||||||
|
}
|
||||||
223
cmd/core/cmd/health.go
Normal file
223
cmd/core/cmd/health.go
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/Snider/Core/pkg/git"
|
||||||
|
"github.com/Snider/Core/pkg/repos"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/leaanthony/clir"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
healthLabelStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#6b7280")) // gray-500
|
||||||
|
|
||||||
|
healthValueStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("#e2e8f0")) // gray-200
|
||||||
|
|
||||||
|
healthGoodStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("#22c55e")) // green-500
|
||||||
|
|
||||||
|
healthWarnStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("#f59e0b")) // amber-500
|
||||||
|
|
||||||
|
healthBadStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("#ef4444")) // red-500
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddHealthCommand adds the 'health' command to the given parent command.
|
||||||
|
func AddHealthCommand(parent *clir.Cli) {
|
||||||
|
var registryPath string
|
||||||
|
var verbose bool
|
||||||
|
|
||||||
|
healthCmd := parent.NewSubCommand("health", "Quick health check across all repos")
|
||||||
|
healthCmd.LongDescription("Shows a summary of repository health:\n" +
|
||||||
|
"total repos, dirty repos, unpushed commits, etc.")
|
||||||
|
|
||||||
|
healthCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", ®istryPath)
|
||||||
|
healthCmd.BoolFlag("verbose", "Show detailed breakdown", &verbose)
|
||||||
|
|
||||||
|
healthCmd.Action(func() error {
|
||||||
|
return runHealth(registryPath, verbose)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runHealth(registryPath string, verbose bool) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Find or use provided registry, fall back to directory scan
|
||||||
|
var reg *repos.Registry
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if registryPath != "" {
|
||||||
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
registryPath, err = repos.FindRegistry()
|
||||||
|
if err == nil {
|
||||||
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: scan current directory
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
reg, err = repos.ScanDirectory(cwd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to scan directory: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build paths and names for git operations
|
||||||
|
var paths []string
|
||||||
|
names := make(map[string]string)
|
||||||
|
|
||||||
|
for _, repo := range reg.List() {
|
||||||
|
if repo.IsGitRepo() {
|
||||||
|
paths = append(paths, repo.Path)
|
||||||
|
names[repo.Path] = repo.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(paths) == 0 {
|
||||||
|
fmt.Println("No git repositories found.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get status for all repos
|
||||||
|
statuses := git.Status(ctx, git.StatusOptions{
|
||||||
|
Paths: paths,
|
||||||
|
Names: names,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sort for consistent verbose output
|
||||||
|
sort.Slice(statuses, func(i, j int) bool {
|
||||||
|
return statuses[i].Name < statuses[j].Name
|
||||||
|
})
|
||||||
|
|
||||||
|
// Aggregate stats
|
||||||
|
var (
|
||||||
|
totalRepos = len(statuses)
|
||||||
|
dirtyRepos []string
|
||||||
|
aheadRepos []string
|
||||||
|
behindRepos []string
|
||||||
|
errorRepos []string
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, s := range statuses {
|
||||||
|
if s.Error != nil {
|
||||||
|
errorRepos = append(errorRepos, s.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s.IsDirty() {
|
||||||
|
dirtyRepos = append(dirtyRepos, s.Name)
|
||||||
|
}
|
||||||
|
if s.HasUnpushed() {
|
||||||
|
aheadRepos = append(aheadRepos, s.Name)
|
||||||
|
}
|
||||||
|
if s.HasUnpulled() {
|
||||||
|
behindRepos = append(behindRepos, s.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print summary line
|
||||||
|
fmt.Println()
|
||||||
|
printHealthSummary(totalRepos, dirtyRepos, aheadRepos, behindRepos, errorRepos)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Verbose output
|
||||||
|
if verbose {
|
||||||
|
if len(dirtyRepos) > 0 {
|
||||||
|
fmt.Printf("%s %s\n", healthWarnStyle.Render("Dirty:"), formatRepoList(dirtyRepos))
|
||||||
|
}
|
||||||
|
if len(aheadRepos) > 0 {
|
||||||
|
fmt.Printf("%s %s\n", healthGoodStyle.Render("Ahead:"), formatRepoList(aheadRepos))
|
||||||
|
}
|
||||||
|
if len(behindRepos) > 0 {
|
||||||
|
fmt.Printf("%s %s\n", healthWarnStyle.Render("Behind:"), formatRepoList(behindRepos))
|
||||||
|
}
|
||||||
|
if len(errorRepos) > 0 {
|
||||||
|
fmt.Printf("%s %s\n", healthBadStyle.Render("Errors:"), formatRepoList(errorRepos))
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printHealthSummary(total int, dirty, ahead, behind, errors []string) {
|
||||||
|
// Total repos
|
||||||
|
fmt.Print(healthValueStyle.Render(fmt.Sprintf("%d", total)))
|
||||||
|
fmt.Print(healthLabelStyle.Render(" repos"))
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
fmt.Print(healthLabelStyle.Render(" │ "))
|
||||||
|
|
||||||
|
// Dirty
|
||||||
|
if len(dirty) > 0 {
|
||||||
|
fmt.Print(healthWarnStyle.Render(fmt.Sprintf("%d", len(dirty))))
|
||||||
|
fmt.Print(healthLabelStyle.Render(" dirty"))
|
||||||
|
} else {
|
||||||
|
fmt.Print(healthGoodStyle.Render("clean"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
fmt.Print(healthLabelStyle.Render(" │ "))
|
||||||
|
|
||||||
|
// Ahead
|
||||||
|
if len(ahead) > 0 {
|
||||||
|
fmt.Print(healthValueStyle.Render(fmt.Sprintf("%d", len(ahead))))
|
||||||
|
fmt.Print(healthLabelStyle.Render(" to push"))
|
||||||
|
} else {
|
||||||
|
fmt.Print(healthGoodStyle.Render("synced"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
fmt.Print(healthLabelStyle.Render(" │ "))
|
||||||
|
|
||||||
|
// Behind
|
||||||
|
if len(behind) > 0 {
|
||||||
|
fmt.Print(healthWarnStyle.Render(fmt.Sprintf("%d", len(behind))))
|
||||||
|
fmt.Print(healthLabelStyle.Render(" to pull"))
|
||||||
|
} else {
|
||||||
|
fmt.Print(healthGoodStyle.Render("up to date"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors (only if any)
|
||||||
|
if len(errors) > 0 {
|
||||||
|
fmt.Print(healthLabelStyle.Render(" │ "))
|
||||||
|
fmt.Print(healthBadStyle.Render(fmt.Sprintf("%d", len(errors))))
|
||||||
|
fmt.Print(healthLabelStyle.Render(" errors"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatRepoList(repos []string) string {
|
||||||
|
if len(repos) <= 5 {
|
||||||
|
return joinRepos(repos)
|
||||||
|
}
|
||||||
|
return joinRepos(repos[:5]) + fmt.Sprintf(" +%d more", len(repos)-5)
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinRepos(repos []string) string {
|
||||||
|
result := ""
|
||||||
|
for i, r := range repos {
|
||||||
|
if i > 0 {
|
||||||
|
result += ", "
|
||||||
|
}
|
||||||
|
result += r
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
193
cmd/core/cmd/impact.go
Normal file
193
cmd/core/cmd/impact.go
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/Snider/Core/pkg/repos"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/leaanthony/clir"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
impactDirectStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("#ef4444")) // red-500
|
||||||
|
|
||||||
|
impactIndirectStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#f59e0b")) // amber-500
|
||||||
|
|
||||||
|
impactSafeStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#22c55e")) // green-500
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddImpactCommand adds the 'impact' command to the given parent command.
|
||||||
|
func AddImpactCommand(parent *clir.Cli) {
|
||||||
|
var registryPath string
|
||||||
|
|
||||||
|
impactCmd := parent.NewSubCommand("impact", "Show impact of changing a repo")
|
||||||
|
impactCmd.LongDescription("Analyzes the dependency graph to show which repos\n" +
|
||||||
|
"would be affected by changes to the specified repo.")
|
||||||
|
|
||||||
|
impactCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", ®istryPath)
|
||||||
|
|
||||||
|
impactCmd.Action(func() error {
|
||||||
|
args := os.Args[2:] // Skip "core" and "impact"
|
||||||
|
// Filter out flags
|
||||||
|
var repoName string
|
||||||
|
for _, arg := range args {
|
||||||
|
if arg[0] != '-' {
|
||||||
|
repoName = arg
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if repoName == "" {
|
||||||
|
return fmt.Errorf("usage: core impact <repo-name>")
|
||||||
|
}
|
||||||
|
return runImpact(registryPath, repoName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runImpact(registryPath string, repoName string) error {
|
||||||
|
// Find or use provided registry
|
||||||
|
var reg *repos.Registry
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if registryPath != "" {
|
||||||
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
registryPath, err = repos.FindRegistry()
|
||||||
|
if err == nil {
|
||||||
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("impact analysis requires repos.yaml with dependency information")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check repo exists
|
||||||
|
repo, exists := reg.Get(repoName)
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("repo '%s' not found in registry", repoName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build reverse dependency graph
|
||||||
|
dependents := buildDependentsGraph(reg)
|
||||||
|
|
||||||
|
// Find all affected repos (direct and transitive)
|
||||||
|
direct := dependents[repoName]
|
||||||
|
allAffected := findAllDependents(repoName, dependents)
|
||||||
|
|
||||||
|
// Separate direct vs indirect
|
||||||
|
directSet := make(map[string]bool)
|
||||||
|
for _, d := range direct {
|
||||||
|
directSet[d] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var indirect []string
|
||||||
|
for _, a := range allAffected {
|
||||||
|
if !directSet[a] {
|
||||||
|
indirect = append(indirect, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort for consistent output
|
||||||
|
sort.Strings(direct)
|
||||||
|
sort.Strings(indirect)
|
||||||
|
|
||||||
|
// Print results
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("%s %s\n", dimStyle.Render("Impact analysis for"), repoNameStyle.Render(repoName))
|
||||||
|
if repo.Description != "" {
|
||||||
|
fmt.Printf("%s\n", dimStyle.Render(repo.Description))
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
if len(allAffected) == 0 {
|
||||||
|
fmt.Printf("%s No repos depend on %s\n", impactSafeStyle.Render("✓"), repoName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct dependents
|
||||||
|
if len(direct) > 0 {
|
||||||
|
fmt.Printf("%s %d direct dependent(s):\n",
|
||||||
|
impactDirectStyle.Render("●"),
|
||||||
|
len(direct),
|
||||||
|
)
|
||||||
|
for _, d := range direct {
|
||||||
|
r, _ := reg.Get(d)
|
||||||
|
desc := ""
|
||||||
|
if r != nil && r.Description != "" {
|
||||||
|
desc = dimStyle.Render(" - " + truncate(r.Description, 40))
|
||||||
|
}
|
||||||
|
fmt.Printf(" %s%s\n", d, desc)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indirect dependents
|
||||||
|
if len(indirect) > 0 {
|
||||||
|
fmt.Printf("%s %d transitive dependent(s):\n",
|
||||||
|
impactIndirectStyle.Render("○"),
|
||||||
|
len(indirect),
|
||||||
|
)
|
||||||
|
for _, d := range indirect {
|
||||||
|
r, _ := reg.Get(d)
|
||||||
|
desc := ""
|
||||||
|
if r != nil && r.Description != "" {
|
||||||
|
desc = dimStyle.Render(" - " + truncate(r.Description, 40))
|
||||||
|
}
|
||||||
|
fmt.Printf(" %s%s\n", d, desc)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
fmt.Printf("%s Changes to %s affect %s\n",
|
||||||
|
dimStyle.Render("Summary:"),
|
||||||
|
repoNameStyle.Render(repoName),
|
||||||
|
impactDirectStyle.Render(fmt.Sprintf("%d/%d repos", len(allAffected), len(reg.Repos)-1)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildDependentsGraph creates a reverse dependency map
|
||||||
|
// key = repo, value = repos that depend on it
|
||||||
|
func buildDependentsGraph(reg *repos.Registry) map[string][]string {
|
||||||
|
dependents := make(map[string][]string)
|
||||||
|
|
||||||
|
for name, repo := range reg.Repos {
|
||||||
|
for _, dep := range repo.DependsOn {
|
||||||
|
dependents[dep] = append(dependents[dep], name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependents
|
||||||
|
}
|
||||||
|
|
||||||
|
// findAllDependents recursively finds all repos that depend on the given repo
|
||||||
|
func findAllDependents(repoName string, dependents map[string][]string) []string {
|
||||||
|
visited := make(map[string]bool)
|
||||||
|
var result []string
|
||||||
|
|
||||||
|
var visit func(name string)
|
||||||
|
visit = func(name string) {
|
||||||
|
for _, dep := range dependents[name] {
|
||||||
|
if !visited[dep] {
|
||||||
|
visited[dep] = true
|
||||||
|
result = append(result, dep)
|
||||||
|
visit(dep) // Recurse for transitive deps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visit(repoName)
|
||||||
|
return result
|
||||||
|
}
|
||||||
259
cmd/core/cmd/issues.go
Normal file
259
cmd/core/cmd/issues.go
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Snider/Core/pkg/repos"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/leaanthony/clir"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
issueRepoStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#6b7280")) // gray-500
|
||||||
|
|
||||||
|
issueNumberStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("#3b82f6")) // blue-500
|
||||||
|
|
||||||
|
issueTitleStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#e2e8f0")) // gray-200
|
||||||
|
|
||||||
|
issueLabelStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#f59e0b")) // amber-500
|
||||||
|
|
||||||
|
issueAssigneeStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#22c55e")) // green-500
|
||||||
|
|
||||||
|
issueAgeStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#6b7280")) // gray-500
|
||||||
|
)
|
||||||
|
|
||||||
|
// GitHubIssue represents a GitHub issue from the API.
|
||||||
|
type GitHubIssue struct {
|
||||||
|
Number int `json:"number"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
State string `json:"state"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
Author struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
} `json:"author"`
|
||||||
|
Assignees struct {
|
||||||
|
Nodes []struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
} `json:"nodes"`
|
||||||
|
} `json:"assignees"`
|
||||||
|
Labels struct {
|
||||||
|
Nodes []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"nodes"`
|
||||||
|
} `json:"labels"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
|
||||||
|
// Added by us
|
||||||
|
RepoName string `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddIssuesCommand adds the 'issues' command to the given parent command.
|
||||||
|
func AddIssuesCommand(parent *clir.Cli) {
|
||||||
|
var registryPath string
|
||||||
|
var limit int
|
||||||
|
var assignee string
|
||||||
|
|
||||||
|
issuesCmd := parent.NewSubCommand("issues", "List open issues across all repos")
|
||||||
|
issuesCmd.LongDescription("Fetches open issues from GitHub for all repos in the registry.\n" +
|
||||||
|
"Requires the 'gh' CLI to be installed and authenticated.")
|
||||||
|
|
||||||
|
issuesCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", ®istryPath)
|
||||||
|
issuesCmd.IntFlag("limit", "Max issues per repo (default 10)", &limit)
|
||||||
|
issuesCmd.StringFlag("assignee", "Filter by assignee (use @me for yourself)", &assignee)
|
||||||
|
|
||||||
|
issuesCmd.Action(func() error {
|
||||||
|
if limit == 0 {
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
|
return runIssues(registryPath, limit, assignee)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runIssues(registryPath string, limit int, assignee string) error {
|
||||||
|
// Check gh is available
|
||||||
|
if _, err := exec.LookPath("gh"); err != nil {
|
||||||
|
return fmt.Errorf("'gh' CLI not found. Install from https://cli.github.com/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find or use provided registry, fall back to directory scan
|
||||||
|
var reg *repos.Registry
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if registryPath != "" {
|
||||||
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
registryPath, err = repos.FindRegistry()
|
||||||
|
if err == nil {
|
||||||
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: scan current directory
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
reg, err = repos.ScanDirectory(cwd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to scan directory: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch issues sequentially (avoid GitHub rate limits)
|
||||||
|
var allIssues []GitHubIssue
|
||||||
|
var fetchErrors []error
|
||||||
|
|
||||||
|
repoList := reg.List()
|
||||||
|
for i, repo := range repoList {
|
||||||
|
repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name)
|
||||||
|
fmt.Printf("\033[2K\r%s %d/%d %s", dimStyle.Render("Fetching"), i+1, len(repoList), repo.Name)
|
||||||
|
|
||||||
|
issues, err := fetchIssues(repoFullName, repo.Name, limit, assignee)
|
||||||
|
if err != nil {
|
||||||
|
fetchErrors = append(fetchErrors, fmt.Errorf("%s: %w", repo.Name, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
allIssues = append(allIssues, issues...)
|
||||||
|
}
|
||||||
|
fmt.Print("\033[2K\r") // Clear progress line
|
||||||
|
|
||||||
|
// Sort by created date (newest first)
|
||||||
|
sort.Slice(allIssues, func(i, j int) bool {
|
||||||
|
return allIssues[i].CreatedAt.After(allIssues[j].CreatedAt)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Print issues
|
||||||
|
if len(allIssues) == 0 {
|
||||||
|
fmt.Println("No open issues found.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n%d open issue(s):\n\n", len(allIssues))
|
||||||
|
|
||||||
|
for _, issue := range allIssues {
|
||||||
|
printIssue(issue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print any errors
|
||||||
|
if len(fetchErrors) > 0 {
|
||||||
|
fmt.Println()
|
||||||
|
for _, err := range fetchErrors {
|
||||||
|
fmt.Printf("%s %s\n", errorStyle.Render("Error:"), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchIssues(repoFullName, repoName string, limit int, assignee string) ([]GitHubIssue, error) {
|
||||||
|
args := []string{
|
||||||
|
"issue", "list",
|
||||||
|
"--repo", repoFullName,
|
||||||
|
"--state", "open",
|
||||||
|
"--limit", fmt.Sprintf("%d", limit),
|
||||||
|
"--json", "number,title,state,createdAt,author,assignees,labels,url",
|
||||||
|
}
|
||||||
|
|
||||||
|
if assignee != "" {
|
||||||
|
args = append(args, "--assignee", assignee)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("gh", args...)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
// Check if it's just "no issues" vs actual error
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
stderr := string(exitErr.Stderr)
|
||||||
|
if strings.Contains(stderr, "no issues") || strings.Contains(stderr, "Could not resolve") {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("%s", stderr)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var issues []GitHubIssue
|
||||||
|
if err := json.Unmarshal(output, &issues); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag with repo name
|
||||||
|
for i := range issues {
|
||||||
|
issues[i].RepoName = repoName
|
||||||
|
}
|
||||||
|
|
||||||
|
return issues, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printIssue(issue GitHubIssue) {
|
||||||
|
// #42 [core-bio] Fix avatar upload
|
||||||
|
num := issueNumberStyle.Render(fmt.Sprintf("#%d", issue.Number))
|
||||||
|
repo := issueRepoStyle.Render(fmt.Sprintf("[%s]", issue.RepoName))
|
||||||
|
title := issueTitleStyle.Render(truncate(issue.Title, 60))
|
||||||
|
|
||||||
|
line := fmt.Sprintf(" %s %s %s", num, repo, title)
|
||||||
|
|
||||||
|
// Add labels if any
|
||||||
|
if len(issue.Labels.Nodes) > 0 {
|
||||||
|
var labels []string
|
||||||
|
for _, l := range issue.Labels.Nodes {
|
||||||
|
labels = append(labels, l.Name)
|
||||||
|
}
|
||||||
|
line += " " + issueLabelStyle.Render("["+strings.Join(labels, ", ")+"]")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add assignee if any
|
||||||
|
if len(issue.Assignees.Nodes) > 0 {
|
||||||
|
var assignees []string
|
||||||
|
for _, a := range issue.Assignees.Nodes {
|
||||||
|
assignees = append(assignees, "@"+a.Login)
|
||||||
|
}
|
||||||
|
line += " " + issueAssigneeStyle.Render(strings.Join(assignees, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add age
|
||||||
|
age := formatAge(issue.CreatedAt)
|
||||||
|
line += " " + issueAgeStyle.Render(age)
|
||||||
|
|
||||||
|
fmt.Println(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncate(s string, max int) string {
|
||||||
|
if len(s) <= max {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return s[:max-3] + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatAge(t time.Time) string {
|
||||||
|
d := time.Since(t)
|
||||||
|
|
||||||
|
if d < time.Hour {
|
||||||
|
return fmt.Sprintf("%dm ago", int(d.Minutes()))
|
||||||
|
}
|
||||||
|
if d < 24*time.Hour {
|
||||||
|
return fmt.Sprintf("%dh ago", int(d.Hours()))
|
||||||
|
}
|
||||||
|
if d < 7*24*time.Hour {
|
||||||
|
return fmt.Sprintf("%dd ago", int(d.Hours()/24))
|
||||||
|
}
|
||||||
|
if d < 30*24*time.Hour {
|
||||||
|
return fmt.Sprintf("%dw ago", int(d.Hours()/(24*7)))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%dmo ago", int(d.Hours()/(24*30)))
|
||||||
|
}
|
||||||
146
cmd/core/cmd/pull.go
Normal file
146
cmd/core/cmd/pull.go
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/Snider/Core/pkg/git"
|
||||||
|
"github.com/Snider/Core/pkg/repos"
|
||||||
|
"github.com/leaanthony/clir"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddPullCommand adds the 'pull' command to the given parent command.
|
||||||
|
func AddPullCommand(parent *clir.Cli) {
|
||||||
|
var registryPath string
|
||||||
|
var all bool
|
||||||
|
|
||||||
|
pullCmd := parent.NewSubCommand("pull", "Pull updates across all repos")
|
||||||
|
pullCmd.LongDescription("Pulls updates for all repos.\n" +
|
||||||
|
"By default only pulls repos that are behind. Use --all to pull all repos.")
|
||||||
|
|
||||||
|
pullCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", ®istryPath)
|
||||||
|
pullCmd.BoolFlag("all", "Pull all repos, not just those behind", &all)
|
||||||
|
|
||||||
|
pullCmd.Action(func() error {
|
||||||
|
return runPull(registryPath, all)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPull(registryPath string, all bool) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Find or use provided registry, fall back to directory scan
|
||||||
|
var reg *repos.Registry
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if registryPath != "" {
|
||||||
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
registryPath, err = repos.FindRegistry()
|
||||||
|
if err == nil {
|
||||||
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: scan current directory
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
reg, err = repos.ScanDirectory(cwd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to scan directory: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build paths and names for git operations
|
||||||
|
var paths []string
|
||||||
|
names := make(map[string]string)
|
||||||
|
|
||||||
|
for _, repo := range reg.List() {
|
||||||
|
if repo.IsGitRepo() {
|
||||||
|
paths = append(paths, repo.Path)
|
||||||
|
names[repo.Path] = repo.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(paths) == 0 {
|
||||||
|
fmt.Println("No git repositories found.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get status for all repos
|
||||||
|
statuses := git.Status(ctx, git.StatusOptions{
|
||||||
|
Paths: paths,
|
||||||
|
Names: names,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Find repos to pull
|
||||||
|
var toPull []git.RepoStatus
|
||||||
|
for _, s := range statuses {
|
||||||
|
if s.Error != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if all || s.HasUnpulled() {
|
||||||
|
toPull = append(toPull, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(toPull) == 0 {
|
||||||
|
fmt.Println("All repos up to date. Nothing to pull.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show what we're pulling
|
||||||
|
if all {
|
||||||
|
fmt.Printf("\nPulling %d repo(s):\n\n", len(toPull))
|
||||||
|
} else {
|
||||||
|
fmt.Printf("\n%d repo(s) behind upstream:\n\n", len(toPull))
|
||||||
|
for _, s := range toPull {
|
||||||
|
fmt.Printf(" %s: %s\n",
|
||||||
|
repoNameStyle.Render(s.Name),
|
||||||
|
dimStyle.Render(fmt.Sprintf("%d commit(s) behind", s.Behind)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull each repo
|
||||||
|
var succeeded, failed int
|
||||||
|
for _, s := range toPull {
|
||||||
|
fmt.Printf(" %s %s... ", dimStyle.Render("Pulling"), s.Name)
|
||||||
|
|
||||||
|
err := gitPull(ctx, s.Path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("%s\n", errorStyle.Render("✗ "+err.Error()))
|
||||||
|
failed++
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s\n", successStyle.Render("✓"))
|
||||||
|
succeeded++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("%s %d pulled", successStyle.Render("Done:"), succeeded)
|
||||||
|
if failed > 0 {
|
||||||
|
fmt.Printf(", %s", errorStyle.Render(fmt.Sprintf("%d failed", failed)))
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gitPull(ctx context.Context, path string) error {
|
||||||
|
cmd := exec.CommandContext(ctx, "git", "pull", "--ff-only")
|
||||||
|
cmd.Dir = path
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s", string(output))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
144
cmd/core/cmd/push.go
Normal file
144
cmd/core/cmd/push.go
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/Snider/Core/pkg/git"
|
||||||
|
"github.com/Snider/Core/pkg/repos"
|
||||||
|
"github.com/leaanthony/clir"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddPushCommand adds the 'push' command to the given parent command.
|
||||||
|
func AddPushCommand(parent *clir.Cli) {
|
||||||
|
var registryPath string
|
||||||
|
var force bool
|
||||||
|
|
||||||
|
pushCmd := parent.NewSubCommand("push", "Push commits across all repos")
|
||||||
|
pushCmd.LongDescription("Pushes unpushed commits for all repos.\n" +
|
||||||
|
"Shows repos with commits to push and confirms before pushing.")
|
||||||
|
|
||||||
|
pushCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", ®istryPath)
|
||||||
|
pushCmd.BoolFlag("force", "Skip confirmation prompt", &force)
|
||||||
|
|
||||||
|
pushCmd.Action(func() error {
|
||||||
|
return runPush(registryPath, force)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPush(registryPath string, force bool) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Find or use provided registry, fall back to directory scan
|
||||||
|
var reg *repos.Registry
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if registryPath != "" {
|
||||||
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
registryPath, err = repos.FindRegistry()
|
||||||
|
if err == nil {
|
||||||
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: scan current directory
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
reg, err = repos.ScanDirectory(cwd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to scan directory: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build paths and names for git operations
|
||||||
|
var paths []string
|
||||||
|
names := make(map[string]string)
|
||||||
|
|
||||||
|
for _, repo := range reg.List() {
|
||||||
|
if repo.IsGitRepo() {
|
||||||
|
paths = append(paths, repo.Path)
|
||||||
|
names[repo.Path] = repo.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(paths) == 0 {
|
||||||
|
fmt.Println("No git repositories found.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get status for all repos
|
||||||
|
statuses := git.Status(ctx, git.StatusOptions{
|
||||||
|
Paths: paths,
|
||||||
|
Names: names,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Find repos with unpushed commits
|
||||||
|
var aheadRepos []git.RepoStatus
|
||||||
|
for _, s := range statuses {
|
||||||
|
if s.Error == nil && s.HasUnpushed() {
|
||||||
|
aheadRepos = append(aheadRepos, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(aheadRepos) == 0 {
|
||||||
|
fmt.Println("All repos up to date. Nothing to push.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show repos to push
|
||||||
|
fmt.Printf("\n%d repo(s) with unpushed commits:\n\n", len(aheadRepos))
|
||||||
|
totalCommits := 0
|
||||||
|
for _, s := range aheadRepos {
|
||||||
|
fmt.Printf(" %s: %s\n",
|
||||||
|
repoNameStyle.Render(s.Name),
|
||||||
|
aheadStyle.Render(fmt.Sprintf("%d commit(s)", s.Ahead)),
|
||||||
|
)
|
||||||
|
totalCommits += s.Ahead
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm unless --force
|
||||||
|
if !force {
|
||||||
|
fmt.Println()
|
||||||
|
if !confirm(fmt.Sprintf("Push %d commit(s) to %d repo(s)?", totalCommits, len(aheadRepos))) {
|
||||||
|
fmt.Println("Aborted.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Push sequentially (SSH passphrase needs interaction)
|
||||||
|
var pushPaths []string
|
||||||
|
for _, s := range aheadRepos {
|
||||||
|
pushPaths = append(pushPaths, s.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
results := git.PushMultiple(ctx, pushPaths, names)
|
||||||
|
|
||||||
|
var succeeded, failed int
|
||||||
|
for _, r := range results {
|
||||||
|
if r.Success {
|
||||||
|
fmt.Printf(" %s %s\n", successStyle.Render("✓"), r.Name)
|
||||||
|
succeeded++
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" %s %s: %s\n", errorStyle.Render("✗"), r.Name, r.Error)
|
||||||
|
failed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("%s %d pushed", successStyle.Render("Done:"), succeeded)
|
||||||
|
if failed > 0 {
|
||||||
|
fmt.Printf(", %s", errorStyle.Render(fmt.Sprintf("%d failed", failed)))
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
269
cmd/core/cmd/reviews.go
Normal file
269
cmd/core/cmd/reviews.go
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Snider/Core/pkg/repos"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/leaanthony/clir"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
prNumberStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("#a855f7")) // purple-500
|
||||||
|
|
||||||
|
prTitleStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#e2e8f0")) // gray-200
|
||||||
|
|
||||||
|
prAuthorStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#3b82f6")) // blue-500
|
||||||
|
|
||||||
|
prApprovedStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("#22c55e")) // green-500
|
||||||
|
|
||||||
|
prChangesStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("#f59e0b")) // amber-500
|
||||||
|
|
||||||
|
prPendingStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#6b7280")) // gray-500
|
||||||
|
|
||||||
|
prDraftStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#6b7280")) // gray-500
|
||||||
|
)
|
||||||
|
|
||||||
|
// GitHubPR represents a GitHub pull request.
|
||||||
|
type GitHubPR struct {
|
||||||
|
Number int `json:"number"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
State string `json:"state"`
|
||||||
|
IsDraft bool `json:"isDraft"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
Author struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
} `json:"author"`
|
||||||
|
ReviewDecision string `json:"reviewDecision"`
|
||||||
|
Reviews struct {
|
||||||
|
Nodes []struct {
|
||||||
|
State string `json:"state"`
|
||||||
|
Author struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
} `json:"author"`
|
||||||
|
} `json:"nodes"`
|
||||||
|
} `json:"reviews"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
|
||||||
|
// Added by us
|
||||||
|
RepoName string `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddReviewsCommand adds the 'reviews' command to the given parent command.
|
||||||
|
func AddReviewsCommand(parent *clir.Cli) {
|
||||||
|
var registryPath string
|
||||||
|
var author string
|
||||||
|
var showAll bool
|
||||||
|
|
||||||
|
reviewsCmd := parent.NewSubCommand("reviews", "List PRs needing review across all repos")
|
||||||
|
reviewsCmd.LongDescription("Fetches open PRs from GitHub for all repos in the registry.\n" +
|
||||||
|
"Shows review status (approved, changes requested, pending).\n" +
|
||||||
|
"Requires the 'gh' CLI to be installed and authenticated.")
|
||||||
|
|
||||||
|
reviewsCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", ®istryPath)
|
||||||
|
reviewsCmd.StringFlag("author", "Filter by PR author", &author)
|
||||||
|
reviewsCmd.BoolFlag("all", "Show all PRs including drafts", &showAll)
|
||||||
|
|
||||||
|
reviewsCmd.Action(func() error {
|
||||||
|
return runReviews(registryPath, author, showAll)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runReviews(registryPath string, author string, showAll bool) error {
|
||||||
|
// Check gh is available
|
||||||
|
if _, err := exec.LookPath("gh"); err != nil {
|
||||||
|
return fmt.Errorf("'gh' CLI not found. Install from https://cli.github.com/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find or use provided registry, fall back to directory scan
|
||||||
|
var reg *repos.Registry
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if registryPath != "" {
|
||||||
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
registryPath, err = repos.FindRegistry()
|
||||||
|
if err == nil {
|
||||||
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: scan current directory
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
reg, err = repos.ScanDirectory(cwd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to scan directory: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch PRs sequentially (avoid GitHub rate limits)
|
||||||
|
var allPRs []GitHubPR
|
||||||
|
var fetchErrors []error
|
||||||
|
|
||||||
|
repoList := reg.List()
|
||||||
|
for i, repo := range repoList {
|
||||||
|
repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name)
|
||||||
|
fmt.Printf("\033[2K\r%s %d/%d %s", dimStyle.Render("Fetching"), i+1, len(repoList), repo.Name)
|
||||||
|
|
||||||
|
prs, err := fetchPRs(repoFullName, repo.Name, author)
|
||||||
|
if err != nil {
|
||||||
|
fetchErrors = append(fetchErrors, fmt.Errorf("%s: %w", repo.Name, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pr := range prs {
|
||||||
|
// Filter drafts unless --all
|
||||||
|
if !showAll && pr.IsDraft {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
allPRs = append(allPRs, pr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Print("\033[2K\r") // Clear progress line
|
||||||
|
|
||||||
|
// Sort: pending review first, then by date
|
||||||
|
sort.Slice(allPRs, func(i, j int) bool {
|
||||||
|
// Pending reviews come first
|
||||||
|
iPending := allPRs[i].ReviewDecision == "" || allPRs[i].ReviewDecision == "REVIEW_REQUIRED"
|
||||||
|
jPending := allPRs[j].ReviewDecision == "" || allPRs[j].ReviewDecision == "REVIEW_REQUIRED"
|
||||||
|
if iPending != jPending {
|
||||||
|
return iPending
|
||||||
|
}
|
||||||
|
return allPRs[i].CreatedAt.After(allPRs[j].CreatedAt)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Print PRs
|
||||||
|
if len(allPRs) == 0 {
|
||||||
|
fmt.Println("No open PRs found.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count by status
|
||||||
|
var pending, approved, changesRequested int
|
||||||
|
for _, pr := range allPRs {
|
||||||
|
switch pr.ReviewDecision {
|
||||||
|
case "APPROVED":
|
||||||
|
approved++
|
||||||
|
case "CHANGES_REQUESTED":
|
||||||
|
changesRequested++
|
||||||
|
default:
|
||||||
|
pending++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("%d open PR(s)", len(allPRs))
|
||||||
|
if pending > 0 {
|
||||||
|
fmt.Printf(" · %s", prPendingStyle.Render(fmt.Sprintf("%d pending", pending)))
|
||||||
|
}
|
||||||
|
if approved > 0 {
|
||||||
|
fmt.Printf(" · %s", prApprovedStyle.Render(fmt.Sprintf("%d approved", approved)))
|
||||||
|
}
|
||||||
|
if changesRequested > 0 {
|
||||||
|
fmt.Printf(" · %s", prChangesStyle.Render(fmt.Sprintf("%d changes requested", changesRequested)))
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
for _, pr := range allPRs {
|
||||||
|
printPR(pr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print any errors
|
||||||
|
if len(fetchErrors) > 0 {
|
||||||
|
fmt.Println()
|
||||||
|
for _, err := range fetchErrors {
|
||||||
|
fmt.Printf("%s %s\n", errorStyle.Render("Error:"), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchPRs(repoFullName, repoName string, author string) ([]GitHubPR, error) {
|
||||||
|
args := []string{
|
||||||
|
"pr", "list",
|
||||||
|
"--repo", repoFullName,
|
||||||
|
"--state", "open",
|
||||||
|
"--json", "number,title,state,isDraft,createdAt,author,reviewDecision,reviews,url",
|
||||||
|
}
|
||||||
|
|
||||||
|
if author != "" {
|
||||||
|
args = append(args, "--author", author)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("gh", args...)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
stderr := string(exitErr.Stderr)
|
||||||
|
if strings.Contains(stderr, "no pull requests") || strings.Contains(stderr, "Could not resolve") {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("%s", stderr)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var prs []GitHubPR
|
||||||
|
if err := json.Unmarshal(output, &prs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag with repo name
|
||||||
|
for i := range prs {
|
||||||
|
prs[i].RepoName = repoName
|
||||||
|
}
|
||||||
|
|
||||||
|
return prs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printPR(pr GitHubPR) {
|
||||||
|
// #12 [core-php] Webhook validation
|
||||||
|
num := prNumberStyle.Render(fmt.Sprintf("#%d", pr.Number))
|
||||||
|
repo := issueRepoStyle.Render(fmt.Sprintf("[%s]", pr.RepoName))
|
||||||
|
title := prTitleStyle.Render(truncate(pr.Title, 50))
|
||||||
|
author := prAuthorStyle.Render("@" + pr.Author.Login)
|
||||||
|
|
||||||
|
// Review status
|
||||||
|
var status string
|
||||||
|
switch pr.ReviewDecision {
|
||||||
|
case "APPROVED":
|
||||||
|
status = prApprovedStyle.Render("✓ approved")
|
||||||
|
case "CHANGES_REQUESTED":
|
||||||
|
status = prChangesStyle.Render("● changes requested")
|
||||||
|
default:
|
||||||
|
status = prPendingStyle.Render("○ pending review")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draft indicator
|
||||||
|
draft := ""
|
||||||
|
if pr.IsDraft {
|
||||||
|
draft = prDraftStyle.Render(" [draft]")
|
||||||
|
}
|
||||||
|
|
||||||
|
age := formatAge(pr.CreatedAt)
|
||||||
|
|
||||||
|
fmt.Printf(" %s %s %s%s %s %s %s\n", num, repo, title, draft, author, status, issueAgeStyle.Render(age))
|
||||||
|
}
|
||||||
|
|
@ -70,6 +70,16 @@ func Execute() error {
|
||||||
AddSyncCommand(devCmd)
|
AddSyncCommand(devCmd)
|
||||||
AddBuildCommand(app)
|
AddBuildCommand(app)
|
||||||
AddTviewCommand(app)
|
AddTviewCommand(app)
|
||||||
|
AddWorkCommand(app)
|
||||||
|
AddHealthCommand(app)
|
||||||
|
AddIssuesCommand(app)
|
||||||
|
AddReviewsCommand(app)
|
||||||
|
AddCommitCommand(app)
|
||||||
|
AddPushCommand(app)
|
||||||
|
AddPullCommand(app)
|
||||||
|
AddImpactCommand(app)
|
||||||
|
AddDocsCommand(app)
|
||||||
|
AddCICommand(app)
|
||||||
// Run the application
|
// Run the application
|
||||||
return app.Run()
|
return app.Run()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
334
cmd/core/cmd/work.go
Normal file
334
cmd/core/cmd/work.go
Normal file
|
|
@ -0,0 +1,334 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Snider/Core/pkg/git"
|
||||||
|
"github.com/Snider/Core/pkg/repos"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/leaanthony/clir"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Table styles
|
||||||
|
headerStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("#3b82f6")). // blue-500
|
||||||
|
Padding(0, 1)
|
||||||
|
|
||||||
|
cellStyle = lipgloss.NewStyle().
|
||||||
|
Padding(0, 1)
|
||||||
|
|
||||||
|
dirtyStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#ef4444")). // red-500
|
||||||
|
Padding(0, 1)
|
||||||
|
|
||||||
|
aheadStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#22c55e")). // green-500
|
||||||
|
Padding(0, 1)
|
||||||
|
|
||||||
|
cleanStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#6b7280")). // gray-500
|
||||||
|
Padding(0, 1)
|
||||||
|
|
||||||
|
repoNameStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("#e2e8f0")). // gray-200
|
||||||
|
Padding(0, 1)
|
||||||
|
|
||||||
|
successStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#22c55e")). // green-500
|
||||||
|
Bold(true)
|
||||||
|
|
||||||
|
errorStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#ef4444")). // red-500
|
||||||
|
Bold(true)
|
||||||
|
|
||||||
|
dimStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#6b7280")) // gray-500
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddWorkCommand adds the 'work' command to the given parent command.
|
||||||
|
func AddWorkCommand(parent *clir.Cli) {
|
||||||
|
var statusOnly bool
|
||||||
|
var autoCommit bool
|
||||||
|
var registryPath string
|
||||||
|
|
||||||
|
workCmd := parent.NewSubCommand("work", "Multi-repo git operations")
|
||||||
|
workCmd.LongDescription("Manage git status, commits, and pushes across multiple repositories.\n\n" +
|
||||||
|
"Reads repos.yaml to discover repositories and their relationships.\n" +
|
||||||
|
"Shows status, optionally commits with Claude, and pushes changes.")
|
||||||
|
|
||||||
|
workCmd.BoolFlag("status", "Show status only, don't push", &statusOnly)
|
||||||
|
workCmd.BoolFlag("commit", "Use Claude to commit dirty repos before pushing", &autoCommit)
|
||||||
|
workCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", ®istryPath)
|
||||||
|
|
||||||
|
workCmd.Action(func() error {
|
||||||
|
return runWork(registryPath, statusOnly, autoCommit)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWork(registryPath string, statusOnly, autoCommit bool) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Find or use provided registry, fall back to directory scan
|
||||||
|
var reg *repos.Registry
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if registryPath != "" {
|
||||||
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%s %s\n\n", dimStyle.Render("Registry:"), registryPath)
|
||||||
|
} else {
|
||||||
|
registryPath, err = repos.FindRegistry()
|
||||||
|
if err == nil {
|
||||||
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%s %s\n\n", dimStyle.Render("Registry:"), registryPath)
|
||||||
|
} else {
|
||||||
|
// Fallback: scan current directory
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
reg, err = repos.ScanDirectory(cwd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to scan directory: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%s %s\n\n", dimStyle.Render("Scanning:"), cwd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build paths and names for git operations
|
||||||
|
var paths []string
|
||||||
|
names := make(map[string]string)
|
||||||
|
|
||||||
|
for _, repo := range reg.List() {
|
||||||
|
if repo.IsGitRepo() {
|
||||||
|
paths = append(paths, repo.Path)
|
||||||
|
names[repo.Path] = repo.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(paths) == 0 {
|
||||||
|
fmt.Println("No git repositories found.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get status for all repos
|
||||||
|
statuses := git.Status(ctx, git.StatusOptions{
|
||||||
|
Paths: paths,
|
||||||
|
Names: names,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sort by repo name for consistent output
|
||||||
|
sort.Slice(statuses, func(i, j int) bool {
|
||||||
|
return statuses[i].Name < statuses[j].Name
|
||||||
|
})
|
||||||
|
|
||||||
|
// Display status table
|
||||||
|
printStatusTable(statuses)
|
||||||
|
|
||||||
|
// Collect dirty and ahead repos
|
||||||
|
var dirtyRepos []git.RepoStatus
|
||||||
|
var aheadRepos []git.RepoStatus
|
||||||
|
|
||||||
|
for _, s := range statuses {
|
||||||
|
if s.Error != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s.IsDirty() {
|
||||||
|
dirtyRepos = append(dirtyRepos, s)
|
||||||
|
}
|
||||||
|
if s.HasUnpushed() {
|
||||||
|
aheadRepos = append(aheadRepos, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-commit dirty repos if requested
|
||||||
|
if autoCommit && len(dirtyRepos) > 0 {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("%s\n", headerStyle.Render("Committing dirty repos with Claude..."))
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
for _, s := range dirtyRepos {
|
||||||
|
if err := claudeCommit(ctx, s.Path, s.Name, registryPath); err != nil {
|
||||||
|
fmt.Printf(" %s %s: %s\n", errorStyle.Render("✗"), s.Name, err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" %s %s\n", successStyle.Render("✓"), s.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-check status after commits
|
||||||
|
statuses = git.Status(ctx, git.StatusOptions{
|
||||||
|
Paths: paths,
|
||||||
|
Names: names,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Rebuild ahead repos list
|
||||||
|
aheadRepos = nil
|
||||||
|
for _, s := range statuses {
|
||||||
|
if s.Error == nil && s.HasUnpushed() {
|
||||||
|
aheadRepos = append(aheadRepos, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If status only, we're done
|
||||||
|
if statusOnly {
|
||||||
|
if len(dirtyRepos) > 0 && !autoCommit {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("%s\n", dimStyle.Render("Use --commit to have Claude create commits"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push repos with unpushed commits
|
||||||
|
if len(aheadRepos) == 0 {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("All repos up to date.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("%d repo(s) with unpushed commits:\n", len(aheadRepos))
|
||||||
|
for _, s := range aheadRepos {
|
||||||
|
fmt.Printf(" %s: %d commit(s)\n", s.Name, s.Ahead)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
if !confirm("Push all?") {
|
||||||
|
fmt.Println("Aborted.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Push sequentially (SSH passphrase needs interaction)
|
||||||
|
var pushPaths []string
|
||||||
|
for _, s := range aheadRepos {
|
||||||
|
pushPaths = append(pushPaths, s.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
results := git.PushMultiple(ctx, pushPaths, names)
|
||||||
|
|
||||||
|
for _, r := range results {
|
||||||
|
if r.Success {
|
||||||
|
fmt.Printf(" %s %s\n", successStyle.Render("✓"), r.Name)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" %s %s: %s\n", errorStyle.Render("✗"), r.Name, r.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printStatusTable(statuses []git.RepoStatus) {
|
||||||
|
// Calculate column widths
|
||||||
|
nameWidth := 4 // "Repo"
|
||||||
|
for _, s := range statuses {
|
||||||
|
if len(s.Name) > nameWidth {
|
||||||
|
nameWidth = len(s.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print header with fixed-width formatting
|
||||||
|
hdrStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#3b82f6"))
|
||||||
|
fmt.Printf("%-*s %8s %9s %6s %5s\n",
|
||||||
|
nameWidth,
|
||||||
|
hdrStyle.Render("Repo"),
|
||||||
|
hdrStyle.Render("Modified"),
|
||||||
|
hdrStyle.Render("Untracked"),
|
||||||
|
hdrStyle.Render("Staged"),
|
||||||
|
hdrStyle.Render("Ahead"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Print separator
|
||||||
|
fmt.Println(strings.Repeat("─", nameWidth+2+10+11+8+7))
|
||||||
|
|
||||||
|
// Print rows
|
||||||
|
for _, s := range statuses {
|
||||||
|
if s.Error != nil {
|
||||||
|
paddedName := fmt.Sprintf("%-*s", nameWidth, s.Name)
|
||||||
|
fmt.Printf("%s %s\n",
|
||||||
|
repoNameStyle.Render(paddedName),
|
||||||
|
errorStyle.Render("error: "+s.Error.Error()),
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style numbers based on values
|
||||||
|
modStr := fmt.Sprintf("%d", s.Modified)
|
||||||
|
if s.Modified > 0 {
|
||||||
|
modStr = dirtyStyle.Render(modStr)
|
||||||
|
} else {
|
||||||
|
modStr = cleanStyle.Render(modStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
untrackedStr := fmt.Sprintf("%d", s.Untracked)
|
||||||
|
if s.Untracked > 0 {
|
||||||
|
untrackedStr = dirtyStyle.Render(untrackedStr)
|
||||||
|
} else {
|
||||||
|
untrackedStr = cleanStyle.Render(untrackedStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
stagedStr := fmt.Sprintf("%d", s.Staged)
|
||||||
|
if s.Staged > 0 {
|
||||||
|
stagedStr = aheadStyle.Render(stagedStr)
|
||||||
|
} else {
|
||||||
|
stagedStr = cleanStyle.Render(stagedStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
aheadStr := fmt.Sprintf("%d", s.Ahead)
|
||||||
|
if s.Ahead > 0 {
|
||||||
|
aheadStr = aheadStyle.Render(aheadStr)
|
||||||
|
} else {
|
||||||
|
aheadStr = cleanStyle.Render(aheadStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pad name before styling to avoid ANSI code length issues
|
||||||
|
paddedName := fmt.Sprintf("%-*s", nameWidth, s.Name)
|
||||||
|
fmt.Printf("%s %8s %9s %6s %5s\n",
|
||||||
|
repoNameStyle.Render(paddedName),
|
||||||
|
modStr,
|
||||||
|
untrackedStr,
|
||||||
|
stagedStr,
|
||||||
|
aheadStr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func claudeCommit(ctx context.Context, repoPath, repoName, registryPath string) error {
|
||||||
|
// Load AGENTS.md context if available
|
||||||
|
agentsPath := filepath.Join(filepath.Dir(registryPath), "AGENTS.md")
|
||||||
|
var context string
|
||||||
|
if data, err := os.ReadFile(agentsPath); err == nil {
|
||||||
|
context = string(data) + "\n\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt := context + "Review the uncommitted changes and create an appropriate commit. " +
|
||||||
|
"Use Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>. Be concise."
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "claude", "-p", prompt, "--allowedTools", "Bash,Read,Glob,Grep")
|
||||||
|
cmd.Dir = repoPath
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func confirm(prompt string) bool {
|
||||||
|
fmt.Printf("%s [y/N] ", prompt)
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
response = strings.ToLower(strings.TrimSpace(response))
|
||||||
|
return response == "y" || response == "yes"
|
||||||
|
}
|
||||||
|
|
@ -1,32 +1,38 @@
|
||||||
module github.com/Snider/Core/cmd/core
|
module github.com/Snider/Core/cmd/core
|
||||||
|
|
||||||
go 1.25
|
go 1.25.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/Snider/Core/pkg/git v0.0.0
|
||||||
|
github.com/Snider/Core/pkg/repos v0.0.0
|
||||||
|
github.com/charmbracelet/lipgloss v1.0.0
|
||||||
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be
|
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be
|
||||||
github.com/leaanthony/clir v1.7.0
|
github.com/leaanthony/clir v1.7.0
|
||||||
github.com/leaanthony/debme v1.2.1
|
github.com/leaanthony/debme v1.2.1
|
||||||
github.com/leaanthony/gosod v1.0.4
|
github.com/leaanthony/gosod v1.0.4
|
||||||
github.com/rivo/tview v0.42.0
|
github.com/rivo/tview v0.42.0
|
||||||
golang.org/x/net v0.38.0
|
golang.org/x/net v0.49.0
|
||||||
golang.org/x/text v0.23.0
|
golang.org/x/text v0.33.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
github.com/charmbracelet/x/ansi v0.4.2 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
|
||||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
|
||||||
github.com/gdamore/encoding v1.0.1 // indirect
|
github.com/gdamore/encoding v1.0.1 // indirect
|
||||||
github.com/gdamore/tcell/v2 v2.8.1 // indirect
|
github.com/gdamore/tcell/v2 v2.8.1 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.15.2 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/term v0.39.0 // indirect
|
||||||
golang.org/x/term v0.32.0 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
replace (
|
||||||
|
github.com/Snider/Core => ../../
|
||||||
|
github.com/Snider/Core/pkg/git => ../../pkg/git
|
||||||
|
github.com/Snider/Core/pkg/repos => ../../pkg/repos
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,21 @@
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
|
||||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
|
||||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
|
||||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
|
||||||
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ=
|
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ=
|
||||||
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w=
|
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
||||||
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
||||||
github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
|
github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
|
||||||
github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw=
|
github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/leaanthony/clir v1.7.0 h1:xiAnhl7ryPwuH3ERwPWZp/pCHk8wTeiwuAOt6MiNyAw=
|
github.com/leaanthony/clir v1.7.0 h1:xiAnhl7ryPwuH3ERwPWZp/pCHk8wTeiwuAOt6MiNyAw=
|
||||||
github.com/leaanthony/clir v1.7.0/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0=
|
github.com/leaanthony/clir v1.7.0/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0=
|
||||||
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
||||||
|
|
@ -34,24 +33,22 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||||
github.com/rivo/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c=
|
github.com/rivo/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c=
|
||||||
github.com/rivo/tview v0.42.0/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY=
|
github.com/rivo/tview v0.42.0/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
|
||||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
|
@ -65,8 +62,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
|
@ -86,8 +83,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
|
@ -97,8 +94,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
|
@ -108,8 +105,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
|
@ -117,3 +114,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
||||||
3
go.work
3
go.work
|
|
@ -2,6 +2,7 @@ go 1.25.5
|
||||||
|
|
||||||
use (
|
use (
|
||||||
.
|
.
|
||||||
|
./cmd/core
|
||||||
./cmd/core-gui
|
./cmd/core-gui
|
||||||
./cmd/core-mcp
|
./cmd/core-mcp
|
||||||
./cmd/examples/core-static-di
|
./cmd/examples/core-static-di
|
||||||
|
|
@ -10,12 +11,14 @@ use (
|
||||||
./pkg/core
|
./pkg/core
|
||||||
./pkg/display
|
./pkg/display
|
||||||
./pkg/docs
|
./pkg/docs
|
||||||
|
./pkg/git
|
||||||
./pkg/help
|
./pkg/help
|
||||||
./pkg/i18n
|
./pkg/i18n
|
||||||
./pkg/ide
|
./pkg/ide
|
||||||
./pkg/mcp
|
./pkg/mcp
|
||||||
./pkg/module
|
./pkg/module
|
||||||
./pkg/process
|
./pkg/process
|
||||||
|
./pkg/repos
|
||||||
./pkg/updater
|
./pkg/updater
|
||||||
./pkg/webview
|
./pkg/webview
|
||||||
./pkg/ws
|
./pkg/ws
|
||||||
|
|
|
||||||
106
go.work.sum
106
go.work.sum
|
|
@ -18,8 +18,6 @@ github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0
|
||||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
|
github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
|
||||||
github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
||||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
|
||||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
|
||||||
github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg=
|
github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg=
|
||||||
github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4=
|
github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4=
|
||||||
github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
|
github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
|
||||||
|
|
@ -35,8 +33,6 @@ github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJ
|
||||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||||
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||||
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
|
|
||||||
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
|
||||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||||
|
|
@ -44,6 +40,10 @@ github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ek
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1 h1:Awsg7MPc2gD3I7IFac2qE3Gdls0lZW8SzrFZ3k1oz0s=
|
github.com/ProtonMail/gopenpgp/v2 v2.7.1 h1:Awsg7MPc2gD3I7IFac2qE3Gdls0lZW8SzrFZ3k1oz0s=
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1/go.mod h1:/BU5gfAVwqyd8EfC3Eu7zmuhwYQpKs+cGD8M//iiaxs=
|
github.com/ProtonMail/gopenpgp/v2 v2.7.1/go.mod h1:/BU5gfAVwqyd8EfC3Eu7zmuhwYQpKs+cGD8M//iiaxs=
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||||
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
|
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
|
||||||
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
|
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
|
|
@ -58,8 +58,6 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
|
||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
github.com/atterpac/refresh v0.8.6 h1:Q5miKV2qs9jW+USw8WZ/54Zz8/RSh/bOz5U6JvvDZmM=
|
github.com/atterpac/refresh v0.8.6 h1:Q5miKV2qs9jW+USw8WZ/54Zz8/RSh/bOz5U6JvvDZmM=
|
||||||
github.com/atterpac/refresh v0.8.6/go.mod h1:fJpWySLdpbANS8Ej5OvfZVZIVvi/9bmnhTjKS5EjQes=
|
github.com/atterpac/refresh v0.8.6/go.mod h1:fJpWySLdpbANS8Ej5OvfZVZIVvi/9bmnhTjKS5EjQes=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
|
||||||
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||||
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
|
|
@ -108,6 +106,8 @@ github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQ
|
||||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wph7Ika2k4=
|
github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wph7Ika2k4=
|
||||||
github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ=
|
github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||||
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||||
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
|
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
|
||||||
|
|
@ -134,31 +134,9 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
|
||||||
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
|
|
||||||
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
|
|
||||||
github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A=
|
github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A=
|
||||||
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
|
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||||
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
|
|
||||||
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
|
|
||||||
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
|
|
||||||
github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw=
|
|
||||||
github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0=
|
|
||||||
github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0=
|
|
||||||
github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs=
|
|
||||||
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
|
|
||||||
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
|
|
||||||
github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8=
|
|
||||||
github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo=
|
|
||||||
github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw=
|
|
||||||
github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc=
|
|
||||||
github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw=
|
|
||||||
github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg=
|
|
||||||
github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA=
|
|
||||||
github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8=
|
|
||||||
github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk=
|
|
||||||
github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg=
|
|
||||||
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
||||||
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
|
|
@ -169,7 +147,6 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
github.com/google/go-github/v39 v39.2.0 h1:rNNM311XtPOz5rDdsJXAp2o8F67X9FnROXTvto3aSnQ=
|
github.com/google/go-github/v39 v39.2.0 h1:rNNM311XtPOz5rDdsJXAp2o8F67X9FnROXTvto3aSnQ=
|
||||||
github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE=
|
github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE=
|
||||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||||
|
|
@ -216,6 +193,8 @@ github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGAR
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||||
|
|
@ -232,30 +211,21 @@ github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU
|
||||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
|
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
|
||||||
github.com/leaanthony/clir v1.0.4/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0=
|
github.com/leaanthony/clir v1.0.4/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0=
|
||||||
github.com/leaanthony/clir v1.7.0 h1:xiAnhl7ryPwuH3ERwPWZp/pCHk8wTeiwuAOt6MiNyAw=
|
|
||||||
github.com/leaanthony/clir v1.7.0/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0=
|
|
||||||
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
|
||||||
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
|
|
||||||
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
|
|
||||||
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
|
|
||||||
github.com/leaanthony/winicon v1.0.0 h1:ZNt5U5dY71oEoKZ97UVwJRT4e+5xo5o/ieKuHuk8NqQ=
|
github.com/leaanthony/winicon v1.0.0 h1:ZNt5U5dY71oEoKZ97UVwJRT4e+5xo5o/ieKuHuk8NqQ=
|
||||||
github.com/leaanthony/winicon v1.0.0/go.mod h1:en5xhijl92aphrJdmRPlh4NI1L6wq3gEm0LpXAPghjU=
|
github.com/leaanthony/winicon v1.0.0/go.mod h1:en5xhijl92aphrJdmRPlh4NI1L6wq3gEm0LpXAPghjU=
|
||||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||||
github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
|
||||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
|
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||||
|
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
|
||||||
github.com/mattn/go-zglob v0.0.6 h1:mP8RnmCgho4oaUYDIDn6GNxYk+qJGUs8fJLn+twYj2A=
|
github.com/mattn/go-zglob v0.0.6 h1:mP8RnmCgho4oaUYDIDn6GNxYk+qJGUs8fJLn+twYj2A=
|
||||||
github.com/mattn/go-zglob v0.0.6/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY=
|
github.com/mattn/go-zglob v0.0.6/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||||
|
|
@ -300,7 +270,6 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
|
||||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||||
|
|
@ -341,10 +310,10 @@ github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQ
|
||||||
github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
|
github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
|
||||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
github.com/shirou/gopsutil/v4 v4.25.10 h1:at8lk/5T1OgtuCp+AwrDofFRjnvosn0nkN2OLQ6g8tA=
|
|
||||||
github.com/shirou/gopsutil/v4 v4.25.10/go.mod h1:+kSwyC8DRUD9XXEHCAFjK+0nuArFJM0lva+StQAcskM=
|
|
||||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
||||||
|
|
@ -373,20 +342,14 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
|
||||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
|
||||||
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
|
|
||||||
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
|
|
||||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
|
||||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
|
||||||
github.com/tc-hib/winres v0.3.1 h1:CwRjEGrKdbi5CvZ4ID+iyVhgyfatxFoizjPhzez9Io4=
|
github.com/tc-hib/winres v0.3.1 h1:CwRjEGrKdbi5CvZ4ID+iyVhgyfatxFoizjPhzez9Io4=
|
||||||
github.com/tc-hib/winres v0.3.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
|
github.com/tc-hib/winres v0.3.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
|
||||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
|
||||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
|
||||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
|
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
||||||
|
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||||
github.com/wailsapp/task/v3 v3.40.1-patched3 h1:i6O1WNdSur9CGaiMDIYGjsmj/qS4465zqv+WEs6sPRs=
|
github.com/wailsapp/task/v3 v3.40.1-patched3 h1:i6O1WNdSur9CGaiMDIYGjsmj/qS4465zqv+WEs6sPRs=
|
||||||
github.com/wailsapp/task/v3 v3.40.1-patched3/go.mod h1:jIP48r8ftoSQNlxFP4+aEnkvGQqQXqCnRi/B7ROaecE=
|
github.com/wailsapp/task/v3 v3.40.1-patched3/go.mod h1:jIP48r8ftoSQNlxFP4+aEnkvGQqQXqCnRi/B7ROaecE=
|
||||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||||
|
|
@ -396,13 +359,11 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMx
|
||||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
|
||||||
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
|
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
|
||||||
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
|
||||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||||
|
|
@ -413,45 +374,36 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
|
||||||
golang.org/x/crypto v0.11.1-0.20230711161743-2e82bdd1719d/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
golang.org/x/crypto v0.11.1-0.20230711161743-2e82bdd1719d/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||||
|
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||||
|
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||||
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac h1:TSSpLIG4v+p0rPv1pNOQtl1I8knsO4S9trOxNMOLVP4=
|
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac h1:TSSpLIG4v+p0rPv1pNOQtl1I8knsO4S9trOxNMOLVP4=
|
||||||
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||||
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|
||||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
|
@ -460,37 +412,31 @@ golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8=
|
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8=
|
||||||
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE=
|
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE=
|
||||||
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU=
|
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU=
|
||||||
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
|
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
|
||||||
golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 h1:E2/AqCUMZGgd73TQkxUMcMla25GB9i/5HOdLr+uH7Vo=
|
golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 h1:E2/AqCUMZGgd73TQkxUMcMla25GB9i/5HOdLr+uH7Vo=
|
||||||
golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ=
|
golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ=
|
||||||
|
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA=
|
||||||
|
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ=
|
||||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
|
||||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
|
||||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||||
|
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||||
|
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU=
|
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU=
|
||||||
|
|
@ -535,3 +481,5 @@ mvdan.cc/sh/v3 v3.10.0 h1:v9z7N1DLZ7owyLM/SXZQkBSXcwr2IGMm2LY2pmhVXj4=
|
||||||
mvdan.cc/sh/v3 v3.10.0/go.mod h1:z/mSSVyLFGZzqb3ZIKojjyqIx/xbmz/UHdCSv9HmqXY=
|
mvdan.cc/sh/v3 v3.10.0/go.mod h1:z/mSSVyLFGZzqb3ZIKojjyqIx/xbmz/UHdCSv9HmqXY=
|
||||||
rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4=
|
rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4=
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
|
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||||
|
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||||
|
|
|
||||||
198
pkg/git/git.go
Normal file
198
pkg/git/git.go
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
// Package git provides utilities for git operations across multiple repositories.
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RepoStatus represents the git status of a single repository.
|
||||||
|
type RepoStatus struct {
|
||||||
|
Name string
|
||||||
|
Path string
|
||||||
|
Modified int
|
||||||
|
Untracked int
|
||||||
|
Staged int
|
||||||
|
Ahead int
|
||||||
|
Behind int
|
||||||
|
Branch string
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDirty returns true if there are uncommitted changes.
|
||||||
|
func (s *RepoStatus) IsDirty() bool {
|
||||||
|
return s.Modified > 0 || s.Untracked > 0 || s.Staged > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasUnpushed returns true if there are commits to push.
|
||||||
|
func (s *RepoStatus) HasUnpushed() bool {
|
||||||
|
return s.Ahead > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasUnpulled returns true if there are commits to pull.
|
||||||
|
func (s *RepoStatus) HasUnpulled() bool {
|
||||||
|
return s.Behind > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusOptions configures the status check.
|
||||||
|
type StatusOptions struct {
|
||||||
|
// Paths is a list of repo paths to check
|
||||||
|
Paths []string
|
||||||
|
// Names maps paths to display names
|
||||||
|
Names map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status checks git status for multiple repositories in parallel.
|
||||||
|
func Status(ctx context.Context, opts StatusOptions) []RepoStatus {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
results := make([]RepoStatus, len(opts.Paths))
|
||||||
|
|
||||||
|
for i, path := range opts.Paths {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(idx int, repoPath string) {
|
||||||
|
defer wg.Done()
|
||||||
|
name := opts.Names[repoPath]
|
||||||
|
if name == "" {
|
||||||
|
name = repoPath
|
||||||
|
}
|
||||||
|
results[idx] = getStatus(ctx, repoPath, name)
|
||||||
|
}(i, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// getStatus gets the git status for a single repository.
|
||||||
|
func getStatus(ctx context.Context, path, name string) RepoStatus {
|
||||||
|
status := RepoStatus{
|
||||||
|
Name: name,
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current branch
|
||||||
|
branch, err := gitCommand(ctx, path, "rev-parse", "--abbrev-ref", "HEAD")
|
||||||
|
if err != nil {
|
||||||
|
status.Error = err
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
status.Branch = strings.TrimSpace(branch)
|
||||||
|
|
||||||
|
// Get porcelain status
|
||||||
|
porcelain, err := gitCommand(ctx, path, "status", "--porcelain")
|
||||||
|
if err != nil {
|
||||||
|
status.Error = err
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse status output
|
||||||
|
for _, line := range strings.Split(porcelain, "\n") {
|
||||||
|
if len(line) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
x, y := line[0], line[1]
|
||||||
|
|
||||||
|
// Untracked
|
||||||
|
if x == '?' && y == '?' {
|
||||||
|
status.Untracked++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Staged (index has changes)
|
||||||
|
if x == 'A' || x == 'D' || x == 'R' || x == 'M' {
|
||||||
|
status.Staged++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modified in working tree
|
||||||
|
if y == 'M' || y == 'D' {
|
||||||
|
status.Modified++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ahead/behind counts
|
||||||
|
ahead, behind := getAheadBehind(ctx, path)
|
||||||
|
status.Ahead = ahead
|
||||||
|
status.Behind = behind
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAheadBehind returns the number of commits ahead and behind upstream.
|
||||||
|
func getAheadBehind(ctx context.Context, path string) (ahead, behind int) {
|
||||||
|
// Try to get ahead count
|
||||||
|
aheadStr, err := gitCommand(ctx, path, "rev-list", "--count", "@{u}..HEAD")
|
||||||
|
if err == nil {
|
||||||
|
ahead, _ = strconv.Atoi(strings.TrimSpace(aheadStr))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get behind count
|
||||||
|
behindStr, err := gitCommand(ctx, path, "rev-list", "--count", "HEAD..@{u}")
|
||||||
|
if err == nil {
|
||||||
|
behind, _ = strconv.Atoi(strings.TrimSpace(behindStr))
|
||||||
|
}
|
||||||
|
|
||||||
|
return ahead, behind
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push pushes commits for a single repository.
|
||||||
|
func Push(ctx context.Context, path string) error {
|
||||||
|
_, err := gitCommand(ctx, path, "push")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushResult represents the result of a push operation.
|
||||||
|
type PushResult struct {
|
||||||
|
Name string
|
||||||
|
Path string
|
||||||
|
Success bool
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushMultiple pushes multiple repositories sequentially.
|
||||||
|
// Sequential because SSH passphrase prompts need user interaction.
|
||||||
|
func PushMultiple(ctx context.Context, paths []string, names map[string]string) []PushResult {
|
||||||
|
results := make([]PushResult, len(paths))
|
||||||
|
|
||||||
|
for i, path := range paths {
|
||||||
|
name := names[path]
|
||||||
|
if name == "" {
|
||||||
|
name = path
|
||||||
|
}
|
||||||
|
|
||||||
|
result := PushResult{
|
||||||
|
Name: name,
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Push(ctx, path)
|
||||||
|
if err != nil {
|
||||||
|
result.Error = err
|
||||||
|
} else {
|
||||||
|
result.Success = true
|
||||||
|
}
|
||||||
|
|
||||||
|
results[i] = result
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// gitCommand runs a git command and returns stdout.
|
||||||
|
func gitCommand(ctx context.Context, dir string, args ...string) (string, error) {
|
||||||
|
cmd := exec.CommandContext(ctx, "git", args...)
|
||||||
|
cmd.Dir = dir
|
||||||
|
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return stdout.String(), nil
|
||||||
|
}
|
||||||
3
pkg/git/go.mod
Normal file
3
pkg/git/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
module github.com/Snider/Core/pkg/git
|
||||||
|
|
||||||
|
go 1.25
|
||||||
11
pkg/repos/go.mod
Normal file
11
pkg/repos/go.mod
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
module github.com/Snider/Core/pkg/repos
|
||||||
|
|
||||||
|
go 1.25
|
||||||
|
|
||||||
|
require gopkg.in/yaml.v3 v3.0.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
|
)
|
||||||
5
pkg/repos/go.sum
Normal file
5
pkg/repos/go.sum
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
310
pkg/repos/registry.go
Normal file
310
pkg/repos/registry.go
Normal file
|
|
@ -0,0 +1,310 @@
|
||||||
|
// Package repos provides functionality for managing multi-repo workspaces.
|
||||||
|
// It reads a repos.yaml registry file that defines repositories, their types,
|
||||||
|
// dependencies, and metadata.
|
||||||
|
package repos
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Registry represents a collection of repositories defined in repos.yaml.
|
||||||
|
type Registry struct {
|
||||||
|
Version int `yaml:"version"`
|
||||||
|
Org string `yaml:"org"`
|
||||||
|
BasePath string `yaml:"base_path"`
|
||||||
|
Repos map[string]*Repo `yaml:"repos"`
|
||||||
|
Defaults RegistryDefaults `yaml:"defaults"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegistryDefaults contains default values applied to all repos.
|
||||||
|
type RegistryDefaults struct {
|
||||||
|
CI string `yaml:"ci"`
|
||||||
|
License string `yaml:"license"`
|
||||||
|
Branch string `yaml:"branch"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RepoType indicates the role of a repository in the ecosystem.
|
||||||
|
type RepoType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RepoTypeFoundation RepoType = "foundation"
|
||||||
|
RepoTypeModule RepoType = "module"
|
||||||
|
RepoTypeProduct RepoType = "product"
|
||||||
|
RepoTypeTemplate RepoType = "template"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Repo represents a single repository in the registry.
|
||||||
|
type Repo struct {
|
||||||
|
Name string `yaml:"-"` // Set from map key
|
||||||
|
Type RepoType `yaml:"type"`
|
||||||
|
DependsOn []string `yaml:"depends_on"`
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
Docs bool `yaml:"docs"`
|
||||||
|
CI string `yaml:"ci"`
|
||||||
|
Domain string `yaml:"domain,omitempty"`
|
||||||
|
|
||||||
|
// Computed fields
|
||||||
|
Path string `yaml:"-"` // Full path to repo directory
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadRegistry reads and parses a repos.yaml file.
|
||||||
|
func LoadRegistry(path string) (*Registry, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read registry file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var reg Registry
|
||||||
|
if err := yaml.Unmarshal(data, ®); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse registry file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand base path
|
||||||
|
reg.BasePath = expandPath(reg.BasePath)
|
||||||
|
|
||||||
|
// Set computed fields on each repo
|
||||||
|
for name, repo := range reg.Repos {
|
||||||
|
repo.Name = name
|
||||||
|
repo.Path = filepath.Join(reg.BasePath, name)
|
||||||
|
|
||||||
|
// Apply defaults if not set
|
||||||
|
if repo.CI == "" {
|
||||||
|
repo.CI = reg.Defaults.CI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ®, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindRegistry searches for repos.yaml in common locations.
|
||||||
|
// It checks: current directory, parent directories, and home directory.
|
||||||
|
func FindRegistry() (string, error) {
|
||||||
|
// Check current directory and parents
|
||||||
|
dir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
candidate := filepath.Join(dir, "repos.yaml")
|
||||||
|
if _, err := os.Stat(candidate); err == nil {
|
||||||
|
return candidate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parent := filepath.Dir(dir)
|
||||||
|
if parent == dir {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
dir = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check home directory common locations
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
commonPaths := []string{
|
||||||
|
filepath.Join(home, "Code", "host-uk", "repos.yaml"),
|
||||||
|
filepath.Join(home, ".config", "core", "repos.yaml"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range commonPaths {
|
||||||
|
if _, err := os.Stat(p); err == nil {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("repos.yaml not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanDirectory creates a Registry by scanning a directory for git repos.
|
||||||
|
// This is used as a fallback when no repos.yaml is found.
|
||||||
|
func ScanDirectory(dir string) (*Registry, error) {
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reg := &Registry{
|
||||||
|
Version: 1,
|
||||||
|
BasePath: dir,
|
||||||
|
Repos: make(map[string]*Repo),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to detect org from git remote
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
repoPath := filepath.Join(dir, entry.Name())
|
||||||
|
gitPath := filepath.Join(repoPath, ".git")
|
||||||
|
|
||||||
|
if _, err := os.Stat(gitPath); err != nil {
|
||||||
|
continue // Not a git repo
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := &Repo{
|
||||||
|
Name: entry.Name(),
|
||||||
|
Path: repoPath,
|
||||||
|
Type: RepoTypeModule, // Default type
|
||||||
|
}
|
||||||
|
|
||||||
|
reg.Repos[entry.Name()] = repo
|
||||||
|
|
||||||
|
// Try to detect org from first repo's remote
|
||||||
|
if reg.Org == "" {
|
||||||
|
reg.Org = detectOrg(repoPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectOrg tries to extract the GitHub org from a repo's origin remote.
|
||||||
|
func detectOrg(repoPath string) string {
|
||||||
|
// Try to read git remote
|
||||||
|
cmd := filepath.Join(repoPath, ".git", "config")
|
||||||
|
data, err := os.ReadFile(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple parse for github.com URLs
|
||||||
|
content := string(data)
|
||||||
|
// Look for patterns like github.com:org/repo or github.com/org/repo
|
||||||
|
for _, line := range strings.Split(content, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if !strings.HasPrefix(line, "url = ") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
url := strings.TrimPrefix(line, "url = ")
|
||||||
|
|
||||||
|
// git@github.com:org/repo.git
|
||||||
|
if strings.Contains(url, "github.com:") {
|
||||||
|
parts := strings.Split(url, ":")
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
orgRepo := strings.TrimSuffix(parts[1], ".git")
|
||||||
|
orgParts := strings.Split(orgRepo, "/")
|
||||||
|
if len(orgParts) >= 1 {
|
||||||
|
return orgParts[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/org/repo.git
|
||||||
|
if strings.Contains(url, "github.com/") {
|
||||||
|
parts := strings.Split(url, "github.com/")
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
orgRepo := strings.TrimSuffix(parts[1], ".git")
|
||||||
|
orgParts := strings.Split(orgRepo, "/")
|
||||||
|
if len(orgParts) >= 1 {
|
||||||
|
return orgParts[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns all repos in the registry.
|
||||||
|
func (r *Registry) List() []*Repo {
|
||||||
|
repos := make([]*Repo, 0, len(r.Repos))
|
||||||
|
for _, repo := range r.Repos {
|
||||||
|
repos = repos
|
||||||
|
repos = append(repos, repo)
|
||||||
|
}
|
||||||
|
return repos
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a repo by name.
|
||||||
|
func (r *Registry) Get(name string) (*Repo, bool) {
|
||||||
|
repo, ok := r.Repos[name]
|
||||||
|
return repo, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByType returns repos filtered by type.
|
||||||
|
func (r *Registry) ByType(t RepoType) []*Repo {
|
||||||
|
var repos []*Repo
|
||||||
|
for _, repo := range r.Repos {
|
||||||
|
if repo.Type == t {
|
||||||
|
repos = append(repos, repo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return repos
|
||||||
|
}
|
||||||
|
|
||||||
|
// TopologicalOrder returns repos sorted by dependency order.
|
||||||
|
// Foundation repos come first, then modules, then products.
|
||||||
|
func (r *Registry) TopologicalOrder() ([]*Repo, error) {
|
||||||
|
// Build dependency graph
|
||||||
|
visited := make(map[string]bool)
|
||||||
|
visiting := make(map[string]bool)
|
||||||
|
var result []*Repo
|
||||||
|
|
||||||
|
var visit func(name string) error
|
||||||
|
visit = func(name string) error {
|
||||||
|
if visited[name] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if visiting[name] {
|
||||||
|
return fmt.Errorf("circular dependency detected: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
repo, ok := r.Repos[name]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unknown repo: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
visiting[name] = true
|
||||||
|
for _, dep := range repo.DependsOn {
|
||||||
|
if err := visit(dep); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visiting[name] = false
|
||||||
|
visited[name] = true
|
||||||
|
result = append(result, repo)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for name := range r.Repos {
|
||||||
|
if err := visit(name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exists checks if the repo directory exists on disk.
|
||||||
|
func (repo *Repo) Exists() bool {
|
||||||
|
info, err := os.Stat(repo.Path)
|
||||||
|
return err == nil && info.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsGitRepo checks if the repo directory contains a .git folder.
|
||||||
|
func (repo *Repo) IsGitRepo() bool {
|
||||||
|
gitPath := filepath.Join(repo.Path, ".git")
|
||||||
|
info, err := os.Stat(gitPath)
|
||||||
|
return err == nil && info.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
// expandPath expands ~ to home directory.
|
||||||
|
func expandPath(path string) string {
|
||||||
|
if strings.HasPrefix(path, "~/") {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return filepath.Join(home, path[2:])
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue