backend.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. // Package backend provides transport-agnostic operations for managing
  2. // workspaces, sessions, agents, permissions, and events. It is consumed
  3. // by protocol-specific layers such as HTTP (server) and ACP.
  4. package backend
  5. import (
  6. "context"
  7. "errors"
  8. "fmt"
  9. "log/slog"
  10. "runtime"
  11. "github.com/charmbracelet/crush/internal/app"
  12. "github.com/charmbracelet/crush/internal/config"
  13. "github.com/charmbracelet/crush/internal/csync"
  14. "github.com/charmbracelet/crush/internal/db"
  15. "github.com/charmbracelet/crush/internal/proto"
  16. "github.com/charmbracelet/crush/internal/ui/util"
  17. "github.com/charmbracelet/crush/internal/version"
  18. "github.com/google/uuid"
  19. )
  20. // Common errors returned by backend operations.
  21. var (
  22. ErrWorkspaceNotFound = errors.New("workspace not found")
  23. ErrLSPClientNotFound = errors.New("LSP client not found")
  24. ErrAgentNotInitialized = errors.New("agent coordinator not initialized")
  25. ErrPathRequired = errors.New("path is required")
  26. ErrInvalidPermissionAction = errors.New("invalid permission action")
  27. ErrUnknownCommand = errors.New("unknown command")
  28. )
  29. // ShutdownFunc is called when the backend needs to trigger a server
  30. // shutdown (e.g. when the last workspace is removed).
  31. type ShutdownFunc func()
  32. // Backend provides transport-agnostic business logic for the Crush
  33. // server. It manages workspaces and delegates to [app.App] services.
  34. type Backend struct {
  35. workspaces *csync.Map[string, *Workspace]
  36. cfg *config.ConfigStore
  37. ctx context.Context
  38. shutdownFn ShutdownFunc
  39. }
  40. // Workspace represents a running [app.App] workspace with its
  41. // associated resources and state.
  42. type Workspace struct {
  43. *app.App
  44. ID string
  45. Path string
  46. Cfg *config.ConfigStore
  47. Env []string
  48. }
  49. // New creates a new [Backend].
  50. func New(ctx context.Context, cfg *config.ConfigStore, shutdownFn ShutdownFunc) *Backend {
  51. return &Backend{
  52. workspaces: csync.NewMap[string, *Workspace](),
  53. cfg: cfg,
  54. ctx: ctx,
  55. shutdownFn: shutdownFn,
  56. }
  57. }
  58. // GetWorkspace retrieves a workspace by ID.
  59. func (b *Backend) GetWorkspace(id string) (*Workspace, error) {
  60. ws, ok := b.workspaces.Get(id)
  61. if !ok {
  62. return nil, ErrWorkspaceNotFound
  63. }
  64. return ws, nil
  65. }
  66. // ListWorkspaces returns all running workspaces.
  67. func (b *Backend) ListWorkspaces() []proto.Workspace {
  68. workspaces := []proto.Workspace{}
  69. for _, ws := range b.workspaces.Seq2() {
  70. workspaces = append(workspaces, workspaceToProto(ws))
  71. }
  72. return workspaces
  73. }
  74. // CreateWorkspace initializes a new workspace from the given
  75. // parameters. It creates the config, database connection, and
  76. // [app.App] instance.
  77. func (b *Backend) CreateWorkspace(args proto.Workspace) (*Workspace, proto.Workspace, error) {
  78. if args.Path == "" {
  79. return nil, proto.Workspace{}, ErrPathRequired
  80. }
  81. id := uuid.New().String()
  82. cfg, err := config.Init(args.Path, args.DataDir, args.Debug)
  83. if err != nil {
  84. return nil, proto.Workspace{}, fmt.Errorf("failed to initialize config: %w", err)
  85. }
  86. cfg.Overrides().SkipPermissionRequests = args.YOLO
  87. if err := createDotCrushDir(cfg.Config().Options.DataDirectory); err != nil {
  88. return nil, proto.Workspace{}, fmt.Errorf("failed to create data directory: %w", err)
  89. }
  90. conn, err := db.Connect(b.ctx, cfg.Config().Options.DataDirectory)
  91. if err != nil {
  92. return nil, proto.Workspace{}, fmt.Errorf("failed to connect to database: %w", err)
  93. }
  94. appWorkspace, err := app.New(b.ctx, conn, cfg)
  95. if err != nil {
  96. return nil, proto.Workspace{}, fmt.Errorf("failed to create app workspace: %w", err)
  97. }
  98. ws := &Workspace{
  99. App: appWorkspace,
  100. ID: id,
  101. Path: args.Path,
  102. Cfg: cfg,
  103. Env: args.Env,
  104. }
  105. b.workspaces.Set(id, ws)
  106. if args.Version != "" && args.Version != version.Version {
  107. slog.Warn("Client/server version mismatch",
  108. "client", args.Version,
  109. "server", version.Version,
  110. )
  111. appWorkspace.SendEvent(util.NewWarnMsg(fmt.Sprintf(
  112. "Server version %q differs from client version %q. Consider restarting the server.",
  113. version.Version, args.Version,
  114. )))
  115. }
  116. result := proto.Workspace{
  117. ID: id,
  118. Path: args.Path,
  119. DataDir: cfg.Config().Options.DataDirectory,
  120. Debug: cfg.Config().Options.Debug,
  121. YOLO: cfg.Overrides().SkipPermissionRequests,
  122. Config: cfg.Config(),
  123. Env: args.Env,
  124. }
  125. return ws, result, nil
  126. }
  127. // DeleteWorkspace shuts down and removes a workspace. If it was the
  128. // last workspace, the shutdown callback is invoked.
  129. func (b *Backend) DeleteWorkspace(id string) {
  130. ws, ok := b.workspaces.Get(id)
  131. if ok {
  132. ws.Shutdown()
  133. }
  134. b.workspaces.Del(id)
  135. if b.workspaces.Len() == 0 && b.shutdownFn != nil {
  136. slog.Info("Last workspace removed, shutting down server...")
  137. b.shutdownFn()
  138. }
  139. }
  140. // GetWorkspaceProto returns the proto representation of a workspace.
  141. func (b *Backend) GetWorkspaceProto(id string) (proto.Workspace, error) {
  142. ws, err := b.GetWorkspace(id)
  143. if err != nil {
  144. return proto.Workspace{}, err
  145. }
  146. return workspaceToProto(ws), nil
  147. }
  148. // VersionInfo returns server version information.
  149. func (b *Backend) VersionInfo() proto.VersionInfo {
  150. return proto.VersionInfo{
  151. Version: version.Version,
  152. Commit: version.Commit,
  153. GoVersion: runtime.Version(),
  154. Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
  155. }
  156. }
  157. // Config returns the server-level configuration.
  158. func (b *Backend) Config() *config.ConfigStore {
  159. return b.cfg
  160. }
  161. // Shutdown initiates a graceful server shutdown.
  162. func (b *Backend) Shutdown() {
  163. if b.shutdownFn != nil {
  164. b.shutdownFn()
  165. }
  166. }
  167. func workspaceToProto(ws *Workspace) proto.Workspace {
  168. cfg := ws.Cfg.Config()
  169. return proto.Workspace{
  170. ID: ws.ID,
  171. Path: ws.Path,
  172. YOLO: ws.Cfg.Overrides().SkipPermissionRequests,
  173. DataDir: cfg.Options.DataDirectory,
  174. Debug: cfg.Options.Debug,
  175. Config: cfg,
  176. }
  177. }