on main help sign in
feat: send server client version info
1b37d55Ayman Bagabas
internal/app/app.go+9
@@ -148,6 +148,15 @@ func (app *App) Events() <-chan tea.Msg {
148 return app.events
148 return app.events
149}
149}
150
150
151+// SendEvent pushes a message into the application's events channel.
152+// It is non-blocking; the message is dropped if the channel is full.
153+func (app *App) SendEvent(msg tea.Msg) {
154+ select {
155+ case app.events <- msg:
156+ default:
157+ }
158+}
159+
151// RunNonInteractive runs the application in non-interactive mode with the
160// RunNonInteractive runs the application in non-interactive mode with the
152// given prompt, printing to stdout.
161// given prompt, printing to stdout.
153func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt, largeModel, smallModel string, hideSpinner bool) error {
162func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt, largeModel, smallModel string, hideSpinner bool) error {
internal/cmd/root.go+40−7
@@ -87,18 +87,51 @@ crush --yolo
87crush --data-dir /path/to/custom/.crush
87crush --data-dir /path/to/custom/.crush
88 `,
88 `,
89 RunE: func(cmd *cobra.Command, args []string) error {
89 RunE: func(cmd *cobra.Command, args []string) error {
90− app, err := setupAppWithProgressBar(cmd)
90+ hostURL, err := server.ParseHostURL(clientHost)
91 if err != nil {
91 if err != nil {
92+ return fmt.Errorf("invalid host URL: %v", err)
93+ }
94+
95+ if err := ensureServer(cmd, hostURL); err != nil {
92 return err
96 return err
93 }
97 }
98+
99+ appInstance, err := setupAppWithProgressBar(cmd)
100+ if err != nil {
101+ return err
102+ }
94− defer app.Shutdown()
103+ defer appInstance.Shutdown()
104+
105+ // Register the workspace with the server so it tracks active
106+ // clients and auto-shuts down when the last one exits.
107+ cwd, _ := ResolveCwd(cmd)
108+ dataDir, _ := cmd.Flags().GetString("data-dir")
109+ debug, _ := cmd.Flags().GetBool("debug")
110+ yolo, _ := cmd.Flags().GetBool("yolo")
111+
112+ c, err := client.NewClient(cwd, hostURL.Scheme, hostURL.Host)
113+ if err != nil {
114+ return fmt.Errorf("failed to create client: %v", err)
115+ }
116+
117+ ws, err := c.CreateWorkspace(cmd.Context(), proto.Workspace{
118+ Path: cwd,
119+ DataDir: dataDir,
120+ Debug: debug,
121+ YOLO: yolo,
122+ Version: version.Version,
123+ })
124+ if err != nil {
125+ return fmt.Errorf("failed to register workspace: %v", err)
126+ }
127+ defer func() { _ = c.DeleteWorkspace(cmd.Context(), ws.ID) }()
95
128
96 event.AppInitialized()
129 event.AppInitialized()
97
130
98 // Set up the TUI.
131 // Set up the TUI.
99 var env uv.Environ = os.Environ()
132 var env uv.Environ = os.Environ()
100
133
101− com := common.DefaultCommon(app)
134+ com := common.DefaultCommon(appInstance)
102 model := ui.New(com)
135 model := ui.New(com)
103
136
104 program := tea.NewProgram(
137 program := tea.NewProgram(
@@ -107,7 +140,7 @@ crush --data-dir /path/to/custom/.crush
107 tea.WithContext(cmd.Context()),
140 tea.WithContext(cmd.Context()),
108 tea.WithFilter(ui.MouseEventFilter), // Filter mouse events based on focus state
141 tea.WithFilter(ui.MouseEventFilter), // Filter mouse events based on focus state
109 )
142 )
110− go app.Subscribe(program)
143+ go appInstance.Subscribe(program)
111
144
112 if _, err := program.Run(); err != nil {
145 if _, err := program.Run(); err != nil {
113 event.Error(err)
146 event.Error(err)
@@ -283,7 +316,8 @@ func setupClientApp(cmd *cobra.Command, hostURL *url.URL) (*client.Client, *prot
283}
316}
284
317
285// ensureServer auto-starts a detached server if the socket file does not
318// ensureServer auto-starts a detached server if the socket file does not
286−// exist.
319+// exist. When connecting to an existing server, it waits for the health
320+// endpoint to respond.
287func ensureServer(cmd *cobra.Command, hostURL *url.URL) error {
321func ensureServer(cmd *cobra.Command, hostURL *url.URL) error {
288 switch hostURL.Scheme {
322 switch hostURL.Scheme {
289 case "unix", "npipe":
323 case "unix", "npipe":
@@ -308,9 +342,8 @@ func ensureServer(cmd *cobra.Command, hostURL *url.URL) error {
308 if err != nil {
342 if err != nil {
309 return fmt.Errorf("failed to initialize crush server: %v", err)
343 return fmt.Errorf("failed to initialize crush server: %v", err)
310 }
344 }
311− default:
312− // TCP: assume server is already running.
313 }
345 }
346+
314 return nil
347 return nil
315}
348}
316
349
internal/proto/proto.go+1
@@ -16,6 +16,7 @@ type Workspace struct {
16 YOLO bool `json:"yolo,omitempty"`
16 YOLO bool `json:"yolo,omitempty"`
17 Debug bool `json:"debug,omitempty"`
17 Debug bool `json:"debug,omitempty"`
18 DataDir string `json:"data_dir,omitempty"`
18 DataDir string `json:"data_dir,omitempty"`
19+ Version string `json:"version,omitempty"`
19 Config *config.Config `json:"config,omitempty"`
20 Config *config.Config `json:"config,omitempty"`
20 Env []string `json:"env,omitempty"`
21 Env []string `json:"env,omitempty"`
21}
22}
internal/server/proto.go+23
@@ -16,6 +16,7 @@ import (
16 "github.com/charmbracelet/crush/internal/permission"
16 "github.com/charmbracelet/crush/internal/permission"
17 "github.com/charmbracelet/crush/internal/proto"
17 "github.com/charmbracelet/crush/internal/proto"
18 "github.com/charmbracelet/crush/internal/session"
18 "github.com/charmbracelet/crush/internal/session"
19+ "github.com/charmbracelet/crush/internal/ui/util"
19 "github.com/charmbracelet/crush/internal/version"
20 "github.com/charmbracelet/crush/internal/version"
20 "github.com/google/uuid"
21 "github.com/google/uuid"
21)
22)
@@ -541,6 +542,16 @@ func (c *controllerV1) handleDeleteWorkspaces(w http.ResponseWriter, r *http.Req
541 ws.App.Shutdown()
542 ws.App.Shutdown()
542 }
543 }
543 c.workspaces.Del(id)
544 c.workspaces.Del(id)
545+
546+ // When the last workspace is removed, shut down the server.
547+ if c.workspaces.Len() == 0 {
548+ slog.Info("Last workspace removed, shutting down server...")
549+ go func() {
550+ if err := c.Shutdown(context.Background()); err != nil {
551+ slog.Error("Failed to shutdown server", "error", err)
552+ }
553+ }()
554+ }
544}
555}
545
556
546func (c *controllerV1) handleGetWorkspace(w http.ResponseWriter, r *http.Request) {
557func (c *controllerV1) handleGetWorkspace(w http.ResponseWriter, r *http.Request) {
@@ -618,6 +629,18 @@ func (c *controllerV1) handlePostWorkspaces(w http.ResponseWriter, r *http.Reque
618 }
629 }
619
630
620 c.workspaces.Set(id, ws)
631 c.workspaces.Set(id, ws)
632+
633+ if args.Version != "" && args.Version != version.Version {
634+ slog.Warn("Client/server version mismatch",
635+ "client", args.Version,
636+ "server", version.Version,
637+ )
638+ appWorkspace.SendEvent(util.NewWarnMsg(fmt.Sprintf(
639+ "Server version %q differs from client version %q. Consider restarting the server.",
640+ version.Version, args.Version,
641+ )))
642+ }
643+
621 jsonEncode(w, proto.Workspace{
644 jsonEncode(w, proto.Workspace{
622 ID: id,
645 ID: id,
623 Path: args.Path,
646 Path: args.Path,