Compare commits

..

1 Commits

Author SHA1 Message Date
Jiayuan Zhang
ca232a725c fix(views): display comment attachments uploaded via CLI
commentToTimelineEntry() was dropping the attachments field, and
comment-card never rendered entry.attachments. Attachments uploaded
through the CLI (not embedded in markdown) were invisible in the UI.

- Add attachments to commentToTimelineEntry() conversion
- Add AttachmentList component that renders standalone attachments
  (skipping those already referenced in the markdown content)
- Render AttachmentList in both CommentRow and CommentCard
2026-04-12 00:07:00 +08:00
17 changed files with 502 additions and 1563 deletions

View File

@@ -11,7 +11,6 @@ builds:
- -s -w
- -X main.version={{.Version}}
- -X main.commit={{.ShortCommit}}
- -X main.date={{.Date}}
env:
- CGO_ENABLED=0
goos:

View File

@@ -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>
```

View File

@@ -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
View File

@@ -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

View File

@@ -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 agentClaude 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)。
## 快速上手

View File

@@ -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.

View File

@@ -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.

View File

@@ -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`

View File

@@ -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

View File

@@ -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; };

View File

@@ -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>
);
}

View File

@@ -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 "$@"

View File

@@ -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

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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)
},
}

View File

@@ -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)