Skip to content

Commit 17dc5bd

Browse files
Copilotalexec
andauthored
Move bootstrap scripts from frontmatter to separate files (#7)
* Initial plan * Move bootstrap from frontmatter to separate files Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> * Update documentation for new bootstrap file format Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> * Fix trailing whitespace in main.go Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> * Fix build: skip non-existent memory directories Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> * Use strings.TrimSuffix for clearer code Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexec <1142830+alexec@users.noreply.github.com>
1 parent f8c79a4 commit 17dc5bd

3 files changed

Lines changed: 272 additions & 15 deletions

File tree

README.md

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,20 +109,25 @@ coding-agent-context -p feature="User Login" -p language=Go add-feature
109109

110110
### Memory Files
111111

112-
Markdown files included in every generated context. Can include bootstrap scripts in frontmatter.
112+
Markdown files included in every generated context. Bootstrap scripts can be provided in separate files.
113113

114114
**Example** (`.coding-agent-context/memories/setup.md`):
115115
```markdown
116116
---
117-
bootstrap: |
118-
#!/bin/bash
119-
npm install
120117
---
121118
# Development Setup
122119

123120
This project requires Node.js dependencies.
124121
```
125122

123+
**Bootstrap file** (`.coding-agent-context/memories/setup-bootstrap`):
124+
```bash
125+
#!/bin/bash
126+
npm install
127+
```
128+
129+
For each memory file `<name>.md`, you can optionally create a corresponding `<name>-bootstrap` file that will be executed during setup.
130+
126131

127132
## Output Files
128133

@@ -186,15 +191,18 @@ coding-agent-context -p featureName="Authentication" -p language=Go add-feature
186191
```bash
187192
cat > .coding-agent-context/memories/setup.md << 'EOF'
188193
---
189-
bootstrap: |
190-
#!/bin/bash
191-
go mod download
192194
---
193195
# Project Setup
194196
195197
This Go project uses modules.
196198
EOF
197199

200+
cat > .coding-agent-context/memories/setup-bootstrap << 'EOF'
201+
#!/bin/bash
202+
go mod download
203+
EOF
204+
chmod +x .coding-agent-context/memories/setup-bootstrap
205+
198206
coding-agent-context -o ./output my-task
199207
cd output && ./bootstrap
200208
```

integration_test.go

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
"path/filepath"
7+
"testing"
8+
)
9+
10+
func TestBootstrapFromFile(t *testing.T) {
11+
// Build the binary
12+
binaryPath := filepath.Join(t.TempDir(), "coding-agent-context")
13+
cmd := exec.Command("go", "build", "-o", binaryPath, ".")
14+
if output, err := cmd.CombinedOutput(); err != nil {
15+
t.Fatalf("failed to build binary: %v\n%s", err, output)
16+
}
17+
18+
// Create a temporary directory structure
19+
tmpDir := t.TempDir()
20+
contextDir := filepath.Join(tmpDir, ".coding-agent-context")
21+
memoriesDir := filepath.Join(contextDir, "memories")
22+
promptsDir := filepath.Join(contextDir, "prompts")
23+
outputDir := filepath.Join(tmpDir, "output")
24+
25+
if err := os.MkdirAll(memoriesDir, 0755); err != nil {
26+
t.Fatalf("failed to create memories dir: %v", err)
27+
}
28+
if err := os.MkdirAll(promptsDir, 0755); err != nil {
29+
t.Fatalf("failed to create prompts dir: %v", err)
30+
}
31+
32+
// Create a memory file
33+
memoryFile := filepath.Join(memoriesDir, "setup.md")
34+
memoryContent := `---
35+
---
36+
# Development Setup
37+
38+
This is a setup guide.
39+
`
40+
if err := os.WriteFile(memoryFile, []byte(memoryContent), 0644); err != nil {
41+
t.Fatalf("failed to write memory file: %v", err)
42+
}
43+
44+
// Create a bootstrap file for the memory (setup.md -> setup-bootstrap)
45+
bootstrapFile := filepath.Join(memoriesDir, "setup-bootstrap")
46+
bootstrapContent := `#!/bin/bash
47+
echo "Running bootstrap"
48+
npm install
49+
`
50+
if err := os.WriteFile(bootstrapFile, []byte(bootstrapContent), 0755); err != nil {
51+
t.Fatalf("failed to write bootstrap file: %v", err)
52+
}
53+
54+
// Create a prompt file
55+
promptFile := filepath.Join(promptsDir, "test-task.md")
56+
promptContent := `---
57+
---
58+
# Test Task
59+
60+
Please help with this task.
61+
`
62+
if err := os.WriteFile(promptFile, []byte(promptContent), 0644); err != nil {
63+
t.Fatalf("failed to write prompt file: %v", err)
64+
}
65+
66+
// Run the binary
67+
cmd = exec.Command(binaryPath, "-d", contextDir, "-o", outputDir, "test-task")
68+
cmd.Dir = tmpDir
69+
if output, err := cmd.CombinedOutput(); err != nil {
70+
t.Fatalf("failed to run binary: %v\n%s", err, output)
71+
}
72+
73+
// Check that the bootstrap.d directory was created
74+
bootstrapDDir := filepath.Join(outputDir, "bootstrap.d")
75+
if _, err := os.Stat(bootstrapDDir); os.IsNotExist(err) {
76+
t.Errorf("bootstrap.d directory was not created")
77+
}
78+
79+
// Check that a bootstrap file exists in bootstrap.d
80+
files, err := os.ReadDir(bootstrapDDir)
81+
if err != nil {
82+
t.Fatalf("failed to read bootstrap.d dir: %v", err)
83+
}
84+
if len(files) != 1 {
85+
t.Errorf("expected 1 bootstrap file, got %d", len(files))
86+
}
87+
88+
// Check that the bootstrap file has the correct content
89+
if len(files) > 0 {
90+
bootstrapPath := filepath.Join(bootstrapDDir, files[0].Name())
91+
content, err := os.ReadFile(bootstrapPath)
92+
if err != nil {
93+
t.Fatalf("failed to read bootstrap file: %v", err)
94+
}
95+
if string(content) != bootstrapContent {
96+
t.Errorf("bootstrap content mismatch:\ngot: %q\nwant: %q", string(content), bootstrapContent)
97+
}
98+
}
99+
100+
// Check that the prompt.md file was created
101+
promptOutput := filepath.Join(outputDir, "prompt.md")
102+
if _, err := os.Stat(promptOutput); os.IsNotExist(err) {
103+
t.Errorf("prompt.md file was not created")
104+
}
105+
}
106+
107+
func TestBootstrapFileNotRequired(t *testing.T) {
108+
// Build the binary
109+
binaryPath := filepath.Join(t.TempDir(), "coding-agent-context")
110+
cmd := exec.Command("go", "build", "-o", binaryPath, ".")
111+
if output, err := cmd.CombinedOutput(); err != nil {
112+
t.Fatalf("failed to build binary: %v\n%s", err, output)
113+
}
114+
115+
// Create a temporary directory structure
116+
tmpDir := t.TempDir()
117+
contextDir := filepath.Join(tmpDir, ".coding-agent-context")
118+
memoriesDir := filepath.Join(contextDir, "memories")
119+
promptsDir := filepath.Join(contextDir, "prompts")
120+
outputDir := filepath.Join(tmpDir, "output")
121+
122+
if err := os.MkdirAll(memoriesDir, 0755); err != nil {
123+
t.Fatalf("failed to create memories dir: %v", err)
124+
}
125+
if err := os.MkdirAll(promptsDir, 0755); err != nil {
126+
t.Fatalf("failed to create prompts dir: %v", err)
127+
}
128+
129+
// Create a memory file WITHOUT a bootstrap
130+
memoryFile := filepath.Join(memoriesDir, "info.md")
131+
memoryContent := `---
132+
---
133+
# Project Info
134+
135+
Just some information.
136+
`
137+
if err := os.WriteFile(memoryFile, []byte(memoryContent), 0644); err != nil {
138+
t.Fatalf("failed to write memory file: %v", err)
139+
}
140+
141+
// Create a prompt file
142+
promptFile := filepath.Join(promptsDir, "test-task.md")
143+
promptContent := `---
144+
---
145+
# Test Task
146+
147+
Please help with this task.
148+
`
149+
if err := os.WriteFile(promptFile, []byte(promptContent), 0644); err != nil {
150+
t.Fatalf("failed to write prompt file: %v", err)
151+
}
152+
153+
// Run the binary
154+
cmd = exec.Command(binaryPath, "-d", contextDir, "-o", outputDir, "test-task")
155+
cmd.Dir = tmpDir
156+
if output, err := cmd.CombinedOutput(); err != nil {
157+
t.Fatalf("failed to run binary: %v\n%s", err, output)
158+
}
159+
160+
// Check that the bootstrap.d directory was created but is empty
161+
bootstrapDDir := filepath.Join(outputDir, "bootstrap.d")
162+
files, err := os.ReadDir(bootstrapDDir)
163+
if err != nil {
164+
t.Fatalf("failed to read bootstrap.d dir: %v", err)
165+
}
166+
if len(files) != 0 {
167+
t.Errorf("expected 0 bootstrap files, got %d", len(files))
168+
}
169+
170+
// Check that the prompt.md file was still created
171+
promptOutput := filepath.Join(outputDir, "prompt.md")
172+
if _, err := os.Stat(promptOutput); os.IsNotExist(err) {
173+
t.Errorf("prompt.md file was not created")
174+
}
175+
}
176+
177+
func TestMultipleBootstrapFiles(t *testing.T) {
178+
// Build the binary
179+
binaryPath := filepath.Join(t.TempDir(), "coding-agent-context")
180+
cmd := exec.Command("go", "build", "-o", binaryPath, ".")
181+
if output, err := cmd.CombinedOutput(); err != nil {
182+
t.Fatalf("failed to build binary: %v\n%s", err, output)
183+
}
184+
185+
// Create a temporary directory structure
186+
tmpDir := t.TempDir()
187+
contextDir := filepath.Join(tmpDir, ".coding-agent-context")
188+
memoriesDir := filepath.Join(contextDir, "memories")
189+
promptsDir := filepath.Join(contextDir, "prompts")
190+
outputDir := filepath.Join(tmpDir, "output")
191+
192+
if err := os.MkdirAll(memoriesDir, 0755); err != nil {
193+
t.Fatalf("failed to create memories dir: %v", err)
194+
}
195+
if err := os.MkdirAll(promptsDir, 0755); err != nil {
196+
t.Fatalf("failed to create prompts dir: %v", err)
197+
}
198+
199+
// Create first memory file with bootstrap
200+
if err := os.WriteFile(filepath.Join(memoriesDir, "setup.md"), []byte("---\n---\n# Setup\n"), 0644); err != nil {
201+
t.Fatalf("failed to write memory file: %v", err)
202+
}
203+
if err := os.WriteFile(filepath.Join(memoriesDir, "setup-bootstrap"), []byte("#!/bin/bash\necho setup\n"), 0755); err != nil {
204+
t.Fatalf("failed to write bootstrap file: %v", err)
205+
}
206+
207+
// Create second memory file with bootstrap
208+
if err := os.WriteFile(filepath.Join(memoriesDir, "deps.md"), []byte("---\n---\n# Dependencies\n"), 0644); err != nil {
209+
t.Fatalf("failed to write memory file: %v", err)
210+
}
211+
if err := os.WriteFile(filepath.Join(memoriesDir, "deps-bootstrap"), []byte("#!/bin/bash\necho deps\n"), 0755); err != nil {
212+
t.Fatalf("failed to write bootstrap file: %v", err)
213+
}
214+
215+
// Create a prompt file
216+
if err := os.WriteFile(filepath.Join(promptsDir, "test-task.md"), []byte("---\n---\n# Test\n"), 0644); err != nil {
217+
t.Fatalf("failed to write prompt file: %v", err)
218+
}
219+
220+
// Run the binary
221+
cmd = exec.Command(binaryPath, "-d", contextDir, "-o", outputDir, "test-task")
222+
cmd.Dir = tmpDir
223+
if output, err := cmd.CombinedOutput(); err != nil {
224+
t.Fatalf("failed to run binary: %v\n%s", err, output)
225+
}
226+
227+
// Check that both bootstrap files exist in bootstrap.d
228+
bootstrapDDir := filepath.Join(outputDir, "bootstrap.d")
229+
files, err := os.ReadDir(bootstrapDDir)
230+
if err != nil {
231+
t.Fatalf("failed to read bootstrap.d dir: %v", err)
232+
}
233+
if len(files) != 2 {
234+
t.Errorf("expected 2 bootstrap files, got %d", len(files))
235+
}
236+
}

