Improve local CLI auth and skills UX
This commit is contained in:
parent
8bd476f47c
commit
ed426872cc
9 changed files with 92 additions and 11 deletions
12
Makefile
12
Makefile
|
|
@ -1,4 +1,4 @@
|
|||
.PHONY: dev daemon cli build test migrate-up migrate-down sqlc seed clean setup start stop check worktree-env setup-main start-main stop-main check-main setup-worktree start-worktree stop-worktree check-worktree db-up db-down
|
||||
.PHONY: dev daemon cli multica build test migrate-up migrate-down sqlc seed clean setup start stop check worktree-env setup-main start-main stop-main check-main setup-worktree start-worktree stop-worktree check-worktree db-up db-down
|
||||
|
||||
MAIN_ENV_FILE ?= .env
|
||||
WORKTREE_ENV_FILE ?= .env.worktree
|
||||
|
|
@ -15,6 +15,7 @@ POSTGRES_PORT ?= 5432
|
|||
PORT ?= 8080
|
||||
FRONTEND_PORT ?= 3000
|
||||
FRONTEND_ORIGIN ?= http://localhost:$(FRONTEND_PORT)
|
||||
MULTICA_APP_URL ?= $(FRONTEND_ORIGIN)
|
||||
DATABASE_URL ?= postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@localhost:$(POSTGRES_PORT)/$(POSTGRES_DB)?sslmode=disable
|
||||
NEXT_PUBLIC_API_URL ?= http://localhost:$(PORT)
|
||||
NEXT_PUBLIC_WS_URL ?= ws://localhost:$(PORT)/ws
|
||||
|
|
@ -23,6 +24,8 @@ MULTICA_SERVER_URL ?= ws://localhost:$(PORT)/ws
|
|||
|
||||
export
|
||||
|
||||
MULTICA_ARGS ?= $(ARGS)
|
||||
|
||||
COMPOSE := docker compose
|
||||
|
||||
define REQUIRE_ENV
|
||||
|
|
@ -117,10 +120,13 @@ dev:
|
|||
cd server && go run ./cmd/server
|
||||
|
||||
daemon:
|
||||
cd server && go run ./cmd/multica daemon
|
||||
@$(MAKE) multica MULTICA_ARGS="daemon"
|
||||
|
||||
cli:
|
||||
cd server && go run ./cmd/multica $(ARGS)
|
||||
@$(MAKE) multica MULTICA_ARGS="$(MULTICA_ARGS)"
|
||||
|
||||
multica:
|
||||
cd server && go run ./cmd/multica $(MULTICA_ARGS)
|
||||
|
||||
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
|
||||
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -84,6 +84,7 @@ That keeps one Docker container and one volume, while still isolating schema and
|
|||
|---------|-------------|
|
||||
| `make dev` | Run Go server (uses `PORT`, default `8080`) |
|
||||
| `make daemon` | Run local agent daemon |
|
||||
| `make multica ARGS="version"` | Run the local `multica` CLI without installing it |
|
||||
| `make test` | Run Go tests |
|
||||
| `make build` | Build server & daemon binaries |
|
||||
| `make sqlc` | Regenerate sqlc code from SQL |
|
||||
|
|
@ -118,6 +119,15 @@ make build
|
|||
cp server/bin/multica /usr/local/bin/multica # or ~/.local/bin/multica
|
||||
```
|
||||
|
||||
For local development, you can also run the CLI directly from the repo:
|
||||
|
||||
```bash
|
||||
make multica ARGS="version"
|
||||
make multica ARGS="auth status"
|
||||
```
|
||||
|
||||
For browser-based auth from source, make sure the local frontend is running at `FRONTEND_ORIGIN` first, for example with `make start`, `make start-main`, or `make start-worktree`.
|
||||
|
||||
### Authentication
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
LogOut,
|
||||
Plus,
|
||||
Check,
|
||||
Sparkles,
|
||||
BookOpenText,
|
||||
SquarePen,
|
||||
} from "lucide-react";
|
||||
import { WorkspaceAvatar } from "@/features/workspace";
|
||||
|
|
@ -51,7 +51,7 @@ const primaryNav = [
|
|||
const workspaceNav = [
|
||||
{ href: "/agents", label: "Agents", icon: Bot },
|
||||
{ href: "/runtimes", label: "Runtimes", icon: Monitor },
|
||||
{ href: "/skills", label: "Skills", icon: Sparkles },
|
||||
{ href: "/skills", label: "Skills", icon: BookOpenText },
|
||||
{ href: "/settings", label: "Settings", icon: Settings },
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
ListTodo,
|
||||
Wrench,
|
||||
FileText,
|
||||
BookOpenText,
|
||||
Timer,
|
||||
Trash2,
|
||||
Save,
|
||||
|
|
@ -976,7 +977,7 @@ function TasksTab({ agent }: { agent: Agent }) {
|
|||
type DetailTab = "skills" | "tools" | "triggers" | "tasks";
|
||||
|
||||
const detailTabs: { id: DetailTab; label: string; icon: typeof FileText }[] = [
|
||||
{ id: "skills", label: "Skills", icon: FileText },
|
||||
{ id: "skills", label: "Skills", icon: BookOpenText },
|
||||
{ id: "tools", label: "Tools", icon: Wrench },
|
||||
{ id: "triggers", label: "Triggers", icon: Timer },
|
||||
{ id: "tasks", label: "Tasks", icon: ListTodo },
|
||||
|
|
|
|||
|
|
@ -398,7 +398,7 @@ function SkillDetail({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 min-h-0 flex-col">
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ DATABASE_URL=postgres://multica:multica@localhost:${postgres_port}/${postgres_db
|
|||
PORT=${backend_port}
|
||||
JWT_SECRET=change-me-in-production
|
||||
MULTICA_SERVER_URL=ws://localhost:${backend_port}/ws
|
||||
MULTICA_APP_URL=${frontend_origin}
|
||||
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/multica-ai/multica/server/internal/cli"
|
||||
"github.com/multica-ai/multica/server/internal/daemon"
|
||||
)
|
||||
|
||||
var agentCmd = &cobra.Command{
|
||||
|
|
@ -69,18 +70,26 @@ func newAPIClient(cmd *cobra.Command) (*cli.APIClient, error) {
|
|||
func resolveServerURL(cmd *cobra.Command) string {
|
||||
val := cli.FlagOrEnv(cmd, "server-url", "MULTICA_SERVER_URL", "")
|
||||
if val != "" {
|
||||
return val
|
||||
return normalizeAPIBaseURL(val)
|
||||
}
|
||||
cfg, err := cli.LoadCLIConfig()
|
||||
if err != nil {
|
||||
return "http://localhost:8080"
|
||||
}
|
||||
if cfg.ServerURL != "" {
|
||||
return cfg.ServerURL
|
||||
return normalizeAPIBaseURL(cfg.ServerURL)
|
||||
}
|
||||
return "http://localhost:8080"
|
||||
}
|
||||
|
||||
func normalizeAPIBaseURL(raw string) string {
|
||||
normalized, err := daemon.NormalizeServerBaseURL(raw)
|
||||
if err == nil {
|
||||
return normalized
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func resolveWorkspaceID(cmd *cobra.Command) string {
|
||||
val := cli.FlagOrEnv(cmd, "workspace-id", "MULTICA_WORKSPACE_ID", "")
|
||||
if val != "" {
|
||||
|
|
|
|||
|
|
@ -59,8 +59,10 @@ func resolveToken() string {
|
|||
}
|
||||
|
||||
func resolveAppURL() string {
|
||||
if val := strings.TrimSpace(os.Getenv("MULTICA_APP_URL")); val != "" {
|
||||
return strings.TrimRight(val, "/")
|
||||
for _, key := range []string{"MULTICA_APP_URL", "FRONTEND_ORIGIN"} {
|
||||
if val := strings.TrimSpace(os.Getenv(key)); val != "" {
|
||||
return strings.TrimRight(val, "/")
|
||||
}
|
||||
}
|
||||
return "http://localhost:3000"
|
||||
}
|
||||
|
|
|
|||
52
server/cmd/multica/cmd_auth_test.go
Normal file
52
server/cmd/multica/cmd_auth_test.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestResolveAppURL(t *testing.T) {
|
||||
t.Run("prefers MULTICA_APP_URL", func(t *testing.T) {
|
||||
t.Setenv("MULTICA_APP_URL", "http://localhost:14000")
|
||||
t.Setenv("FRONTEND_ORIGIN", "http://localhost:13000")
|
||||
|
||||
if got := resolveAppURL(); got != "http://localhost:14000" {
|
||||
t.Fatalf("resolveAppURL() = %q, want %q", got, "http://localhost:14000")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("falls back to FRONTEND_ORIGIN", func(t *testing.T) {
|
||||
t.Setenv("MULTICA_APP_URL", "")
|
||||
t.Setenv("FRONTEND_ORIGIN", "http://localhost:13026")
|
||||
|
||||
if got := resolveAppURL(); got != "http://localhost:13026" {
|
||||
t.Fatalf("resolveAppURL() = %q, want %q", got, "http://localhost:13026")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("defaults to localhost 3000", func(t *testing.T) {
|
||||
t.Setenv("MULTICA_APP_URL", "")
|
||||
t.Setenv("FRONTEND_ORIGIN", "")
|
||||
|
||||
if got := resolveAppURL(); got != "http://localhost:3000" {
|
||||
t.Fatalf("resolveAppURL() = %q, want %q", got, "http://localhost:3000")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNormalizeAPIBaseURL(t *testing.T) {
|
||||
t.Run("converts websocket base URL", func(t *testing.T) {
|
||||
if got := normalizeAPIBaseURL("ws://localhost:18106/ws"); got != "http://localhost:18106" {
|
||||
t.Fatalf("normalizeAPIBaseURL() = %q, want %q", got, "http://localhost:18106")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("keeps http base URL", func(t *testing.T) {
|
||||
if got := normalizeAPIBaseURL("http://localhost:8080"); got != "http://localhost:8080" {
|
||||
t.Fatalf("normalizeAPIBaseURL() = %q, want %q", got, "http://localhost:8080")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("falls back to raw value for invalid URL", func(t *testing.T) {
|
||||
if got := normalizeAPIBaseURL("://bad-url"); got != "://bad-url" {
|
||||
t.Fatalf("normalizeAPIBaseURL() = %q, want %q", got, "://bad-url")
|
||||
}
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue