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
1 change: 1 addition & 0 deletions cmd/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Examples:
Production: production,
Install: install,
CI: ci,
Writer: os.Stderr,
}); err != nil {
errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithContextMessage("Failed to bundle project")).ShowErrorAndExit()
}
Expand Down
185 changes: 96 additions & 89 deletions cmd/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"context"
"fmt"
"io"
"os"
"os/signal"
"runtime"
Expand All @@ -15,11 +16,9 @@ import (
"github.com/agentuity/cli/internal/project"
"github.com/agentuity/cli/internal/util"
"github.com/agentuity/go-common/env"
cstr "github.com/agentuity/go-common/string"
"github.com/agentuity/go-common/tui"
"github.com/bep/debounce"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

var devCmd = &cobra.Command{
Expand All @@ -41,9 +40,8 @@ Examples:
agentuity dev --dir /path/to/project`,
Run: func(cmd *cobra.Command, args []string) {
log := env.NewLogger(cmd)
_, appUrl, _ := util.GetURLs(log)
websocketUrl := viper.GetString("overrides.websocket_url")
websocketId, _ := cmd.Flags().GetString("websocket-id")
logLevel := env.LogLevel(cmd)
apiUrl, appUrl, transportUrl := util.GetURLs(log)

signals := []os.Signal{os.Interrupt, syscall.SIGINT}
if runtime.GOOS != "windows" {
Expand Down Expand Up @@ -75,10 +73,6 @@ Examples:

orgId := project.OrgId

if websocketId == "" {
websocketId = cstr.NewHash(orgId, userId)
}

port, _ := cmd.Flags().GetInt("port")
if port == 0 {
port, err = dev.FindAvailablePort(theproject)
Expand All @@ -87,184 +81,197 @@ Examples:
}
}

websocketConn, err := dev.NewWebsocket(dev.WebsocketArgs{
serverAddr, _ := cmd.Flags().GetString("server")

server, err := dev.New(dev.ServerArgs{
Ctx: ctx,
Logger: log,
WebsocketId: websocketId,
WebsocketUrl: websocketUrl,
LogLevel: logLevel,
APIURL: apiUrl,
TransportURL: transportUrl,
APIKey: apiKey,
OrgId: orgId,
Project: theproject,
Version: Version,
OrgId: orgId,
UserId: userId,
Port: port,
ServerAddr: serverAddr,
})
if err != nil {
log.Fatal("failed to create live dev connection: %s", err)
}
defer websocketConn.Close()
defer server.Close()

processCtx := context.Background()
var pid int

projectServerCmd, err := dev.CreateRunProjectCmd(processCtx, log, theproject, websocketConn, dir, orgId, port)
consoleUrl := server.WebURL(appUrl)
devModeUrl := fmt.Sprintf("http://127.0.0.1:%d", port)

agents := make([]*dev.Agent, 0)
for _, agent := range theproject.Project.Agents {
agents = append(agents, &dev.Agent{
ID: agent.ID,
Name: agent.Name,
LocalURL: fmt.Sprintf("%s/%s", devModeUrl, agent.ID),
})
}

ui := dev.NewDevModeUI(ctx, dev.DevModeConfig{
DevModeUrl: devModeUrl,
AppUrl: consoleUrl,
Agents: agents,
})

ui.Start()

defer ui.Close(false)

tuiLogger := dev.NewTUILogger(logLevel, ui)

if err := server.Connect(ui, tuiLogger); err != nil {
log.Error("failed to start live dev connection: %s", err)
ui.Close(true)
return
}

publicUrl := server.PublicURL(appUrl)
ui.SetPublicURL(publicUrl)

for _, agent := range agents {
agent.PublicURL = fmt.Sprintf("%s/%s", publicUrl, agent.ID)
}

projectServerCmd, err := dev.CreateRunProjectCmd(processCtx, tuiLogger, theproject, server, dir, orgId, port, tuiLogger)
if err != nil {
errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithContextMessage("Failed to run project")).ShowErrorAndExit()
}

build := func(initial bool) {
build := func(initial bool) bool {
started := time.Now()
var ok bool
tui.ShowSpinner("Building project ...", func() {
ui.ShowSpinner("Building project ...", func() {
var w io.Writer = tuiLogger
if err := bundler.Bundle(bundler.BundleContext{
Context: ctx,
Logger: log,
Logger: tuiLogger,
ProjectDir: dir,
Production: false,
DevMode: !initial,
DevMode: true,
Writer: w,
}); err != nil {
if err == bundler.ErrBuildFailed {
log.Error("build failed ...")
return
}
errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithContextMessage(fmt.Sprintf("Failed to bundle project: %s", err))).ShowErrorAndExit()
}
ok = true
})
if ok {
fmt.Println(tui.Text(fmt.Sprintf("✨ Built in %s", time.Since(started).Round(time.Millisecond))))
if ok && !initial {
ui.SetStatusMessage("✨ Built in %s", time.Since(started).Round(time.Millisecond))
}
return ok
}

// Initial build
build(true)
// Initial build must exit if it fails
if !build(true) {
ui.Close(true)
return
}

restart := func() {
isDeliberateRestart = true
build(false)
log.Debug("killing project server")
dev.KillProjectServer(log, projectServerCmd, pid)
log.Debug("killing project server done")
tuiLogger.Debug("killing project server")
dev.KillProjectServer(tuiLogger, projectServerCmd, pid)
tuiLogger.Debug("killing project server done")
}

ui.SetStatusMessage("starting ...")
ui.SetSpinner(true)

// debounce a lot of changes at once to avoid multiple restarts in succession
debounced := debounce.New(250 * time.Millisecond)

// Watch for changes
watcher, err := dev.NewWatcher(log, dir, theproject.Project.Development.Watch.Files, func(path string) {
log.Trace("%s has changed", path)
watcher, err := dev.NewWatcher(tuiLogger, dir, theproject.Project.Development.Watch.Files, func(path string) {
tuiLogger.Trace("%s has changed", path)
debounced(restart)
})
if err != nil {
errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithContextMessage(fmt.Sprintf("Failed to start watcher: %s", err))).ShowErrorAndExit()
}
defer watcher.Close(log)
defer watcher.Close(tuiLogger)

if err := projectServerCmd.Start(); err != nil {
errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithContextMessage(fmt.Sprintf("Failed to start project: %s", err))).ShowErrorAndExit()
}

pid = projectServerCmd.Process.Pid

websocketConn.StartReadingMessages(ctx, log, port)
devUrl := websocketConn.WebURL(appUrl)
if err := server.HealthCheck(devModeUrl); err != nil {
tuiLogger.Error("failed to health check connection: %s", err)
ui.Close(true)
return
}

// Display local interaction instructions
displayLocalInstructions(port, theproject.Project.Agents, devUrl)
ui.SetStatusMessage("🚀 DevMode ready")
ui.SetSpinner(false)

go func() {
for {
log.Trace("waiting for project server to exit (pid: %d)", pid)
tuiLogger.Trace("waiting for project server to exit (pid: %d)", pid)
if err := projectServerCmd.Wait(); err != nil {
if !isDeliberateRestart {
log.Error("project server (pid: %d) exited with error: %s", pid, err)
tuiLogger.Error("project server (pid: %d) exited with error: %s", pid, err)
}
}
if projectServerCmd.ProcessState != nil {
log.Debug("project server (pid: %d) exited with code %d", pid, projectServerCmd.ProcessState.ExitCode())
tuiLogger.Debug("project server (pid: %d) exited with code %d", pid, projectServerCmd.ProcessState.ExitCode())
} else {
log.Debug("project server (pid: %d) exited", pid)
tuiLogger.Debug("project server (pid: %d) exited", pid)
}
log.Debug("isDeliberateRestart: %t, pid: %d", isDeliberateRestart, pid)
tuiLogger.Debug("isDeliberateRestart: %t, pid: %d", isDeliberateRestart, pid)
if !isDeliberateRestart {
return
}

// If it was a deliberate restart, start the new process here
if isDeliberateRestart {
isDeliberateRestart = false
log.Trace("restarting project server")
projectServerCmd, err = dev.CreateRunProjectCmd(processCtx, log, theproject, websocketConn, dir, orgId, port)
tuiLogger.Trace("restarting project server")
projectServerCmd, err = dev.CreateRunProjectCmd(processCtx, tuiLogger, theproject, server, dir, orgId, port, tuiLogger)
if err != nil {
errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithContextMessage("Failed to run project")).ShowErrorAndExit()
}
if err := projectServerCmd.Start(); err != nil {
errsystem.New(errsystem.ErrInvalidConfiguration, err, errsystem.WithContextMessage(fmt.Sprintf("Failed to start project: %s", err))).ShowErrorAndExit()
}
pid = projectServerCmd.Process.Pid
log.Trace("restarted project server (pid: %d)", pid)
tuiLogger.Trace("restarted project server (pid: %d)", pid)
}
}
}()

teardown := func() {
watcher.Close(log)
websocketConn.Close()
dev.KillProjectServer(log, projectServerCmd, pid)
watcher.Close(tuiLogger)
server.Close()
dev.KillProjectServer(tuiLogger, projectServerCmd, pid)
}

select {
case <-websocketConn.Done():
log.Info("live dev connection closed, shutting down")
case <-ui.Done():
teardown()
case <-ctx.Done():
log.Info("context done, shutting down")
teardown()
}
},
}

func displayLocalInstructions(port int, agents []project.AgentConfig, devModeUrl string) {
title := tui.Title("🚀 Local Agent Interaction")

// Combine all elements with appropriate spacing
fmt.Println()
fmt.Println(title)

// Create list of available agents
if len(agents) > 0 {
fmt.Println()

for _, agent := range agents {
// Display agent name and ID
fmt.Println(tui.Text(" • ") + tui.PadRight(agent.Name, 20, " ") + " " + tui.Muted(agent.ID))
}
}

// Get a sample agent ID if available
sampleAgentID := "agent_ID"
if len(agents) > 0 {
sampleAgentID = agents[0].ID
}

curlCommand := fmt.Sprintf("curl -v http://127.0.0.1:%d/%s --json '{\"input\": \"Hello, world!\"}'", port, sampleAgentID)

fmt.Println()
fmt.Println(tui.Text("To interact with your agents locally, you can use:"))
fmt.Println()
fmt.Println(tui.Highlight(curlCommand))
fmt.Println()

fmt.Print(tui.Text("Or use the 💻 Dev Mode in our app: "))
fmt.Println(tui.Link("%s", devModeUrl))

fmt.Println()
}

func init() {
rootCmd.AddCommand(devCmd)
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")
devCmd.Flags().String("server", "echo.agentuity.cloud", "the echo server to connect to")
devCmd.Flags().MarkHidden("server")
}
10 changes: 5 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.24.2

require (
github.com/Masterminds/semver v1.5.0
github.com/agentuity/go-common v1.0.47
github.com/agentuity/go-common v1.0.59
github.com/agentuity/mcp-golang/v2 v2.0.2
github.com/bep/debounce v1.2.1
github.com/bmatcuk/doublestar/v4 v4.8.1
Expand All @@ -16,7 +16,6 @@ require (
github.com/evanw/esbuild v0.25.0
github.com/fsnotify/fsnotify v1.7.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/marcozac/go-jsonc v0.1.1
github.com/mattn/go-isatty v0.0.20
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
Expand All @@ -25,6 +24,7 @@ require (
github.com/stretchr/testify v1.10.0
github.com/zijiren233/yaml-comment v0.2.2
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
golang.org/x/term v0.30.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.32.1
)
Expand All @@ -48,6 +48,7 @@ require (
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
Expand All @@ -58,7 +59,6 @@ require (
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/term v0.30.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

Expand All @@ -67,7 +67,7 @@ require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/buger/goterm v1.0.4 // indirect
github.com/catppuccin/go v0.2.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
Expand Down Expand Up @@ -131,7 +131,7 @@ require (
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/net v0.38.0
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.32.0
golang.org/x/text v0.23.0 // indirect
Expand Down
Loading
Loading