main.go

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"log/slog"
99
"os"
1010
"path/filepath"
11+
"strings"
1112
"text/template"
1213
)
1314

@@ -77,6 +78,12 @@ func run(args []string) error {
7778

7879
for _, dir := range dirs {
7980
memoryDir := filepath.Join(dir, "memories")
81+
82+
// Skip if the directory doesn't exist
83+
if _, err := os.Stat(memoryDir); os.IsNotExist(err) {
84+
continue
85+
}
86+
8087
err := filepath.Walk(memoryDir, func(path string, info os.FileInfo, err error) error {
8188
if err != nil {
8289
return err
@@ -85,21 +92,27 @@ func run(args []string) error {
8592
return nil
8693
}
8794

88-
slog.Info("Including memory file", "path", path)
89-
90-
var frontmatter struct {
91-
Bootstrap string `yaml:"bootstrap"`
95+
// Only process .md files as memory files
96+
if filepath.Ext(path) != ".md" {
97+
return nil
9298
}
9399

94-
content, err := parseMarkdownFile(path, &frontmatter)
100+
slog.Info("Including memory file", "path", path)
101+
102+
content, err := parseMarkdownFile(path, &struct{}{})
95103
if err != nil {
96104
return fmt.Errorf("failed to parse markdown file: %w", err)
97105
}
98106

99-
if bootstrap := frontmatter.Bootstrap; bootstrap != "" {
100-
hash := sha256.Sum256([]byte(bootstrap))
107+
// Check for a bootstrap file named <markdown-file-without-md-suffix>-bootstrap
108+
// For example, setup.md -> setup-bootstrap
109+
baseNameWithoutExt := strings.TrimSuffix(path, ".md")
110+
bootstrapFilePath := baseNameWithoutExt + "-bootstrap"
111+
112+
if bootstrapContent, err := os.ReadFile(bootstrapFilePath); err == nil {
113+
hash := sha256.Sum256(bootstrapContent)
101114
bootstrapPath := filepath.Join(bootstrapDir, fmt.Sprintf("%x", hash))
102-
if err := os.WriteFile(bootstrapPath, []byte(bootstrap), 0700); err != nil {
115+
if err := os.WriteFile(bootstrapPath, bootstrapContent, 0700); err != nil {
103116
return fmt.Errorf("failed to write bootstrap file: %w", err)
104117
}
105118
}

0 commit comments

Comments
 (0)