diff --git a/cmd/dev.go b/cmd/dev.go index b4a90991..58569ab9 100644 --- a/cmd/dev.go +++ b/cmd/dev.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "os/signal" + "runtime" "syscall" "time" @@ -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) @@ -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 @@ -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() diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index d241d8dd..19c9dfd0 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -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 diff --git a/internal/templates/steps.go b/internal/templates/steps.go index 64f8d951..3526bccb 100644 --- a/internal/templates/steps.go +++ b/internal/templates/steps.go @@ -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) } diff --git a/internal/templates/template.go b/internal/templates/template.go index b52fa4f0..9457e66f 100644 --- a/internal/templates/template.go +++ b/internal/templates/template.go @@ -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) }, }) @@ -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 { diff --git a/internal/util/strings.go b/internal/util/strings.go index 9d7e1856..89067dc5 100644 --- a/internal/util/strings.go +++ b/internal/util/strings.go @@ -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) diff --git a/internal/util/strings_test.go b/internal/util/strings_test.go index 087fe246..5f791299 100644 --- a/internal/util/strings_test.go +++ b/internal/util/strings_test.go @@ -1,6 +1,7 @@ package util import ( + "strings" "testing" "github.com/stretchr/testify/assert" @@ -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) + }) + } +}