diff --git a/README.md b/README.md index 044b11c..c1a1622 100644 --- a/README.md +++ b/README.md @@ -109,20 +109,25 @@ coding-agent-context -p feature="User Login" -p language=Go add-feature ### Memory Files -Markdown files included in every generated context. Can include bootstrap scripts in frontmatter. +Markdown files included in every generated context. Bootstrap scripts can be provided in separate files. **Example** (`.coding-agent-context/memories/setup.md`): ```markdown --- -bootstrap: | - #!/bin/bash - npm install --- # Development Setup This project requires Node.js dependencies. ``` +**Bootstrap file** (`.coding-agent-context/memories/setup-bootstrap`): +```bash +#!/bin/bash +npm install +``` + +For each memory file `.md`, you can optionally create a corresponding `-bootstrap` file that will be executed during setup. + ## Output Files @@ -186,15 +191,18 @@ coding-agent-context -p featureName="Authentication" -p language=Go add-feature ```bash cat > .coding-agent-context/memories/setup.md << 'EOF' --- -bootstrap: | - #!/bin/bash - go mod download --- # Project Setup This Go project uses modules. EOF +cat > .coding-agent-context/memories/setup-bootstrap << 'EOF' +#!/bin/bash +go mod download +EOF +chmod +x .coding-agent-context/memories/setup-bootstrap + coding-agent-context -o ./output my-task cd output && ./bootstrap ``` diff --git a/integration_test.go b/integration_test.go new file mode 100644 index 0000000..ac61041 --- /dev/null +++ b/integration_test.go @@ -0,0 +1,236 @@ +package main + +import ( + "os" + "os/exec" + "path/filepath" + "testing" +) + +func TestBootstrapFromFile(t *testing.T) { + // Build the binary + binaryPath := filepath.Join(t.TempDir(), "coding-agent-context") + cmd := exec.Command("go", "build", "-o", binaryPath, ".") + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("failed to build binary: %v\n%s", err, output) + } + + // Create a temporary directory structure + tmpDir := t.TempDir() + contextDir := filepath.Join(tmpDir, ".coding-agent-context") + memoriesDir := filepath.Join(contextDir, "memories") + promptsDir := filepath.Join(contextDir, "prompts") + outputDir := filepath.Join(tmpDir, "output") + + if err := os.MkdirAll(memoriesDir, 0755); err != nil { + t.Fatalf("failed to create memories dir: %v", err) + } + if err := os.MkdirAll(promptsDir, 0755); err != nil { + t.Fatalf("failed to create prompts dir: %v", err) + } + + // Create a memory file + memoryFile := filepath.Join(memoriesDir, "setup.md") + memoryContent := `--- +--- +# Development Setup + +This is a setup guide. +` + if err := os.WriteFile(memoryFile, []byte(memoryContent), 0644); err != nil { + t.Fatalf("failed to write memory file: %v", err) + } + + // Create a bootstrap file for the memory (setup.md -> setup-bootstrap) + bootstrapFile := filepath.Join(memoriesDir, "setup-bootstrap") + bootstrapContent := `#!/bin/bash +echo "Running bootstrap" +npm install +` + if err := os.WriteFile(bootstrapFile, []byte(bootstrapContent), 0755); err != nil { + t.Fatalf("failed to write bootstrap file: %v", err) + } + + // Create a prompt file + promptFile := filepath.Join(promptsDir, "test-task.md") + promptContent := `--- +--- +# Test Task + +Please help with this task. +` + if err := os.WriteFile(promptFile, []byte(promptContent), 0644); err != nil { + t.Fatalf("failed to write prompt file: %v", err) + } + + // Run the binary + cmd = exec.Command(binaryPath, "-d", contextDir, "-o", outputDir, "test-task") + cmd.Dir = tmpDir + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("failed to run binary: %v\n%s", err, output) + } + + // Check that the bootstrap.d directory was created + bootstrapDDir := filepath.Join(outputDir, "bootstrap.d") + if _, err := os.Stat(bootstrapDDir); os.IsNotExist(err) { + t.Errorf("bootstrap.d directory was not created") + } + + // Check that a bootstrap file exists in bootstrap.d + files, err := os.ReadDir(bootstrapDDir) + if err != nil { + t.Fatalf("failed to read bootstrap.d dir: %v", err) + } + if len(files) != 1 { + t.Errorf("expected 1 bootstrap file, got %d", len(files)) + } + + // Check that the bootstrap file has the correct content + if len(files) > 0 { + bootstrapPath := filepath.Join(bootstrapDDir, files[0].Name()) + content, err := os.ReadFile(bootstrapPath) + if err != nil { + t.Fatalf("failed to read bootstrap file: %v", err) + } + if string(content) != bootstrapContent { + t.Errorf("bootstrap content mismatch:\ngot: %q\nwant: %q", string(content), bootstrapContent) + } + } + + // Check that the prompt.md file was created + promptOutput := filepath.Join(outputDir, "prompt.md") + if _, err := os.Stat(promptOutput); os.IsNotExist(err) { + t.Errorf("prompt.md file was not created") + } +} + +func TestBootstrapFileNotRequired(t *testing.T) { + // Build the binary + binaryPath := filepath.Join(t.TempDir(), "coding-agent-context") + cmd := exec.Command("go", "build", "-o", binaryPath, ".") + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("failed to build binary: %v\n%s", err, output) + } + + // Create a temporary directory structure + tmpDir := t.TempDir() + contextDir := filepath.Join(tmpDir, ".coding-agent-context") + memoriesDir := filepath.Join(contextDir, "memories") + promptsDir := filepath.Join(contextDir, "prompts") + outputDir := filepath.Join(tmpDir, "output") + + if err := os.MkdirAll(memoriesDir, 0755); err != nil { + t.Fatalf("failed to create memories dir: %v", err) + } + if err := os.MkdirAll(promptsDir, 0755); err != nil { + t.Fatalf("failed to create prompts dir: %v", err) + } + + // Create a memory file WITHOUT a bootstrap + memoryFile := filepath.Join(memoriesDir, "info.md") + memoryContent := `--- +--- +# Project Info + +Just some information. +` + if err := os.WriteFile(memoryFile, []byte(memoryContent), 0644); err != nil { + t.Fatalf("failed to write memory file: %v", err) + } + + // Create a prompt file + promptFile := filepath.Join(promptsDir, "test-task.md") + promptContent := `--- +--- +# Test Task + +Please help with this task. +` + if err := os.WriteFile(promptFile, []byte(promptContent), 0644); err != nil { + t.Fatalf("failed to write prompt file: %v", err) + } + + // Run the binary + cmd = exec.Command(binaryPath, "-d", contextDir, "-o", outputDir, "test-task") + cmd.Dir = tmpDir + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("failed to run binary: %v\n%s", err, output) + } + + // Check that the bootstrap.d directory was created but is empty + bootstrapDDir := filepath.Join(outputDir, "bootstrap.d") + files, err := os.ReadDir(bootstrapDDir) + if err != nil { + t.Fatalf("failed to read bootstrap.d dir: %v", err) + } + if len(files) != 0 { + t.Errorf("expected 0 bootstrap files, got %d", len(files)) + } + + // Check that the prompt.md file was still created + promptOutput := filepath.Join(outputDir, "prompt.md") + if _, err := os.Stat(promptOutput); os.IsNotExist(err) { + t.Errorf("prompt.md file was not created") + } +} + +func TestMultipleBootstrapFiles(t *testing.T) { + // Build the binary + binaryPath := filepath.Join(t.TempDir(), "coding-agent-context") + cmd := exec.Command("go", "build", "-o", binaryPath, ".") + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("failed to build binary: %v\n%s", err, output) + } + + // Create a temporary directory structure + tmpDir := t.TempDir() + contextDir := filepath.Join(tmpDir, ".coding-agent-context") + memoriesDir := filepath.Join(contextDir, "memories") + promptsDir := filepath.Join(contextDir, "prompts") + outputDir := filepath.Join(tmpDir, "output") + + if err := os.MkdirAll(memoriesDir, 0755); err != nil { + t.Fatalf("failed to create memories dir: %v", err) + } + if err := os.MkdirAll(promptsDir, 0755); err != nil { + t.Fatalf("failed to create prompts dir: %v", err) + } + + // Create first memory file with bootstrap + if err := os.WriteFile(filepath.Join(memoriesDir, "setup.md"), []byte("---\n---\n# Setup\n"), 0644); err != nil { + t.Fatalf("failed to write memory file: %v", err) + } + if err := os.WriteFile(filepath.Join(memoriesDir, "setup-bootstrap"), []byte("#!/bin/bash\necho setup\n"), 0755); err != nil { + t.Fatalf("failed to write bootstrap file: %v", err) + } + + // Create second memory file with bootstrap + if err := os.WriteFile(filepath.Join(memoriesDir, "deps.md"), []byte("---\n---\n# Dependencies\n"), 0644); err != nil { + t.Fatalf("failed to write memory file: %v", err) + } + if err := os.WriteFile(filepath.Join(memoriesDir, "deps-bootstrap"), []byte("#!/bin/bash\necho deps\n"), 0755); err != nil { + t.Fatalf("failed to write bootstrap file: %v", err) + } + + // Create a prompt file + if err := os.WriteFile(filepath.Join(promptsDir, "test-task.md"), []byte("---\n---\n# Test\n"), 0644); err != nil { + t.Fatalf("failed to write prompt file: %v", err) + } + + // Run the binary + cmd = exec.Command(binaryPath, "-d", contextDir, "-o", outputDir, "test-task") + cmd.Dir = tmpDir + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("failed to run binary: %v\n%s", err, output) + } + + // Check that both bootstrap files exist in bootstrap.d + bootstrapDDir := filepath.Join(outputDir, "bootstrap.d") + files, err := os.ReadDir(bootstrapDDir) + if err != nil { + t.Fatalf("failed to read bootstrap.d dir: %v", err) + } + if len(files) != 2 { + t.Errorf("expected 2 bootstrap files, got %d", len(files)) + } +} diff --git a/main.go b/main.go index 20ef0e3..0cf071b 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "log/slog" "os" "path/filepath" + "strings" "text/template" ) @@ -77,6 +78,12 @@ func run(args []string) error { for _, dir := range dirs { memoryDir := filepath.Join(dir, "memories") + + // Skip if the directory doesn't exist + if _, err := os.Stat(memoryDir); os.IsNotExist(err) { + continue + } + err := filepath.Walk(memoryDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err @@ -85,21 +92,27 @@ func run(args []string) error { return nil } - slog.Info("Including memory file", "path", path) - - var frontmatter struct { - Bootstrap string `yaml:"bootstrap"` + // Only process .md files as memory files + if filepath.Ext(path) != ".md" { + return nil } - content, err := parseMarkdownFile(path, &frontmatter) + slog.Info("Including memory file", "path", path) + + content, err := parseMarkdownFile(path, &struct{}{}) if err != nil { return fmt.Errorf("failed to parse markdown file: %w", err) } - if bootstrap := frontmatter.Bootstrap; bootstrap != "" { - hash := sha256.Sum256([]byte(bootstrap)) + // Check for a bootstrap file named -bootstrap + // For example, setup.md -> setup-bootstrap + baseNameWithoutExt := strings.TrimSuffix(path, ".md") + bootstrapFilePath := baseNameWithoutExt + "-bootstrap" + + if bootstrapContent, err := os.ReadFile(bootstrapFilePath); err == nil { + hash := sha256.Sum256(bootstrapContent) bootstrapPath := filepath.Join(bootstrapDir, fmt.Sprintf("%x", hash)) - if err := os.WriteFile(bootstrapPath, []byte(bootstrap), 0700); err != nil { + if err := os.WriteFile(bootstrapPath, bootstrapContent, 0700); err != nil { return fmt.Errorf("failed to write bootstrap file: %w", err) } }