Skip to content

Commit 88a5281

Browse files
committed
fraglet-entrypoint to support full replacements
1 parent b684fb5 commit 88a5281

24 files changed

Lines changed: 352 additions & 36 deletions

entrypoint/cmd/main.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ func markerDisplay(inj fragletpkg.InjectionConfig) string {
153153
if inj.MatchStart != "" && inj.MatchEnd != "" {
154154
return fmt.Sprintf("`%s` ... `%s`", inj.MatchStart, inj.MatchEnd)
155155
}
156+
// Direct file replacement mode: codePath set without match markers
157+
if inj.CodePath != "" {
158+
return "entire file replacement (no marker)"
159+
}
156160
return "<no marker configured>"
157161
}
158162

@@ -166,6 +170,22 @@ func normalizePath(path string) string {
166170
}
167171

168172
func extractExampleCode(inj fragletpkg.InjectionConfig) string {
173+
// Direct file replacement mode: return entire file content
174+
if inj.CodePath != "" && inj.Match == "" && inj.MatchStart == "" {
175+
data, err := os.ReadFile(inj.CodePath)
176+
if err != nil {
177+
// Return a simple placeholder if target file doesn't exist
178+
return "echo 'Hello, World!'"
179+
}
180+
content := string(data)
181+
// Trim trailing newlines
182+
content = strings.TrimRight(content, "\n")
183+
if content == "" {
184+
return "echo 'Hello, World!'"
185+
}
186+
return content
187+
}
188+
169189
// Read the target file to extract the code at the marker location
170190
data, err := os.ReadFile(inj.CodePath)
171191
if err != nil {

entrypoint/internal/fraglet/injector.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package fraglet
22

33
import (
4+
"fmt"
5+
"io"
46
"os"
57

68
"github.com/ofthemachine/fraglet/pkg/fraglet"
@@ -15,11 +17,69 @@ func NewInjector() *Injector {
1517

1618
// Inject performs fraglet injection using the file injector.
1719
// The injection config already contains the CodePath, so we just pass it through.
20+
// If codePath is set without match/match_start markers, performs direct file replacement.
1821
func (i *Injector) Inject(fragletPath string, injection fraglet.InjectionConfig) error {
22+
// Detect direct file replacement mode: codePath set, no match markers
23+
if injection.CodePath != "" && injection.Match == "" && injection.MatchStart == "" {
24+
if err := copyFile(fragletPath, injection.CodePath); err != nil {
25+
return fmt.Errorf("direct file replacement failed: %w", err)
26+
}
27+
// Remove temp fraglet file after copy
28+
_ = os.Remove(fragletPath)
29+
return nil
30+
}
31+
32+
// Existing template injection logic
1933
if err := fraglet.InjectFile(fragletPath, &injection); err != nil {
2034
return err
2135
}
2236
// Remove temp fraglet file after injection
2337
_ = os.Remove(fragletPath)
2438
return nil
2539
}
40+
41+
// copyFile copies the source file to the destination, preserving file permissions.
42+
func copyFile(src, dst string) error {
43+
// Read source file
44+
srcFile, err := os.Open(src)
45+
if err != nil {
46+
return fmt.Errorf("failed to open source file: %w", err)
47+
}
48+
defer srcFile.Close()
49+
50+
// Get source file info for permissions
51+
srcInfo, err := srcFile.Stat()
52+
if err != nil {
53+
return fmt.Errorf("failed to stat source file: %w", err)
54+
}
55+
56+
// Preserve destination file mode if it exists, otherwise use source mode
57+
mode := srcInfo.Mode()
58+
if dstInfo, err := os.Stat(dst); err == nil {
59+
mode = dstInfo.Mode()
60+
}
61+
62+
// Create destination file
63+
dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
64+
if err != nil {
65+
return fmt.Errorf("failed to create destination file: %w", err)
66+
}
67+
68+
// Copy contents
69+
if _, err := io.Copy(dstFile, srcFile); err != nil {
70+
dstFile.Close()
71+
return fmt.Errorf("failed to copy file contents: %w", err)
72+
}
73+
74+
// Close file before chmod to ensure data is flushed
75+
if err := dstFile.Close(); err != nil {
76+
return fmt.Errorf("failed to close destination file: %w", err)
77+
}
78+
79+
// Ensure file permissions are set correctly after writing
80+
if err := os.Chmod(dst, mode); err != nil {
81+
return fmt.Errorf("failed to set file permissions: %w", err)
82+
}
83+
84+
return nil
85+
}

entrypoint/releases/v0.3.0.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Release v0.3.0
2+
3+
## Key Highlights
4+
- **Direct file replacement**: fraglet-entrypoint can now overwrite an entire target file when `injection.codePath` is provided without match markers. Enables Brainfuck-style workflows where the program is the whole file.
5+
- **Smarter config parsing**: loader now distinguishes between line, range, and file injections instead of silently resetting `codePath`-only configs.
6+
- **Expanded test coverage**: integration suite exercises the file replacement path and new unit tests lock the injection-mode helpers.
7+
8+
## ✨ New Features
9+
10+
### Direct File Replacement (Brainfuck-ready)
11+
- Detects `codePath` without `match` / `match_start` / `match_end` and copies the fraglet payload directly over the destination file.
12+
- Runs as the container user, preserving permissions and honoring `execution.makeExecutable`.
13+
- Works seamlessly with existing `usage` / `guide` commands; usage now reports “entire file replacement” when no markers exist.
14+
15+
### Injection Mode Helpers
16+
- Added `isLineInjection`, `isRangeInjection`, and `isFileInjection` helpers.
17+
- `isEmptyInjection` now only fires when *no* injection mode is configured, preventing accidental fallbacks to defaults.
18+
19+
## 🔧 Configuration
20+
21+
### File Replacement Example (`fraglet.yaml`)
22+
```yaml
23+
fragletTempPath: /FRAGLET
24+
injection:
25+
codePath: /code/hello-world.bf # no markers → entire file is replaced
26+
guide: /guide.md
27+
execution:
28+
path: /code/hello-world.bf
29+
makeExecutable: true
30+
```
31+
32+
### Line / Range Modes (unchanged)
33+
- Still specify `match` for single-line replacement or `match_start` + `match_end` for regions.
34+
- You can mix in a custom `codePath` with either mode.
35+
36+
## 🧪 Testing
37+
- `entrypoint/tests/docker_integration` now includes **Test 3** which mounts a shebang’d fraglet, replaces `/code/hello-world-replace.sh`, and proves execution works end-to-end.
38+
- Added `pkg/fraglet/config_test.go` to enforce the injection helper behavior.
39+
- `make test-entrypoint` and `go test ./pkg/fraglet/...` must be green before tagging.
40+
41+
## 🚀 Building the Binary
42+
```bash
43+
cd entrypoint
44+
GOOS=linux GOARCH=amd64 go build -o dist/linux_amd64/fraglet-entrypoint ./cmd
45+
# Repeat for other arches as needed
46+
```
47+
48+
## 🧩 Integrating with 100hellos/brainfuck
49+
1. Copy the new `fraglet-entrypoint` binary into the image (e.g. `/usr/local/bin/fraglet-entrypoint`).
50+
2. Add a `fraglet.yaml` like the file replacement example above.
51+
3. Set the container entrypoint to the binary:
52+
```dockerfile
53+
COPY fraglet-entrypoint /usr/local/bin/fraglet-entrypoint
54+
COPY fraglet-brainfuck.yaml /fraglet.yaml
55+
ENTRYPOINT ["/usr/local/bin/fraglet-entrypoint"]
56+
```
57+
4. Build the image (tag `100hellos/brainfuck:local`) and test it via MCP:
58+
```bash
59+
docker build -t 100hellos/brainfuck:local -f brainfuck/Dockerfile .
60+
mcp run brainfuck '++++++++++[>+>+++>+++++++>++++++++++<<<<-]>>>++.'
61+
```
62+
63+

entrypoint/tests/docker_integration/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ COPY fraglet-entrypoint /usr/local/bin/fraglet-entrypoint
99
# Copy all files from files/ directory to root
1010
COPY ./files /
1111

12-
# Make the script executable
13-
RUN chmod +x /code/hello-world.sh
12+
# Make the scripts executable
13+
RUN chmod +x /code/hello-world.sh /code/hello-world-range.sh /code/hello-world-replace.sh
1414

1515
# Set the entrypoint
1616
ENTRYPOINT ["/usr/local/bin/fraglet-entrypoint"]

entrypoint/tests/docker_integration/act.sh

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@ set -e
44
# Build the Docker image (binary is already in temp dir from harness)
55
docker build -t fraglet-test:latest -f Dockerfile . > /dev/null 2>&1
66

7-
# Create a temporary fraglet file
7+
# Create temporary fraglet files
88
echo 'echo "🎉 Fraglet injected successfully!"' > /tmp/test-fraglet.sh
9+
cat <<'EOF' > /tmp/test-fraglet-exec.sh
10+
#!/bin/sh
11+
echo "🎉 Fraglet injected successfully!"
12+
EOF
13+
chmod +x /tmp/test-fraglet-exec.sh
914

1015
# Test 1: Single match substitution (default config)
1116
echo "=== Test 1: Single match ==="
@@ -16,7 +21,15 @@ echo ""
1621
echo "=== Test 2: Range-based match ==="
1722
docker run --rm \
1823
-v /tmp/test-fraglet.sh:/FRAGLET:ro \
19-
-e FRAGLET_CONFIG=/fraglet-alternative.yaml \
24+
-e FRAGLET_CONFIG=/fraglet-range.yaml \
25+
fraglet-test:latest 2>&1
26+
27+
# Test 3: Direct file replacement (no match markers)
28+
echo ""
29+
echo "=== Test 3: Direct file replacement ==="
30+
docker run --rm \
31+
-v /tmp/test-fraglet-exec.sh:/FRAGLET:ro \
32+
-e FRAGLET_CONFIG=/fraglet-replacement.yaml \
2033
fraglet-test:latest 2>&1
2134

2235
# Test the usage command
@@ -25,6 +38,11 @@ echo "---"
2538
echo "Testing usage:"
2639
docker run --rm fraglet-test:latest usage
2740

41+
echo ""
42+
echo "---"
43+
echo "Testing usage (replacement config):"
44+
docker run --rm -e FRAGLET_CONFIG=/fraglet-replacement.yaml fraglet-test:latest usage
45+
2846
echo ""
2947
echo "---"
3048
echo "Testing guide:"

entrypoint/tests/docker_integration/assert.txt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ A
88
🎉 Fraglet injected successfully!
99
C
1010

11+
=== Test 3: Direct file replacement ===
12+
🎉 Fraglet injected successfully!
13+
1114
---
1215
Testing usage:
1316
# Container Usage
@@ -44,6 +47,47 @@ docker run --rm -v /tmp/fraglet.sh:/FRAGLET:ro <container>
4447

4548
## Documentation
4649

50+
- `usage` - Container usage (this document)
51+
- `guide` - Authoring guide for writing fraglets
52+
53+
---
54+
Testing usage (replacement config):
55+
# Container Usage
56+
57+
## Mount and Run
58+
59+
Mount your fraglet code to /FRAGLET and run:
60+
61+
```bash
62+
docker run --rm -v /path/to/fraglet:/FRAGLET:ro <container>
63+
```
64+
65+
## Code Injection
66+
67+
Code will be injected into `/code/hello-world-replace.sh` at the marker: entire file replacement (no marker)
68+
69+
## Execution
70+
71+
After injection, the container executes: `/code/hello-world-replace.sh`
72+
73+
## Example
74+
75+
Here's a functional example using the existing code:
76+
77+
```bash
78+
# Read the existing code
79+
cat > /tmp/fraglet.sh << 'EOF'
80+
#!/bin/bash
81+
echo "This file will be completely replaced"
82+
echo "Original content here"
83+
EOF
84+
85+
# Mount and run
86+
docker run --rm -v /tmp/fraglet.sh:/FRAGLET:ro <container>
87+
```
88+
89+
## Documentation
90+
4791
- `usage` - Container usage (this document)
4892
- `guide` - Authoring guide for writing fraglets
4993
---
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/bash
2+
echo "This file will be completely replaced"
3+
echo "Original content here"
4+

entrypoint/tests/docker_integration/files/fraglet-alternative.yaml renamed to entrypoint/tests/docker_integration/files/fraglet-range.yaml

File renamed without changes.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
fragletTempPath: /FRAGLET
2+
injection:
3+
codePath: /code/hello-world-replace.sh
4+
guide: /guide.md
5+
execution:
6+
path: /code/hello-world-replace.sh
7+
makeExecutable: true
8+

envelopes/brainfuck.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
name: brainfuck
2+
language: brainfuck
3+
container: 100hellos/brainfuck:local
4+
fragletPath: /FRAGLET
5+

0 commit comments

Comments
 (0)