Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cmd/container-use/stdio.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package main

import (
"context"
"log/slog"
"os"

"dagger.io/dagger"
"github.com/dagger/container-use/environment"
"github.com/dagger/container-use/mcpserver"
"github.com/spf13/cobra"
)
Expand All @@ -30,10 +32,16 @@ var stdioCmd = &cobra.Command{
}
defer dag.Close()

go warmCache(ctx, dag)
return mcpserver.RunStdioServer(ctx, dag)
},
}

func warmCache(ctx context.Context, dag *dagger.Client) {
environment.EditUtil(dag).Sync(ctx)
environment.GrepUtil(dag).Sync(ctx)
}

func init() {
rootCmd.AddCommand(stdioCmd)
}
39 changes: 39 additions & 0 deletions edit/cmd/edit/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package main

import (
"fmt"
"io"
"os"
"strconv"

"github.com/tiborvass/replace"
"golang.org/x/text/transform"
)

func main() {
if len(os.Args)%3 != 0 || len(os.Args) < 6 {
fmt.Fprintf(os.Stderr, "usage: %s <source> <destination> <old_string1> <new_string1> <replace_count1> [...<old_stringN> <new_stringN> <replace_countN>]\n", os.Args[0])
fmt.Fprintln(os.Stderr, " Reads stream from source and replaces in it replace_count times, old_string with new_string and writes to destination.")
fmt.Fprintln(os.Stderr, " If replace_count is -1, it replaces all occurrences.")
os.Exit(1)
}
n := len(os.Args)/3 - 1
t := make([]transform.Transformer, n)
for i := range t {
replaceCount, err := strconv.Atoi(os.Args[5+i])
if err != nil {
fmt.Fprintf(os.Stderr, "replace_count must be an integer, received: %q\n", replaceCount)
}
t[i] = replace.StringN(os.Args[3+i], os.Args[4+i], replaceCount)
}
src, err := os.Open(os.Args[1])
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
dest, err := os.Create(os.Args[2])
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
io.Copy(dest, replace.Chain(src, t...))
}
12 changes: 12 additions & 0 deletions edit/edit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package edit

import _ "embed"

//go:embed cmd/edit/main.go
var Src string

//go:embed go.mod
var GoMod string

//go:embed go.sum
var GoSum string
10 changes: 10 additions & 0 deletions edit/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module github.com/dagger/container-use

go 1.24.3

require (
github.com/tiborvass/replace v0.0.0-20250708165616-d642c0f9c3ff
golang.org/x/text v0.26.0
)

require github.com/google/go-cmp v0.6.0 // indirect
20 changes: 20 additions & 0 deletions edit/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/tiborvass/replace v0.0.0-20250708165616-d642c0f9c3ff h1:zpP7bpKTsqLLgsUngIXlmd1X9PpJD1Bca1dQfYacNf8=
github.com/tiborvass/replace v0.0.0-20250708165616-d642c0f9c3ff/go.mod h1:9+jQ4zDLeiANhfwMbvl6qKw/sW+aN1m6AjhxHZKN40s=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
8 changes: 6 additions & 2 deletions environment/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ import (
)

const (
defaultImage = "ubuntu:24.04"
alpineImage = "alpine:3.21.3@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c"
defaultImage = "ubuntu:24.04"

// WARNING: for maximum efficiency, please ensure golangImage is always based on this specific alpineImage
alpineImage = "alpine:3.22.0@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be023728e11715"
golangImage = "golang:1.24.5-alpine@sha256:ddf52008bce1be455fe2b22d780b6693259aaf97b16383b6372f4b22dd33ad66"

configDir = ".container-use"
environmentFile = "environment.json"
)
Expand Down
79 changes: 75 additions & 4 deletions environment/filesystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package environment

import (
"context"
_ "embed"
"fmt"
"strings"

"dagger.io/dagger"
"github.com/dagger/container-use/edit"
)

