first commit
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
package initcmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// projectMarker maps a marker file name to its detected project type.
|
||||
var projectMarkers = map[string]string{
|
||||
"go.mod": "Go",
|
||||
"go.sum": "Go",
|
||||
"package.json": "Node.js",
|
||||
"Cargo.toml": "Rust",
|
||||
"pyproject.toml": "Python",
|
||||
"requirements.txt": "Python",
|
||||
"setup.py": "Python",
|
||||
"Pipfile": "Python",
|
||||
"Gemfile": "Ruby",
|
||||
"pom.xml": "Java (Maven)",
|
||||
"build.gradle": "Java (Gradle)",
|
||||
"build.gradle.kts": "Kotlin (Gradle)",
|
||||
"CMakeLists.txt": "C/C++ (CMake)",
|
||||
"Makefile": "Make",
|
||||
"Taskfile.yml": "Taskfile",
|
||||
"Taskfile.yaml": "Taskfile",
|
||||
"docker-compose.yml": "Docker Compose",
|
||||
"docker-compose.yaml": "Docker Compose",
|
||||
"Dockerfile": "Docker",
|
||||
".gitignore": "Git",
|
||||
}
|
||||
|
||||
// Options configures the behaviour of Run.
|
||||
type Options struct {
|
||||
// Force overwrites an existing AGENT.md.
|
||||
Force bool
|
||||
}
|
||||
|
||||
// Run scans dir for project markers and generates an AGENT.md file.
|
||||
// It returns an error if AGENT.md already exists unless opts.Force is true.
|
||||
func Run(dir string, opts Options) error {
|
||||
agentPath := filepath.Join(dir, "AGENT.md")
|
||||
|
||||
if !opts.Force {
|
||||
if _, err := os.Stat(agentPath); err == nil {
|
||||
return fmt.Errorf("AGENT.md already exists in %s (use --force to overwrite)", dir)
|
||||
}
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading directory: %w", err)
|
||||
}
|
||||
|
||||
// Detect project types from marker files.
|
||||
detectedTypes := detectProjectTypes(entries)
|
||||
|
||||
// Build directory listing.
|
||||
listing := buildDirectoryListing(dir, entries)
|
||||
|
||||
// Generate AGENT.md content.
|
||||
content := generateAgentMD(detectedTypes, listing)
|
||||
|
||||
if err := os.WriteFile(agentPath, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("writing AGENT.md: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// detectProjectTypes returns a deduplicated, sorted list of project types
|
||||
// found based on marker files in the directory entries.
|
||||
func detectProjectTypes(entries []os.DirEntry) []string {
|
||||
seen := make(map[string]bool)
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
if pt, ok := projectMarkers[e.Name()]; ok {
|
||||
seen[pt] = true
|
||||
}
|
||||
}
|
||||
|
||||
types := make([]string, 0, len(seen))
|
||||
for t := range seen {
|
||||
types = append(types, t)
|
||||
}
|
||||
sort.Strings(types)
|
||||
return types
|
||||
}
|
||||
|
||||
// buildDirectoryListing returns a formatted string of top-level files and
|
||||
// first-level subdirectory contents.
|
||||
func buildDirectoryListing(dir string, entries []os.DirEntry) string {
|
||||
var b strings.Builder
|
||||
|
||||
var files []string
|
||||
var dirs []string
|
||||
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
// Skip hidden files/dirs except well-known ones.
|
||||
if strings.HasPrefix(name, ".") && name != ".gitignore" {
|
||||
continue
|
||||
}
|
||||
if e.IsDir() {
|
||||
dirs = append(dirs, name)
|
||||
} else {
|
||||
files = append(files, name)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(files)
|
||||
sort.Strings(dirs)
|
||||
|
||||
for _, f := range files {
|
||||
b.WriteString(f)
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
|
||||
for _, d := range dirs {
|
||||
b.WriteString(d + "/\n")
|
||||
subEntries, err := os.ReadDir(filepath.Join(dir, d))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var subNames []string
|
||||
for _, se := range subEntries {
|
||||
n := se.Name()
|
||||
if strings.HasPrefix(n, ".") {
|
||||
continue
|
||||
}
|
||||
if se.IsDir() {
|
||||
subNames = append(subNames, n+"/")
|
||||
} else {
|
||||
subNames = append(subNames, n)
|
||||
}
|
||||
}
|
||||
sort.Strings(subNames)
|
||||
for _, sn := range subNames {
|
||||
b.WriteString(" " + sn + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// generateAgentMD produces the Markdown content for the AGENT.md file.
|
||||
func generateAgentMD(projectTypes []string, listing string) string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString("# AGENT.md\n\n")
|
||||
|
||||
// Project type section.
|
||||
b.WriteString("## Project Type\n\n")
|
||||
if len(projectTypes) == 0 {
|
||||
b.WriteString("Unknown\n")
|
||||
} else {
|
||||
b.WriteString(strings.Join(projectTypes, ", ") + "\n")
|
||||
}
|
||||
|
||||
// Directory structure.
|
||||
b.WriteString("\n## Directory Structure\n\n")
|
||||
b.WriteString("```\n")
|
||||
b.WriteString(listing)
|
||||
b.WriteString("```\n")
|
||||
|
||||
// Placeholder sections.
|
||||
b.WriteString("\n## Build Commands\n\n")
|
||||
b.WriteString("<!-- Add build, test, and run commands here -->\n")
|
||||
|
||||
b.WriteString("\n## Architecture\n\n")
|
||||
b.WriteString("<!-- Describe the high-level architecture here -->\n")
|
||||
|
||||
b.WriteString("\n## Key Files\n\n")
|
||||
b.WriteString("<!-- List important files and their purposes here -->\n")
|
||||
|
||||
b.WriteString("\n## Notes\n\n")
|
||||
b.WriteString("<!-- Any additional notes for the agent -->\n")
|
||||
|
||||
return b.String()
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package initcmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRun_GoProject(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
// Create a go.mod marker file.
|
||||
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/test\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Create a source directory with a file.
|
||||
if err := os.Mkdir(filepath.Join(dir, "cmd"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "cmd", "main.go"), []byte("package main\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := Run(dir, Options{}); err != nil {
|
||||
t.Fatalf("Run() returned error: %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(dir, "AGENT.md"))
|
||||
if err != nil {
|
||||
t.Fatalf("reading AGENT.md: %v", err)
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
|
||||
// Check project type detection.
|
||||
if !strings.Contains(content, "Go") {
|
||||
t.Error("expected AGENT.md to contain 'Go' project type")
|
||||
}
|
||||
|
||||
// Check directory listing includes go.mod.
|
||||
if !strings.Contains(content, "go.mod") {
|
||||
t.Error("expected AGENT.md to list go.mod")
|
||||
}
|
||||
|
||||
// Check directory listing includes cmd/ subdirectory.
|
||||
if !strings.Contains(content, "cmd/") {
|
||||
t.Error("expected AGENT.md to list cmd/ directory")
|
||||
}
|
||||
|
||||
// Check that main.go appears under cmd/.
|
||||
if !strings.Contains(content, "main.go") {
|
||||
t.Error("expected AGENT.md to list main.go inside cmd/")
|
||||
}
|
||||
|
||||
// Check placeholder sections exist.
|
||||
for _, section := range []string{"## Build Commands", "## Architecture", "## Key Files", "## Notes"} {
|
||||
if !strings.Contains(content, section) {
|
||||
t.Errorf("expected AGENT.md to contain section %q", section)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_EmptyDirectory(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
if err := Run(dir, Options{}); err != nil {
|
||||
t.Fatalf("Run() returned error: %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(dir, "AGENT.md"))
|
||||
if err != nil {
|
||||
t.Fatalf("reading AGENT.md: %v", err)
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
|
||||
// With no marker files, project type should be "Unknown".
|
||||
if !strings.Contains(content, "Unknown") {
|
||||
t.Error("expected AGENT.md to contain 'Unknown' project type for empty dir")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_ExistingAgentMD_NoOverwrite(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
agentPath := filepath.Join(dir, "AGENT.md")
|
||||
original := "# Original content\n"
|
||||
if err := os.WriteFile(agentPath, []byte(original), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err := Run(dir, Options{})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when AGENT.md already exists")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "already exists") {
|
||||
t.Errorf("expected 'already exists' in error, got: %v", err)
|
||||
}
|
||||
|
||||
// Verify file was not modified.
|
||||
data, err := os.ReadFile(agentPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(data) != original {
|
||||
t.Error("AGENT.md was unexpectedly modified")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_Force(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
agentPath := filepath.Join(dir, "AGENT.md")
|
||||
if err := os.WriteFile(agentPath, []byte("# Old content\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a go.mod so the new content is distinguishable.
|
||||
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module test\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := Run(dir, Options{Force: true}); err != nil {
|
||||
t.Fatalf("Run() with Force=true returned error: %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(agentPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
if strings.Contains(content, "Old content") {
|
||||
t.Error("AGENT.md should have been overwritten with Force=true")
|
||||
}
|
||||
if !strings.Contains(content, "Go") {
|
||||
t.Error("expected new AGENT.md to detect Go project type")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user