mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-18 12:18:45 +02:00
Compare commits
1 Commits
feat/cli-v
...
fix/commen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca232a725c |
@@ -11,7 +11,6 @@ builds:
|
||||
- -s -w
|
||||
- -X main.version={{.Version}}
|
||||
- -X main.commit={{.ShortCommit}}
|
||||
- -X main.date={{.Date}}
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
|
||||
@@ -30,16 +30,6 @@ This auto-detects your installation method (Homebrew or manual) and upgrades acc
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# One-command setup: configure, authenticate, and start the daemon
|
||||
multica setup
|
||||
|
||||
# For self-hosted (local) deployments:
|
||||
multica setup --local
|
||||
```
|
||||
|
||||
Or step by step:
|
||||
|
||||
```bash
|
||||
# 1. Authenticate (opens browser for login)
|
||||
multica login
|
||||
@@ -172,31 +162,28 @@ Agent-specific overrides:
|
||||
|
||||
### Self-Hosted Server
|
||||
|
||||
When connecting to a self-hosted Multica instance, the easiest approach is:
|
||||
When connecting to a self-hosted Multica instance, you **must** point the CLI to your server before logging in. The CLI defaults to the hosted Multica service — skipping this step means the daemon will authenticate against the wrong server.
|
||||
|
||||
```bash
|
||||
# One command — auto-detects local server, configures, authenticates, starts daemon
|
||||
multica setup --local
|
||||
```
|
||||
# Local Docker Compose (default ports):
|
||||
export MULTICA_APP_URL=http://localhost:3000
|
||||
export MULTICA_SERVER_URL=ws://localhost:8080/ws
|
||||
|
||||
Or configure manually:
|
||||
|
||||
```bash
|
||||
# Configure for local Docker Compose (default ports)
|
||||
multica config local
|
||||
|
||||
# Or set URLs individually:
|
||||
# multica config set app_url http://localhost:3000
|
||||
# multica config set server_url http://localhost:8080
|
||||
|
||||
# For production with TLS:
|
||||
# multica config set app_url https://app.example.com
|
||||
# multica config set server_url https://api.example.com
|
||||
# Production with TLS:
|
||||
# export MULTICA_APP_URL=https://app.example.com
|
||||
# export MULTICA_SERVER_URL=wss://api.example.com/ws
|
||||
|
||||
multica login
|
||||
multica daemon start
|
||||
```
|
||||
|
||||
Or set them persistently:
|
||||
|
||||
```bash
|
||||
multica config set app_url http://localhost:3000
|
||||
multica config set server_url ws://localhost:8080/ws
|
||||
```
|
||||
|
||||
### Profiles
|
||||
|
||||
Profiles let you run multiple daemons on the same machine — for example, one for production and one for a staging server.
|
||||
@@ -324,21 +311,6 @@ multica issue run-messages <task-id> --since 42 --output json
|
||||
|
||||
The `runs` command shows all past and current executions for an issue, including running tasks. The `run-messages` command shows the detailed message log (tool calls, thinking, text, errors) for a single run. Use `--since` for efficient polling of in-progress runs.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# One-command setup: configure, authenticate, and start the daemon
|
||||
multica setup
|
||||
|
||||
# For local self-hosted deployments (auto-detects or forces local mode)
|
||||
multica setup --local
|
||||
|
||||
# Custom ports
|
||||
multica setup --local --port 9090 --frontend-port 4000
|
||||
```
|
||||
|
||||
`multica setup` detects whether a local Multica server is running, configures the CLI, opens your browser for authentication, and starts the daemon — all in one step.
|
||||
|
||||
## Configuration
|
||||
|
||||
### View Config
|
||||
@@ -349,19 +321,10 @@ multica config show
|
||||
|
||||
Shows config file path, server URL, app URL, and default workspace.
|
||||
|
||||
### Configure for Local Self-Hosted
|
||||
|
||||
```bash
|
||||
multica config local # Uses default ports (8080/3000)
|
||||
multica config local --port 9090 --frontend-port 4000 # Custom ports
|
||||
```
|
||||
|
||||
Sets `server_url` and `app_url` for a local Docker Compose deployment in one command.
|
||||
|
||||
### Set Values
|
||||
|
||||
```bash
|
||||
multica config set server_url https://api.example.com
|
||||
multica config set server_url wss://api.example.com/ws
|
||||
multica config set app_url https://app.example.com
|
||||
multica config set workspace_id <workspace-id>
|
||||
```
|
||||
|
||||
52
Makefile
52
Makefile
@@ -1,4 +1,4 @@
|
||||
.PHONY: dev server 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 selfhost selfhost-stop
|
||||
.PHONY: dev server 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
|
||||
@@ -36,53 +36,6 @@ define REQUIRE_ENV
|
||||
fi
|
||||
endef
|
||||
|
||||
# ---------- Self-hosting (Docker Compose) ----------
|
||||
|
||||
# One-command self-host: create env, start Docker Compose, wait for health
|
||||
selfhost:
|
||||
@if [ ! -f .env ]; then \
|
||||
echo "==> Creating .env from .env.example..."; \
|
||||
cp .env.example .env; \
|
||||
JWT=$$(openssl rand -hex 32); \
|
||||
if [ "$$(uname)" = "Darwin" ]; then \
|
||||
sed -i '' "s/^JWT_SECRET=.*/JWT_SECRET=$$JWT/" .env; \
|
||||
else \
|
||||
sed -i "s/^JWT_SECRET=.*/JWT_SECRET=$$JWT/" .env; \
|
||||
fi; \
|
||||
echo "==> Generated random JWT_SECRET"; \
|
||||
fi
|
||||
@echo "==> Starting Multica via Docker Compose..."
|
||||
docker compose -f docker-compose.selfhost.yml up -d --build
|
||||
@echo "==> Waiting for backend to be ready..."
|
||||
@for i in $$(seq 1 30); do \
|
||||
if curl -sf http://localhost:$${PORT:-8080}/health > /dev/null 2>&1; then \
|
||||
break; \
|
||||
fi; \
|
||||
sleep 2; \
|
||||
done
|
||||
@if curl -sf http://localhost:$${PORT:-8080}/health > /dev/null 2>&1; then \
|
||||
echo ""; \
|
||||
echo "✓ Multica is running!"; \
|
||||
echo " Frontend: http://localhost:$${FRONTEND_PORT:-3000}"; \
|
||||
echo " Backend: http://localhost:$${PORT:-8080}"; \
|
||||
echo ""; \
|
||||
echo "Log in with any email + verification code: 888888"; \
|
||||
echo ""; \
|
||||
echo "Next — install the CLI and connect your machine:"; \
|
||||
echo " brew install multica-ai/tap/multica"; \
|
||||
echo " multica setup --local"; \
|
||||
else \
|
||||
echo ""; \
|
||||
echo "Services are still starting. Check logs:"; \
|
||||
echo " docker compose -f docker-compose.selfhost.yml logs"; \
|
||||
fi
|
||||
|
||||
# Stop all Docker Compose self-host services
|
||||
selfhost-stop:
|
||||
@echo "==> Stopping Multica services..."
|
||||
docker compose -f docker-compose.selfhost.yml down
|
||||
@echo "✓ All services stopped."
|
||||
|
||||
# ---------- One-click commands ----------
|
||||
|
||||
# First-time setup: install deps, start DB, run migrations
|
||||
@@ -190,11 +143,10 @@ multica:
|
||||
|
||||
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
|
||||
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||
DATE ?= $(shell date -u '+%Y-%m-%dT%H:%M:%SZ')
|
||||
|
||||
build:
|
||||
cd server && go build -o bin/server ./cmd/server
|
||||
cd server && go build -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DATE)" -o bin/multica ./cmd/multica
|
||||
cd server && go build -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT)" -o bin/multica ./cmd/multica
|
||||
cd server && go build -o bin/migrate ./cmd/migrate
|
||||
|
||||
test:
|
||||
|
||||
102
README.md
102
README.md
@@ -47,36 +47,58 @@ Multica manages the full agent lifecycle: from task assignment to execution moni
|
||||
- **Unified Runtimes** — one dashboard for all your compute. Local daemons and cloud runtimes, auto-detection of available CLIs, real-time monitoring.
|
||||
- **Multi-Workspace** — organize work across teams with workspace-level isolation. Each workspace has its own agents, issues, and settings.
|
||||
|
||||
---
|
||||
|
||||
## Quick Install
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash
|
||||
```
|
||||
|
||||
Installs the Multica CLI on macOS and Linux. Works with Homebrew or downloads the binary directly.
|
||||
|
||||
After installation:
|
||||
|
||||
```bash
|
||||
multica login # Authenticate (opens browser)
|
||||
multica daemon start # Start the local agent runtime
|
||||
multica daemon stop # Stop the daemon when done
|
||||
```
|
||||
|
||||
> **Self-hosting?** Add `--local` to deploy a full Multica server on your machine:
|
||||
>
|
||||
> ```bash
|
||||
> curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --local
|
||||
> ```
|
||||
>
|
||||
> Requires Docker. See the [Self-Hosting Guide](SELF_HOSTING.md) for details.
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Multica Cloud
|
||||
|
||||
The fastest way to get started — no setup required: **[multica.ai](https://multica.ai)**
|
||||
|
||||
### Self-Host with Docker
|
||||
|
||||
**Prerequisites:** Docker and Docker Compose.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/multica-ai/multica.git
|
||||
cd multica
|
||||
cp .env.example .env
|
||||
# Edit .env — change JWT_SECRET at minimum
|
||||
docker compose -f docker-compose.selfhost.yml up -d
|
||||
```
|
||||
|
||||
This builds and starts PostgreSQL, the backend (with auto-migration), and the frontend. Open http://localhost:3000 when ready.
|
||||
|
||||
See the [Self-Hosting Guide](SELF_HOSTING.md) for full configuration, reverse proxy setup, and CLI/daemon instructions.
|
||||
|
||||
## CLI
|
||||
|
||||
The `multica` CLI connects your local machine to Multica — authenticate, manage workspaces, and run the agent daemon.
|
||||
|
||||
**Option A — paste this to your coding agent (Claude Code, Codex, OpenClaw, OpenCode, etc.):**
|
||||
|
||||
```
|
||||
Fetch https://github.com/multica-ai/multica/blob/main/CLI_INSTALL.md and follow the instructions to install Multica CLI, log in, and start the daemon on this machine.
|
||||
```
|
||||
|
||||
**Option B — install manually:**
|
||||
|
||||
```bash
|
||||
# Install
|
||||
brew tap multica-ai/tap
|
||||
brew install multica
|
||||
|
||||
# Authenticate and start
|
||||
multica login
|
||||
multica daemon start
|
||||
```
|
||||
|
||||
The daemon auto-detects available agent CLIs (`claude`, `codex`, `openclaw`, `opencode`) on your PATH. When an agent is assigned a task, the daemon creates an isolated environment, runs the agent, and reports results back.
|
||||
|
||||
See the [CLI and Daemon Guide](CLI_AND_DAEMON.md) for the full command reference, daemon configuration, and advanced usage.
|
||||
|
||||
## Quickstart
|
||||
|
||||
Once you have the CLI installed (or signed up for [Multica Cloud](https://multica.ai)), follow these steps to assign your first task to an agent:
|
||||
|
||||
### 1. Log in and start the daemon
|
||||
|
||||
```bash
|
||||
@@ -84,7 +106,7 @@ multica login # Authenticate with your Multica account
|
||||
multica daemon start # Start the local agent runtime
|
||||
```
|
||||
|
||||
The daemon runs in the background and auto-detects agent CLIs (`claude`, `codex`, `openclaw`, `opencode`) on your PATH.
|
||||
The daemon runs in the background and keeps your machine connected to Multica. It auto-detects agent CLIs (`claude`, `codex`, `openclaw`, `opencode`) available on your PATH.
|
||||
|
||||
### 2. Verify your runtime
|
||||
|
||||
@@ -100,27 +122,7 @@ Go to **Settings → Agents** and click **New Agent**. Pick the runtime you just
|
||||
|
||||
Create an issue from the board (or via `multica issue create`), then assign it to your new agent. The agent will automatically pick up the task, execute it on your runtime, and report progress — just like a human teammate.
|
||||
|
||||
---
|
||||
|
||||
## CLI
|
||||
|
||||
The `multica` CLI connects your local machine to Multica — authenticate, manage workspaces, and run the agent daemon.
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `multica login` | Authenticate (opens browser) |
|
||||
| `multica daemon start` | Start the local agent runtime |
|
||||
| `multica daemon status` | Check daemon status |
|
||||
| `multica setup` | One-command setup (configure + login + start daemon) |
|
||||
| `multica setup --local` | Same, but for self-hosted deployments |
|
||||
| `multica config local` | Configure CLI for a local self-hosted server |
|
||||
| `multica issue list` | List issues in your workspace |
|
||||
| `multica issue create` | Create a new issue |
|
||||
| `multica update` | Update to the latest version |
|
||||
|
||||
See the [CLI and Daemon Guide](CLI_AND_DAEMON.md) for the full command reference.
|
||||
|
||||
---
|
||||
That's it! Your agent is now part of the team. 🎉
|
||||
|
||||
## Architecture
|
||||
|
||||
|
||||
@@ -47,33 +47,52 @@ Multica 管理完整的 Agent 生命周期:从任务分配到执行监控再
|
||||
- **统一运行时** — 一个控制台管理所有算力。本地 daemon 和云端运行时,自动检测可用 CLI,实时监控。
|
||||
- **多工作区** — 按团队组织工作,工作区级别隔离。每个工作区有独立的 Agent、Issue 和设置。
|
||||
|
||||
---
|
||||
## 快速开始
|
||||
|
||||
## 快速安装
|
||||
### Multica 云服务
|
||||
|
||||
最快的上手方式,无需任何配置:**[multica.ai](https://multica.ai)**
|
||||
|
||||
### Docker 自部署
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash
|
||||
git clone https://github.com/multica-ai/multica.git
|
||||
cd multica
|
||||
cp .env.example .env
|
||||
# 编辑 .env — 至少修改 JWT_SECRET
|
||||
|
||||
docker compose up -d # 启动 PostgreSQL
|
||||
cd server && go run ./cmd/migrate up && cd .. # 运行数据库迁移
|
||||
make start # 启动应用
|
||||
```
|
||||
|
||||
安装 Multica CLI,支持 macOS 和 Linux。有 Homebrew 用 Homebrew,没有则直接下载二进制。
|
||||
完整部署文档请参阅 [自部署指南](SELF_HOSTING.md)。
|
||||
|
||||
安装完成后:
|
||||
## CLI
|
||||
|
||||
`multica` CLI 将你的本地机器连接到 Multica — 用于认证、管理工作区和运行 Agent daemon。
|
||||
|
||||
**方式 A — 将以下指令粘贴给你的 coding agent(Claude Code、Codex、OpenClaw、OpenCode 等):**
|
||||
|
||||
```
|
||||
Fetch https://github.com/multica-ai/multica/blob/main/CLI_INSTALL.md and follow the instructions to install Multica CLI, log in, and start the daemon on this machine.
|
||||
```
|
||||
|
||||
**方式 B — 手动安装:**
|
||||
|
||||
```bash
|
||||
multica login # 认证(打开浏览器)
|
||||
multica daemon start # 启动本地 Agent 运行时
|
||||
multica daemon stop # 停止 daemon
|
||||
# 安装
|
||||
brew tap multica-ai/tap
|
||||
brew install multica
|
||||
|
||||
# 认证并启动
|
||||
multica login
|
||||
multica daemon start
|
||||
```
|
||||
|
||||
> **自部署?** 加上 `--local` 在本地部署完整的 Multica 服务:
|
||||
>
|
||||
> ```bash
|
||||
> curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --local
|
||||
> ```
|
||||
>
|
||||
> 需要 Docker。详见 [自部署指南](SELF_HOSTING.md)。
|
||||
daemon 会自动检测 PATH 中可用的 Agent CLI(`claude`、`codex`、`openclaw`、`opencode`)。当 Agent 被分配任务时,daemon 会创建隔离环境、运行 Agent、并将结果回传。
|
||||
|
||||
---
|
||||
完整命令参考请参阅 [CLI 与 Daemon 指南](CLI_AND_DAEMON.md)。
|
||||
|
||||
## 快速上手
|
||||
|
||||
|
||||
455
SELF_HOSTING.md
455
SELF_HOSTING.md
@@ -1,8 +1,10 @@
|
||||
# Self-Hosting Guide
|
||||
|
||||
Deploy Multica on your own infrastructure in minutes.
|
||||
This guide walks you through deploying Multica on your own infrastructure.
|
||||
|
||||
## Architecture
|
||||
## Architecture Overview
|
||||
|
||||
Multica has three components:
|
||||
|
||||
| Component | Description | Technology |
|
||||
|-----------|-------------|------------|
|
||||
@@ -10,152 +12,12 @@ Deploy Multica on your own infrastructure in minutes.
|
||||
| **Frontend** | Web application | Next.js 16 |
|
||||
| **Database** | Primary data store | PostgreSQL 17 with pgvector |
|
||||
|
||||
Each user who runs AI agents locally also installs the **`multica` CLI** and runs the **agent daemon** on their own machine.
|
||||
Additionally, each user who wants to run AI agents locally installs the **`multica` CLI** and runs the **agent daemon** on their own machine.
|
||||
|
||||
## Quick Install (Recommended)
|
||||
|
||||
One command to set up everything — server, CLI, and configuration:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --local
|
||||
```
|
||||
|
||||
This automatically clones the repository, starts all services via Docker Compose, and installs the `multica` CLI.
|
||||
|
||||
Once complete, open http://localhost:3000, log in with any email + verification code **`888888`**, then:
|
||||
|
||||
```bash
|
||||
multica login # Authenticate (opens browser)
|
||||
multica daemon start # Start the agent daemon
|
||||
```
|
||||
|
||||
> **Prerequisites:** Docker and Docker Compose must be installed. The script checks for this and provides install links if missing.
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step Setup (Alternative)
|
||||
|
||||
If you prefer to run each step manually:
|
||||
|
||||
### Step 1 — Start the Server
|
||||
## Quick Start (Docker Compose)
|
||||
|
||||
**Prerequisites:** Docker and Docker Compose.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/multica-ai/multica.git
|
||||
cd multica
|
||||
make selfhost
|
||||
```
|
||||
|
||||
`make selfhost` automatically creates `.env` from the example, generates a random `JWT_SECRET`, and starts all services via Docker Compose.
|
||||
|
||||
Once ready:
|
||||
|
||||
- **Frontend:** http://localhost:3000
|
||||
- **Backend API:** http://localhost:8080
|
||||
|
||||
> **Note:** If you prefer to run the Docker Compose steps manually, see [Manual Docker Compose Setup](#manual-docker-compose-setup) below.
|
||||
|
||||
### Step 2 — Log In
|
||||
|
||||
Open http://localhost:3000 in your browser. Enter any email address and use verification code **`888888`** to log in.
|
||||
|
||||
> This master code works in all non-production environments (i.e. when `APP_ENV` is not set to `production`). For production, configure an email provider — see [Advanced Configuration](SELF_HOSTING_ADVANCED.md#email-required-for-authentication).
|
||||
|
||||
### Step 3 — Install CLI & Start Daemon
|
||||
|
||||
The daemon runs on your local machine (not inside Docker). It detects installed AI agent CLIs, registers them with the server, and executes tasks when agents are assigned work.
|
||||
|
||||
Each team member who wants to run AI agents locally needs to:
|
||||
|
||||
### a) Install the CLI and an AI agent
|
||||
|
||||
```bash
|
||||
brew install multica-ai/tap/multica
|
||||
```
|
||||
|
||||
You also need at least one AI agent CLI installed:
|
||||
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`claude` on PATH)
|
||||
- [Codex](https://github.com/openai/codex) (`codex` on PATH)
|
||||
|
||||
### b) One-command setup
|
||||
|
||||
```bash
|
||||
multica setup --local
|
||||
```
|
||||
|
||||
This automatically:
|
||||
1. Configures the CLI to connect to `localhost` (ports 8080/3000)
|
||||
2. Opens your browser for authentication
|
||||
3. Discovers your workspaces
|
||||
4. Starts the daemon in the background
|
||||
|
||||
To verify the daemon is running:
|
||||
|
||||
```bash
|
||||
multica daemon status
|
||||
```
|
||||
|
||||
> **Alternative:** If you prefer manual steps, see [Manual CLI Configuration](#manual-cli-configuration) below.
|
||||
|
||||
### Step 4 — Verify & Start Using
|
||||
|
||||
1. Open your workspace in the web app at http://localhost:3000
|
||||
2. Navigate to **Settings → Runtimes** — you should see your machine listed
|
||||
3. Go to **Settings → Agents** and create a new agent
|
||||
4. Create an issue and assign it to your agent — it will pick up the task automatically
|
||||
|
||||
## Stopping Services
|
||||
|
||||
If you installed via the install script:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --stop
|
||||
```
|
||||
|
||||
If you cloned the repo manually:
|
||||
|
||||
```bash
|
||||
# Stop the Docker Compose services (backend, frontend, database)
|
||||
make selfhost-stop
|
||||
|
||||
# Stop the local daemon
|
||||
multica daemon stop
|
||||
```
|
||||
|
||||
## Switching to Multica Cloud
|
||||
|
||||
If you've been self-hosting and want to switch your CLI to [Multica Cloud](https://multica.ai):
|
||||
|
||||
```bash
|
||||
multica config set server_url https://api.multica.ai
|
||||
multica config set app_url https://multica.ai
|
||||
multica login
|
||||
```
|
||||
|
||||
Or re-run the install script without `--local` — it will reconfigure the CLI automatically:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash
|
||||
```
|
||||
|
||||
> Your local Docker services are unaffected. Stop them separately if you no longer need them.
|
||||
|
||||
## Rebuilding After Updates
|
||||
|
||||
```bash
|
||||
git pull
|
||||
make selfhost
|
||||
```
|
||||
|
||||
Migrations run automatically on backend startup.
|
||||
|
||||
---
|
||||
|
||||
## Manual Docker Compose Setup
|
||||
|
||||
If you prefer running Docker Compose steps manually instead of `make selfhost`:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/multica-ai/multica.git
|
||||
cd multica
|
||||
@@ -174,34 +36,297 @@ Then start everything:
|
||||
docker compose -f docker-compose.selfhost.yml up -d
|
||||
```
|
||||
|
||||
## Manual CLI Configuration
|
||||
This builds and starts PostgreSQL, the backend (with auto-migration), and the frontend:
|
||||
|
||||
If you prefer configuring the CLI step by step instead of `multica setup`:
|
||||
- **Frontend:** http://localhost:3000
|
||||
- **Backend API:** http://localhost:8080
|
||||
|
||||
The backend automatically runs database migrations on startup — no manual migration step needed.
|
||||
|
||||
To run AI agents, you also need to set up the daemon on your local machine. See [Setting Up the Agent Daemon](#setting-up-the-agent-daemon) below.
|
||||
|
||||
### Rebuilding After Updates
|
||||
|
||||
```bash
|
||||
# Point CLI to your local server
|
||||
multica config local
|
||||
|
||||
# Or set URLs manually:
|
||||
# multica config set app_url http://localhost:3000
|
||||
# multica config set server_url http://localhost:8080
|
||||
|
||||
# Login (opens browser)
|
||||
multica login
|
||||
|
||||
# Start the daemon
|
||||
multica daemon start
|
||||
git pull
|
||||
docker compose -f docker-compose.selfhost.yml up -d --build
|
||||
```
|
||||
|
||||
For production deployments with TLS:
|
||||
Migrations run automatically on each backend startup.
|
||||
|
||||
## Configuration
|
||||
|
||||
All configuration is done via environment variables. Copy `.env.example` as a starting point.
|
||||
|
||||
### Required Variables
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `DATABASE_URL` | PostgreSQL connection string | `postgres://multica:multica@localhost:5432/multica?sslmode=disable` |
|
||||
| `JWT_SECRET` | **Must change from default.** Secret key for signing JWT tokens. Use a long random string. | `openssl rand -hex 32` |
|
||||
| `FRONTEND_ORIGIN` | URL where the frontend is served (used for CORS) | `https://app.example.com` |
|
||||
|
||||
### Email (Required for Authentication)
|
||||
|
||||
Multica uses email-based magic link authentication via [Resend](https://resend.com).
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `RESEND_API_KEY` | Your Resend API key |
|
||||
| `RESEND_FROM_EMAIL` | Sender email address (default: `noreply@multica.ai`) |
|
||||
|
||||
### Google OAuth (Optional)
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `GOOGLE_CLIENT_ID` | Google OAuth client ID |
|
||||
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret |
|
||||
| `GOOGLE_REDIRECT_URI` | OAuth callback URL (e.g. `https://app.example.com/auth/callback`) |
|
||||
|
||||
### File Storage (Optional)
|
||||
|
||||
For file uploads and attachments, configure S3 and CloudFront:
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `S3_BUCKET` | S3 bucket name |
|
||||
| `S3_REGION` | AWS region (default: `us-west-2`) |
|
||||
| `CLOUDFRONT_DOMAIN` | CloudFront distribution domain |
|
||||
| `CLOUDFRONT_KEY_PAIR_ID` | CloudFront key pair ID for signed URLs |
|
||||
| `CLOUDFRONT_PRIVATE_KEY` | CloudFront private key (PEM format) |
|
||||
| `COOKIE_DOMAIN` | Domain for CloudFront auth cookies |
|
||||
|
||||
### Server
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `PORT` | `8080` | Backend server port |
|
||||
| `FRONTEND_PORT` | `3000` | Frontend port |
|
||||
| `CORS_ALLOWED_ORIGINS` | Value of `FRONTEND_ORIGIN` | Comma-separated list of allowed origins |
|
||||
| `LOG_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` |
|
||||
|
||||
### CLI / Daemon
|
||||
|
||||
These are configured on each user's machine, not on the server:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `MULTICA_SERVER_URL` | `ws://localhost:8080/ws` | WebSocket URL for daemon → server connection |
|
||||
| `MULTICA_APP_URL` | `http://localhost:3000` | Frontend URL for CLI login flow |
|
||||
| `MULTICA_DAEMON_POLL_INTERVAL` | `3s` | How often the daemon polls for tasks |
|
||||
| `MULTICA_DAEMON_HEARTBEAT_INTERVAL` | `15s` | Heartbeat frequency |
|
||||
|
||||
## Database Setup
|
||||
|
||||
Multica requires PostgreSQL 17 with the pgvector extension.
|
||||
|
||||
### Using Docker Compose (Recommended)
|
||||
|
||||
The `docker-compose.selfhost.yml` includes PostgreSQL. No separate setup needed.
|
||||
|
||||
### Using Your Own PostgreSQL
|
||||
|
||||
If you prefer to use an existing PostgreSQL instance, ensure the pgvector extension is available:
|
||||
|
||||
```sql
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
```
|
||||
|
||||
Set `DATABASE_URL` in your `.env` and remove the `postgres` service from the compose file.
|
||||
|
||||
### Running Migrations Manually
|
||||
|
||||
The Docker Compose setup runs migrations automatically. If you need to run them manually:
|
||||
|
||||
```bash
|
||||
multica config set app_url https://app.example.com
|
||||
multica config set server_url https://api.example.com
|
||||
multica login
|
||||
multica daemon start
|
||||
# Using the built binary
|
||||
./server/bin/migrate up
|
||||
|
||||
# Or from source
|
||||
cd server && go run ./cmd/migrate up
|
||||
```
|
||||
|
||||
## Advanced Configuration
|
||||
## Manual Setup (Without Docker Compose)
|
||||
|
||||
For environment variables, manual setup (without Docker), reverse proxy configuration, database setup, and more, see the [Advanced Configuration Guide](SELF_HOSTING_ADVANCED.md).
|
||||
If you prefer to build and run services manually:
|
||||
|
||||
**Prerequisites:** Go 1.26+, Node.js 20+, pnpm 10.28+, PostgreSQL 17 with pgvector.
|
||||
|
||||
```bash
|
||||
# Start your PostgreSQL (or use: docker compose up -d postgres)
|
||||
|
||||
# Build the backend
|
||||
make build
|
||||
|
||||
# Run database migrations
|
||||
DATABASE_URL="your-database-url" ./server/bin/migrate up
|
||||
|
||||
# Start the backend server
|
||||
DATABASE_URL="your-database-url" PORT=8080 JWT_SECRET="your-secret" ./server/bin/server
|
||||
```
|
||||
|
||||
For the frontend:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm build
|
||||
|
||||
# Start the frontend (production mode)
|
||||
cd apps/web
|
||||
REMOTE_API_URL=http://localhost:8080 pnpm start
|
||||
```
|
||||
|
||||
## Reverse Proxy
|
||||
|
||||
In production, put a reverse proxy in front of both the backend and frontend to handle TLS and routing.
|
||||
|
||||
### Caddy (Recommended)
|
||||
|
||||
```
|
||||
app.example.com {
|
||||
reverse_proxy localhost:3000
|
||||
}
|
||||
|
||||
api.example.com {
|
||||
reverse_proxy localhost:8080
|
||||
}
|
||||
```
|
||||
|
||||
### Nginx
|
||||
|
||||
```nginx
|
||||
# Frontend
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name app.example.com;
|
||||
|
||||
ssl_certificate /path/to/cert.pem;
|
||||
ssl_certificate_key /path/to/key.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
# Backend API
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name api.example.com;
|
||||
|
||||
ssl_certificate /path/to/cert.pem;
|
||||
ssl_certificate_key /path/to/key.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# WebSocket support
|
||||
location /ws {
|
||||
proxy_pass http://localhost:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When using separate domains for frontend and backend, set these environment variables accordingly:
|
||||
|
||||
```bash
|
||||
# Backend
|
||||
FRONTEND_ORIGIN=https://app.example.com
|
||||
CORS_ALLOWED_ORIGINS=https://app.example.com
|
||||
|
||||
# Frontend (set before building the frontend image)
|
||||
REMOTE_API_URL=https://api.example.com
|
||||
NEXT_PUBLIC_API_URL=https://api.example.com
|
||||
NEXT_PUBLIC_WS_URL=wss://api.example.com/ws
|
||||
```
|
||||
|
||||
## Health Check
|
||||
|
||||
The backend exposes a health check endpoint:
|
||||
|
||||
```
|
||||
GET /health
|
||||
→ {"status":"ok"}
|
||||
```
|
||||
|
||||
Use this for load balancer health checks or monitoring.
|
||||
|
||||
## Setting Up the Agent Daemon
|
||||
|
||||
The daemon runs on your local machine (not inside Docker). It detects installed AI agent CLIs, registers them as runtimes with the server, and executes tasks when agents are assigned work.
|
||||
|
||||
Each team member who wants to run AI agents locally needs to:
|
||||
|
||||
1. **Install the CLI**
|
||||
|
||||
```bash
|
||||
brew tap multica-ai/tap
|
||||
brew install multica-cli
|
||||
```
|
||||
|
||||
2. **Install an AI agent CLI** — at least one of:
|
||||
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`claude` on PATH)
|
||||
- [Codex](https://github.com/openai/codex) (`codex` on PATH)
|
||||
|
||||
3. **Point the CLI to your server**
|
||||
|
||||
The CLI defaults to the hosted Multica service. For self-hosted setups, you **must** set the server URLs before logging in:
|
||||
|
||||
```bash
|
||||
# Local Docker Compose deployment (default ports):
|
||||
export MULTICA_APP_URL=http://localhost:3000
|
||||
export MULTICA_SERVER_URL=ws://localhost:8080/ws
|
||||
|
||||
# Production deployment with TLS:
|
||||
# export MULTICA_APP_URL=https://app.example.com
|
||||
# export MULTICA_SERVER_URL=wss://api.example.com/ws
|
||||
```
|
||||
|
||||
> **Note:** Use `http://` and `ws://` for local deployments without TLS. Use `https://` and `wss://` for production deployments behind a TLS-terminating reverse proxy.
|
||||
|
||||
You can also set these persistently so you don't need to export them each time:
|
||||
|
||||
```bash
|
||||
multica config set app_url http://localhost:3000
|
||||
multica config set server_url ws://localhost:8080/ws
|
||||
```
|
||||
|
||||
4. **Authenticate and start**
|
||||
|
||||
```bash
|
||||
# Login (opens browser to your frontend)
|
||||
multica login
|
||||
|
||||
# Start the daemon
|
||||
multica daemon start
|
||||
```
|
||||
|
||||
The login flow opens your browser, authenticates you via the frontend, and stores a personal access token locally. The daemon then uses this token to register with the backend.
|
||||
|
||||
To verify the daemon is running:
|
||||
|
||||
```bash
|
||||
multica daemon status
|
||||
```
|
||||
|
||||
## Upgrading
|
||||
|
||||
```bash
|
||||
git pull
|
||||
docker compose -f docker-compose.selfhost.yml up -d --build
|
||||
```
|
||||
|
||||
Migrations run automatically on backend startup. They are idempotent — running them multiple times has no effect.
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
# Self-Hosting — Advanced Configuration
|
||||
|
||||
This document covers advanced configuration for self-hosted Multica deployments. For the quick start guide, see [SELF_HOSTING.md](SELF_HOSTING.md).
|
||||
|
||||
## Configuration
|
||||
|
||||
All configuration is done via environment variables. Copy `.env.example` as a starting point.
|
||||
|
||||
### Required Variables
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `DATABASE_URL` | PostgreSQL connection string | `postgres://multica:multica@localhost:5432/multica?sslmode=disable` |
|
||||
| `JWT_SECRET` | **Must change from default.** Secret key for signing JWT tokens. Use a long random string. | `openssl rand -hex 32` |
|
||||
| `FRONTEND_ORIGIN` | URL where the frontend is served (used for CORS) | `https://app.example.com` |
|
||||
|
||||
### Email (Required for Authentication)
|
||||
|
||||
Multica uses email-based magic link authentication via [Resend](https://resend.com).
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `RESEND_API_KEY` | Your Resend API key |
|
||||
| `RESEND_FROM_EMAIL` | Sender email address (default: `noreply@multica.ai`) |
|
||||
|
||||
> **Note:** For local/development deployments without email configured, you can use the master verification code `888888` to log in.
|
||||
|
||||
### Google OAuth (Optional)
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `GOOGLE_CLIENT_ID` | Google OAuth client ID |
|
||||
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret |
|
||||
| `GOOGLE_REDIRECT_URI` | OAuth callback URL (e.g. `https://app.example.com/auth/callback`) |
|
||||
|
||||
### File Storage (Optional)
|
||||
|
||||
For file uploads and attachments, configure S3 and CloudFront:
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `S3_BUCKET` | S3 bucket name |
|
||||
| `S3_REGION` | AWS region (default: `us-west-2`) |
|
||||
| `CLOUDFRONT_DOMAIN` | CloudFront distribution domain |
|
||||
| `CLOUDFRONT_KEY_PAIR_ID` | CloudFront key pair ID for signed URLs |
|
||||
| `CLOUDFRONT_PRIVATE_KEY` | CloudFront private key (PEM format) |
|
||||
| `COOKIE_DOMAIN` | Domain for CloudFront auth cookies |
|
||||
|
||||
### Server
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `PORT` | `8080` | Backend server port |
|
||||
| `FRONTEND_PORT` | `3000` | Frontend port |
|
||||
| `CORS_ALLOWED_ORIGINS` | Value of `FRONTEND_ORIGIN` | Comma-separated list of allowed origins |
|
||||
| `LOG_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` |
|
||||
|
||||
### CLI / Daemon
|
||||
|
||||
These are configured on each user's machine, not on the server:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `MULTICA_SERVER_URL` | `ws://localhost:8080/ws` | WebSocket URL for daemon → server connection |
|
||||
| `MULTICA_APP_URL` | `http://localhost:3000` | Frontend URL for CLI login flow |
|
||||
| `MULTICA_DAEMON_POLL_INTERVAL` | `3s` | How often the daemon polls for tasks |
|
||||
| `MULTICA_DAEMON_HEARTBEAT_INTERVAL` | `15s` | Heartbeat frequency |
|
||||
|
||||
## Database Setup
|
||||
|
||||
Multica requires PostgreSQL 17 with the pgvector extension.
|
||||
|
||||
### Using Docker Compose (Recommended)
|
||||
|
||||
The `docker-compose.selfhost.yml` includes PostgreSQL. No separate setup needed.
|
||||
|
||||
### Using Your Own PostgreSQL
|
||||
|
||||
If you prefer to use an existing PostgreSQL instance, ensure the pgvector extension is available:
|
||||
|
||||
```sql
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
```
|
||||
|
||||
Set `DATABASE_URL` in your `.env` and remove the `postgres` service from the compose file.
|
||||
|
||||
### Running Migrations Manually
|
||||
|
||||
The Docker Compose setup runs migrations automatically. If you need to run them manually:
|
||||
|
||||
```bash
|
||||
# Using the built binary
|
||||
./server/bin/migrate up
|
||||
|
||||
# Or from source
|
||||
cd server && go run ./cmd/migrate up
|
||||
```
|
||||
|
||||
## Manual Setup (Without Docker Compose)
|
||||
|
||||
If you prefer to build and run services manually:
|
||||
|
||||
**Prerequisites:** Go 1.26+, Node.js 20+, pnpm 10.28+, PostgreSQL 17 with pgvector.
|
||||
|
||||
```bash
|
||||
# Start your PostgreSQL (or use: docker compose up -d postgres)
|
||||
|
||||
# Build the backend
|
||||
make build
|
||||
|
||||
# Run database migrations
|
||||
DATABASE_URL="your-database-url" ./server/bin/migrate up
|
||||
|
||||
# Start the backend server
|
||||
DATABASE_URL="your-database-url" PORT=8080 JWT_SECRET="your-secret" ./server/bin/server
|
||||
```
|
||||
|
||||
For the frontend:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm build
|
||||
|
||||
# Start the frontend (production mode)
|
||||
cd apps/web
|
||||
REMOTE_API_URL=http://localhost:8080 pnpm start
|
||||
```
|
||||
|
||||
## Reverse Proxy
|
||||
|
||||
In production, put a reverse proxy in front of both the backend and frontend to handle TLS and routing.
|
||||
|
||||
### Caddy (Recommended)
|
||||
|
||||
```
|
||||
app.example.com {
|
||||
reverse_proxy localhost:3000
|
||||
}
|
||||
|
||||
api.example.com {
|
||||
reverse_proxy localhost:8080
|
||||
}
|
||||
```
|
||||
|
||||
### Nginx
|
||||
|
||||
```nginx
|
||||
# Frontend
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name app.example.com;
|
||||
|
||||
ssl_certificate /path/to/cert.pem;
|
||||
ssl_certificate_key /path/to/key.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
# Backend API
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name api.example.com;
|
||||
|
||||
ssl_certificate /path/to/cert.pem;
|
||||
ssl_certificate_key /path/to/key.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# WebSocket support
|
||||
location /ws {
|
||||
proxy_pass http://localhost:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When using separate domains for frontend and backend, set these environment variables accordingly:
|
||||
|
||||
```bash
|
||||
# Backend
|
||||
FRONTEND_ORIGIN=https://app.example.com
|
||||
CORS_ALLOWED_ORIGINS=https://app.example.com
|
||||
|
||||
# Frontend (set before building the frontend image)
|
||||
REMOTE_API_URL=https://api.example.com
|
||||
NEXT_PUBLIC_API_URL=https://api.example.com
|
||||
NEXT_PUBLIC_WS_URL=wss://api.example.com/ws
|
||||
```
|
||||
|
||||
## Health Check
|
||||
|
||||
The backend exposes a health check endpoint:
|
||||
|
||||
```
|
||||
GET /health
|
||||
→ {"status":"ok"}
|
||||
```
|
||||
|
||||
Use this for load balancer health checks or monitoring.
|
||||
|
||||
## Upgrading
|
||||
|
||||
```bash
|
||||
git pull
|
||||
docker compose -f docker-compose.selfhost.yml up -d --build
|
||||
```
|
||||
|
||||
Migrations run automatically on backend startup. They are idempotent — running them multiple times has no effect.
|
||||
@@ -1,74 +0,0 @@
|
||||
# Self-Hosting Setup (for AI Agents)
|
||||
|
||||
This document is designed for AI agents to execute. Follow these steps exactly to deploy a local Multica instance and connect to it.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker and Docker Compose installed
|
||||
- Homebrew installed (for CLI)
|
||||
- At least one AI agent CLI on PATH: `claude` or `codex`
|
||||
|
||||
## One-Command Install
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --local
|
||||
```
|
||||
|
||||
This installs everything: clones the repo, starts Docker services, and installs the CLI.
|
||||
|
||||
Wait for the output `✓ Multica is installed and running!` before proceeding.
|
||||
|
||||
**Expected result:**
|
||||
- Frontend at http://localhost:3000
|
||||
- Backend at http://localhost:8080
|
||||
- `multica` CLI installed and configured for localhost
|
||||
|
||||
## Alternative: Manual Setup
|
||||
|
||||
```bash
|
||||
git clone https://github.com/multica-ai/multica.git
|
||||
cd multica
|
||||
make selfhost
|
||||
brew install multica-ai/tap/multica
|
||||
multica setup --local
|
||||
```
|
||||
|
||||
The `multica setup --local` command will:
|
||||
1. Configure CLI to connect to localhost:8080 / localhost:3000
|
||||
2. Open a browser for login — use verification code `888888` with any email
|
||||
3. Discover workspaces automatically
|
||||
4. Start the daemon in the background
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
multica daemon status
|
||||
```
|
||||
|
||||
Should show `running` with detected agents.
|
||||
|
||||
## Stopping
|
||||
|
||||
```bash
|
||||
# Stop the daemon
|
||||
multica daemon stop
|
||||
|
||||
# Stop all Docker services
|
||||
cd multica
|
||||
make selfhost-stop
|
||||
```
|
||||
|
||||
## Custom Ports
|
||||
|
||||
If the default ports (8080/3000) are in use:
|
||||
|
||||
1. Edit `.env` and change `PORT` and `FRONTEND_PORT`
|
||||
2. Run `make selfhost`
|
||||
3. Run `multica setup --local --port <PORT> --frontend-port <FRONTEND_PORT>`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Backend not ready:** `docker compose -f docker-compose.selfhost.yml logs backend`
|
||||
- **Frontend not ready:** `docker compose -f docker-compose.selfhost.yml logs frontend`
|
||||
- **Daemon issues:** `multica daemon logs`
|
||||
- **Health check:** `curl http://localhost:8080/health`
|
||||
@@ -13,142 +13,50 @@ Multica has three components:
|
||||
| **Frontend** | Web application | Next.js 16 |
|
||||
| **Database** | Primary data store | PostgreSQL 17 with pgvector |
|
||||
|
||||
Each user who wants to run AI agents locally also installs the **`multica` CLI** and runs the **agent daemon** on their own machine.
|
||||
Additionally, each user who wants to run AI agents locally installs the **`multica` CLI** and runs the **agent daemon** on their own machine.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker and Docker Compose
|
||||
- Docker and Docker Compose (recommended), or:
|
||||
- Go 1.26+ (to build from source)
|
||||
- Node.js 20+ and pnpm 10.28+ (to build the frontend)
|
||||
- PostgreSQL 17 with the pgvector extension
|
||||
|
||||
## Quick Install
|
||||
|
||||
One command to set up everything:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --local
|
||||
```
|
||||
|
||||
This clones the repo, starts all services, installs the CLI, and configures everything. Then:
|
||||
|
||||
1. Open http://localhost:3000 — log in with any email + code **`888888`**
|
||||
2. Run `multica login` and `multica daemon start`
|
||||
|
||||
<Callout>
|
||||
For a step-by-step setup, see below.
|
||||
</Callout>
|
||||
|
||||
## Step-by-Step Setup
|
||||
|
||||
### Step 1 — Start the Server
|
||||
## Quick Start (Docker Compose)
|
||||
|
||||
```bash
|
||||
git clone https://github.com/multica-ai/multica.git
|
||||
cd multica
|
||||
make selfhost
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
`make selfhost` automatically creates `.env`, generates a random `JWT_SECRET`, and starts all services via Docker Compose.
|
||||
|
||||
Once ready:
|
||||
|
||||
- **Frontend:** http://localhost:3000
|
||||
- **Backend API:** http://localhost:8080
|
||||
|
||||
<Callout>
|
||||
If you prefer running the Docker Compose steps manually: `cp .env.example .env`, edit `JWT_SECRET`, then `docker compose -f docker-compose.selfhost.yml up -d`.
|
||||
</Callout>
|
||||
|
||||
### Step 2 — Log In
|
||||
|
||||
Open http://localhost:3000. Enter any email address and use verification code **`888888`** to log in.
|
||||
|
||||
<Callout>
|
||||
This master code works in all non-production environments (when `APP_ENV` is not set to `production`). For production, configure an email provider — see [Configuration](#configuration) below.
|
||||
</Callout>
|
||||
|
||||
### Step 3 — Install CLI & Start Daemon
|
||||
|
||||
The daemon runs on your local machine (not inside Docker). It detects installed AI agent CLIs, registers them with the server, and executes tasks.
|
||||
|
||||
### a) Install the CLI and an AI agent
|
||||
Edit `.env` with your production values (see [Configuration](#configuration) below), then:
|
||||
|
||||
```bash
|
||||
brew install multica-ai/tap/multica
|
||||
# Start PostgreSQL
|
||||
docker compose up -d
|
||||
|
||||
# Build the backend
|
||||
make build
|
||||
|
||||
# Run database migrations
|
||||
DATABASE_URL="your-database-url" ./server/bin/migrate up
|
||||
|
||||
# Start the backend server
|
||||
DATABASE_URL="your-database-url" PORT=8080 ./server/bin/server
|
||||
```
|
||||
|
||||
You also need at least one AI agent CLI:
|
||||
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`claude` on PATH)
|
||||
- [Codex](https://github.com/openai/codex) (`codex` on PATH)
|
||||
|
||||
### b) One-command setup
|
||||
For the frontend:
|
||||
|
||||
```bash
|
||||
multica setup --local
|
||||
pnpm install
|
||||
pnpm build
|
||||
|
||||
# Start the frontend (production mode)
|
||||
cd apps/web
|
||||
REMOTE_API_URL=http://localhost:8080 pnpm start
|
||||
```
|
||||
|
||||
This automatically:
|
||||
1. Configures the CLI to connect to `localhost`
|
||||
2. Opens your browser for authentication
|
||||
3. Discovers your workspaces
|
||||
4. Starts the daemon in the background
|
||||
|
||||
Verify the daemon is running:
|
||||
|
||||
```bash
|
||||
multica daemon status
|
||||
```
|
||||
|
||||
<Callout>
|
||||
Alternatively, configure manually: `multica config local && multica login && multica daemon start`
|
||||
</Callout>
|
||||
|
||||
### Step 4 — Verify & Start Using
|
||||
|
||||
1. Open your workspace at http://localhost:3000
|
||||
2. Navigate to **Settings → Runtimes** — you should see your machine listed
|
||||
3. Go to **Settings → Agents** and create a new agent
|
||||
4. Create an issue and assign it to your agent
|
||||
|
||||
## Stopping Services
|
||||
|
||||
```bash
|
||||
# Stop Docker Compose services
|
||||
make selfhost-stop
|
||||
|
||||
# Stop the local daemon
|
||||
multica daemon stop
|
||||
```
|
||||
|
||||
## Switching to Multica Cloud
|
||||
|
||||
If you've been self-hosting and want to switch your CLI to [Multica Cloud](https://multica.ai):
|
||||
|
||||
```bash
|
||||
multica config set server_url https://api.multica.ai
|
||||
multica config set app_url https://multica.ai
|
||||
multica login
|
||||
```
|
||||
|
||||
Or re-run the install script without `--local` — it will reconfigure the CLI automatically:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash
|
||||
```
|
||||
|
||||
<Callout>
|
||||
Your local Docker services are unaffected. Stop them separately if you no longer need them.
|
||||
</Callout>
|
||||
|
||||
## Rebuilding After Updates
|
||||
|
||||
```bash
|
||||
git pull
|
||||
make selfhost
|
||||
```
|
||||
|
||||
Migrations run automatically on backend startup.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
All configuration is done via environment variables. Copy `.env.example` as a starting point.
|
||||
@@ -243,36 +151,6 @@ Migrations must be run before starting the server:
|
||||
cd server && go run ./cmd/migrate up
|
||||
```
|
||||
|
||||
## Manual Setup (Without Docker Compose)
|
||||
|
||||
If you prefer to build and run services manually:
|
||||
|
||||
**Prerequisites:** Go 1.26+, Node.js 20+, pnpm 10.28+, PostgreSQL 17 with pgvector.
|
||||
|
||||
```bash
|
||||
# Start your PostgreSQL (or use: docker compose up -d postgres)
|
||||
|
||||
# Build the backend
|
||||
make build
|
||||
|
||||
# Run database migrations
|
||||
DATABASE_URL="your-database-url" ./server/bin/migrate up
|
||||
|
||||
# Start the backend server
|
||||
DATABASE_URL="your-database-url" PORT=8080 JWT_SECRET="your-secret" ./server/bin/server
|
||||
```
|
||||
|
||||
For the frontend:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm build
|
||||
|
||||
# Start the frontend (production mode)
|
||||
cd apps/web
|
||||
REMOTE_API_URL=http://localhost:8080 pnpm start
|
||||
```
|
||||
|
||||
## Reverse Proxy
|
||||
|
||||
In production, put a reverse proxy in front of both the backend and frontend to handle TLS and routing.
|
||||
@@ -361,6 +239,39 @@ GET /health
|
||||
|
||||
Use this for load balancer health checks or monitoring.
|
||||
|
||||
## Setting Up the Agent Daemon
|
||||
|
||||
Each team member who wants to run AI agents locally needs to:
|
||||
|
||||
1. **Install the CLI**
|
||||
|
||||
```bash
|
||||
brew tap multica-ai/tap
|
||||
brew install multica-cli
|
||||
```
|
||||
|
||||
2. **Install an AI agent CLI** — at least one of:
|
||||
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`claude` on PATH)
|
||||
- [Codex](https://github.com/openai/codex) (`codex` on PATH)
|
||||
|
||||
3. **Authenticate and start**
|
||||
|
||||
```bash
|
||||
# Point CLI to your server
|
||||
export MULTICA_APP_URL=https://app.example.com
|
||||
export MULTICA_SERVER_URL=wss://api.example.com/ws
|
||||
|
||||
# Login (opens browser)
|
||||
multica login
|
||||
|
||||
# Start the daemon
|
||||
multica daemon start
|
||||
```
|
||||
|
||||
> **Note:** Use `https://` and `wss://` for production deployments behind a TLS-terminating reverse proxy. For local or development deployments without TLS, use `http://` and `ws://` instead.
|
||||
|
||||
The daemon auto-detects installed agent CLIs and registers itself with the server.
|
||||
|
||||
## Upgrading
|
||||
|
||||
1. Pull the latest code or image
|
||||
|
||||
@@ -119,40 +119,20 @@ export function AgentLiveCard({ issueId }: AgentLiveCardProps) {
|
||||
let cancelled = false;
|
||||
api.getActiveTasksForIssue(issueId).then(({ tasks }) => {
|
||||
if (cancelled || tasks.length === 0) return;
|
||||
|
||||
// Show cards immediately with empty timeline
|
||||
setTaskStates((prev) => {
|
||||
const next = new Map(prev);
|
||||
for (const task of tasks) {
|
||||
if (!next.has(task.id)) {
|
||||
next.set(task.id, { task, items: [] });
|
||||
}
|
||||
}
|
||||
return next;
|
||||
});
|
||||
|
||||
// Load messages per task in the background
|
||||
for (const task of tasks) {
|
||||
api.listTaskMessages(task.id).then((msgs) => {
|
||||
if (cancelled) return;
|
||||
const newStates = new Map<string, TaskState>();
|
||||
const loadPromises = tasks.map(async (task) => {
|
||||
try {
|
||||
const msgs = await api.listTaskMessages(task.id);
|
||||
const timeline = buildTimeline(msgs);
|
||||
for (const m of msgs) seenSeqs.current.add(`${m.task_id}:${m.seq}`);
|
||||
setTaskStates((prev) => {
|
||||
const next = new Map(prev);
|
||||
const existing = next.get(task.id);
|
||||
if (existing) {
|
||||
// Merge: keep any WS-delivered items not in the loaded batch
|
||||
const loadedSeqs = new Set(timeline.map((i) => i.seq));
|
||||
const wsOnly = existing.items.filter((i) => !loadedSeqs.has(i.seq));
|
||||
const merged = [...timeline, ...wsOnly].sort((a, b) => a.seq - b.seq);
|
||||
next.set(task.id, { task: existing.task, items: merged });
|
||||
} else {
|
||||
next.set(task.id, { task, items: timeline });
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}).catch(console.error);
|
||||
}
|
||||
newStates.set(task.id, { task, items: timeline });
|
||||
} catch {
|
||||
newStates.set(task.id, { task, items: [] });
|
||||
}
|
||||
});
|
||||
Promise.all(loadPromises).then(() => {
|
||||
if (!cancelled) setTaskStates(newStates);
|
||||
});
|
||||
}).catch(console.error);
|
||||
|
||||
return () => { cancelled = true; };
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useCallback } from "react";
|
||||
import { Plus, FolderKanban, ChevronRight, Maximize2, Minimize2, X as XIcon, UserMinus, Check } from "lucide-react";
|
||||
import { useState, useRef } from "react";
|
||||
import { Plus, FolderKanban, ChevronRight, Maximize2, Minimize2, X as XIcon, UserMinus } from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { projectListOptions } from "@multica/core/projects/queries";
|
||||
import { useCreateProject, useUpdateProject } from "@multica/core/projects/mutations";
|
||||
import { useCreateProject } from "@multica/core/projects/mutations";
|
||||
import { PROJECT_STATUS_CONFIG, PROJECT_STATUS_ORDER, PROJECT_PRIORITY_CONFIG, PROJECT_PRIORITY_ORDER } from "@multica/core/projects/config";
|
||||
import { useWorkspaceId } from "@multica/core/hooks";
|
||||
import { useWorkspaceStore } from "@multica/core/workspace";
|
||||
@@ -36,7 +36,7 @@ import { Tooltip, TooltipTrigger, TooltipContent } from "@multica/ui/components/
|
||||
import { ContentEditor, type ContentEditorRef } from "../../editor";
|
||||
import { TitleEditor } from "../../editor";
|
||||
import { EmojiPicker } from "@multica/ui/components/common/emoji-picker";
|
||||
import type { Project, ProjectStatus, ProjectPriority, UpdateProjectRequest } from "@multica/core/types";
|
||||
import type { Project, ProjectStatus, ProjectPriority } from "@multica/core/types";
|
||||
import { PriorityIcon } from "../../issues/components/priority-icon";
|
||||
|
||||
function formatRelativeDate(date: string): string {
|
||||
@@ -50,83 +50,32 @@ function formatRelativeDate(date: string): string {
|
||||
}
|
||||
|
||||
function ProjectRow({ project }: { project: Project }) {
|
||||
const wsId = useWorkspaceId();
|
||||
const statusCfg = PROJECT_STATUS_CONFIG[project.status];
|
||||
const priorityCfg = PROJECT_PRIORITY_CONFIG[project.priority];
|
||||
const updateProject = useUpdateProject();
|
||||
const { data: members = [] } = useQuery(memberListOptions(wsId));
|
||||
const { data: agents = [] } = useQuery(agentListOptions(wsId));
|
||||
const { getActorName } = useActorName();
|
||||
|
||||
const [leadOpen, setLeadOpen] = useState(false);
|
||||
const [leadFilter, setLeadFilter] = useState("");
|
||||
const leadQuery = leadFilter.toLowerCase();
|
||||
const filteredMembers = members.filter((m) => m.name.toLowerCase().includes(leadQuery));
|
||||
const filteredAgents = agents.filter((a) => !a.archived_at && a.name.toLowerCase().includes(leadQuery));
|
||||
|
||||
const handleUpdate = useCallback(
|
||||
(data: UpdateProjectRequest) => {
|
||||
updateProject.mutate({ id: project.id, ...data });
|
||||
},
|
||||
[project.id, updateProject],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="group/row flex h-11 items-center gap-2 px-5 text-sm transition-colors hover:bg-accent/40">
|
||||
{/* Icon + Name (navigates to detail) */}
|
||||
<AppLink
|
||||
href={`/projects/${project.id}`}
|
||||
className="flex min-w-0 flex-1 items-center gap-2"
|
||||
>
|
||||
<span className="shrink-0 w-[24px] text-center text-base">{project.icon || "📁"}</span>
|
||||
<span className="min-w-0 flex-1 truncate font-medium">{project.title}</span>
|
||||
</AppLink>
|
||||
<AppLink
|
||||
href={`/projects/${project.id}`}
|
||||
className="group/row flex h-11 items-center gap-2 px-5 text-sm transition-colors hover:bg-accent/40"
|
||||
>
|
||||
{/* Icon + Name */}
|
||||
<span className="shrink-0 w-[24px] text-center text-base">{project.icon || "📁"}</span>
|
||||
<span className="min-w-0 flex-1 truncate font-medium">{project.title}</span>
|
||||
|
||||
{/* Priority — dropdown */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<button type="button" className="flex w-24 items-center justify-center gap-1 shrink-0 rounded px-1 py-0.5 hover:bg-accent/60 transition-colors cursor-pointer">
|
||||
<PriorityIcon priority={project.priority} />
|
||||
<span className={cn("text-xs", priorityCfg.color)}>{priorityCfg.label}</span>
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuContent align="start" className="w-44">
|
||||
{PROJECT_PRIORITY_ORDER.map((p) => (
|
||||
<DropdownMenuItem key={p} onClick={() => handleUpdate({ priority: p as ProjectPriority })}>
|
||||
<PriorityIcon priority={p} />
|
||||
<span>{PROJECT_PRIORITY_CONFIG[p].label}</span>
|
||||
{p === project.priority && <Check className="ml-auto h-3.5 w-3.5" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{/* Priority */}
|
||||
<span className="flex w-24 items-center justify-center gap-1 shrink-0">
|
||||
<PriorityIcon priority={project.priority} />
|
||||
<span className={cn("text-xs", priorityCfg.color)}>{priorityCfg.label}</span>
|
||||
</span>
|
||||
|
||||
{/* Status — dropdown */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<button type="button" className={cn(
|
||||
"inline-flex items-center gap-1 rounded px-2 py-0.5 text-xs font-medium shrink-0 w-28 justify-center cursor-pointer hover:opacity-80 transition-opacity",
|
||||
statusCfg.badgeBg, statusCfg.badgeText,
|
||||
)}>
|
||||
{statusCfg.label}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuContent align="start" className="w-44">
|
||||
{PROJECT_STATUS_ORDER.map((s) => (
|
||||
<DropdownMenuItem key={s} onClick={() => handleUpdate({ status: s as ProjectStatus })}>
|
||||
<span className={cn("size-2 rounded-full", PROJECT_STATUS_CONFIG[s].dotColor)} />
|
||||
<span>{PROJECT_STATUS_CONFIG[s].label}</span>
|
||||
{s === project.status && <Check className="ml-auto h-3.5 w-3.5" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{/* Status */}
|
||||
<span className={cn(
|
||||
"inline-flex items-center gap-1 rounded px-2 py-0.5 text-xs font-medium shrink-0 w-28 justify-center",
|
||||
statusCfg.badgeBg, statusCfg.badgeText,
|
||||
)}>
|
||||
{statusCfg.label}
|
||||
</span>
|
||||
|
||||
{/* Progress (read-only) */}
|
||||
{/* Progress */}
|
||||
<span className="flex w-24 items-center justify-center gap-1.5 shrink-0">
|
||||
{project.issue_count > 0 ? (
|
||||
<>
|
||||
@@ -145,85 +94,20 @@ function ProjectRow({ project }: { project: Project }) {
|
||||
)}
|
||||
</span>
|
||||
|
||||
{/* Lead — popover */}
|
||||
<Popover open={leadOpen} onOpenChange={(v) => { setLeadOpen(v); if (!v) setLeadFilter(""); }}>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<button type="button" className="flex w-10 items-center justify-center shrink-0 rounded-full hover:ring-2 hover:ring-accent transition-all cursor-pointer">
|
||||
{project.lead_type && project.lead_id ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<span><ActorAvatar actorType={project.lead_type} actorId={project.lead_id} size={22} /></span>} />
|
||||
<TooltipContent side="bottom">{getActorName(project.lead_type, project.lead_id)}</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span className="h-[22px] w-[22px] rounded-full border border-dashed border-muted-foreground/30" />
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<PopoverContent align="start" className="w-52 p-0">
|
||||
<div className="px-2 py-1.5 border-b">
|
||||
<input
|
||||
type="text"
|
||||
value={leadFilter}
|
||||
onChange={(e) => setLeadFilter(e.target.value)}
|
||||
placeholder="Assign lead..."
|
||||
className="w-full bg-transparent text-sm placeholder:text-muted-foreground outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-1 max-h-60 overflow-y-auto">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { handleUpdate({ lead_type: null, lead_id: null }); setLeadOpen(false); }}
|
||||
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors"
|
||||
>
|
||||
<UserMinus className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">No lead</span>
|
||||
</button>
|
||||
{filteredMembers.length > 0 && (
|
||||
<>
|
||||
<div className="px-2 pt-2 pb-1 text-xs font-medium text-muted-foreground uppercase tracking-wider">Members</div>
|
||||
{filteredMembers.map((m) => (
|
||||
<button
|
||||
type="button"
|
||||
key={m.user_id}
|
||||
onClick={() => { handleUpdate({ lead_type: "member", lead_id: m.user_id }); setLeadOpen(false); }}
|
||||
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors"
|
||||
>
|
||||
<ActorAvatar actorType="member" actorId={m.user_id} size={16} />
|
||||
<span>{m.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{filteredAgents.length > 0 && (
|
||||
<>
|
||||
<div className="px-2 pt-2 pb-1 text-xs font-medium text-muted-foreground uppercase tracking-wider">Agents</div>
|
||||
{filteredAgents.map((a) => (
|
||||
<button
|
||||
type="button"
|
||||
key={a.id}
|
||||
onClick={() => { handleUpdate({ lead_type: "agent", lead_id: a.id }); setLeadOpen(false); }}
|
||||
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors"
|
||||
>
|
||||
<ActorAvatar actorType="agent" actorId={a.id} size={16} />
|
||||
<span>{a.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{filteredMembers.length === 0 && filteredAgents.length === 0 && leadFilter && (
|
||||
<div className="px-2 py-3 text-center text-sm text-muted-foreground">No results</div>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{/* Lead */}
|
||||
<span className="flex w-10 items-center justify-center shrink-0">
|
||||
{project.lead_type && project.lead_id ? (
|
||||
<ActorAvatar actorType={project.lead_type} actorId={project.lead_id} size={22} />
|
||||
) : (
|
||||
<span className="h-[22px] w-[22px] rounded-full border border-dashed border-muted-foreground/30" />
|
||||
)}
|
||||
</span>
|
||||
|
||||
{/* Created */}
|
||||
<span className="w-20 shrink-0 text-right text-xs text-muted-foreground tabular-nums">
|
||||
{formatRelativeDate(project.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
</AppLink>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,433 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Multica installer — one command to get started.
|
||||
#
|
||||
# Install CLI (default): connects to multica.ai
|
||||
# curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash
|
||||
#
|
||||
# Self-host: starts a local Multica server + installs CLI + configures
|
||||
# curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --local
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
REPO_URL="https://github.com/multica-ai/multica.git"
|
||||
REPO_WEB_URL="https://github.com/multica-ai/multica" # without .git, for GitHub web APIs
|
||||
INSTALL_DIR="${MULTICA_INSTALL_DIR:-$HOME/.multica/server}"
|
||||
BREW_PACKAGE="multica-ai/tap/multica"
|
||||
|
||||
# Colors (disabled when not a terminal)
|
||||
if [ -t 1 ] || [ -t 2 ]; then
|
||||
BOLD='\033[1m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
RED='\033[0;31m'
|
||||
CYAN='\033[0;36m'
|
||||
RESET='\033[0m'
|
||||
else
|
||||
BOLD='' GREEN='' YELLOW='' RED='' CYAN='' RESET=''
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
info() { printf "${BOLD}${CYAN}==> %s${RESET}\n" "$*"; }
|
||||
ok() { printf "${BOLD}${GREEN}✓ %s${RESET}\n" "$*"; }
|
||||
warn() { printf "${BOLD}${YELLOW}⚠ %s${RESET}\n" "$*" >&2; }
|
||||
fail() { printf "${BOLD}${RED}✗ %s${RESET}\n" "$*" >&2; exit 1; }
|
||||
|
||||
command_exists() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
detect_os() {
|
||||
case "$(uname -s)" in
|
||||
Darwin) OS="darwin" ;;
|
||||
Linux) OS="linux" ;;
|
||||
*) fail "Unsupported operating system: $(uname -s). Multica supports macOS and Linux." ;;
|
||||
esac
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
case "$ARCH" in
|
||||
x86_64) ARCH="amd64" ;;
|
||||
aarch64) ARCH="arm64" ;;
|
||||
arm64) ARCH="arm64" ;;
|
||||
*) fail "Unsupported architecture: $ARCH" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI Installation
|
||||
# ---------------------------------------------------------------------------
|
||||
install_cli_brew() {
|
||||
info "Installing Multica CLI via Homebrew..."
|
||||
if ! brew tap multica-ai/tap 2>/dev/null; then
|
||||
fail "Failed to add Homebrew tap. Check your network connection."
|
||||
fi
|
||||
# brew install exits non-zero if already installed on older Homebrew versions
|
||||
if ! brew install multica 2>/dev/null; then
|
||||
if brew list multica >/dev/null 2>&1; then
|
||||
ok "Multica CLI already installed via Homebrew"
|
||||
else
|
||||
fail "Failed to install multica via Homebrew."
|
||||
fi
|
||||
else
|
||||
ok "Multica CLI installed via Homebrew"
|
||||
fi
|
||||
}
|
||||
|
||||
install_cli_binary() {
|
||||
info "Installing Multica CLI from GitHub Releases..."
|
||||
|
||||
# Get latest release tag
|
||||
local latest
|
||||
latest=$(curl -sI "$REPO_WEB_URL/releases/latest" 2>/dev/null | grep -i '^location:' | sed 's/.*tag\///' | tr -d '\r\n' || true)
|
||||
if [ -z "$latest" ]; then
|
||||
fail "Could not determine latest release. Check your network connection."
|
||||
fi
|
||||
|
||||
local url="https://github.com/multica-ai/multica/releases/download/${latest}/multica_${OS}_${ARCH}.tar.gz"
|
||||
local tmp_dir
|
||||
tmp_dir=$(mktemp -d)
|
||||
|
||||
info "Downloading $url ..."
|
||||
if ! curl -fsSL "$url" -o "$tmp_dir/multica.tar.gz"; then
|
||||
rm -rf "$tmp_dir"
|
||||
fail "Failed to download CLI binary."
|
||||
fi
|
||||
|
||||
tar -xzf "$tmp_dir/multica.tar.gz" -C "$tmp_dir" multica
|
||||
|
||||
# Try /usr/local/bin first, fall back to ~/.local/bin
|
||||
local bin_dir="/usr/local/bin"
|
||||
if [ -w "$bin_dir" ]; then
|
||||
mv "$tmp_dir/multica" "$bin_dir/multica"
|
||||
elif command_exists sudo; then
|
||||
sudo mv "$tmp_dir/multica" "$bin_dir/multica"
|
||||
else
|
||||
bin_dir="$HOME/.local/bin"
|
||||
mkdir -p "$bin_dir"
|
||||
mv "$tmp_dir/multica" "$bin_dir/multica"
|
||||
chmod +x "$bin_dir/multica"
|
||||
# Add to PATH if not already there
|
||||
if ! echo "$PATH" | tr ':' '\n' | grep -q "^$bin_dir$"; then
|
||||
export PATH="$bin_dir:$PATH"
|
||||
add_to_path "$bin_dir"
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -rf "$tmp_dir"
|
||||
ok "Multica CLI installed to $bin_dir/multica"
|
||||
}
|
||||
|
||||
add_to_path() {
|
||||
local dir="$1"
|
||||
local line="export PATH=\"$dir:\$PATH\""
|
||||
for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
|
||||
if [ -f "$rc" ] && ! grep -qF "$dir" "$rc"; then
|
||||
printf '\n# Added by Multica installer\n%s\n' "$line" >> "$rc"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
get_latest_version() {
|
||||
# grep exits 1 when no match; use `|| true` to avoid triggering pipefail
|
||||
curl -sI "$REPO_WEB_URL/releases/latest" 2>/dev/null | grep -i '^location:' | sed 's/.*tag\///' | tr -d '\r\n' || true
|
||||
}
|
||||
|
||||
upgrade_cli_brew() {
|
||||
info "Upgrading Multica CLI via Homebrew..."
|
||||
brew update 2>/dev/null || true
|
||||
if brew upgrade multica 2>/dev/null; then
|
||||
ok "Multica CLI upgraded via Homebrew"
|
||||
else
|
||||
# brew upgrade exits non-zero if already up to date
|
||||
ok "Multica CLI is already the latest version"
|
||||
fi
|
||||
}
|
||||
|
||||
install_cli() {
|
||||
if command_exists multica; then
|
||||
local current_ver
|
||||
# `multica version` outputs "multica v0.1.13 (commit: abc1234)" — extract just the version
|
||||
current_ver=$(multica version 2>/dev/null | awk '{print $2}' || echo "unknown")
|
||||
|
||||
local latest_ver
|
||||
latest_ver=$(get_latest_version)
|
||||
|
||||
# Normalize: strip leading 'v' for comparison
|
||||
local current_cmp="${current_ver#v}"
|
||||
local latest_cmp="${latest_ver#v}"
|
||||
|
||||
if [ -z "$latest_ver" ] || [ "$current_cmp" = "$latest_cmp" ]; then
|
||||
ok "Multica CLI is up to date ($current_ver)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
info "Multica CLI $current_ver installed, latest is $latest_ver — upgrading..."
|
||||
if command_exists brew && brew list multica >/dev/null 2>&1; then
|
||||
upgrade_cli_brew
|
||||
else
|
||||
install_cli_binary
|
||||
fi
|
||||
|
||||
local new_ver
|
||||
new_ver=$(multica version 2>/dev/null | awk '{print $2}' || echo "unknown")
|
||||
ok "Multica CLI upgraded ($current_ver → $new_ver)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command_exists brew; then
|
||||
install_cli_brew
|
||||
else
|
||||
install_cli_binary
|
||||
fi
|
||||
|
||||
# Verify
|
||||
if ! command_exists multica; then
|
||||
fail "CLI installed but 'multica' not found on PATH. You may need to restart your shell."
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Docker check
|
||||
# ---------------------------------------------------------------------------
|
||||
check_docker() {
|
||||
if ! command_exists docker; then
|
||||
printf "\n"
|
||||
fail "Docker is not installed. Multica self-hosting requires Docker and Docker Compose.
|
||||
|
||||
Install Docker:
|
||||
macOS: https://docs.docker.com/desktop/install/mac-install/
|
||||
Linux: https://docs.docker.com/engine/install/
|
||||
|
||||
After installing Docker, re-run this script with --local."
|
||||
fi
|
||||
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
fail "Docker is installed but not running. Please start Docker and re-run this script."
|
||||
fi
|
||||
|
||||
ok "Docker is available"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Server setup (self-host / --local)
|
||||
# ---------------------------------------------------------------------------
|
||||
setup_server() {
|
||||
info "Setting up Multica server..."
|
||||
|
||||
if [ -d "$INSTALL_DIR/.git" ]; then
|
||||
info "Updating existing installation at $INSTALL_DIR..."
|
||||
cd "$INSTALL_DIR"
|
||||
git fetch origin main --depth 1 2>/dev/null || true
|
||||
git reset --hard origin/main 2>/dev/null || true
|
||||
else
|
||||
info "Cloning Multica repository..."
|
||||
if ! command_exists git; then
|
||||
fail "Git is not installed. Please install git and re-run."
|
||||
fi
|
||||
# Remove leftover directory from a previously interrupted clone
|
||||
if [ -d "$INSTALL_DIR" ]; then
|
||||
warn "Removing incomplete installation at $INSTALL_DIR..."
|
||||
rm -rf "$INSTALL_DIR"
|
||||
fi
|
||||
mkdir -p "$(dirname "$INSTALL_DIR")"
|
||||
git clone --depth 1 "$REPO_URL" "$INSTALL_DIR"
|
||||
cd "$INSTALL_DIR"
|
||||
fi
|
||||
|
||||
ok "Repository ready at $INSTALL_DIR"
|
||||
|
||||
# Generate .env if needed
|
||||
if [ ! -f .env ]; then
|
||||
info "Creating .env with random JWT_SECRET..."
|
||||
cp .env.example .env
|
||||
local jwt
|
||||
jwt=$(openssl rand -hex 32)
|
||||
if [ "$(uname -s)" = "Darwin" ]; then
|
||||
sed -i '' "s/^JWT_SECRET=.*/JWT_SECRET=$jwt/" .env
|
||||
else
|
||||
sed -i "s/^JWT_SECRET=.*/JWT_SECRET=$jwt/" .env
|
||||
fi
|
||||
ok "Generated .env with random JWT_SECRET"
|
||||
else
|
||||
ok "Using existing .env"
|
||||
fi
|
||||
|
||||
# Start Docker Compose
|
||||
info "Starting Multica services (this may take a few minutes on first run)..."
|
||||
docker compose -f docker-compose.selfhost.yml up -d --build
|
||||
|
||||
# Wait for health check
|
||||
info "Waiting for backend to be ready..."
|
||||
local ready=false
|
||||
for i in $(seq 1 45); do
|
||||
if curl -sf http://localhost:8080/health >/dev/null 2>&1; then
|
||||
ready=true
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [ "$ready" = true ]; then
|
||||
ok "Multica server is running"
|
||||
else
|
||||
warn "Server is still starting. You can check logs with:"
|
||||
echo " cd $INSTALL_DIR && docker compose -f docker-compose.selfhost.yml logs"
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configure CLI for local server
|
||||
# ---------------------------------------------------------------------------
|
||||
configure_local() {
|
||||
info "Configuring CLI for local server..."
|
||||
multica config local 2>/dev/null || {
|
||||
# Fallback if config local doesn't exist in installed version
|
||||
multica config set app_url http://localhost:3000 2>/dev/null || true
|
||||
multica config set server_url http://localhost:8080 2>/dev/null || true
|
||||
}
|
||||
ok "CLI configured for localhost (backend :8080, frontend :3000)"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configure CLI for Multica Cloud
|
||||
# ---------------------------------------------------------------------------
|
||||
configure_cloud() {
|
||||
info "Configuring CLI for Multica Cloud..."
|
||||
multica config set server_url https://api.multica.ai 2>/dev/null || true
|
||||
multica config set app_url https://multica.ai 2>/dev/null || true
|
||||
ok "CLI configured for multica.ai"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main: Default mode (cloud — install CLI to connect to multica.ai)
|
||||
# ---------------------------------------------------------------------------
|
||||
run_default() {
|
||||
printf "\n"
|
||||
printf "${BOLD} Multica — Installer${RESET}\n"
|
||||
printf " Installing the CLI to connect to ${CYAN}multica.ai${RESET}\n"
|
||||
printf "\n"
|
||||
|
||||
detect_os
|
||||
install_cli
|
||||
configure_cloud
|
||||
|
||||
printf "\n"
|
||||
printf "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n"
|
||||
printf "${BOLD}${GREEN} ✓ Multica CLI is installed!${RESET}\n"
|
||||
printf "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n"
|
||||
printf "\n"
|
||||
printf " ${BOLD}Next steps:${RESET}\n"
|
||||
printf "\n"
|
||||
printf " ${CYAN}multica login${RESET} # Authenticate with multica.ai\n"
|
||||
printf " ${CYAN}multica daemon start${RESET} # Start the agent daemon\n"
|
||||
printf "\n"
|
||||
printf " Or do it all in one command:\n"
|
||||
printf "\n"
|
||||
printf " ${CYAN}multica setup${RESET}\n"
|
||||
printf "\n"
|
||||
printf " ${BOLD}Self-hosting?${RESET} Re-run with --local:\n"
|
||||
printf " curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --local\n"
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main: Local mode (self-host — full server + CLI)
|
||||
# ---------------------------------------------------------------------------
|
||||
run_local() {
|
||||
printf "\n"
|
||||
printf "${BOLD} Multica — Self-Host Installer${RESET}\n"
|
||||
printf " Setting up a local Multica server + CLI\n"
|
||||
printf "\n"
|
||||
|
||||
detect_os
|
||||
check_docker
|
||||
setup_server
|
||||
install_cli
|
||||
configure_local
|
||||
|
||||
printf "\n"
|
||||
printf "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n"
|
||||
printf "${BOLD}${GREEN} ✓ Multica is installed and running!${RESET}\n"
|
||||
printf "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n"
|
||||
printf "\n"
|
||||
printf " ${BOLD}Frontend:${RESET} http://localhost:3000\n"
|
||||
printf " ${BOLD}Backend:${RESET} http://localhost:8080\n"
|
||||
printf " ${BOLD}Server at:${RESET} %s\n" "$INSTALL_DIR"
|
||||
printf "\n"
|
||||
printf " ${BOLD}Next steps:${RESET}\n"
|
||||
printf " 1. Open ${CYAN}http://localhost:3000${RESET} in your browser\n"
|
||||
printf " 2. Log in with any email + verification code: ${BOLD}888888${RESET}\n"
|
||||
printf " 3. Then run:\n"
|
||||
printf "\n"
|
||||
printf " ${CYAN}multica login${RESET} # Authenticate (opens browser)\n"
|
||||
printf " ${CYAN}multica daemon start${RESET} # Start the agent daemon\n"
|
||||
printf "\n"
|
||||
printf " ${BOLD}To stop all services:${RESET}\n"
|
||||
printf " curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --stop\n"
|
||||
printf "\n"
|
||||
printf " Or manually:\n"
|
||||
printf " cd %s && docker compose -f docker-compose.selfhost.yml down\n" "$INSTALL_DIR"
|
||||
printf " multica daemon stop\n"
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stop: shut down a self-hosted installation
|
||||
# ---------------------------------------------------------------------------
|
||||
run_stop() {
|
||||
printf "\n"
|
||||
info "Stopping Multica services..."
|
||||
|
||||
if [ -d "$INSTALL_DIR" ]; then
|
||||
cd "$INSTALL_DIR"
|
||||
if [ -f docker-compose.selfhost.yml ]; then
|
||||
docker compose -f docker-compose.selfhost.yml down
|
||||
ok "Docker services stopped"
|
||||
else
|
||||
warn "No docker-compose.selfhost.yml found at $INSTALL_DIR"
|
||||
fi
|
||||
else
|
||||
warn "No Multica installation found at $INSTALL_DIR"
|
||||
fi
|
||||
|
||||
if command_exists multica; then
|
||||
multica daemon stop 2>/dev/null && ok "Daemon stopped" || true
|
||||
fi
|
||||
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
main() {
|
||||
local mode="default"
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--local) mode="local" ;;
|
||||
--stop) mode="stop" ;;
|
||||
--help|-h)
|
||||
echo "Usage: install.sh [--local | --stop]"
|
||||
echo ""
|
||||
echo " (default) Install the Multica CLI to connect to multica.ai"
|
||||
echo " --local Self-host: set up a local Multica server + CLI"
|
||||
echo " --stop Stop a self-hosted installation"
|
||||
exit 0
|
||||
;;
|
||||
*) warn "Unknown option: $1" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
case "$mode" in
|
||||
default) run_default ;;
|
||||
local) run_local ;;
|
||||
stop) run_stop ;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -29,20 +29,9 @@ var configSetCmd = &cobra.Command{
|
||||
RunE: runConfigSet,
|
||||
}
|
||||
|
||||
var configLocalCmd = &cobra.Command{
|
||||
Use: "local",
|
||||
Short: "Configure CLI for a local Docker Compose deployment",
|
||||
Long: "Sets server_url and app_url to localhost defaults for a local self-hosted deployment.",
|
||||
RunE: runConfigLocal,
|
||||
}
|
||||
|
||||
func init() {
|
||||
configLocalCmd.Flags().Int("port", 8080, "Backend server port")
|
||||
configLocalCmd.Flags().Int("frontend-port", 3000, "Frontend port")
|
||||
|
||||
configCmd.AddCommand(configShowCmd)
|
||||
configCmd.AddCommand(configSetCmd)
|
||||
configCmd.AddCommand(configLocalCmd)
|
||||
}
|
||||
|
||||
func runConfigShow(cmd *cobra.Command, _ []string) error {
|
||||
@@ -91,30 +80,6 @@ func runConfigSet(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func runConfigLocal(cmd *cobra.Command, _ []string) error {
|
||||
port, _ := cmd.Flags().GetInt("port")
|
||||
frontendPort, _ := cmd.Flags().GetInt("frontend-port")
|
||||
|
||||
profile := resolveProfile(cmd)
|
||||
cfg, err := cli.LoadCLIConfigForProfile(profile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.AppURL = fmt.Sprintf("http://localhost:%d", frontendPort)
|
||||
cfg.ServerURL = fmt.Sprintf("http://localhost:%d", port)
|
||||
|
||||
if err := cli.SaveCLIConfigForProfile(cfg, profile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "Configured for local deployment:\n")
|
||||
fmt.Fprintf(os.Stderr, " app_url: %s\n", cfg.AppURL)
|
||||
fmt.Fprintf(os.Stderr, " server_url: %s\n", cfg.ServerURL)
|
||||
fmt.Fprintf(os.Stderr, "\nNext: run 'multica login' to authenticate.\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func valueOrDefault(v, fallback string) string {
|
||||
if v == "" {
|
||||
return fallback
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/multica-ai/multica/server/internal/cli"
|
||||
)
|
||||
|
||||
var setupCmd = &cobra.Command{
|
||||
Use: "setup",
|
||||
Short: "One-command setup: configure, authenticate, and start the daemon",
|
||||
Long: `Detects a local Multica server, configures the CLI, authenticates via browser,
|
||||
and starts the agent daemon — all in one step.
|
||||
|
||||
Use --local to skip auto-detection and force local mode.`,
|
||||
RunE: runSetup,
|
||||
}
|
||||
|
||||
func init() {
|
||||
setupCmd.Flags().Bool("local", false, "Force local mode (skip server auto-detection)")
|
||||
setupCmd.Flags().Int("port", 8080, "Backend server port (for local mode)")
|
||||
setupCmd.Flags().Int("frontend-port", 3000, "Frontend port (for local mode)")
|
||||
}
|
||||
|
||||
func runSetup(cmd *cobra.Command, args []string) error {
|
||||
forceLocal, _ := cmd.Flags().GetBool("local")
|
||||
port, _ := cmd.Flags().GetInt("port")
|
||||
frontendPort, _ := cmd.Flags().GetInt("frontend-port")
|
||||
|
||||
profile := resolveProfile(cmd)
|
||||
|
||||
isLocal := forceLocal
|
||||
if !forceLocal {
|
||||
// Auto-detect a local server on the default port.
|
||||
isLocal = probeLocalServer(port)
|
||||
}
|
||||
|
||||
if isLocal {
|
||||
fmt.Fprintln(os.Stderr, "Detected local Multica server.")
|
||||
|
||||
cfg, _ := cli.LoadCLIConfigForProfile(profile)
|
||||
cfg.AppURL = fmt.Sprintf("http://localhost:%d", frontendPort)
|
||||
cfg.ServerURL = fmt.Sprintf("http://localhost:%d", port)
|
||||
if err := cli.SaveCLIConfigForProfile(cfg, profile); err != nil {
|
||||
return fmt.Errorf("save config: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, " app_url: %s\n", cfg.AppURL)
|
||||
fmt.Fprintf(os.Stderr, " server_url: %s\n", cfg.ServerURL)
|
||||
} else if !forceLocal {
|
||||
fmt.Fprintln(os.Stderr, "No local server detected — using Multica Cloud (https://multica.ai).")
|
||||
|
||||
cfg, _ := cli.LoadCLIConfigForProfile(profile)
|
||||
cfg.AppURL = "https://multica.ai"
|
||||
cfg.ServerURL = "https://api.multica.ai"
|
||||
if err := cli.SaveCLIConfigForProfile(cfg, profile); err != nil {
|
||||
return fmt.Errorf("save config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Authenticate.
|
||||
fmt.Fprintln(os.Stderr, "")
|
||||
if err := runLogin(cmd, args); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "\n✓ Setup complete! Your machine is now connected to Multica.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// probeLocalServer checks whether a Multica backend is running on localhost.
|
||||
func probeLocalServer(port int) bool {
|
||||
url := fmt.Sprintf("http://localhost:%d/health", port)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
resp, err := (&http.Client{Timeout: 2 * time.Second}).Do(req)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return resp.StatusCode == http.StatusOK
|
||||
}
|
||||
@@ -17,7 +17,7 @@ var updateCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
func runUpdate(_ *cobra.Command, _ []string) error {
|
||||
fmt.Fprintf(os.Stderr, "Current version: %s (commit: %s, built: %s)\n", version, commit, date)
|
||||
fmt.Fprintf(os.Stderr, "Current version: %s (commit: %s)\n", version, commit)
|
||||
|
||||
// Check latest version from GitHub.
|
||||
latest, err := cli.FetchLatestRelease()
|
||||
|
||||
@@ -1,42 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
versionCmd.Flags().String("output", "text", "Output format: text or json")
|
||||
}
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print version information",
|
||||
RunE: runVersion,
|
||||
}
|
||||
|
||||
func runVersion(cmd *cobra.Command, _ []string) error {
|
||||
output, _ := cmd.Flags().GetString("output")
|
||||
|
||||
if output == "json" {
|
||||
info := map[string]string{
|
||||
"version": version,
|
||||
"commit": commit,
|
||||
"date": date,
|
||||
"go": runtime.Version(),
|
||||
"os": runtime.GOOS,
|
||||
"arch": runtime.GOARCH,
|
||||
}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(info)
|
||||
}
|
||||
|
||||
fmt.Printf("multica %s (commit: %s, built: %s)\n", version, commit, date)
|
||||
fmt.Printf("go: %s, os/arch: %s/%s\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
|
||||
return nil
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
fmt.Printf("multica %s (commit: %s)\n", version, commit)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
var (
|
||||
version = "dev"
|
||||
commit = "unknown"
|
||||
date = "unknown"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
@@ -41,7 +40,6 @@ func init() {
|
||||
// Additional commands
|
||||
authCmd.GroupID = groupAdditional
|
||||
loginCmd.GroupID = groupAdditional
|
||||
setupCmd.GroupID = groupAdditional
|
||||
attachmentCmd.GroupID = groupAdditional
|
||||
configCmd.GroupID = groupAdditional
|
||||
updateCmd.GroupID = groupAdditional
|
||||
@@ -57,7 +55,6 @@ func init() {
|
||||
rootCmd.AddCommand(runtimeCmd)
|
||||
rootCmd.AddCommand(authCmd)
|
||||
rootCmd.AddCommand(loginCmd)
|
||||
rootCmd.AddCommand(setupCmd)
|
||||
rootCmd.AddCommand(attachmentCmd)
|
||||
rootCmd.AddCommand(configCmd)
|
||||
rootCmd.AddCommand(updateCmd)
|
||||
|
||||
Reference in New Issue
Block a user