multica/server/cmd/multica/cmd_workspace.go
Jiayuan 8fa1b163a6 feat(daemon): add --profile flag for multi-environment isolation
Allow running multiple daemon instances against different servers (e.g.
production and local dev) simultaneously. Each profile gets isolated
config, PID file, log file, health port, and workspaces root.

Usage:
  multica login --profile dev --server-url http://localhost:8080
  multica daemon start --profile dev

Default profile (no --profile flag) behavior is unchanged.

Closes MUL-42
2026-03-30 20:21:23 +08:00

265 lines
6.4 KiB
Go

package main
import (
"context"
"fmt"
"os"
"text/tabwriter"
"time"
"unicode/utf8"
"github.com/spf13/cobra"
"github.com/multica-ai/multica/server/internal/cli"
)
var workspaceCmd = &cobra.Command{
Use: "workspace",
Short: "Manage workspaces",
}
var workspaceListCmd = &cobra.Command{
Use: "list",
Short: "List all workspaces you belong to",
RunE: runWorkspaceList,
}
var workspaceGetCmd = &cobra.Command{
Use: "get [workspace-id]",
Short: "Get workspace details",
Args: cobra.MaximumNArgs(1),
RunE: runWorkspaceGet,
}
var workspaceMembersCmd = &cobra.Command{
Use: "members [workspace-id]",
Short: "List workspace members",
Args: cobra.MaximumNArgs(1),
RunE: runWorkspaceMembers,
}
var workspaceWatchCmd = &cobra.Command{
Use: "watch <workspace-id>",
Short: "Add a workspace to the daemon watch list",
Args: cobra.ExactArgs(1),
RunE: runWatch,
}
var workspaceUnwatchCmd = &cobra.Command{
Use: "unwatch <workspace-id>",
Short: "Remove a workspace from the daemon watch list",
Args: cobra.ExactArgs(1),
RunE: runUnwatch,
}
func init() {
workspaceCmd.AddCommand(workspaceListCmd)
workspaceCmd.AddCommand(workspaceGetCmd)
workspaceCmd.AddCommand(workspaceMembersCmd)
workspaceCmd.AddCommand(workspaceWatchCmd)
workspaceCmd.AddCommand(workspaceUnwatchCmd)
workspaceGetCmd.Flags().String("output", "json", "Output format: table or json")
workspaceMembersCmd.Flags().String("output", "table", "Output format: table or json")
}
func runWorkspaceList(cmd *cobra.Command, _ []string) error {
serverURL := resolveServerURL(cmd)
token := resolveToken(cmd)
if token == "" {
return fmt.Errorf("not authenticated: run 'multica login' first")
}
client := cli.NewAPIClient(serverURL, "", token)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
var workspaces []struct {
ID string `json:"id"`
Name string `json:"name"`
}
if err := client.GetJSON(ctx, "/api/workspaces", &workspaces); err != nil {
return fmt.Errorf("list workspaces: %w", err)
}
if len(workspaces) == 0 {
fmt.Fprintln(os.Stderr, "No workspaces found.")
return nil
}
// Load watched set for marking.
profile := resolveProfile(cmd)
cfg, _ := cli.LoadCLIConfigForProfile(profile)
watched := make(map[string]bool)
for _, w := range cfg.WatchedWorkspaces {
watched[w.ID] = true
}
w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
fmt.Fprintln(w, "ID\tNAME\tWATCHING")
for _, ws := range workspaces {
mark := ""
if watched[ws.ID] {
mark = "*"
}
fmt.Fprintf(w, "%s\t%s\t%s\n", ws.ID, ws.Name, mark)
}
return w.Flush()
}
func workspaceIDFromArgs(cmd *cobra.Command, args []string) string {
if len(args) > 0 {
return args[0]
}
return resolveWorkspaceID(cmd)
}
func runWorkspaceGet(cmd *cobra.Command, args []string) error {
wsID := workspaceIDFromArgs(cmd, args)
if wsID == "" {
return fmt.Errorf("workspace ID is required: pass as argument or set MULTICA_WORKSPACE_ID")
}
client, err := newAPIClient(cmd)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
var ws map[string]any
if err := client.GetJSON(ctx, "/api/workspaces/"+wsID, &ws); err != nil {
return fmt.Errorf("get workspace: %w", err)
}
output, _ := cmd.Flags().GetString("output")
if output == "table" {
desc := strVal(ws, "description")
if utf8.RuneCountInString(desc) > 60 {
runes := []rune(desc)
desc = string(runes[:57]) + "..."
}
wsContext := strVal(ws, "context")
if utf8.RuneCountInString(wsContext) > 60 {
runes := []rune(wsContext)
wsContext = string(runes[:57]) + "..."
}
headers := []string{"ID", "NAME", "SLUG", "DESCRIPTION", "CONTEXT"}
rows := [][]string{{
strVal(ws, "id"),
strVal(ws, "name"),
strVal(ws, "slug"),
desc,
wsContext,
}}
cli.PrintTable(os.Stdout, headers, rows)
return nil
}
return cli.PrintJSON(os.Stdout, ws)
}
func runWorkspaceMembers(cmd *cobra.Command, args []string) error {
wsID := workspaceIDFromArgs(cmd, args)
if wsID == "" {
return fmt.Errorf("workspace ID is required: pass as argument or set MULTICA_WORKSPACE_ID")
}
client, err := newAPIClient(cmd)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
var members []map[string]any
if err := client.GetJSON(ctx, "/api/workspaces/"+wsID+"/members", &members); err != nil {
return fmt.Errorf("list members: %w", err)
}
output, _ := cmd.Flags().GetString("output")
if output == "json" {
return cli.PrintJSON(os.Stdout, members)
}
headers := []string{"USER ID", "NAME", "EMAIL", "ROLE"}
rows := make([][]string, 0, len(members))
for _, m := range members {
rows = append(rows, []string{
strVal(m, "user_id"),
strVal(m, "name"),
strVal(m, "email"),
strVal(m, "role"),
})
}
cli.PrintTable(os.Stdout, headers, rows)
return nil
}
func runWatch(cmd *cobra.Command, args []string) error {
workspaceID := args[0]
serverURL := resolveServerURL(cmd)
token := resolveToken(cmd)
if token == "" {
return fmt.Errorf("not authenticated: run 'multica login' first")
}
client := cli.NewAPIClient(serverURL, "", token)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
var ws struct {
ID string `json:"id"`
Name string `json:"name"`
}
if err := client.GetJSON(ctx, "/api/workspaces/"+workspaceID, &ws); err != nil {
return fmt.Errorf("workspace not found: %w", err)
}
profile := resolveProfile(cmd)
cfg, err := cli.LoadCLIConfigForProfile(profile)
if err != nil {
return err
}
if !cfg.AddWatchedWorkspace(ws.ID, ws.Name) {
fmt.Fprintf(os.Stderr, "Already watching workspace %s (%s)\n", ws.ID, ws.Name)
return nil
}
if cfg.WorkspaceID == "" {
cfg.WorkspaceID = ws.ID
fmt.Fprintf(os.Stderr, "Set default workspace to %s (%s)\n", ws.ID, ws.Name)
}
if err := cli.SaveCLIConfigForProfile(cfg, profile); err != nil {
return err
}
fmt.Fprintf(os.Stderr, "Watching workspace %s (%s)\n", ws.ID, ws.Name)
return nil
}
func runUnwatch(cmd *cobra.Command, args []string) error {
workspaceID := args[0]
profile := resolveProfile(cmd)
cfg, err := cli.LoadCLIConfigForProfile(profile)
if err != nil {
return err
}
if !cfg.RemoveWatchedWorkspace(workspaceID) {
return fmt.Errorf("workspace %s is not being watched", workspaceID)
}
if err := cli.SaveCLIConfigForProfile(cfg, profile); err != nil {
return err
}
fmt.Fprintf(os.Stderr, "Stopped watching workspace %s\n", workspaceID)
return nil
}