summaryrefslogtreecommitdiff
path: root/internal/showcase
diff options
context:
space:
mode:
Diffstat (limited to 'internal/showcase')
-rw-r--r--internal/showcase/ai_context.go338
-rw-r--r--internal/showcase/code_extractor.go228
-rw-r--r--internal/showcase/images.go78
-rw-r--r--internal/showcase/language_detector.go214
-rw-r--r--internal/showcase/metadata.go33
-rw-r--r--internal/showcase/showcase.go311
6 files changed, 636 insertions, 566 deletions
diff --git a/internal/showcase/ai_context.go b/internal/showcase/ai_context.go
index f418894..1c812f8 100644
--- a/internal/showcase/ai_context.go
+++ b/internal/showcase/ai_context.go
@@ -1,184 +1,214 @@
package showcase
import (
- "fmt"
- "io/fs"
- "os"
- "path/filepath"
- "sort"
- "strings"
+ "fmt"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
)
// buildAIInputContext prepares a textual context for AI tools when no README exists.
// It returns a string to be piped to the AI tool's stdin and a boolean indicating
// whether this was sourced from an actual README (true) or synthesized (false).
func buildAIInputContext(repoPath string) (string, bool) {
- // 1) Try to load a README first
- readmeFiles := []string{
- "README.md", "readme.md", "Readme.md",
- "README.MD", "README.txt", "readme.txt",
- "README", "readme",
- }
- for _, f := range readmeFiles {
- p := filepath.Join(repoPath, f)
- if b, err := os.ReadFile(p); err == nil {
- return string(b), true
- }
- }
+ // 1) Try to load a README first
+ readmeFiles := []string{
+ "README.md", "readme.md", "Readme.md",
+ "README.MD", "README.txt", "readme.txt",
+ "README", "readme",
+ }
+ for _, f := range readmeFiles {
+ p := filepath.Join(repoPath, f)
+ if b, err := os.ReadFile(p); err == nil {
+ return string(b), true
+ }
+ }
- // 2) No README: synthesize compact context
- var sb strings.Builder
+ // 2) No README: synthesize compact context
+ var sb strings.Builder
- // File tree (depth-limited)
- sb.WriteString("[CONTEXT]\n")
- sb.WriteString("Repository does not contain a README.\n")
- sb.WriteString("The following is a compact file tree and key manifests/snippets.\n\n")
+ // File tree (depth-limited)
+ sb.WriteString("[CONTEXT]\n")
+ sb.WriteString("Repository does not contain a README.\n")
+ sb.WriteString("The following is a compact file tree and key manifests/snippets.\n\n")
- sb.WriteString("FILE TREE (depth 2):\n")
- tree := listFileTree(repoPath, 2, 200)
- for _, line := range tree {
- sb.WriteString("- ")
- sb.WriteString(line)
- sb.WriteString("\n")
- }
- sb.WriteString("\n")
+ sb.WriteString("FILE TREE (depth 2):\n")
+ tree := listFileTree(repoPath, 2, 200)
+ for _, line := range tree {
+ sb.WriteString("- ")
+ sb.WriteString(line)
+ sb.WriteString("\n")
+ }
+ sb.WriteString("\n")
- // Key manifests we often care about
- manifests := []string{
- "go.mod", "go.sum", "package.json", "Cargo.toml", "Cargo.lock",
- "pyproject.toml", "requirements.txt", "Makefile", "Dockerfile",
- "build.gradle", "pom.xml", "composer.json",
- }
- wroteHeader := false
- for _, m := range manifests {
- p := filepath.Join(repoPath, m)
- if b, err := os.ReadFile(p); err == nil {
- if !wroteHeader {
- sb.WriteString("KEY MANIFESTS:\n")
- wroteHeader = true
- }
- sb.WriteString(fmt.Sprintf("--- %s ---\n", m))
- sb.WriteString(trimTo(string(b), 2000))
- sb.WriteString("\n\n")
- }
- }
+ // Key manifests we often care about
+ manifests := []string{
+ "go.mod", "go.sum", "package.json", "Cargo.toml", "Cargo.lock",
+ "pyproject.toml", "requirements.txt", "Makefile", "Dockerfile",
+ "build.gradle", "pom.xml", "composer.json",
+ }
+ wroteHeader := false
+ for _, m := range manifests {
+ p := filepath.Join(repoPath, m)
+ if b, err := os.ReadFile(p); err == nil {
+ if !wroteHeader {
+ sb.WriteString("KEY MANIFESTS:\n")
+ wroteHeader = true
+ }
+ sb.WriteString(fmt.Sprintf("--- %s ---\n", m))
+ sb.WriteString(trimTo(string(b), 2000))
+ sb.WriteString("\n\n")
+ }
+ }
- // Source hints: capture first main-ish entry file snippets
- // Priority: Go main, Rust main, Node entry, Python main
- candidates := []string{
- "cmd", // Go convention
- "main.go",
- "cmd/main.go",
- "src/main.rs",
- "index.js",
- "src/index.js",
- "main.py",
- "src/main.py",
- }
- wroteSrc := false
- for _, c := range candidates {
- p := filepath.Join(repoPath, c)
- info, err := os.Stat(p)
- if err != nil {
- continue
- }
- if info.IsDir() {
- // collect a few go files under cmd/*/main.go
- if c == "cmd" {
- _ = filepath.WalkDir(p, func(path string, d fs.DirEntry, err error) error {
- if err != nil { return nil }
- if d.IsDir() { return nil }
- base := filepath.Base(path)
- if base == "main.go" {
- if b, e := os.ReadFile(path); e == nil {
- if !wroteSrc { sb.WriteString("PRIMARY SOURCE SNIPPETS:\n"); wroteSrc = true }
- rel, _ := filepath.Rel(repoPath, path)
- sb.WriteString(fmt.Sprintf("--- %s ---\n", rel))
- sb.WriteString(trimTo(string(b), 2000))
- sb.WriteString("\n\n")
- }
- }
- return nil
- })
- }
- continue
- }
- if b, e := os.ReadFile(p); e == nil {
- if !wroteSrc { sb.WriteString("PRIMARY SOURCE SNIPPETS:\n"); wroteSrc = true }
- rel, _ := filepath.Rel(repoPath, p)
- sb.WriteString(fmt.Sprintf("--- %s ---\n", rel))
- sb.WriteString(trimTo(string(b), 2000))
- sb.WriteString("\n\n")
- }
- }
+ // Source hints: capture first main-ish entry file snippets
+ // Priority: Go main, Rust main, Node entry, Python main
+ candidates := []string{
+ "cmd", // Go convention
+ "main.go",
+ "cmd/main.go",
+ "src/main.rs",
+ "index.js",
+ "src/index.js",
+ "main.py",
+ "src/main.py",
+ }
+ wroteSrc := false
+ for _, c := range candidates {
+ p := filepath.Join(repoPath, c)
+ info, err := os.Stat(p)
+ if err != nil {
+ continue
+ }
+ if info.IsDir() {
+ // collect a few go files under cmd/*/main.go
+ if c == "cmd" {
+ _ = filepath.WalkDir(p, func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return nil
+ }
+ if d.IsDir() {
+ return nil
+ }
+ base := filepath.Base(path)
+ if base == "main.go" {
+ if b, e := os.ReadFile(path); e == nil {
+ if !wroteSrc {
+ sb.WriteString("PRIMARY SOURCE SNIPPETS:\n")
+ wroteSrc = true
+ }
+ rel, _ := filepath.Rel(repoPath, path)
+ sb.WriteString(fmt.Sprintf("--- %s ---\n", rel))
+ sb.WriteString(trimTo(string(b), 2000))
+ sb.WriteString("\n\n")
+ }
+ }
+ return nil
+ })
+ }
+ continue
+ }
+ if b, e := os.ReadFile(p); e == nil {
+ if !wroteSrc {
+ sb.WriteString("PRIMARY SOURCE SNIPPETS:\n")
+ wroteSrc = true
+ }
+ rel, _ := filepath.Rel(repoPath, p)
+ sb.WriteString(fmt.Sprintf("--- %s ---\n", rel))
+ sb.WriteString(trimTo(string(b), 2000))
+ sb.WriteString("\n\n")
+ }
+ }
- // Fallback: include a few top-level .go, .rs, .py, .js files if we still have nothing
- if !wroteSrc {
- topFiles := listTopFiles(repoPath, []string{".go", ".rs", ".py", ".js", ".ts", ".tsx"}, 5)
- for _, f := range topFiles {
- if b, e := os.ReadFile(filepath.Join(repoPath, f)); e == nil {
- if !wroteSrc { sb.WriteString("PRIMARY SOURCE SNIPPETS:\n"); wroteSrc = true }
- sb.WriteString(fmt.Sprintf("--- %s ---\n", f))
- sb.WriteString(trimTo(string(b), 2000))
- sb.WriteString("\n\n")
- }
- }
- }
+ // Fallback: include a few top-level .go, .rs, .py, .js files if we still have nothing
+ if !wroteSrc {
+ topFiles := listTopFiles(repoPath, []string{".go", ".rs", ".py", ".js", ".ts", ".tsx"}, 5)
+ for _, f := range topFiles {
+ if b, e := os.ReadFile(filepath.Join(repoPath, f)); e == nil {
+ if !wroteSrc {
+ sb.WriteString("PRIMARY SOURCE SNIPPETS:\n")
+ wroteSrc = true
+ }
+ sb.WriteString(fmt.Sprintf("--- %s ---\n", f))
+ sb.WriteString(trimTo(string(b), 2000))
+ sb.WriteString("\n\n")
+ }
+ }
+ }
- // Instruction to the model
- sb.WriteString("[TASK]\n")
- sb.WriteString("Summarize this project in 1–2 paragraphs: what it does, why it's useful, and how it's implemented. Mention notable tech choices. Be concise and informative.\n")
+ // Instruction to the model
+ sb.WriteString("[TASK]\n")
+ sb.WriteString("Summarize this project in 1–2 paragraphs: what it does, why it's useful, and how it's implemented. Mention notable tech choices. Be concise and informative.\n")
- return sb.String(), false
+ return sb.String(), false
}
// listFileTree returns a sorted list of relative paths up to a given depth and limit.
func listFileTree(root string, maxDepth int, maxEntries int) []string {
- var entries []string
- var count int
- _ = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
- if err != nil { return nil }
- if path == root { return nil }
- rel, e := filepath.Rel(root, path)
- if e != nil { return nil }
- // depth check
- depth := 1 + strings.Count(rel, string(os.PathSeparator))
- if depth > maxDepth { return fs.SkipDir }
- entries = append(entries, rel)
- count++
- if count >= maxEntries { return fs.SkipDir }
- return nil
- })
- sort.Strings(entries)
- if len(entries) > maxEntries {
- entries = entries[:maxEntries]
- }
- return entries
+ var entries []string
+ var count int
+ _ = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return nil
+ }
+ if path == root {
+ return nil
+ }
+ rel, e := filepath.Rel(root, path)
+ if e != nil {
+ return nil
+ }
+ // depth check
+ depth := 1 + strings.Count(rel, string(os.PathSeparator))
+ if depth > maxDepth {
+ return fs.SkipDir
+ }
+ entries = append(entries, rel)
+ count++
+ if count >= maxEntries {
+ return fs.SkipDir
+ }
+ return nil
+ })
+ sort.Strings(entries)
+ if len(entries) > maxEntries {
+ entries = entries[:maxEntries]
+ }
+ return entries
}
// listTopFiles lists top-level files with certain extensions up to a limit.
func listTopFiles(root string, exts []string, limit int) []string {
- dir, err := os.ReadDir(root)
- if err != nil { return nil }
- var out []string
- for _, e := range dir {
- if e.IsDir() { continue }
- name := e.Name()
- for _, x := range exts {
- if strings.HasSuffix(strings.ToLower(name), strings.ToLower(x)) {
- out = append(out, name)
- break
- }
- }
- if len(out) >= limit { break }
- }
- sort.Strings(out)
- return out
+ dir, err := os.ReadDir(root)
+ if err != nil {
+ return nil
+ }
+ var out []string
+ for _, e := range dir {
+ if e.IsDir() {
+ continue
+ }
+ name := e.Name()
+ for _, x := range exts {
+ if strings.HasSuffix(strings.ToLower(name), strings.ToLower(x)) {
+ out = append(out, name)
+ break
+ }
+ }
+ if len(out) >= limit {
+ break
+ }
+ }
+ sort.Strings(out)
+ return out
}
// trimTo soft-limits content length for inclusion in AI context.
func trimTo(s string, max int) string {
- if len(s) <= max { return s }
- return s[:max] + "\n... [truncated]"
+ if len(s) <= max {
+ return s
+ }
+ return s[:max] + "\n... [truncated]"
}
-
diff --git a/internal/showcase/code_extractor.go b/internal/showcase/code_extractor.go
index 91a0a78..fbf17f6 100644
--- a/internal/showcase/code_extractor.go
+++ b/internal/showcase/code_extractor.go
@@ -22,34 +22,34 @@ func extractCodeSnippet(repoPath string, languages []LanguageStats) (string, str
// Get the primary language (highest percentage)
primaryLang := languages[0].Name
-
+
// Define file extensions for each language
langExtensions := map[string][]string{
- "Go": {".go"},
- "Python": {".py"},
- "JavaScript": {".js"},
- "TypeScript": {".ts"},
- "Java": {".java"},
- "C": {".c", ".h"},
- "C++": {".cpp", ".cc", ".cxx", ".hpp"},
- "C/C++": {".h"},
- "C#": {".cs"},
- "Ruby": {".rb"},
- "PHP": {".php"},
- "Swift": {".swift"},
- "Kotlin": {".kt"},
- "Rust": {".rs"},
- "Shell": {".sh", ".bash"},
- "Perl": {".pl", ".pm"},
- "Raku": {".raku", ".rakumod", ".p6", ".pm6"},
- "Haskell": {".hs"},
- "Lua": {".lua"},
- "HTML": {".html", ".htm"},
- "CSS": {".css"},
- "SQL": {".sql"},
- "Make": {"Makefile", "makefile", "GNUmakefile"},
- "HCL": {".tf", ".tfvars", ".hcl"},
- "AWK": {".awk", ".cgi"}, // .cgi files can be AWK scripts
+ "Go": {".go"},
+ "Python": {".py"},
+ "JavaScript": {".js"},
+ "TypeScript": {".ts"},
+ "Java": {".java"},
+ "C": {".c", ".h"},
+ "C++": {".cpp", ".cc", ".cxx", ".hpp"},
+ "C/C++": {".h"},
+ "C#": {".cs"},
+ "Ruby": {".rb"},
+ "PHP": {".php"},
+ "Swift": {".swift"},
+ "Kotlin": {".kt"},
+ "Rust": {".rs"},
+ "Shell": {".sh", ".bash"},
+ "Perl": {".pl", ".pm"},
+ "Raku": {".raku", ".rakumod", ".p6", ".pm6"},
+ "Haskell": {".hs"},
+ "Lua": {".lua"},
+ "HTML": {".html", ".htm"},
+ "CSS": {".css"},
+ "SQL": {".sql"},
+ "Make": {"Makefile", "makefile", "GNUmakefile"},
+ "HCL": {".tf", ".tfvars", ".hcl"},
+ "AWK": {".awk", ".cgi"}, // .cgi files can be AWK scripts
}
// Get file extensions for the primary language
@@ -79,13 +79,13 @@ func extractCodeSnippet(repoPath string, languages []LanguageStats) (string, str
if info.IsDir() {
name := info.Name()
// Skip hidden directories and common non-code directories
- if strings.HasPrefix(name, ".") && name != "." ||
- name == "node_modules" ||
- name == "vendor" ||
- name == "target" ||
- name == "dist" ||
- name == "build" ||
- name == "__pycache__" {
+ if strings.HasPrefix(name, ".") && name != "." ||
+ name == "node_modules" ||
+ name == "vendor" ||
+ name == "target" ||
+ name == "dist" ||
+ name == "build" ||
+ name == "__pycache__" {
return filepath.SkipDir
}
return nil
@@ -99,7 +99,7 @@ func extractCodeSnippet(repoPath string, languages []LanguageStats) (string, str
// Check if file matches extensions
basename := filepath.Base(path)
ext := filepath.Ext(path)
-
+
matched := false
for _, validExt := range extensions {
if validExt == basename || (strings.HasPrefix(validExt, ".") && ext == validExt) {
@@ -107,7 +107,7 @@ func extractCodeSnippet(repoPath string, languages []LanguageStats) (string, str
break
}
}
-
+
// For executable files, also check shebang if primary language is AWK and file has .cgi extension
if !matched && primaryLang == "AWK" && ext == ".cgi" && info.Mode()&0111 != 0 {
if file, err := os.Open(path); err == nil {
@@ -121,14 +121,14 @@ func extractCodeSnippet(repoPath string, languages []LanguageStats) (string, str
file.Close()
}
}
-
+
if matched {
// Skip test files and generated files
- if !strings.Contains(basename, "_test") &&
- !strings.Contains(basename, ".test.") &&
- !strings.Contains(basename, ".min.") &&
- !strings.Contains(path, "/test/") &&
- !strings.Contains(path, "/tests/") {
+ if !strings.Contains(basename, "_test") &&
+ !strings.Contains(basename, ".test.") &&
+ !strings.Contains(basename, ".min.") &&
+ !strings.Contains(path, "/test/") &&
+ !strings.Contains(path, "/tests/") {
codeFiles = append(codeFiles, path)
}
}
@@ -148,10 +148,10 @@ func extractCodeSnippet(repoPath string, languages []LanguageStats) (string, str
rand.Shuffle(len(codeFiles), func(i, j int) {
codeFiles[i], codeFiles[j] = codeFiles[j], codeFiles[i]
})
-
+
var snippet string
var selectedFile string
-
+
// Try up to 5 files to find a good snippet
for i := 0; i < len(codeFiles) && i < 5; i++ {
candidateFile := codeFiles[i]
@@ -159,28 +159,28 @@ func extractCodeSnippet(repoPath string, languages []LanguageStats) (string, str
if err != nil {
continue
}
-
+
// Check if this snippet has acceptable line lengths
if hasAcceptableLineLength(candidateSnippet, 80) {
snippet = candidateSnippet
selectedFile = candidateFile
break
}
-
+
// Keep the first valid snippet as fallback
if snippet == "" {
snippet = candidateSnippet
selectedFile = candidateFile
}
}
-
+
if snippet == "" {
return "", "", fmt.Errorf("no valid code snippets found")
}
// Get relative path for display
relPath, _ := filepath.Rel(repoPath, selectedFile)
-
+
return snippet, fmt.Sprintf("%s from `%s`", primaryLang, relPath), nil
}
@@ -236,9 +236,9 @@ func extractSnippetFromFile(filePath string, minLines, maxLines int) (string, er
skipLines := 0
for i, line := range lines {
trimmed := strings.TrimSpace(line)
- if trimmed != "" && !strings.HasPrefix(trimmed, "import") &&
- !strings.HasPrefix(trimmed, "package") && !strings.HasPrefix(trimmed, "using") &&
- !strings.HasPrefix(trimmed, "#include") && !strings.HasPrefix(trimmed, "from") {
+ if trimmed != "" && !strings.HasPrefix(trimmed, "import") &&
+ !strings.HasPrefix(trimmed, "package") && !strings.HasPrefix(trimmed, "using") &&
+ !strings.HasPrefix(trimmed, "#include") && !strings.HasPrefix(trimmed, "from") {
skipLines = i
break
}
@@ -260,19 +260,19 @@ func findSmallestCompleteFunction(lines []string) string {
end int
size int
}
-
+
var functions []functionInfo
-
+
// Keywords that typically start functions/methods
functionKeywords := []string{
"func ", "function ", "def ", "public ", "private ", "protected ",
"static ", "async ", "procedure ", "sub ", "method ",
}
-
+
// Find all complete functions
for i := 0; i < len(lines); i++ {
line := strings.TrimSpace(lines[i])
-
+
// Check if this line starts a function
isFunction := false
for _, keyword := range functionKeywords {
@@ -281,11 +281,11 @@ func findSmallestCompleteFunction(lines []string) string {
break
}
}
-
+
if !isFunction {
continue
}
-
+
// Try to find the end of this function
functionEnd := findFunctionEnd(lines, i)
if functionEnd > i {
@@ -300,7 +300,7 @@ func findSmallestCompleteFunction(lines []string) string {
}
}
}
-
+
// Find the smallest function with acceptable line lengths
if len(functions) > 0 {
// First try to find a function with all lines <= 80 chars
@@ -310,7 +310,7 @@ func findSmallestCompleteFunction(lines []string) string {
return snippet
}
}
-
+
// If none found, return the smallest function (will be broken later)
smallest := functions[0]
for _, f := range functions[1:] {
@@ -320,7 +320,7 @@ func findSmallestCompleteFunction(lines []string) string {
}
return strings.Join(lines[smallest.start:smallest.end+1], "\n")
}
-
+
return ""
}
@@ -329,11 +329,11 @@ func findFunctionEnd(lines []string, start int) int {
if start >= len(lines) {
return -1
}
-
+
// For brace-based languages
braceCount := 0
inFunction := false
-
+
// For Python - track initial indentation
isPython := strings.Contains(lines[start], "def ") || strings.Contains(lines[start], "class ")
var initialIndent int
@@ -346,11 +346,11 @@ func findFunctionEnd(lines []string, start int) int {
}
}
}
-
+
for i := start; i < len(lines); i++ {
line := lines[i]
trimmed := strings.TrimSpace(line)
-
+
// Handle Python indentation
if isPython && i > start {
if trimmed == "" {
@@ -361,7 +361,7 @@ func findFunctionEnd(lines []string, start int) int {
return i - 1
}
}
-
+
// Handle brace-based languages
for _, ch := range line {
if ch == '{' {
@@ -375,12 +375,12 @@ func findFunctionEnd(lines []string, start int) int {
}
}
}
-
+
// If we're in Python and reached the end, return the last line
if isPython {
return len(lines) - 1
}
-
+
return -1
}
@@ -391,11 +391,11 @@ func findCompleteFunctionOrMethod(lines []string, minLines, maxLines int) (int,
"func ", "function ", "def ", "public ", "private ", "protected ",
"static ", "async ", "procedure ", "sub ", "method ",
}
-
+
// Try to find a function that fits within our size constraints
for i := 0; i < len(lines); i++ {
line := strings.TrimSpace(lines[i])
-
+
// Check if this line starts a function
isFunction := false
for _, keyword := range functionKeywords {
@@ -404,11 +404,11 @@ func findCompleteFunctionOrMethod(lines []string, minLines, maxLines int) (int,
break
}
}
-
+
if !isFunction {
continue
}
-
+
// Try to find the end of this function
functionEnd := findFunctionEnd(lines, i)
if functionEnd > i {
@@ -418,7 +418,7 @@ func findCompleteFunctionOrMethod(lines []string, minLines, maxLines int) (int,
}
}
}
-
+
return -1, -1
}
@@ -435,7 +435,7 @@ func findInterestingStart(lines []string, snippetSize int) int {
line := strings.TrimSpace(lines[i])
// Skip empty lines and comments
if line == "" || strings.HasPrefix(line, "//") || strings.HasPrefix(line, "#") ||
- strings.HasPrefix(line, "/*") || strings.HasPrefix(line, "*") {
+ strings.HasPrefix(line, "/*") || strings.HasPrefix(line, "*") {
continue
}
@@ -457,10 +457,10 @@ func stripComments(code string) string {
lines := strings.Split(code, "\n")
var result []string
inMultilineComment := false
-
+
for _, line := range lines {
trimmed := strings.TrimSpace(line)
-
+
// Handle multi-line comments for C-style languages
if strings.Contains(line, "/*") {
inMultilineComment = true
@@ -475,19 +475,19 @@ func stripComments(code string) string {
continue
}
}
-
+
if inMultilineComment {
if strings.Contains(line, "*/") {
inMultilineComment = false
}
continue
}
-
+
// Skip single-line comments
if trimmed == "" {
// Keep empty lines for readability
result = append(result, line)
- } else if strings.HasPrefix(trimmed, "//") ||
+ } else if strings.HasPrefix(trimmed, "//") ||
strings.HasPrefix(trimmed, "#") && !strings.HasPrefix(trimmed, "#include") && !strings.HasPrefix(trimmed, "#define") ||
strings.HasPrefix(trimmed, "<!--") ||
strings.HasPrefix(trimmed, "*") && len(trimmed) > 1 && trimmed[1] == ' ' {
@@ -509,7 +509,7 @@ func stripComments(code string) string {
result = append(result, line)
}
}
-
+
// Remove leading and trailing empty lines
for len(result) > 0 && strings.TrimSpace(result[0]) == "" {
result = result[1:]
@@ -517,13 +517,13 @@ func stripComments(code string) string {
for len(result) > 0 && strings.TrimSpace(result[len(result)-1]) == "" {
result = result[:len(result)-1]
}
-
+
// Remove unnecessary indentation
result = removeCommonIndentation(result)
-
+
// Break long lines
result = breakLongLines(result, 80)
-
+
return strings.Join(result, "\n")
}
@@ -532,11 +532,11 @@ func removeCommonIndentation(lines []string) []string {
if len(lines) == 0 {
return lines
}
-
+
// Find the common prefix of whitespace
var commonPrefix string
firstNonEmpty := -1
-
+
// Find first non-empty line to use as reference
for i, line := range lines {
if strings.TrimSpace(line) != "" {
@@ -544,11 +544,11 @@ func removeCommonIndentation(lines []string) []string {
break
}
}
-
+
if firstNonEmpty == -1 {
return lines
}
-
+
// Get the whitespace prefix of the first non-empty line
firstLine := lines[firstNonEmpty]
for i, ch := range firstLine {
@@ -557,18 +557,18 @@ func removeCommonIndentation(lines []string) []string {
break
}
}
-
+
// If the first line has no indentation, return as-is
if commonPrefix == "" {
return lines
}
-
+
// Find the actual common prefix among all non-empty lines
for _, line := range lines {
if strings.TrimSpace(line) == "" {
continue
}
-
+
// Reduce commonPrefix to what this line shares
for i := 0; i < len(commonPrefix); i++ {
if i >= len(line) || line[i] != commonPrefix[i] {
@@ -576,17 +576,17 @@ func removeCommonIndentation(lines []string) []string {
break
}
}
-
+
if commonPrefix == "" {
break
}
}
-
+
// If no common prefix found, return as-is
if commonPrefix == "" {
return lines
}
-
+
// Remove common prefix from all lines
result := make([]string, len(lines))
prefixLen := len(commonPrefix)
@@ -599,7 +599,7 @@ func removeCommonIndentation(lines []string) []string {
result[i] = line
}
}
-
+
return result
}
@@ -617,18 +617,18 @@ func hasAcceptableLineLength(snippet string, maxLength int) bool {
// breakLongLines breaks lines that exceed maxLength at appropriate points
func breakLongLines(lines []string, maxLength int) []string {
var result []string
-
+
for _, line := range lines {
if len(line) <= maxLength {
result = append(result, line)
continue
}
-
+
// Try to break the line intelligently
broken := breakLine(line, maxLength)
result = append(result, broken...)
}
-
+
return result
}
@@ -638,7 +638,7 @@ func breakLine(line string, maxLength int) []string {
if len(line) <= maxLength {
return []string{line}
}
-
+
// Get the indentation of the original line
indent := ""
for _, ch := range line {
@@ -648,43 +648,43 @@ func breakLine(line string, maxLength int) []string {
break
}
}
-
+
// Common break points in order of preference
breakPoints := []string{
- ", ", // After comma
- " && ", // Before logical operators
+ ", ", // After comma
+ " && ", // Before logical operators
" || ",
- " + ", // Before arithmetic operators
+ " + ", // Before arithmetic operators
" - ",
" * ",
" / ",
- " = ", // Before assignment
+ " = ", // Before assignment
" := ",
- " == ", // Before comparison
+ " == ", // Before comparison
" != ",
" < ",
" > ",
" <= ",
" >= ",
- "(", // After opening parenthesis
- " ", // Any space
+ "(", // After opening parenthesis
+ " ", // Any space
}
-
+
var result []string
remaining := line
isFirstLine := true
-
+
for len(remaining) > maxLength {
// Find the best break point
bestBreak := -1
-
+
for _, breakPoint := range breakPoints {
// Look for break point before maxLength
searchIn := remaining
if len(searchIn) > maxLength {
searchIn = remaining[:maxLength]
}
-
+
idx := strings.LastIndex(searchIn, breakPoint)
if idx > 0 && idx < maxLength {
// For some break points, we want to break after them
@@ -696,12 +696,12 @@ func breakLine(line string, maxLength int) []string {
}
}
}
-
+
// If no good break point found, break at maxLength
if bestBreak == -1 {
bestBreak = maxLength
}
-
+
// Add the line
lineToAdd := remaining[:bestBreak]
if !isFirstLine && !strings.HasPrefix(strings.TrimSpace(lineToAdd), "//") {
@@ -709,7 +709,7 @@ func breakLine(line string, maxLength int) []string {
lineToAdd = indent + " " + strings.TrimLeft(lineToAdd, " \t")
}
result = append(result, strings.TrimRight(lineToAdd, " "))
-
+
// Update remaining
remaining = remaining[bestBreak:]
if !isFirstLine && !strings.HasPrefix(strings.TrimSpace(remaining), "//") {
@@ -717,7 +717,7 @@ func breakLine(line string, maxLength int) []string {
}
isFirstLine = false
}
-
+
// Add the last part
if len(remaining) > 0 {
if !isFirstLine && !strings.HasPrefix(strings.TrimSpace(remaining), "//") {
@@ -725,6 +725,6 @@ func breakLine(line string, maxLength int) []string {
}
result = append(result, remaining)
}
-
+
return result
-} \ No newline at end of file
+}
diff --git a/internal/showcase/images.go b/internal/showcase/images.go
index 66372cb..b6fe6d7 100644
--- a/internal/showcase/images.go
+++ b/internal/showcase/images.go
@@ -15,7 +15,7 @@ func extractImagesFromRepo(repoPath, repoName, showcaseDir string) ([]string, er
// Look for README files
readmeFiles := []string{"README.md", "readme.md", "Readme.md", "README.MD"}
var readmePath string
-
+
for _, filename := range readmeFiles {
path := filepath.Join(repoPath, filename)
if _, err := os.Stat(path); err == nil {
@@ -23,30 +23,30 @@ func extractImagesFromRepo(repoPath, repoName, showcaseDir string) ([]string, er
break
}
}
-
+
if readmePath == "" {
return nil, nil // No README found, not an error
}
-
+
// Read README content
content, err := os.ReadFile(readmePath)
if err != nil {
return nil, fmt.Errorf("failed to read README: %w", err)
}
-
+
fmt.Printf("Found README at: %s\n", readmePath)
-
+
// Extract image references
images := extractImageReferences(string(content))
fmt.Printf("Found %d images in README\n", len(images))
for i, img := range images {
fmt.Printf(" Image %d: %s\n", i+1, img)
}
-
+
if len(images) == 0 {
return nil, nil
}
-
+
// Limit to first and last image (max 2)
var selectedImages []string
if len(images) == 1 {
@@ -54,19 +54,19 @@ func extractImagesFromRepo(repoPath, repoName, showcaseDir string) ([]string, er
} else {
selectedImages = []string{images[0], images[len(images)-1]}
}
-
+
// Create showcase subdirectory for this repo
repoShowcaseDir := filepath.Join(showcaseDir, "showcase", repoName)
if err := os.MkdirAll(repoShowcaseDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create showcase directory: %w", err)
}
-
+
// Copy images and collect relative paths
var copiedImages []string
for i, imgPath := range selectedImages {
var destFilename string
var err error
-
+
if strings.HasPrefix(imgPath, "http://") || strings.HasPrefix(imgPath, "https://") {
// Handle URL - download the image
// Extract extension from URL, handling query parameters
@@ -78,7 +78,7 @@ func extractImagesFromRepo(repoPath, repoName, showcaseDir string) ([]string, er
}
destFilename = fmt.Sprintf("image-%d%s", i+1, ext)
destPath := filepath.Join(repoShowcaseDir, destFilename)
-
+
if err = downloadImage(imgPath, destPath); err != nil {
fmt.Printf("Warning: Failed to download image %s: %v\n", imgPath, err)
continue
@@ -89,31 +89,31 @@ func extractImagesFromRepo(repoPath, repoName, showcaseDir string) ([]string, er
if !filepath.IsAbs(imgPath) {
srcPath = filepath.Join(repoPath, imgPath)
}
-
+
// Check if image exists
if _, err := os.Stat(srcPath); err != nil {
fmt.Printf("Warning: Image not found: %s\n", srcPath)
continue
}
-
+
// Generate destination filename
ext := filepath.Ext(srcPath)
destFilename = fmt.Sprintf("image-%d%s", i+1, ext)
destPath := filepath.Join(repoShowcaseDir, destFilename)
-
+
// Copy image
if err := copyFile(srcPath, destPath); err != nil {
fmt.Printf("Warning: Failed to copy image %s: %v\n", srcPath, err)
continue
}
}
-
+
// Store relative path from showcase directory
relativePath := filepath.Join("showcase", repoName, destFilename)
copiedImages = append(copiedImages, relativePath)
fmt.Printf("Copied/Downloaded image: %s -> %s\n", imgPath, relativePath)
}
-
+
return copiedImages, nil
}
@@ -121,24 +121,24 @@ func extractImagesFromRepo(repoPath, repoName, showcaseDir string) ([]string, er
func extractImageReferences(content string) []string {
var images []string
seen := make(map[string]bool)
-
+
// Regex patterns for markdown images
patterns := []string{
- `!\[([^\]]*)\]\(([^)]+)\)`, // ![alt](url)
- `<img[^>]+src=["']([^"']+)["'][^>]*>`, // <img src="url"> with quotes
- `<img[^>]+src=([^\s>]+)[^>]*>`, // <img src=url> without quotes
- `!\[([^\]]*)\]\[([^\]]+)\]`, // ![alt][ref]
- `\[([^\]]+)\]:\s*(.+?)(?:\s+"[^"]+")?\s*$`, // [ref]: url "title"
+ `!\[([^\]]*)\]\(([^)]+)\)`, // ![alt](url)
+ `<img[^>]+src=["']([^"']+)["'][^>]*>`, // <img src="url"> with quotes
+ `<img[^>]+src=([^\s>]+)[^>]*>`, // <img src=url> without quotes
+ `!\[([^\]]*)\]\[([^\]]+)\]`, // ![alt][ref]
+ `\[([^\]]+)\]:\s*(.+?)(?:\s+"[^"]+")?\s*$`, // [ref]: url "title"
}
-
+
fmt.Printf("DEBUG: Content length: %d bytes\n", len(content))
-
+
// Extract from markdown image syntax
for i, pattern := range patterns[:3] { // First three patterns have URLs in different positions
re := regexp.MustCompile(pattern)
matches := re.FindAllStringSubmatch(content, -1)
fmt.Printf("DEBUG: Pattern %d (%s) found %d matches\n", i, pattern, len(matches))
-
+
for _, match := range matches {
var url string
if pattern == patterns[0] {
@@ -146,18 +146,18 @@ func extractImageReferences(content string) []string {
} else {
url = match[1] // For <img src="url"> (both with and without quotes)
}
-
+
// Clean and validate URL
url = strings.TrimSpace(url)
-
+
// Handle markdown image titles - remove anything after a space or quote
if idx := strings.IndexAny(url, " \"'"); idx != -1 {
url = url[:idx]
}
url = strings.TrimSpace(url)
-
+
fmt.Printf("DEBUG: Found potential image URL: %s\n", url)
-
+
if isImageFile(url) {
fmt.Printf("DEBUG: URL is image file\n")
if !seen[url] {
@@ -181,7 +181,7 @@ func extractImageReferences(content string) []string {
}
}
}
-
+
// Handle reference-style images
refPattern := regexp.MustCompile(patterns[4])
refMatches := refPattern.FindAllStringSubmatch(content, -1)
@@ -189,7 +189,7 @@ func extractImageReferences(content string) []string {
for _, match := range refMatches {
refs[match[1]] = strings.TrimSpace(match[2])
}
-
+
// Find reference-style image uses
refUsePattern := regexp.MustCompile(patterns[3])
refUseMatches := refUsePattern.FindAllStringSubmatch(content, -1)
@@ -202,7 +202,7 @@ func extractImageReferences(content string) []string {
}
}
}
-
+
return images
}
@@ -220,7 +220,7 @@ func isImageFile(url string) bool {
// isGitHostedImage checks if URL is from GitHub/Codeberg
func isGitHostedImage(url string) bool {
- return strings.Contains(url, "github.com") ||
+ return strings.Contains(url, "github.com") ||
strings.Contains(url, "githubusercontent.com") ||
strings.Contains(url, "codeberg.org") ||
strings.Contains(url, "codeberg.page")
@@ -233,18 +233,18 @@ func copyFile(src, dst string) error {
return err
}
defer sourceFile.Close()
-
+
destFile, err := os.Create(dst)
if err != nil {
return err
}
defer destFile.Close()
-
+
_, err = io.Copy(destFile, sourceFile)
if err != nil {
return err
}
-
+
return destFile.Sync()
}
@@ -256,11 +256,11 @@ func downloadImage(url, dst string) error {
if err != nil {
return fmt.Errorf("curl failed: %v, output: %s", err, string(output))
}
-
+
// Verify the file was created
if _, err := os.Stat(dst); err != nil {
return fmt.Errorf("downloaded file not found: %v", err)
}
-
+
return nil
-} \ No newline at end of file
+}
diff --git a/internal/showcase/language_detector.go b/internal/showcase/language_detector.go
index 0f356b8..692f048 100644
--- a/internal/showcase/language_detector.go
+++ b/internal/showcase/language_detector.go
@@ -14,111 +14,111 @@ import (
func detectLanguages(repoPath string) (languages []LanguageStats, documentation []LanguageStats, err error) {
languageLines := make(map[string]int)
documentationLines := make(map[string]int)
-
+
// Define common language extensions
langExtensions := map[string]string{
- ".go": "Go",
- ".py": "Python",
- ".js": "JavaScript",
- ".ts": "TypeScript",
- ".java": "Java",
- ".c": "C",
- ".cpp": "C++",
- ".cc": "C++",
- ".cxx": "C++",
- ".h": "C/C++",
- ".hpp": "C++",
- ".hxx": "C++",
- ".cs": "C#",
- ".rb": "Ruby",
- ".php": "PHP",
- ".swift": "Swift",
- ".kt": "Kotlin",
- ".rs": "Rust",
- ".scala": "Scala",
- ".r": "R",
- ".m": "Objective-C",
- ".mm": "Objective-C++",
- ".sh": "Shell",
- ".bash": "Shell",
- ".zsh": "Shell",
- ".fish": "Shell",
- ".pl": "Perl",
- ".pm": "Perl",
- ".raku": "Raku",
- ".rakumod": "Raku",
- ".rakudoc": "Raku",
+ ".go": "Go",
+ ".py": "Python",
+ ".js": "JavaScript",
+ ".ts": "TypeScript",
+ ".java": "Java",
+ ".c": "C",
+ ".cpp": "C++",
+ ".cc": "C++",
+ ".cxx": "C++",
+ ".h": "C/C++",
+ ".hpp": "C++",
+ ".hxx": "C++",
+ ".cs": "C#",
+ ".rb": "Ruby",
+ ".php": "PHP",
+ ".swift": "Swift",
+ ".kt": "Kotlin",
+ ".rs": "Rust",
+ ".scala": "Scala",
+ ".r": "R",
+ ".m": "Objective-C",
+ ".mm": "Objective-C++",
+ ".sh": "Shell",
+ ".bash": "Shell",
+ ".zsh": "Shell",
+ ".fish": "Shell",
+ ".pl": "Perl",
+ ".pm": "Perl",
+ ".raku": "Raku",
+ ".rakumod": "Raku",
+ ".rakudoc": "Raku",
".rakutest": "Raku",
- ".p6": "Raku",
- ".pm6": "Raku",
- ".lua": "Lua",
- ".vim": "Vim Script",
- ".el": "Emacs Lisp",
- ".clj": "Clojure",
- ".hs": "Haskell",
- ".ml": "OCaml",
- ".ex": "Elixir",
- ".exs": "Elixir",
- ".dart": "Dart",
- ".jl": "Julia",
- ".nim": "Nim",
- ".v": "V",
- ".zig": "Zig",
- ".html": "HTML",
- ".htm": "HTML",
- ".css": "CSS",
- ".scss": "SCSS",
- ".sass": "Sass",
- ".less": "Less",
- ".xml": "XML",
- ".json": "JSON",
- ".yaml": "YAML",
- ".yml": "YAML",
- ".toml": "TOML",
- ".ini": "INI",
- ".cfg": "Config",
- ".conf": "Config",
- ".sql": "SQL",
- ".tf": "HCL",
- ".tfvars": "HCL",
- ".hcl": "HCL",
- ".awk": "AWK",
+ ".p6": "Raku",
+ ".pm6": "Raku",
+ ".lua": "Lua",
+ ".vim": "Vim Script",
+ ".el": "Emacs Lisp",
+ ".clj": "Clojure",
+ ".hs": "Haskell",
+ ".ml": "OCaml",
+ ".ex": "Elixir",
+ ".exs": "Elixir",
+ ".dart": "Dart",
+ ".jl": "Julia",
+ ".nim": "Nim",
+ ".v": "V",
+ ".zig": "Zig",
+ ".html": "HTML",
+ ".htm": "HTML",
+ ".css": "CSS",
+ ".scss": "SCSS",
+ ".sass": "Sass",
+ ".less": "Less",
+ ".xml": "XML",
+ ".json": "JSON",
+ ".yaml": "YAML",
+ ".yml": "YAML",
+ ".toml": "TOML",
+ ".ini": "INI",
+ ".cfg": "Config",
+ ".conf": "Config",
+ ".sql": "SQL",
+ ".tf": "HCL",
+ ".tfvars": "HCL",
+ ".hcl": "HCL",
+ ".awk": "AWK",
}
-
+
// Define documentation/text extensions
docExtensions := map[string]string{
- ".md": "Markdown",
- ".rst": "reStructuredText",
- ".tex": "LaTeX",
- ".txt": "Text",
- ".adoc": "AsciiDoc",
- ".org": "Org",
+ ".md": "Markdown",
+ ".rst": "reStructuredText",
+ ".tex": "LaTeX",
+ ".txt": "Text",
+ ".adoc": "AsciiDoc",
+ ".org": "Org",
}
// Special files that indicate specific languages
specialFiles := map[string]string{
- "makefile": "Make",
- "gnumakefile": "Make",
- "dockerfile": "Docker",
- "dockerfile.*": "Docker",
- "cmakelists.txt": "CMake",
- "rakefile": "Ruby",
- "gemfile": "Ruby",
- "package.json": "JavaScript",
- "cargo.toml": "Rust",
- "go.mod": "Go",
- "go.sum": "Go",
- "pom.xml": "Java",
- "build.gradle": "Gradle",
- "build.gradle.kts": "Kotlin",
- "requirements.txt": "Python",
- "setup.py": "Python",
- "pyproject.toml": "Python",
- "composer.json": "PHP",
- "*.dockerfile": "Docker",
- "containerfile": "Docker",
- "jenkinsfile": "Groovy",
- "vagrantfile": "Ruby",
+ "makefile": "Make",
+ "gnumakefile": "Make",
+ "dockerfile": "Docker",
+ "dockerfile.*": "Docker",
+ "cmakelists.txt": "CMake",
+ "rakefile": "Ruby",
+ "gemfile": "Ruby",
+ "package.json": "JavaScript",
+ "cargo.toml": "Rust",
+ "go.mod": "Go",
+ "go.sum": "Go",
+ "pom.xml": "Java",
+ "build.gradle": "Gradle",
+ "build.gradle.kts": "Kotlin",
+ "requirements.txt": "Python",
+ "setup.py": "Python",
+ "pyproject.toml": "Python",
+ "composer.json": "PHP",
+ "*.dockerfile": "Docker",
+ "containerfile": "Docker",
+ "jenkinsfile": "Groovy",
+ "vagrantfile": "Ruby",
}
// Count lines for each language
@@ -131,15 +131,15 @@ func detectLanguages(repoPath string) (languages []LanguageStats, documentation
if info.IsDir() {
name := info.Name()
// Skip hidden directories and common non-code directories
- if strings.HasPrefix(name, ".") && name != "." ||
- name == "node_modules" ||
- name == "vendor" ||
- name == "target" ||
- name == "dist" ||
- name == "build" ||
- name == "out" ||
- name == "__pycache__" ||
- name == "coverage" {
+ if strings.HasPrefix(name, ".") && name != "." ||
+ name == "node_modules" ||
+ name == "vendor" ||
+ name == "target" ||
+ name == "dist" ||
+ name == "build" ||
+ name == "out" ||
+ name == "__pycache__" ||
+ name == "coverage" {
return filepath.SkipDir
}
return nil
@@ -157,7 +157,7 @@ func detectLanguages(repoPath string) (languages []LanguageStats, documentation
// Determine the language or documentation type
var language string
var isDoc bool
-
+
// Check special files first
if lang, ok := specialFiles[basename]; ok {
language = lang
@@ -204,7 +204,7 @@ func detectLanguages(repoPath string) (languages []LanguageStats, documentation
file.Close()
}
}
-
+
// If we identified a language, count its lines
if language != "" {
lines, err := countFileLines(path)
@@ -317,4 +317,4 @@ func FormatLanguagesWithPercentages(languages []LanguageStats) string {
}
return strings.Join(parts, ", ")
-} \ No newline at end of file
+}
diff --git a/internal/showcase/metadata.go b/internal/showcase/metadata.go
index ca8af05..147e714 100644
--- a/internal/showcase/metadata.go
+++ b/internal/showcase/metadata.go
@@ -22,8 +22,8 @@ type RepoMetadata struct {
Languages []LanguageStats // Programming languages with usage statistics
Documentation []LanguageStats // Documentation/text files with usage statistics
CommitCount int
- LinesOfCode int // Lines of code (excluding documentation)
- LinesOfDocs int // Lines of documentation
+ LinesOfCode int // Lines of code (excluding documentation)
+ LinesOfDocs int // Lines of documentation
FirstCommitDate string
LastCommitDate string
License string
@@ -58,7 +58,7 @@ func extractRepoMetadata(repoPath string) (*RepoMetadata, error) {
loc += lang.Lines
}
metadata.LinesOfCode = loc
-
+
locDocs := 0
for _, doc := range metadata.Documentation {
locDocs += doc.Lines
@@ -101,7 +101,6 @@ func extractRepoMetadata(repoPath string) (*RepoMetadata, error) {
return metadata, nil
}
-
// getCommitCount returns the total number of commits
func getCommitCount(repoPath string) (int, error) {
cmd := exec.Command("git", "-C", repoPath, "rev-list", "--all", "--count")
@@ -126,7 +125,7 @@ func countLinesOfCode(repoPath string) (int, error) {
`cd "%s" && git ls-files | grep -E '\.(go|py|js|ts|java|c|cpp|h|hpp|cs|rb|php|swift|kt|rs|scala|r|sh|bash|zsh|pl|lua|vim|el|clj|hs|ml|ex|exs|dart|jl|nim|v|zig|html|css|scss|sass|json|xml|yaml|yml|toml|ini|conf|cfg)$' | xargs wc -l 2>/dev/null | tail -n 1 | awk '{print $1}'`,
repoPath,
))
-
+
output, err := cmd.Output()
if err != nil {
// Fallback: try a simpler approach
@@ -264,12 +263,12 @@ func getAverageCommitAge(repoPath string, commitCount int) (float64, error) {
if line == "" {
continue
}
-
+
timestamp, err := strconv.ParseInt(line, 10, 64)
if err != nil {
continue
}
-
+
age := (now - float64(timestamp)) / 86400 // Convert to days
totalAge += age
validCommits++
@@ -310,12 +309,12 @@ func getLatestTag(repoPath string) (string, string, bool, error) {
break
}
}
-
+
if latestTag == "" {
// No version-like tags found
return "", "", false, nil
}
-
+
// Get the date of the latest tag
cmd = exec.Command("git", "-C", repoPath, "log", "-1", "--format=%ai", latestTag)
dateOutput, err := cmd.Output()
@@ -323,7 +322,7 @@ func getLatestTag(repoPath string) (string, string, bool, error) {
// Tag exists but couldn't get date
return latestTag, "", true, nil
}
-
+
// Extract just the date part (YYYY-MM-DD)
parts := strings.Fields(string(dateOutput))
tagDate := ""
@@ -339,24 +338,24 @@ func getLatestTag(repoPath string) (string, string, bool, error) {
func isVersionTag(tag string) bool {
// Remove 'v' prefix if present
versionStr := strings.TrimPrefix(tag, "v")
-
+
// Check if the remaining string contains at least one digit and one dot
hasDigit := false
hasDot := false
-
+
for _, ch := range versionStr {
if ch >= '0' && ch <= '9' {
hasDigit = true
} else if ch == '.' {
hasDot = true
- } else if ch != '-' && ch != '+' && ch != '_' &&
- (ch < 'a' || ch > 'z') && (ch < 'A' || ch > 'Z') {
+ } else if ch != '-' && ch != '+' && ch != '_' &&
+ (ch < 'a' || ch > 'z') && (ch < 'A' || ch > 'Z') {
// Allow alphanumeric characters and common separators
// but anything else makes it not a version
return false
}
}
-
+
// Must have at least one digit, and either:
// - have a dot (e.g., 1.0, 0.1.2)
// - be just digits (e.g., 2, 2024)
@@ -367,6 +366,6 @@ func isVersionTag(tag string) bool {
return true
}
}
-
+
return hasDigit && hasDot
-} \ No newline at end of file
+}
diff --git a/internal/showcase/showcase.go b/internal/showcase/showcase.go
index 01e0e2b..1c057f3 100644
--- a/internal/showcase/showcase.go
+++ b/internal/showcase/showcase.go
@@ -1,8 +1,8 @@
package showcase
import (
- "context"
- "encoding/json"
+ "context"
+ "encoding/json"
"fmt"
"os"
"os/exec"
@@ -51,7 +51,7 @@ func New(cfg *config.Config, workDir string) *Generator {
return &Generator{
config: cfg,
workDir: workDir,
- aiTool: "claude", // default to claude
+ aiTool: "amp", // default to amp
}
}
@@ -157,25 +157,25 @@ func (g *Generator) GenerateShowcase(repoFilter []string, forceRegenerate bool)
// runCommandWithTimeout runs a command with a short timeout and returns trimmed stdout.
// Stderr is included in the error message for easier debugging when GITSYNCER_DEBUG=1.
func runCommandWithTimeout(name string, args ...string) (string, error) {
- ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
- defer cancel()
- cmd := exec.CommandContext(ctx, name, args...)
- out, err := cmd.CombinedOutput()
- if ctx.Err() == context.DeadlineExceeded {
- return "", fmt.Errorf("command timed out")
- }
- if err != nil {
- // include a snippet of output for debugging
- msg := strings.TrimSpace(string(out))
- if len(msg) > 300 {
- msg = msg[:300] + "..."
- }
- if msg != "" {
- return "", fmt.Errorf("%v: %s", err, msg)
- }
- return "", err
- }
- return string(out), nil
+ ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
+ defer cancel()
+ cmd := exec.CommandContext(ctx, name, args...)
+ out, err := cmd.CombinedOutput()
+ if ctx.Err() == context.DeadlineExceeded {
+ return "", fmt.Errorf("command timed out")
+ }
+ if err != nil {
+ // include a snippet of output for debugging
+ msg := strings.TrimSpace(string(out))
+ if len(msg) > 300 {
+ msg = msg[:300] + "..."
+ }
+ if msg != "" {
+ return "", fmt.Errorf("%v: %s", err, msg)
+ }
+ return "", err
+ }
+ return string(out), nil
}
// getRepositories returns a list of repository directories in the work directory
@@ -222,35 +222,48 @@ func (g *Generator) generateProjectSummary(repoName string, forceRegenerate bool
}
}
- // Determine which AI tool to use (only if we need to run it)
- // Prefer hexai if available when default tool is "claude" (aligns with release flow)
- selectedTool := g.aiTool
- if !haveCachedSummary {
- switch g.aiTool {
- case "claude", "claude-code", "":
- // Try hexai -> claude -> aichat
- 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 "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 = ""
- }
- }
+ // Determine which AI tool to use (only if we need to run it)
+ // 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 = ""
+ }
+ }
// Change to repository directory
originalDir, err := os.Getwd()
@@ -273,50 +286,78 @@ func (g *Generator) generateProjectSummary(repoName string, forceRegenerate bool
// Get the summary - either from cache or by running AI tool
var summary string
- if haveCachedSummary {
- summary = cachedSummary
- 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 "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")
- // Find README file
- readmeFiles := []string{
- "README.md", "readme.md", "Readme.md",
- "README.MD", "README.txt", "readme.txt",
- "README", "readme",
- }
- var readmeContent []byte
- var readmeFound bool
- for _, readmeFile := range readmeFiles {
- content, err := os.ReadFile(readmeFile)
- if err == nil {
- readmeContent = content
- readmeFound = true
- fmt.Printf(" Using %s as input\n", readmeFile)
- break
- }
- }
- if readmeFound {
- fmt.Printf(" echo <README content> | hexai \"%s\"\n", prompt)
- 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 haveCachedSummary {
+ summary = cachedSummary
+ 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")
+ // Find README file
+ readmeFiles := []string{
+ "README.md", "readme.md", "Readme.md",
+ "README.MD", "README.txt", "readme.txt",
+ "README", "readme",
+ }
+ var readmeContent []byte
+ var readmeFound bool
+ for _, readmeFile := range readmeFiles {
+ content, err := os.ReadFile(readmeFile)
+ if err == nil {
+ readmeContent = content
+ readmeFound = true
+ fmt.Printf(" Using %s as input\n", readmeFile)
+ break
+ }
+ }
+ if readmeFound {
+ fmt.Printf(" echo <README content> | amp --execute \"%s\"\n", prompt)
+ 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")
+ // Find README file
+ readmeFiles := []string{
+ "README.md", "readme.md", "Readme.md",
+ "README.MD", "README.txt", "readme.txt",
+ "README", "readme",
+ }
+ var readmeContent []byte
+ var readmeFound bool
+ for _, readmeFile := range readmeFiles {
+ content, err := os.ReadFile(readmeFile)
+ if err == nil {
+ readmeContent = content
+ readmeFound = true
+ fmt.Printf(" Using %s as input\n", readmeFile)
+ break
+ }
+ }
+ if readmeFound {
+ fmt.Printf(" echo <README content> | hexai \"%s\"\n", prompt)
+ 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")
// Find README file
readmeFiles := []string{
@@ -337,46 +378,46 @@ func (g *Generator) generateProjectSummary(repoName string, forceRegenerate bool
}
}
- if readmeFound {
- fmt.Printf(" echo <README content> | aichat \"%s\"\n", prompt)
- 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 {
- if output, err := cmd.Output(); err == nil {
- summary = strings.TrimSpace(string(output))
- }
- }
-
- // Fallback: create a minimal summary from README if AI unavailable/failed
- if summary == "" {
- readmeFiles := []string{
- "README.md", "readme.md", "Readme.md",
- "README.MD", "README.txt", "readme.txt",
- "README", "readme",
- }
- for _, readmeFile := range readmeFiles {
- if content, err := os.ReadFile(readmeFile); err == nil {
- parts := strings.Split(strings.TrimSpace(string(content)), "\n\n")
- if len(parts) > 0 {
- summary = strings.TrimSpace(parts[0])
- break
- }
- }
- }
- if summary == "" {
- summary = fmt.Sprintf("%s: source code repository.", repoName)
- }
- }
- }
+ if readmeFound {
+ fmt.Printf(" echo <README content> | aichat \"%s\"\n", prompt)
+ 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 {
+ if output, err := cmd.Output(); err == nil {
+ summary = strings.TrimSpace(string(output))
+ }
+ }
+
+ // Fallback: create a minimal summary from README if AI unavailable/failed
+ if summary == "" {
+ readmeFiles := []string{
+ "README.md", "readme.md", "Readme.md",
+ "README.MD", "README.txt", "readme.txt",
+ "README", "readme",
+ }
+ for _, readmeFile := range readmeFiles {
+ if content, err := os.ReadFile(readmeFile); err == nil {
+ parts := strings.Split(strings.TrimSpace(string(content)), "\n\n")
+ if len(parts) > 0 {
+ summary = strings.TrimSpace(parts[0])
+ break
+ }
+ }
+ }
+ if summary == "" {
+ summary = fmt.Sprintf("%s: source code repository.", repoName)
+ }
+ }
+ }
// Build URLs
codebergURL := ""