on main help sign in
feat: send server client version info
commit1b37d55bb6f00810032f9d0330750a89e5914ea0 authorAyman Bagabas <ayman.bagabas@gmail.com> dateTue 10 Mar 2026, 22:04 (7 months ago) parent0f2e2f0
4 files changed+73−7
internal/app/app.go+9 internal/cmd/root.go+40−7 internal/proto/proto.go+1 internal/server/proto.go+23
internal/app/app.go+9
@@ -148,6 +148,15 @@ func (app *App) Events() <-chan tea.Msg {
148148 return app.events
149149}
150150
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+
151160// RunNonInteractive runs the application in non-interactive mode with the
152161// given prompt, printing to stdout.
153162func (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
8787crush --data-dir /path/to/custom/.crush
8888 `,
8989 RunE: func(cmd *cobra.Command, args []string) error {
90− app, err := setupAppWithProgressBar(cmd)
90+ hostURL, err := server.ParseHostURL(clientHost)
9191 if err != nil {
92+ return fmt.Errorf("invalid host URL: %v", err)
93+ }
94+
95+ if err := ensureServer(cmd, hostURL); err != nil {
9296 return err
9397 }
94− defer app.Shutdown()
98+
99+ appInstance, err := setupAppWithProgressBar(cmd)
100+ if err != nil {
101+ return err
102+ }
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) }()
95128
96129 event.AppInitialized()
97130
98131 // Set up the TUI.
99132 var env uv.Environ = os.Environ()
100133
101− com := common.DefaultCommon(app)
134+ com := common.DefaultCommon(appInstance)
102135 model := ui.New(com)
103136
104137 program := tea.NewProgram(
@@ -107,7 +140,7 @@ crush --data-dir /path/to/custom/.crush
107140 tea.WithContext(cmd.Context()),
108141 tea.WithFilter(ui.MouseEventFilter), // Filter mouse events based on focus state
109142 )
110− go app.Subscribe(program)
143+ go appInstance.Subscribe(program)
111144
112145 if _, err := program.Run(); err != nil {
113146 event.Error(err)
@@ -283,7 +316,8 @@ func setupClientApp(cmd *cobra.Command, hostURL *url.URL) (*client.Client, *prot
283316}
284317
285318// 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.
287321func ensureServer(cmd *cobra.Command, hostURL *url.URL) error {
288322 switch hostURL.Scheme {
289323 case "unix", "npipe":
@@ -308,9 +342,8 @@ func ensureServer(cmd *cobra.Command, hostURL *url.URL) error {
308342 if err != nil {
309343 return fmt.Errorf("failed to initialize crush server: %v", err)
310344 }
311− default:
312− // TCP: assume server is already running.
313345 }
346+
314347 return nil
315348}
316349
internal/proto/proto.go+1
@@ -16,6 +16,7 @@ type Workspace struct {
1616 YOLO bool `json:"yolo,omitempty"`
1717 Debug bool `json:"debug,omitempty"`
1818 DataDir string `json:"data_dir,omitempty"`
19+ Version string `json:"version,omitempty"`
1920 Config *config.Config `json:"config,omitempty"`
2021 Env []string `json:"env,omitempty"`
2122}
internal/server/proto.go+23
@@ -16,6 +16,7 @@ import (
1616 "github.com/charmbracelet/crush/internal/permission"
1717 "github.com/charmbracelet/crush/internal/proto"
1818 "github.com/charmbracelet/crush/internal/session"
19+ "github.com/charmbracelet/crush/internal/ui/util"
1920 "github.com/charmbracelet/crush/internal/version"
2021 "github.com/google/uuid"
2122)
@@ -541,6 +542,16 @@ func (c *controllerV1) handleDeleteWorkspaces(w http.ResponseWriter, r *http.Req
541542 ws.App.Shutdown()
542543 }
543544 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+ }
544555}
545556
546557func (c *controllerV1) handleGetWorkspace(w http.ResponseWriter, r *http.Request) {
@@ -618,6 +629,18 @@ func (c *controllerV1) handlePostWorkspaces(w http.ResponseWriter, r *http.Reque
618629 }
619630
620631 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+
621644 jsonEncode(w, proto.Workspace{
622645 ID: id,
623646 Path: args.Path,