molecule-cli/internal/cmd/workspace.go
claude-ceo-assistant 76f37d928f
All checks were successful
Release Go binaries / test (pull_request) Successful in 1m37s
Release Go binaries / release (pull_request) Has been skipped
fix(post-suspension): vanity import path go.moleculesai.app/cli (closes molecule-ai/internal#71 phase 2)
Migrates go.mod + 22 Go imports + README + comments + generated config
templates off the dead github.com/Molecule-AI/ identity onto the vanity
host go.moleculesai.app, owned by us.

Surfaces touched:
- go.mod module declaration: github.com/Molecule-AI/molecule-cli ->
  go.moleculesai.app/cli
- Every Go import statement under cmd/ + internal/
- README install section: rewritten to lead with the vanity install
  command (the previous text was migration-in-progress hedging)
- Comment URLs in internal/backends/backend.go + internal/cmd/connect.go
  (https://github.com/Molecule-AI/molecule-cli/issues/10) -> point at
  git.moleculesai.app/molecule-ai/molecule-cli
- Generated config templates in internal/cmd/init.go +
  internal/cmd/config.go: header URL updated so new users land on the
  live SCM
- Adds internal/lint/import_path_lint_test.go — structural test that
  walks every *.go / *.mod / Dockerfile / *.md / *.sh / *.yml in the
  module and rejects future references to github.com/Molecule-AI/ or
  Molecule-AI/molecule-monorepo. Mutation-tested before commit.

Test plan
- go build ./... clean
- go test ./... green (cmd/molecule + 5 internal packages + new lint
  gate, all pass)
- TestNoLegacyGitHubImportPaths fails on injected canary, passes on
  clean tree (no tautology)

Open dependency
- go.moleculesai.app responder must be deployed before
  'go install go.moleculesai.app/cli/cmd/molecule@latest' works
  externally. Internal builds + 'go build ./cmd/molecule' from a fresh
  clone work today (self-referential module path).
- Responder code prepared (worker.js, vendor-portable for CF Workers /
  Vercel Edge); deploy tracked separately under internal#71 phase 1.

Pairs with parallel migrations of plugin-gh-identity (#3) +
molecule-controlplane + molecule-core under the same internal#71 sweep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 22:26:45 +00:00

301 lines
8.9 KiB
Go

// Package cmd implements the CLI command tree.
package cmd
import (
"encoding/json"
"fmt"
"os"
"text/tabwriter"
"go.moleculesai.app/cli/internal/client"
"github.com/spf13/cobra"
)
// ---------------------------------------------------------------------------
// Workspace command group
// ---------------------------------------------------------------------------
var workspaceCmd = &cobra.Command{
Use: "workspace",
Short: "Manage Molecule AI workspaces",
Long: `List, inspect, create, delete, restart, audit, and delegate to workspaces.`,
}
func init() {
workspaceCmd.AddCommand(
workspaceListCmd, workspaceCreateCmd, workspaceInspectCmd,
workspaceDeleteCmd, workspaceRestartCmd, workspaceAuditCmd, workspaceDelegateCmd,
)
}
// ===========================================================================
// mol workspace list
// ===========================================================================
var workspaceListCmd = &cobra.Command{
Use: "list",
Short: "List all workspaces",
RunE: runWorkspaceList,
}
func runWorkspaceList(cmd *cobra.Command, _ []string) error {
cl := client.New(apiURL)
ws, err := cl.ListWorkspaces()
if err != nil {
return fmt.Errorf("workspace list: %w", err)
}
if outputFormat == "json" {
return printJSON(ws)
}
if outputFormat == "yaml" {
return printYAML(ws)
}
if len(ws) == 0 {
fmt.Println("No workspaces found.")
return nil
}
w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
fmt.Fprintln(w, "ID\tNAME\tSTATUS\tROLE\tRUNTIME\tCREATED AT")
for _, s := range ws {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
s.ID, s.Name, s.Status, s.Role, s.Runtime, s.CreatedAt)
}
return w.Flush()
}
// ===========================================================================
// mol workspace create
// ===========================================================================
var createFlags struct {
name string
role string
runtime string
template string
parentID string
workspaceDir string
tier int
}
var workspaceCreateCmd = &cobra.Command{
Use: "create --name <name> [flags]",
Short: "Create a new workspace",
RunE: runWorkspaceCreate,
}
func init() {
f := workspaceCreateCmd.Flags()
f.StringVarP(&createFlags.name, "name", "n", "", "Workspace name (required)")
f.StringVar(&createFlags.role, "role", "", "Role (e.g. pm, report, researcher)")
f.StringVar(&createFlags.runtime, "runtime", "", "Runtime (e.g. claude-code, deepagents)")
f.StringVar(&createFlags.template, "template", "", "Template name or ID")
f.StringVar(&createFlags.parentID, "parent-id", "", "Parent workspace ID")
f.StringVar(&createFlags.workspaceDir, "workspace-dir", "", "Workspace directory path")
f.IntVar(&createFlags.tier, "tier", 0, "Tier value")
workspaceCreateCmd.MarkFlagRequired("name")
}
func runWorkspaceCreate(cmd *cobra.Command, _ []string) error {
cl := client.New(apiURL)
req := client.CreateWorkspaceRequest{Name: createFlags.name}
if createFlags.role != "" {
req.Role = createFlags.role
}
if createFlags.runtime != "" {
req.Runtime = createFlags.runtime
}
if createFlags.template != "" {
req.Template = createFlags.template
}
if createFlags.parentID != "" {
req.ParentID = createFlags.parentID
}
if createFlags.workspaceDir != "" {
req.WorkspaceDir = createFlags.workspaceDir
}
if createFlags.tier > 0 {
req.Tier = createFlags.tier
}
ws, err := cl.CreateWorkspace(req)
if err != nil {
return fmt.Errorf("workspace create: %w", err)
}
if outputFormat == "json" {
return printJSON(ws)
}
if outputFormat == "yaml" {
return printYAML(ws)
}
fmt.Printf("Workspace created: %s (%s)\n", ws.Name, ws.ID)
return nil
}
// ===========================================================================
// mol workspace inspect
// ===========================================================================
var workspaceInspectCmd = &cobra.Command{
Use: "inspect <workspace-id>",
Short: "Show full details for a workspace",
Args: cobra.ExactArgs(1),
RunE: runWorkspaceInspect,
}
func runWorkspaceInspect(cmd *cobra.Command, args []string) error {
cl := client.New(apiURL)
ws, err := cl.GetWorkspace(args[0])
if err != nil {
return fmt.Errorf("workspace inspect: %w", err)
}
if outputFormat == "json" {
return printJSON(ws)
}
if outputFormat == "yaml" {
return printYAML(ws)
}
w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
kv(w, "ID", ws.ID)
kv(w, "Name", ws.Name)
kv(w, "Status", ws.Status)
kv(w, "Role", ws.Role)
kv(w, "Runtime", ws.Runtime)
kv(w, "Tier", fmt.Sprintf("%d", ws.Tier))
kv(w, "ParentID", ws.ParentID)
kv(w, "WorkspaceDir", ws.WorkspaceDir)
kv(w, "CreatedAt", ws.CreatedAt)
if ws.Canvas != nil {
kv(w, "Canvas", fmt.Sprintf("(%.0f, %.0f)", ws.Canvas.X, ws.Canvas.Y))
}
return w.Flush()
}
// ===========================================================================
// mol workspace delete
// ===========================================================================
var workspaceDeleteCmd = &cobra.Command{
Use: "delete <workspace-id>",
Short: "Delete a workspace (irreversible)",
Args: cobra.ExactArgs(1),
RunE: runWorkspaceDelete,
}
func runWorkspaceDelete(cmd *cobra.Command, args []string) error {
cl := client.New(apiURL)
if err := cl.DeleteWorkspace(args[0]); err != nil {
return fmt.Errorf("workspace delete: %w", err)
}
fmt.Printf("Workspace %q deleted.\n", args[0])
return nil
}
// ===========================================================================
// mol workspace restart
// ===========================================================================
var workspaceRestartCmd = &cobra.Command{
Use: "restart <workspace-id>",
Short: "Restart a workspace",
Args: cobra.ExactArgs(1),
RunE: runWorkspaceRestart,
}
func runWorkspaceRestart(cmd *cobra.Command, args []string) error {
cl := client.New(apiURL)
if err := cl.RestartWorkspace(args[0]); err != nil {
return fmt.Errorf("workspace restart: %w", err)
}
fmt.Printf("Restart triggered for workspace %q.\n", args[0])
return nil
}
// ===========================================================================
// mol workspace audit
// ===========================================================================
var workspaceAuditCmd = &cobra.Command{
Use: "audit",
Short: "Full workspace + agent audit report",
RunE: runWorkspaceAudit,
}
func runWorkspaceAudit(cmd *cobra.Command, _ []string) error {
cl := client.New(apiURL)
workspaces, agents, err := cl.AuditWorkspaces()
if err != nil {
return fmt.Errorf("workspace audit: %w", err)
}
type auditReport struct {
Workspaces int `json:"workspaces"`
Agents int `json:"agents"`
ByStatus map[string]int `json:"by_status"`
Items []client.Workspace `json:"workspaces_list"`
AgentList []client.Agent `json:"agents_list"`
}
byStatus := map[string]int{}
for _, ws := range workspaces {
byStatus[ws.Status]++
}
report := auditReport{
Workspaces: len(workspaces),
Agents: len(agents),
ByStatus: byStatus,
Items: workspaces,
AgentList: agents,
}
if outputFormat == "json" {
return printJSON(report)
}
if outputFormat == "yaml" {
return printYAML(report)
}
w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
fmt.Fprintln(w, "WORKSPACES\t")
fmt.Fprintln(w, "ID\tNAME\tSTATUS\tROLE\tRUNTIME")
for _, ws := range workspaces {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
ws.ID, ws.Name, ws.Status, ws.Role, ws.Runtime)
}
fmt.Fprintln(w)
fmt.Fprintln(w, "AGENTS\t")
fmt.Fprintln(w, "ID\tNAME\tWORKSPACE\tSTATUS\tMODEL")
for _, a := range agents {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
a.ID, a.Name, a.WorkspaceID, a.Status, a.Model)
}
return w.Flush()
}
// ===========================================================================
// mol workspace delegate
// ===========================================================================
var workspaceDelegateCmd = &cobra.Command{
Use: "delegate <workspace-id> <target-workspace-id> <task>",
Short: "Delegate a task to another workspace (non-blocking)",
Args: cobra.ExactArgs(3),
RunE: runWorkspaceDelegate,
}
func runWorkspaceDelegate(cmd *cobra.Command, args []string) error {
workspaceID, targetID, task := args[0], args[1], args[2]
cl := client.New(apiURL)
type delReq struct {
TargetID string `json:"target_id"`
Task string `json:"task"`
}
type delResp struct {
DelegationID string `json:"delegation_id,omitempty"`
Status string `json:"status,omitempty"`
}
encoded, _ := json.Marshal(delReq{TargetID: targetID, Task: task})
body, err := runHTTP("POST", cl.BaseURL+"/workspaces/"+workspaceID+"/delegate", encoded)
if err != nil {
return fmt.Errorf("workspace delegate: %w", err)
}
var resp delResp
if err := json.Unmarshal(body, &resp); err != nil {
return fmt.Errorf("workspace delegate: parse response: %w", err)
}
if resp.DelegationID != "" {
fmt.Printf("Delegation queued: %s (status: %s)\n", resp.DelegationID, resp.Status)
} else {
fmt.Printf("Delegation sent to %q.\n", targetID)
}
_ = workspaceID
return nil
}