summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-11 18:39:16 +0200
committerPaul Buetow <paul@buetow.org>2026-03-11 18:39:16 +0200
commit0011f18e8494a4e57dc277b826d56c0a1df041ce (patch)
tree7ffc0ad73b95c32b201adabbaf8baccbdbf6b04f
parent8e3b69cdface52a755ee64003832557e8b15e23b (diff)
refactor(internal): extract sync and summary helpers
-rw-r--r--internal/cli/sync_handlers.go298
-rw-r--r--internal/showcase/showcase.go226
-rw-r--r--internal/showcase/showcase_test.go11
3 files changed, 241 insertions, 294 deletions
diff --git a/internal/cli/sync_handlers.go b/internal/cli/sync_handlers.go
index 538e18c..8fb3a93 100644
--- a/internal/cli/sync_handlers.go
+++ b/internal/cli/sync_handlers.go
@@ -187,32 +187,7 @@ func HandleSyncAll(cfg *config.Config, flags *Flags) int {
fmt.Print(summary)
}
- // Generate script for abandoned branches
- if scriptPath, err := syncer.GenerateDeleteScript(); err != nil {
- fmt.Printf("\n⚠️ Failed to generate script: %v\n", err)
- } else if scriptPath != "" {
- fmt.Printf("\n")
- fmt.Print(strings.Repeat("=", 70))
- fmt.Printf("\n📋 ABANDONED BRANCH MANAGEMENT SCRIPT\n")
- fmt.Print(strings.Repeat("=", 70))
- fmt.Printf("\n")
- fmt.Printf("Generated script: %s\n", scriptPath)
- fmt.Printf("\n")
- fmt.Printf("Usage:\n")
- fmt.Printf(" bash %s --review # Review diffs before deletion\n", scriptPath)
- fmt.Printf(" bash %s --review-full # Review full diffs\n", scriptPath)
- fmt.Printf(" bash %s --dry-run # Preview what will be deleted\n", scriptPath)
- fmt.Printf(" bash %s # Delete branches (with confirmation)\n", scriptPath)
- fmt.Printf("\n")
- fmt.Printf("💡 Recommended workflow:\n")
- fmt.Printf(" 1. Review branches: bash %s --review\n", scriptPath)
- fmt.Printf(" 2. Dry-run delete: bash %s --dry-run\n", scriptPath)
- fmt.Printf(" 3. Delete branches: bash %s\n", scriptPath)
- fmt.Printf("\n")
- fmt.Printf("⚠️ WARNING: Review carefully before deleting branches!\n")
- fmt.Print(strings.Repeat("=", 70))
- fmt.Printf("\n")
- }
+ printDeleteScript(syncer)
return 0
}
@@ -457,6 +432,105 @@ func printFullSyncSeparator() {
fmt.Println(strings.Repeat("=", 70) + "\n")
}
+type syncExecution struct {
+ syncer *sync.Syncer
+ descCache map[string]string
+ throttleManager *state.Manager
+ throttleState *state.State
+}
+
+func newSyncExecution(cfg *config.Config, flags *Flags) *syncExecution {
+ execution := &syncExecution{
+ descCache: loadDescriptionCache(flags.WorkDir),
+ syncer: sync.New(cfg, flags.WorkDir),
+ }
+ execution.syncer.SetBackupEnabled(flags.Backup)
+
+ if flags.Throttle {
+ manager, st, err := loadThrottleState(flags.WorkDir)
+ if err != nil {
+ fmt.Printf("Warning: Failed to load throttle state: %v\n", err)
+ }
+ execution.throttleManager = manager
+ execution.throttleState = st
+ }
+
+ return execution
+}
+
+func (e *syncExecution) maybeThrottle(repoName string, flags *Flags) bool {
+ if !flags.Throttle {
+ return false
+ }
+
+ decision := evaluateThrottle(repoName, e.throttleState, flags.DryRun)
+ if decision.Message != "" {
+ fmt.Println(decision.Message)
+ }
+ if decision.SetNextAllowed && e.throttleManager != nil && !flags.DryRun {
+ e.throttleState.SetNextRepoSyncAllowed(repoName, decision.NextAllowed)
+ if err := e.throttleManager.Save(e.throttleState); err != nil {
+ fmt.Printf("Warning: Failed to save throttle state: %v\n", err)
+ }
+ }
+
+ return decision.Skip
+}
+
+func (e *syncExecution) markSynced(repoName string, flags *Flags) {
+ if !flags.Throttle || e.throttleManager == nil {
+ return
+ }
+
+ updateRepoSyncState(repoName, e.throttleState)
+ if err := e.throttleManager.Save(e.throttleState); err != nil {
+ fmt.Printf("Warning: Failed to save throttle state: %v\n", err)
+ }
+}
+
+func (e *syncExecution) finishDiscoveredSync(successCount int, flags *Flags) {
+ if err := saveDescriptionCache(flags.WorkDir, e.descCache); err != nil {
+ fmt.Printf("Warning: Failed to save descriptions cache: %v\n", err)
+ }
+
+ fmt.Printf("\n=== Summary ===\n")
+ fmt.Printf("Successfully synced: %d repositories\n", successCount)
+
+ if summary := e.syncer.GenerateAbandonedBranchSummary(); summary != "" {
+ fmt.Print(summary)
+ }
+
+ printDeleteScript(e.syncer)
+}
+
+func printDeleteScript(syncer *sync.Syncer) {
+ if scriptPath, err := syncer.GenerateDeleteScript(); err != nil {
+ fmt.Printf("\n⚠️ Failed to generate script: %v\n", err)
+ } else if scriptPath != "" {
+ fmt.Printf("\n")
+ fmt.Print(strings.Repeat("=", 70))
+ fmt.Printf("\n📋 ABANDONED BRANCH MANAGEMENT SCRIPT\n")
+ fmt.Print(strings.Repeat("=", 70))
+ fmt.Printf("\n")
+ fmt.Printf("Generated script: %s\n", scriptPath)
+ fmt.Printf("\n")
+ fmt.Printf("Usage:\n")
+ fmt.Printf(" bash %s --review # Review diffs before deletion\n", scriptPath)
+ fmt.Printf(" bash %s --review-full # Review full diffs\n", scriptPath)
+ fmt.Printf(" bash %s --dry-run # Preview what will be deleted\n", scriptPath)
+ fmt.Printf(" bash %s # Delete branches (with confirmation)\n", scriptPath)
+ fmt.Printf("\n")
+ fmt.Printf("💡 Recommended workflow:\n")
+ fmt.Printf(" 1. Review branches: bash %s --review\n", scriptPath)
+ fmt.Printf(" 2. Dry-run delete: bash %s --dry-run\n", scriptPath)
+ fmt.Printf(" 3. Delete branches: bash %s\n", scriptPath)
+ fmt.Printf("\n")
+ fmt.Printf("⚠️ WARNING: Review carefully before deleting branches!\n")
+ fmt.Print(strings.Repeat("=", 70))
+ fmt.Printf("\n")
+ }
+}
+
func syncCodebergRepos(cfg *config.Config, flags *Flags, repos []codeberg.Repository, repoNames []string) int {
// Initialize GitHub client if needed
var githubClient github.Client
@@ -470,24 +544,9 @@ func syncCodebergRepos(cfg *config.Config, flags *Flags, repos []codeberg.Reposi
fmt.Printf("\nStarting sync of %d repositories...\n", len(repoNames))
- // Load descriptions cache
- descCache := loadDescriptionCache(flags.WorkDir)
-
- syncer := sync.New(cfg, flags.WorkDir)
- syncer.SetBackupEnabled(flags.Backup)
+ execution := newSyncExecution(cfg, flags)
successCount := 0
- var throttleManager *state.Manager
- var throttleState *state.State
- if flags.Throttle {
- manager, st, err := loadThrottleState(flags.WorkDir)
- if err != nil {
- fmt.Printf("Warning: Failed to load throttle state: %v\n", err)
- }
- throttleManager = manager
- throttleState = st
- }
-
// Create map for descriptions
repoMap := make(map[string]codeberg.Repository)
for _, repo := range repos {
@@ -497,20 +556,8 @@ func syncCodebergRepos(cfg *config.Config, flags *Flags, repos []codeberg.Reposi
for i, repoName := range repoNames {
fmt.Printf("\n[%d/%d] Syncing %s...\n", i+1, len(repoNames), repoName)
- if flags.Throttle {
- decision := evaluateThrottle(repoName, throttleState, flags.DryRun)
- if decision.Message != "" {
- fmt.Println(decision.Message)
- }
- if decision.SetNextAllowed && throttleManager != nil && !flags.DryRun {
- throttleState.SetNextRepoSyncAllowed(repoName, decision.NextAllowed)
- if err := throttleManager.Save(throttleState); err != nil {
- fmt.Printf("Warning: Failed to save throttle state: %v\n", err)
- }
- }
- if decision.Skip {
- continue
- }
+ if execution.maybeThrottle(repoName, flags) {
+ continue
}
// Create GitHub repo if needed
@@ -528,66 +575,23 @@ func syncCodebergRepos(cfg *config.Config, flags *Flags, repos []codeberg.Reposi
}
}
- if err := syncer.SyncRepository(repoName); err != nil {
+ if err := execution.syncer.SyncRepository(repoName); err != nil {
fmt.Printf("ERROR: Failed to sync %s: %v\n", repoName, err)
fmt.Printf("Stopping sync due to error.\n")
return 1
}
- if flags.Throttle && throttleManager != nil {
- updateRepoSyncState(repoName, throttleState)
- if err := throttleManager.Save(throttleState); err != nil {
- fmt.Printf("Warning: Failed to save throttle state: %v\n", err)
- }
- }
+ execution.markSynced(repoName, flags)
successCount++
// After syncing, sync descriptions according to precedence
if cbRepo, ok := repoMap[repoName]; ok {
- syncRepoDescriptions(cfg, flags.DryRun, repoName, cbRepo.Description, "", descCache)
+ syncRepoDescriptions(cfg, flags.DryRun, repoName, cbRepo.Description, "", execution.descCache)
} else {
- syncRepoDescriptions(cfg, flags.DryRun, repoName, "", "", descCache)
+ syncRepoDescriptions(cfg, flags.DryRun, repoName, "", "", execution.descCache)
}
}
- // Save descriptions cache
- if err := saveDescriptionCache(flags.WorkDir, descCache); err != nil {
- fmt.Printf("Warning: Failed to save descriptions cache: %v\n", err)
- }
-
- fmt.Printf("\n=== Summary ===\n")
- fmt.Printf("Successfully synced: %d repositories\n", successCount)
-
- // Print abandoned branches summary
- if summary := syncer.GenerateAbandonedBranchSummary(); summary != "" {
- fmt.Print(summary)
- }
-
- // Generate script for abandoned branches
- if scriptPath, err := syncer.GenerateDeleteScript(); err != nil {
- fmt.Printf("\n⚠️ Failed to generate script: %v\n", err)
- } else if scriptPath != "" {
- fmt.Printf("\n")
- fmt.Print(strings.Repeat("=", 70))
- fmt.Printf("\n📋 ABANDONED BRANCH MANAGEMENT SCRIPT\n")
- fmt.Print(strings.Repeat("=", 70))
- fmt.Printf("\n")
- fmt.Printf("Generated script: %s\n", scriptPath)
- fmt.Printf("\n")
- fmt.Printf("Usage:\n")
- fmt.Printf(" bash %s --review # Review diffs before deletion\n", scriptPath)
- fmt.Printf(" bash %s --review-full # Review full diffs\n", scriptPath)
- fmt.Printf(" bash %s --dry-run # Preview what will be deleted\n", scriptPath)
- fmt.Printf(" bash %s # Delete branches (with confirmation)\n", scriptPath)
- fmt.Printf("\n")
- fmt.Printf("💡 Recommended workflow:\n")
- fmt.Printf(" 1. Review branches: bash %s --review\n", scriptPath)
- fmt.Printf(" 2. Dry-run delete: bash %s --dry-run\n", scriptPath)
- fmt.Printf(" 3. Delete branches: bash %s\n", scriptPath)
- fmt.Printf("\n")
- fmt.Printf("⚠️ WARNING: Review carefully before deleting branches!\n")
- fmt.Print(strings.Repeat("=", 70))
- fmt.Printf("\n")
- }
+ execution.finishDiscoveredSync(successCount, flags)
if !flags.SyncGitHubPublic {
return 0
@@ -611,24 +615,9 @@ func syncGitHubRepos(cfg *config.Config, flags *Flags, repos []github.Repository
fmt.Printf("\nStarting sync of %d repositories...\n", len(repoNames))
- // Load descriptions cache
- descCache := loadDescriptionCache(flags.WorkDir)
-
- syncer := sync.New(cfg, flags.WorkDir)
- syncer.SetBackupEnabled(flags.Backup)
+ execution := newSyncExecution(cfg, flags)
successCount := 0
- var throttleManager *state.Manager
- var throttleState *state.State
- if flags.Throttle {
- manager, st, err := loadThrottleState(flags.WorkDir)
- if err != nil {
- fmt.Printf("Warning: Failed to load throttle state: %v\n", err)
- }
- throttleManager = manager
- throttleState = st
- }
-
// Create map for descriptions
repoMap := make(map[string]github.Repository)
for _, repo := range repos {
@@ -638,20 +627,8 @@ func syncGitHubRepos(cfg *config.Config, flags *Flags, repos []github.Repository
for i, repoName := range repoNames {
fmt.Printf("\n[%d/%d] Syncing %s...\n", i+1, len(repoNames), repoName)
- if flags.Throttle {
- decision := evaluateThrottle(repoName, throttleState, flags.DryRun)
- if decision.Message != "" {
- fmt.Println(decision.Message)
- }
- if decision.SetNextAllowed && throttleManager != nil && !flags.DryRun {
- throttleState.SetNextRepoSyncAllowed(repoName, decision.NextAllowed)
- if err := throttleManager.Save(throttleState); err != nil {
- fmt.Printf("Warning: Failed to save throttle state: %v\n", err)
- }
- }
- if decision.Skip {
- continue
- }
+ if execution.maybeThrottle(repoName, flags) {
+ continue
}
// Create Codeberg repo if needed
@@ -669,66 +646,23 @@ func syncGitHubRepos(cfg *config.Config, flags *Flags, repos []github.Repository
}
}
- if err := syncer.SyncRepository(repoName); err != nil {
+ if err := execution.syncer.SyncRepository(repoName); err != nil {
fmt.Printf("ERROR: Failed to sync %s: %v\n", repoName, err)
fmt.Printf("Stopping sync due to error.\n")
return 1
}
- if flags.Throttle && throttleManager != nil {
- updateRepoSyncState(repoName, throttleState)
- if err := throttleManager.Save(throttleState); err != nil {
- fmt.Printf("Warning: Failed to save throttle state: %v\n", err)
- }
- }
+ execution.markSynced(repoName, flags)
successCount++
// After syncing, sync descriptions according to precedence
if ghRepo, ok := repoMap[repoName]; ok {
- syncRepoDescriptions(cfg, flags.DryRun, repoName, "", ghRepo.Description, descCache)
+ syncRepoDescriptions(cfg, flags.DryRun, repoName, "", ghRepo.Description, execution.descCache)
} else {
- syncRepoDescriptions(cfg, flags.DryRun, repoName, "", "", descCache)
+ syncRepoDescriptions(cfg, flags.DryRun, repoName, "", "", execution.descCache)
}
}
- // Save descriptions cache
- if err := saveDescriptionCache(flags.WorkDir, descCache); err != nil {
- fmt.Printf("Warning: Failed to save descriptions cache: %v\n", err)
- }
-
- fmt.Printf("\n=== Summary ===\n")
- fmt.Printf("Successfully synced: %d repositories\n", successCount)
-
- // Print abandoned branches summary
- if summary := syncer.GenerateAbandonedBranchSummary(); summary != "" {
- fmt.Print(summary)
- }
-
- // Generate script for abandoned branches
- if scriptPath, err := syncer.GenerateDeleteScript(); err != nil {
- fmt.Printf("\n⚠️ Failed to generate script: %v\n", err)
- } else if scriptPath != "" {
- fmt.Printf("\n")
- fmt.Print(strings.Repeat("=", 70))
- fmt.Printf("\n📋 ABANDONED BRANCH MANAGEMENT SCRIPT\n")
- fmt.Print(strings.Repeat("=", 70))
- fmt.Printf("\n")
- fmt.Printf("Generated script: %s\n", scriptPath)
- fmt.Printf("\n")
- fmt.Printf("Usage:\n")
- fmt.Printf(" bash %s --review # Review diffs before deletion\n", scriptPath)
- fmt.Printf(" bash %s --review-full # Review full diffs\n", scriptPath)
- fmt.Printf(" bash %s --dry-run # Preview what will be deleted\n", scriptPath)
- fmt.Printf(" bash %s # Delete branches (with confirmation)\n", scriptPath)
- fmt.Printf("\n")
- fmt.Printf("💡 Recommended workflow:\n")
- fmt.Printf(" 1. Review branches: bash %s --review\n", scriptPath)
- fmt.Printf(" 2. Dry-run delete: bash %s --dry-run\n", scriptPath)
- fmt.Printf(" 3. Delete branches: bash %s\n", scriptPath)
- fmt.Printf("\n")
- fmt.Printf("⚠️ WARNING: Review carefully before deleting branches!\n")
- fmt.Print(strings.Repeat("=", 70))
- fmt.Printf("\n")
- }
+ execution.finishDiscoveredSync(successCount, flags)
return 0
}
diff --git a/internal/showcase/showcase.go b/internal/showcase/showcase.go
index 7fef655..dfc3a0d 100644
--- a/internal/showcase/showcase.go
+++ b/internal/showcase/showcase.go
@@ -212,6 +212,101 @@ func findReadmeContent(repoPath string) ([]byte, string, bool) {
return nil, "", false
}
+func selectSummaryTool(aiTool string) string {
+ switch aiTool {
+ case "amp", "":
+ if _, err := exec.LookPath("amp"); err == nil {
+ return "amp"
+ }
+ if _, err := exec.LookPath("hexai"); err == nil {
+ return "hexai"
+ }
+ if _, err := exec.LookPath("claude"); err == nil {
+ return "claude"
+ }
+ if _, err := exec.LookPath("aichat"); err == nil {
+ return "aichat"
+ }
+ case "claude", "claude-code":
+ if _, err := exec.LookPath("claude"); err == nil {
+ return "claude"
+ }
+ if _, err := exec.LookPath("hexai"); err == nil {
+ return "hexai"
+ }
+ if _, err := exec.LookPath("aichat"); err == nil {
+ return "aichat"
+ }
+ case "hexai", "aichat":
+ if _, err := exec.LookPath(aiTool); err == nil {
+ return aiTool
+ }
+ }
+
+ return ""
+}
+
+func runSummaryTool(selectedTool, prompt, repoPath, readmeFile string, readmeContent []byte, readmeFound bool) string {
+ var cmd *exec.Cmd
+
+ switch selectedTool {
+ case "amp":
+ fmt.Printf("Running amp command (stdin payload)\n")
+ if readmeFound {
+ fmt.Printf(" echo <README content> | amp --execute \"%s\"\n", prompt)
+ fmt.Printf(" Using %s as input\n", readmeFile)
+ cmd = exec.Command("amp", "--execute", prompt)
+ cmd.Stdin = strings.NewReader(string(readmeContent))
+ }
+ case "claude":
+ fmt.Printf("Running Claude command:\n")
+ fmt.Printf(" claude --model sonnet \"%s\"\n", prompt)
+ cmd = exec.Command("claude", "--model", "sonnet", prompt)
+ case "hexai":
+ fmt.Printf("Running hexai command (stdin payload)\n")
+ if readmeFound {
+ fmt.Printf(" echo <README content> | hexai \"%s\"\n", prompt)
+ fmt.Printf(" Using %s as input\n", readmeFile)
+ cmd = exec.Command("hexai", prompt)
+ cmd.Stdin = strings.NewReader(string(readmeContent))
+ }
+ case "aichat":
+ fmt.Printf("Running aichat command:\n")
+ if readmeFound {
+ fmt.Printf(" echo <README content> | aichat \"%s\"\n", prompt)
+ fmt.Printf(" Using %s as input\n", readmeFile)
+ cmd = exec.Command("aichat", prompt)
+ cmd.Stdin = strings.NewReader(string(readmeContent))
+ }
+ }
+
+ if cmd == nil {
+ return ""
+ }
+
+ cmd.Dir = repoPath
+ output, err := cmd.Output()
+ if err != nil {
+ return ""
+ }
+
+ return strings.TrimSpace(string(output))
+}
+
+func fallbackSummary(repoName string, readmeContent []byte, readmeFound bool) string {
+ if readmeFound {
+ parts := strings.Split(strings.TrimSpace(string(readmeContent)), "\n\n")
+ if len(parts) > 0 {
+ summary := strings.TrimSpace(parts[0])
+ if summary != "" {
+ return summary
+ }
+ }
+ }
+
+ return fmt.Sprintf("%s: source code repository.", repoName)
+}
+
// getRepositories returns a list of repository directories in the work directory
func (g *Generator) getRepositories() ([]string, error) {
entries, err := os.ReadDir(g.workDir)
@@ -237,6 +332,21 @@ func (g *Generator) getRepositories() ([]string, error) {
return repos, nil
}
+func (g *Generator) buildProjectLinks(repoName string) (string, string) {
+ codebergURL := ""
+ githubURL := ""
+
+ if codebergOrg := g.config.FindCodebergOrg(); codebergOrg != nil {
+ codebergURL = fmt.Sprintf("https://codeberg.org/%s/%s", codebergOrg.Name, repoName)
+ }
+
+ if githubOrg := g.config.FindGitHubOrg(); githubOrg != nil {
+ githubURL = fmt.Sprintf("https://github.com/%s/%s", githubOrg.Name, repoName)
+ }
+
+ return codebergURL, githubURL
+}
+
// generateProjectSummary generates a summary for a single project
func (g *Generator) generateProjectSummary(repoName string, forceRegenerate bool) (*ProjectSummary, error) {
repoPath := filepath.Join(g.workDir, repoName)
@@ -260,43 +370,7 @@ func (g *Generator) generateProjectSummary(repoName string, forceRegenerate bool
// Prefer amp if available when default tool is "" (aligns with release flow)
selectedTool := g.aiTool
if !haveCachedSummary {
- switch g.aiTool {
- case "amp", "":
- // Try amp -> hexai -> claude -> aichat
- if _, err := exec.LookPath("amp"); err == nil {
- selectedTool = "amp"
- } else if _, err := exec.LookPath("hexai"); err == nil {
- selectedTool = "hexai"
- } else if _, err := exec.LookPath("claude"); err == nil {
- selectedTool = "claude"
- } else if _, err := exec.LookPath("aichat"); err == nil {
- selectedTool = "aichat"
- } else {
- // No AI tool available; fall back to README-based summary later
- selectedTool = ""
- }
- case "claude", "claude-code":
- // Try claude -> hexai -> aichat
- if _, err := exec.LookPath("claude"); err == nil {
- selectedTool = "claude"
- } else if _, err := exec.LookPath("hexai"); err == nil {
- selectedTool = "hexai"
- } else if _, err := exec.LookPath("aichat"); err == nil {
- selectedTool = "aichat"
- } else {
- selectedTool = ""
- }
- case "hexai", "aichat":
- if _, err := exec.LookPath(g.aiTool); err != nil {
- // Requested tool missing; fall back to README-based summary later
- selectedTool = ""
- } else {
- selectedTool = g.aiTool
- }
- default:
- // Unsupported tool configured; fall back to README-based summary later
- selectedTool = ""
- }
+ selectedTool = selectSummaryTool(g.aiTool)
}
readmeContent, readmeFile, readmeFound := findReadmeContent(repoPath)
@@ -316,88 +390,16 @@ func (g *Generator) generateProjectSummary(repoName string, forceRegenerate bool
fmt.Printf("Using cached AI summary\n")
} else {
prompt := "Please provide a 1-2 paragraph summary of this project, explaining what it does, why it's useful, and how it's implemented. Focus on the key features and architecture. Be concise but informative."
-
- var cmd *exec.Cmd
-
- switch selectedTool {
- case "amp":
- // Use README content as stdin and pass the prompt as --execute argument
- fmt.Printf("Running amp command (stdin payload)\n")
- if readmeFound {
- fmt.Printf(" echo <README content> | amp --execute \"%s\"\n", prompt)
- fmt.Printf(" Using %s as input\n", readmeFile)
- cmd = exec.Command("amp", "--execute", prompt)
- cmd.Stdin = strings.NewReader(string(readmeContent))
- } else {
- // Will fall back below
- cmd = nil
- }
- case "claude":
- fmt.Printf("Running Claude command:\n")
- fmt.Printf(" claude --model sonnet \"%s\"\n", prompt)
- cmd = exec.Command("claude", "--model", "sonnet", prompt)
- case "hexai":
- // Use README content as stdin and pass the prompt as argument
- fmt.Printf("Running hexai command (stdin payload)\n")
- if readmeFound {
- fmt.Printf(" echo <README content> | hexai \"%s\"\n", prompt)
- fmt.Printf(" Using %s as input\n", readmeFile)
- cmd = exec.Command("hexai", prompt)
- cmd.Stdin = strings.NewReader(string(readmeContent))
- } else {
- // Will fall back below
- cmd = nil
- }
- case "aichat":
- // For aichat, we need to read README.md and pipe it to aichat
- fmt.Printf("Running aichat command:\n")
-
- if readmeFound {
- fmt.Printf(" echo <README content> | aichat \"%s\"\n", prompt)
- fmt.Printf(" Using %s as input\n", readmeFile)
- cmd = exec.Command("aichat", prompt)
- cmd.Stdin = strings.NewReader(string(readmeContent))
- } else {
- // Will fall back below
- cmd = nil
- }
- default:
- // No/unsupported tool; will fall back below
- cmd = nil
- }
-
- if cmd != nil {
- cmd.Dir = repoPath
- if output, err := cmd.Output(); err == nil {
- summary = strings.TrimSpace(string(output))
- }
- }
+ summary = runSummaryTool(selectedTool, prompt, repoPath, readmeFile, readmeContent, readmeFound)
// Fallback: create a minimal summary from README if AI unavailable/failed
if summary == "" {
- if readmeFound {
- parts := strings.Split(strings.TrimSpace(string(readmeContent)), "\n\n")
- if len(parts) > 0 {
- summary = strings.TrimSpace(parts[0])
- }
- }
- if summary == "" {
- summary = fmt.Sprintf("%s: source code repository.", repoName)
- }
+ summary = fallbackSummary(repoName, readmeContent, readmeFound)
}
}
// Build URLs
- codebergURL := ""
- githubURL := ""
-
- if codebergOrg := g.config.FindCodebergOrg(); codebergOrg != nil {
- codebergURL = fmt.Sprintf("https://codeberg.org/%s/%s", codebergOrg.Name, repoName)
- }
-
- if githubOrg := g.config.FindGitHubOrg(); githubOrg != nil {
- githubURL = fmt.Sprintf("https://github.com/%s/%s", githubOrg.Name, repoName)
- }
+ codebergURL, githubURL := g.buildProjectLinks(repoName)
// Always extract images from README (not cached)
fmt.Printf("Extracting images from README...\n")
diff --git a/internal/showcase/showcase_test.go b/internal/showcase/showcase_test.go
index f480bdb..fb20c96 100644
--- a/internal/showcase/showcase_test.go
+++ b/internal/showcase/showcase_test.go
@@ -149,3 +149,14 @@ func TestFindReadmeContent_UsesRepoPathWithoutChangingCWD(t *testing.T) {
t.Fatalf("unexpected README content: %q", string(content))
}
}
+
+func TestFallbackSummary_UsesFirstReadmeParagraph(t *testing.T) {
+ t.Parallel()
+
+ readme := []byte("first paragraph\n\nsecond paragraph")
+ summary := fallbackSummary("repo", readme, true)
+
+ if summary != "first paragraph" {
+ t.Fatalf("expected first paragraph summary, got %q", summary)
+ }
+}