Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.
Merged
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
21 changes: 17 additions & 4 deletions cmd/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"os/signal"
"runtime"
"syscall"
"time"

Expand Down Expand Up @@ -44,7 +45,12 @@ Examples:
websocketUrl := viper.GetString("overrides.websocket_url")
websocketId, _ := cmd.Flags().GetString("websocket-id")

ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
signals := []os.Signal{os.Interrupt, syscall.SIGINT}
if runtime.GOOS != "windows" {
signals = append(signals, syscall.SIGTERM)
}

ctx, cancel := signal.NotifyContext(context.Background(), signals...)
defer cancel()

apiKey, userId := util.EnsureLoggedIn(ctx, log, cmd)
Expand Down Expand Up @@ -143,9 +149,15 @@ Examples:

go func() {
for {
defer cancel()
projectServerCmd.Wait()
log.Debug("project server exited")
log.Trace("waiting for project server to exit")
if err := projectServerCmd.Wait(); err != nil {
log.Error("project server exited with error: %s", err)
}
if projectServerCmd.ProcessState != nil {
log.Debug("project server exited with code %d", projectServerCmd.ProcessState.ExitCode())
} else {
log.Debug("project server exited")
}
log.Debug("isDeliberateRestart: %t", isDeliberateRestart)
if !isDeliberateRestart {
return
Expand All @@ -154,6 +166,7 @@ Examples:
// If it was a deliberate restart, start the new process here
if isDeliberateRestart {
isDeliberateRestart = false
log.Trace("restarting project server")
projectServerCmd, err = dev.CreateRunProjectCmd(ctx, log, theproject, websocketConn, dir, orgId, port)
if err != nil {
errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithContextMessage("Failed to run project")).ShowErrorAndExit()
Expand Down
8 changes: 7 additions & 1 deletion internal/bundler/bundler.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,10 +231,16 @@ func bundlePython(ctx BundleContext, dir string, outdir string, theproject *proj
func getAgents(theproject *project.Project, filename string) []AgentConfig {
var agents []AgentConfig
for _, agent := range theproject.Agents {
var agentfilename string
if theproject.Bundler.Language == "python" {
agentfilename = util.SafePythonFilename(agent.Name)
} else {
agentfilename = util.SafeFilename(agent.Name)
}
agents = append(agents, AgentConfig{
ID: agent.ID,
Name: agent.Name,
Filename: filepath.Join(theproject.Bundler.AgentConfig.Dir, util.SafeFilename(agent.Name), filename),
Filename: filepath.Join(theproject.Bundler.AgentConfig.Dir, agentfilename, filename),
})
}
return agents
Expand Down
2 changes: 1 addition & 1 deletion internal/templates/steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ func (s *CreateFileAction) Run(ctx TemplateContext) error {
return fmt.Errorf("failed to read embedded file: %w", err)
}
tmpl := template.New(s.Template)
tmpl, err = funcTemplates(tmpl).Parse(string(tbuf))
tmpl, err = funcTemplates(tmpl, ctx.Template.Language == "python").Parse(string(tbuf))
if err != nil {
return fmt.Errorf("failed to parse template: %w", err)
}
Expand Down
7 changes: 5 additions & 2 deletions internal/templates/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ type TemplateContext struct {
AgentuityCommand string
}

func funcTemplates(t *template.Template) *template.Template {
func funcTemplates(t *template.Template, isPython bool) *template.Template {
return t.Funcs(template.FuncMap{
"safe_filename": func(s string) string {
if isPython {
return util.SafePythonFilename(s)
}
return util.SafeFilename(s)
},
})
Expand All @@ -40,7 +43,7 @@ func (t *TemplateContext) Interpolate(val any) any {
if s, ok := val.(string); ok && s != "" && strings.Contains(s, "{{") {
tmpl := template.New(t.Name)
if strings.Contains(s, "|") { // slight optimization to avoid loading if not needed
tmpl = funcTemplates(tmpl)
tmpl = funcTemplates(tmpl, t.Template.Language == "python")
}
tmpl, err := tmpl.Parse(s)
if err != nil {
Expand Down
18 changes: 18 additions & 0 deletions internal/util/strings.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,29 @@ import (
)

var safeNameTransformer = regexp.MustCompile(`[^a-zA-Z0-9_-]`)
var safePythonNameTransformer = regexp.MustCompile(`[^a-zA-Z0-9_]`)
var beginsWithNumber = regexp.MustCompile(`^[0-9]+`)
var removeStartingDashes = regexp.MustCompile(`^[-]+`)
var removeEndingDashes = regexp.MustCompile(`[-]+$`)

func SafeFilename(name string) string {
return safeNameTransformer.ReplaceAllString(name, "-")
}

func SafePythonFilename(name string) string {
if beginsWithNumber.MatchString(name) {
name = beginsWithNumber.ReplaceAllString(name, "")
}
name = safePythonNameTransformer.ReplaceAllString(name, "_")
if removeStartingDashes.MatchString(name) {
name = removeStartingDashes.ReplaceAllString(name, "")
}
if removeEndingDashes.MatchString(name) {
name = removeEndingDashes.ReplaceAllString(name, "")
}
return name
}

func Pluralize(count int, singular string, plural string) string {
if count == 0 {
return fmt.Sprintf("no %s", plural)
Expand Down
52 changes: 52 additions & 0 deletions internal/util/strings_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package util

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -52,3 +53,54 @@ func TestPluralize(t *testing.T) {
})
}
}

func TestSafePythonFilename(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
// Basic cases
{"empty string", "", ""},
{"simple name", "module", "module"},
{"with underscores", "my_module", "my_module"},
{"with numbers", "module123", "module123"},
{"mixed case", "MyModule", "MyModule"},

// Special characters
{"with spaces", "my module", "my_module"},
{"with hyphens", "my-module", "my_module"},
{"with dots", "my.module", "my_module"},
{"with multiple special chars", "my@#$%^&*()module", "my_________module"},

// Numbers at start
{"starts with number", "123module", "module"},
{"starts with number and special chars", "123@#$module", "___module"},
{"starts with number and spaces", "123 module", "_module"},

// Edge cases
{"all special chars", "@#$%^&*()", "_________"},
{"all numbers", "12345", ""},
{"single underscore", "_", "_"},
{"multiple underscores", "___", "___"},

// Python keywords
{"python keyword", "import", "import"},
{"python keyword with special chars", "def@#$", "def___"},

// Unicode
{"unicode characters", "módulé", "m_dul_"},
{"unicode with numbers", "módulé123", "m_dul_123"},

// Length considerations
{"very long name", "a" + strings.Repeat("b", 100) + "c", "a" + strings.Repeat("b", 100) + "c"},
{"single character", "a", "a"},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := SafePythonFilename(test.input)
assert.Equal(t, test.expected, result)
})
}
}
Loading