feat(cli): restructure CLI commands for better UX

- Add top-level `multica login` that combines auth + workspace auto-discovery
- Restructure daemon into subcommands: start, stop, status, logs
- Add background daemon mode with PID management
- Add daemon deregistration on shutdown (new API endpoint + SQL query)
- Remove unused commands: runtime list, status, agent get/delete/stop
- Make `config` show config directly instead of requiring `config show`
- Update README to reflect new CLI structure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jiayuan 2026-03-29 01:43:45 +08:00
parent 3bb79564ed
commit 38d595d81d
17 changed files with 568 additions and 276 deletions

View file

@ -109,6 +109,51 @@ func (h *Handler) DaemonRegister(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{"runtimes": resp})
}
// DaemonDeregister marks runtimes as offline when the daemon shuts down.
func (h *Handler) DaemonDeregister(w http.ResponseWriter, r *http.Request) {
var req struct {
RuntimeIDs []string `json:"runtime_ids"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if len(req.RuntimeIDs) == 0 {
writeError(w, http.StatusBadRequest, "runtime_ids is required")
return
}
// Track affected workspaces for WS notifications.
affectedWorkspaces := make(map[string]bool)
for _, rid := range req.RuntimeIDs {
// Look up the runtime to find its workspace.
rt, err := h.Queries.GetAgentRuntime(r.Context(), parseUUID(rid))
if err != nil {
slog.Warn("deregister: runtime not found", "runtime_id", rid, "error", err)
continue
}
if err := h.Queries.SetAgentRuntimeOffline(r.Context(), parseUUID(rid)); err != nil {
slog.Warn("deregister: failed to set offline", "runtime_id", rid, "error", err)
continue
}
affectedWorkspaces[uuidToString(rt.WorkspaceID)] = true
}
// Notify frontend clients so they re-fetch runtime list.
for wsID := range affectedWorkspaces {
h.publish(protocol.EventDaemonRegister, wsID, "system", "", map[string]any{
"action": "deregister",
})
}
slog.Info("daemon deregistered", "runtime_ids", req.RuntimeIDs)
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
type DaemonHeartbeatRequest struct {
RuntimeID string `json:"runtime_id"`
}