func (env *Environment) FileRead(ctx context.Context, targetFile string, shouldReadEntireFile bool, startLineOneIndexedInclusive int, endLineOneIndexedInclusive int) (string, error) {
Expand Down Expand Up @@ -36,7 +40,7 @@ func (env *Environment) FileRead(ctx context.Context, targetFile string, shouldR
return strings.Join(lines[start:end], "\n"), nil
}

func (env *Environment) FileWrite(ctx context.Context, explanation, targetFile, contents string) error {
func (env *Environment) FileWrite(ctx context.Context, targetFile, contents string) error {
err := env.apply(ctx, env.container().WithNewFile(targetFile, contents))
if err != nil {
return fmt.Errorf("failed applying file write, skipping git propagation: %w", err)
Expand All @@ -45,7 +49,7 @@ func (env *Environment) FileWrite(ctx context.Context, explanation, targetFile,
return nil
}

func (env *Environment) FileDelete(ctx context.Context, explanation, targetFile string) error {
func (env *Environment) FileDelete(ctx context.Context, targetFile string) error {
err := env.apply(ctx, env.container().WithoutFile(targetFile))
if err != nil {
return fmt.Errorf("failed applying file delete, skipping git propagation: %w", err)
Expand All @@ -54,8 +58,18 @@ func (env *Environment) FileDelete(ctx context.Context, explanation, targetFile
return nil
}

func (env *Environment) FileList(ctx context.Context, path string) (string, error) {
entries, err := env.container().Directory(path).Entries(ctx)
func (env *Environment) FileList(ctx context.Context, path string, ignore []string) (string, error) {
filter := dagger.DirectoryFilterOpts{Exclude: ignore}
return env.ls(ctx, path, filter)
}

func (env *Environment) FileGlob(ctx context.Context, path string, pattern string) (string, error) {
filter := dagger.DirectoryFilterOpts{Include: []string{pattern}}
return env.ls(ctx, path, filter)
}

func (env *Environment) ls(ctx context.Context, path string, filter dagger.DirectoryFilterOpts) (string, error) {
entries, err := env.container().Directory(path).Filter(filter).Entries(ctx)
if err != nil {
return "", err
}
Expand All @@ -65,3 +79,60 @@ func (env *Environment) FileList(ctx context.Context, path string) (string, erro
}
return out.String(), nil
}

func (env *Environment) FileGrep(ctx context.Context, path, pattern, include string) (string, error) {
// Hack: use busybox to run `sed` since dagger doesn't have native file editing primitives.
args := []string{"/usr/bin/rg", "--no-unicode", "-g", include, "--", pattern, "."}

dir := env.container().Directory(path)
out, err := GrepUtil(env.dag).
WithMountedDirectory("/workdir", dir).
WithWorkdir("/workdir").
WithExec(args, dagger.ContainerWithExecOpts{Expect: dagger.ReturnTypeAny}).
Stdout(ctx)
if err != nil {
return "", err
}
return out, nil
}

type FileEdit struct {
OldString string
NewString string
ReplaceAll bool
}

func EditUtil(dag *dagger.Client) *dagger.Container {
editBin := dag.Container().From(golangImage).
WithNewFile("/go/src/edit.go", edit.Src).
WithNewFile("/go/src/go.mod", edit.GoMod).
WithNewFile("/go/src/go.sum", edit.GoSum).
WithEnvVariable("CGO_ENABLED", "0").
WithWorkdir("/go/src").
WithExec([]string{"go", "build", "-o", "/edit", "-ldflags", "-w -s", "/go/src/edit.go"}).File("/edit")
return dag.Container().From(alpineImage).WithFile("/edit", editBin).WithEntrypoint([]string{"/edit"})
}

func GrepUtil(dag *dagger.Client) *dagger.Container {
return dag.Container().From(alpineImage).WithExec([]string{"apk", "add", "-U", "ripgrep"})
}

func (env *Environment) FileEdit(ctx context.Context, targetFile string, edits []FileEdit) error {
// Hack: use busybox to run `sed` since dagger doesn't have native file editing primitives.
args := []string{"/edit", "/target", "/new"}
for _, edit := range edits {
replaceCount := "1"
if edit.ReplaceAll {
replaceCount = "-1"
}
args = append(args, edit.OldString, edit.NewString, replaceCount)
}

newFile := EditUtil(env.dag).WithMountedFile("/target", env.container().File(targetFile)).WithExec(args).File("/new")
err := env.apply(ctx, env.container().WithFile(targetFile, newFile))
if err != nil {
return fmt.Errorf("failed applying file edit, skipping git propagation: %w", err)
}
env.Notes.Add("Edit %s", targetFile)
return nil
}
2 changes: 1 addition & 1 deletion environment/integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ func TestWeirdUserScenarios(t *testing.T) {
defer repo1.Delete(ctx, env1.ID)

// Write file in env1
err = env1.FileWrite(ctx, "Add file", "app.js", "console.log('repo1');")
err = env1.FileWrite(ctx, "app.js", "console.log('repo1');")
require.NoError(t, err)

// Try to use env1 while in repo2 (should fail)
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/charmbracelet/bubbletea v1.3.5
github.com/charmbracelet/fang v0.3.0
github.com/charmbracelet/lipgloss v1.1.0
github.com/dagger/container-use/edit v0.0.0-00010101000000-000000000000
github.com/dustin/go-humanize v1.0.1
github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0
github.com/mark3labs/mcp-go v0.29.0
Expand Down Expand Up @@ -87,3 +88,5 @@ require (
google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
)

replace github.com/dagger/container-use/edit => ./edit
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
dagger.io/dagger v0.18.11 h1:6lSfemlbGM2HmdOjhgevrX2+orMDGKU/xTaBMZ+otyY=
dagger.io/dagger v0.18.11/go.mod h1:azlZ24m2br95t0jQHUBpL5SiafeqtVDLl1Itlq6GO+4=
dagger.io/dagger v0.18.12 h1:s7v8aHlzDUogZ/jW92lHC+gljCNRML+0mosfh13R4vs=
dagger.io/dagger v0.18.12/go.mod h1:azlZ24m2br95t0jQHUBpL5SiafeqtVDLl1Itlq6GO+4=
github.com/99designs/gqlgen v0.17.75 h1:GwHJsptXWLHeY7JO8b7YueUI4w9Pom6wJTICosDtQuI=
Expand Down
Loading
Loading