summaryrefslogtreecommitdiff
path: root/internal/showcase/ai_context.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-10-31 20:13:32 +0200
committerPaul Buetow <paul@buetow.org>2025-10-31 20:13:32 +0200
commit11eea6a82cbfdde40ec1457c6ea080da4da6b7dc (patch)
tree8026068f6a3beb3ee02c45f06f4487f4b89caaf1 /internal/showcase/ai_context.go
parent5c3e0b5cf99d028c4f06be7a825388b296e37a22 (diff)
feat: implement amp AI tool support and replace Taskfile with Magev0.10.0
- Add amp as default AI tool for release notes and showcase generation - Fallback chain: amp → hexai → claude → aichat - Replace Taskfile.yaml with magefile.go for build automation - Update all documentation (README.md, AGENTS.md, doc/development.md) - Update version to 0.10.0 Amp-Thread-ID: https://ampcode.com/threads/T-735ba1e2-0255-4b43-8ed1-6c0d2f78301b Co-authored-by: Amp <amp@ampcode.com>
Diffstat (limited to 'internal/showcase/ai_context.go')
-rw-r--r--internal/showcase/ai_context.go338
1 files changed, 184 insertions, 154 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]"
}
-