Compare commits

...

3 Commits

Author SHA1 Message Date
Jiayuan Zhang
44aa1c61d1 fix(cli): address code review — token login crash and misleading success msg
1. Token login (`multica login --token`) on a fresh account no longer
   crashes: waitForOnboarding uses tryResolveAppURL (returns "" instead
   of os.Exit(1)) and falls back to printing manual instructions.

2. Setup commands no longer print "✓ Setup complete!" when onboarding
   was not finished. Shows "⚠ Setup incomplete" with next steps instead.
2026-04-14 01:53:28 +08:00
Jiayuan Zhang
1263a0ab02 fix(cli): redirect to web onboarding instead of auto-creating workspace
When no workspaces exist, the CLI now opens the web onboarding wizard
in the browser and polls until the user completes workspace creation.
This reuses the existing 4-step onboarding flow (workspace → runtime →
agent → done) instead of duplicating creation logic in the CLI.
2026-04-14 01:30:13 +08:00
Jiayuan Zhang
4f8b98aafc fix(cli): auto-create workspace for new users during setup
When a new user runs `multica setup` and has no workspaces,
the onboarding flow now auto-creates a default workspace
(named "<name>'s Workspace") instead of failing when the
daemon tries to start with zero watched workspaces.

As a safety net, setup commands also skip daemon start
gracefully if no workspaces are configured, instead of
erroring out.
2026-04-14 01:19:27 +08:00
2 changed files with 106 additions and 12 deletions

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"strings"
"time"
"github.com/spf13/cobra"
@@ -11,6 +12,22 @@ import (
"github.com/multica-ai/multica/server/internal/cli"
)
// tryResolveAppURL returns the app URL if configured, or "" if not available.
// Unlike resolveAppURL, it never calls os.Exit.
func tryResolveAppURL(cmd *cobra.Command) string {
for _, key := range []string{"MULTICA_APP_URL", "FRONTEND_ORIGIN"} {
if val := strings.TrimSpace(os.Getenv(key)); val != "" {
return strings.TrimRight(val, "/")
}
}
profile := resolveProfile(cmd)
cfg, err := cli.LoadCLIConfigForProfile(profile)
if err == nil && cfg.AppURL != "" {
return strings.TrimRight(cfg.AppURL, "/")
}
return ""
}
var loginCmd = &cobra.Command{
Use: "login",
Short: "Authenticate and set up workspaces",
@@ -59,8 +76,15 @@ func autoWatchWorkspaces(cmd *cobra.Command) error {
}
if len(workspaces) == 0 {
fmt.Fprintln(os.Stderr, "\nNo workspaces found.")
return nil
var err error
workspaces, err = waitForOnboarding(cmd, client)
if err != nil {
return err
}
if len(workspaces) == 0 {
fmt.Fprintln(os.Stderr, "\nNo workspaces found.")
return nil
}
}
profile := resolveProfile(cmd)
@@ -96,3 +120,54 @@ func autoWatchWorkspaces(cmd *cobra.Command) error {
return nil
}
// waitForOnboarding opens the web onboarding page and polls until the user
// creates a workspace, returning the new workspace list.
func waitForOnboarding(cmd *cobra.Command, client *cli.APIClient) ([]struct {
ID string `json:"id"`
Name string `json:"name"`
}, error) {
appURL := tryResolveAppURL(cmd)
if appURL == "" {
// No app URL available (e.g. token login without prior setup).
// Can't open the browser — tell the user to create a workspace manually.
fmt.Fprintln(os.Stderr, "\nNo workspaces found.")
fmt.Fprintln(os.Stderr, "Create a workspace in the web dashboard, then run 'multica login' again.")
return nil, nil
}
onboardingURL := appURL + "/onboarding"
fmt.Fprintln(os.Stderr, "\nNo workspaces found. Opening onboarding in your browser...")
if err := openBrowser(onboardingURL); err != nil {
fmt.Fprintf(os.Stderr, "Could not open browser automatically.\n")
}
fmt.Fprintf(os.Stderr, "If the browser didn't open, visit:\n %s\n", onboardingURL)
fmt.Fprintln(os.Stderr, "\nWaiting for workspace creation...")
// Poll until a workspace appears or timeout (5 minutes).
const pollInterval = 2 * time.Second
const pollTimeout = 5 * time.Minute
deadline := time.Now().Add(pollTimeout)
for time.Now().Before(deadline) {
time.Sleep(pollInterval)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
var workspaces []struct {
ID string `json:"id"`
Name string `json:"name"`
}
err := client.GetJSON(ctx, "/api/workspaces", &workspaces)
cancel()
if err != nil {
continue // transient error, keep polling
}
if len(workspaces) > 0 {
return workspaces, nil
}
}
return nil, fmt.Errorf("timed out waiting for workspace creation")
}

View File

@@ -135,13 +135,18 @@ func runSetupCloud(cmd *cobra.Command, args []string) error {
return err
}
// Start daemon in background.
fmt.Fprintln(os.Stderr, "\nStarting daemon...")
if err := runDaemonBackground(cmd); err != nil {
return fmt.Errorf("start daemon: %w", err)
// Start daemon only if we have workspaces to watch.
if hasWatchedWorkspaces(resolveProfile(cmd)) {
fmt.Fprintln(os.Stderr, "\nStarting daemon...")
if err := runDaemonBackground(cmd); err != nil {
return fmt.Errorf("start daemon: %w", err)
}
fmt.Fprintln(os.Stderr, "\n✓ Setup complete! Your machine is now connected to Multica.")
} else {
fmt.Fprintln(os.Stderr, "\n⚠ Setup incomplete: no workspaces configured.")
fmt.Fprintln(os.Stderr, "Create a workspace at the web dashboard, then run 'multica login' and 'multica daemon start'.")
}
fmt.Fprintln(os.Stderr, "\n✓ Setup complete! Your machine is now connected to Multica.")
return nil
}
@@ -195,16 +200,30 @@ func runSetupSelfHost(cmd *cobra.Command, args []string) error {
return err
}
// Start daemon in background.
fmt.Fprintln(os.Stderr, "\nStarting daemon...")
if err := runDaemonBackground(cmd); err != nil {
return fmt.Errorf("start daemon: %w", err)
// Start daemon only if we have workspaces to watch.
if hasWatchedWorkspaces(resolveProfile(cmd)) {
fmt.Fprintln(os.Stderr, "\nStarting daemon...")
if err := runDaemonBackground(cmd); err != nil {
return fmt.Errorf("start daemon: %w", err)
}
fmt.Fprintln(os.Stderr, "\n✓ Setup complete! Your machine is now connected to Multica.")
} else {
fmt.Fprintln(os.Stderr, "\n⚠ Setup incomplete: no workspaces configured.")
fmt.Fprintln(os.Stderr, "Create a workspace at the web dashboard, then run 'multica login' and 'multica daemon start'.")
}
fmt.Fprintln(os.Stderr, "\n✓ Setup complete! Your machine is now connected to Multica.")
return nil
}
// hasWatchedWorkspaces returns true if the CLI config has at least one watched workspace.
func hasWatchedWorkspaces(profile string) bool {
cfg, err := cli.LoadCLIConfigForProfile(profile)
if err != nil {
return false
}
return len(cfg.WatchedWorkspaces) > 0
}
// probeServer checks whether a Multica backend is reachable at the given URL.
func probeServer(baseURL string) bool {
url := strings.TrimRight(baseURL, "/") + "/health"