diff --git a/cmd/dev.go b/cmd/dev.go index 710bc0c9..b4a90991 100644 --- a/cmd/dev.go +++ b/cmd/dev.go @@ -73,6 +73,14 @@ Examples: websocketId = cstr.NewHash(orgId, userId) } + port, _ := cmd.Flags().GetInt("port") + if port == 0 { + port, err = dev.FindAvailablePort(theproject) + if err != nil { + log.Fatal("failed to find available port: %s", err) + } + } + websocketConn, err := dev.NewWebsocket(dev.WebsocketArgs{ Ctx: ctx, Logger: log, @@ -88,11 +96,6 @@ Examples: } defer websocketConn.Close() - port, err := dev.FindAvailablePort(theproject) - if err != nil { - log.Fatal("failed to find available port: %s", err) - } - 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() @@ -222,6 +225,7 @@ func init() { devCmd.Flags().StringP("dir", "d", ".", "The directory to run the development server in") devCmd.Flags().String("websocket-id", "", "The websocket room id to use for the development agent") devCmd.Flags().String("org-id", "", "The organization to run the project") + devCmd.Flags().Int("port", 0, "The port to run the development server on (uses project default if not provided)") devCmd.Flags().MarkHidden("websocket-id") devCmd.Flags().MarkHidden("org-id") } diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index 6a91748c..d241d8dd 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -120,7 +120,7 @@ func bundleJavascript(ctx BundleContext, dir string, outdir string, theproject * defines["process.env.AGENTUITY_ENVIRONMENT"] = fmt.Sprintf("'%s'", "development") } } - defines["process.env.AGENTUITY_CLOUD_AGENTS_JSON"] = fmt.Sprintf("'%s'", cstr.JSONStringify(agents)) + defines["process.env.AGENTUITY_CLOUD_AGENTS_JSON"] = cstr.JSONStringify(cstr.JSONStringify(agents)) ctx.Logger.Debug("starting build") started := time.Now() diff --git a/internal/dev/websocket.go b/internal/dev/websocket.go index 7526d104..cbd6ec31 100644 --- a/internal/dev/websocket.go +++ b/internal/dev/websocket.go @@ -80,27 +80,47 @@ func (c *Websocket) Done() <-chan struct{} { return c.done } +const maxHealthCheckDuration = time.Second * 30 + +func isConnectionErrorRetryable(err error) bool { + if strings.Contains(err.Error(), "connection refused") { + return true + } + if strings.Contains(err.Error(), "connection reset by peer") { + return true + } + if strings.Contains(err.Error(), "No connection could be made because the target machine actively refused it") { // windows + return true + } + return false +} + func (c *Websocket) getAgentProtocol(ctx context.Context, port int) (bool, error) { url := fmt.Sprintf("http://127.0.0.1:%d/_health", port) - for i := 0; i < 5; i++ { + started := time.Now() + var i int + for time.Since(started) < maxHealthCheckDuration { + i++ req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return false, err } resp, err := http.DefaultClient.Do(req) if err != nil { - if strings.Contains(err.Error(), "connection refused") { + c.logger.Debug("healthcheck attempt #%d failed: %s", i, err) + if isConnectionErrorRetryable(err) { time.Sleep(time.Millisecond * time.Duration(100*i+1)) continue } return false, err } + c.logger.Debug("healthcheck attempt #%d succeeded with status code %d", i, resp.StatusCode) defer resp.Body.Close() if resp.StatusCode == 200 { return resp.Header.Get("x-agentuity-binary") == "true", nil } } - return false, fmt.Errorf("failed to inspect agents after 5 attempts") + return false, fmt.Errorf("failed to inspect agents after %s", maxHealthCheckDuration) } func (c *Websocket) getAgentWelcome(ctx context.Context, port int) (map[string]Welcome, error) { @@ -112,7 +132,7 @@ func (c *Websocket) getAgentWelcome(ctx context.Context, port int) (map[string]W } resp, err := http.DefaultClient.Do(req) if err != nil { - if strings.Contains(err.Error(), "connection refused") { + if isConnectionErrorRetryable(err) { time.Sleep(time.Millisecond * time.Duration(100*i+1)) continue } @@ -135,7 +155,7 @@ func (c *Websocket) StartReadingMessages(ctx context.Context, logger logger.Logg var err error c.binaryProtocol, err = c.getAgentProtocol(ctx, port) if err != nil { - logger.Error("failed to healthcheck agents: %s", err) + logger.Fatal("Your project failed to start. This typically happens when the project cannot compile or this is an underlying issue with starting the project.") return } diff --git a/internal/project/project.go b/internal/project/project.go index d9b8ec9c..6493663c 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -8,6 +8,7 @@ import ( "net/url" "os" "path/filepath" + "strings" "github.com/Masterminds/semver" "github.com/agentuity/cli/internal/errsystem" @@ -486,6 +487,13 @@ func LoadProject(logger logger.Logger, dir string, apiUrl string, appUrl string, } } +func isVersionCheckRequired(ver string) bool { + if ver != "" && ver != "dev" && !strings.Contains(ver, "-next") { + return true + } + return false +} + func EnsureProject(ctx context.Context, cmd *cobra.Command) ProjectContext { logger := env.NewLogger(cmd) dir := ResolveProjectDir(logger, cmd, true) @@ -498,7 +506,7 @@ func EnsureProject(ctx context.Context, cmd *cobra.Command) ProjectContext { token, _ = util.EnsureLoggedIn(ctx, logger, cmd) } p := LoadProject(logger, dir, apiUrl, appUrl, transportUrl, token) - if !p.NewProject && Version != "" && Version != "dev" && p.Project.Version != "" { + if !p.NewProject && isVersionCheckRequired(Version) && p.Project.Version != "" { v := semver.MustParse(Version) c, err := semver.NewConstraint(p.Project.Version) if err != nil { @@ -538,7 +546,7 @@ func TryProject(ctx context.Context, cmd *cobra.Command) ProjectContext { } } p := LoadProject(logger, dir, apiUrl, appUrl, transportUrl, token) - if !p.NewProject && Version != "" && Version != "dev" && p.Project.Version != "" { + if !p.NewProject && isVersionCheckRequired(Version) && p.Project.Version != "" { v := semver.MustParse(Version) c, err := semver.NewConstraint(p.Project.Version) if err != nil {