Mac-native deployment control plane for AI-generated code. — v0.5.1
Shared business logic: project model, runtime detection, process executor, SQLite store.
Tauri 2.0 desktop app. Rust backend with 18 commands + React frontend with 5 pages. Agent-based execution via UDS.
MCP server for AI agent control. 17 tools via JSON-RPC 2.0 stdio. E2E tested.
Persistent execution agent. 14 gRPC RPCs, SQLite store (~/.berth/agent.db), agent-side scheduler, store-and-forward events, remote upgrade. Deployed on Linux via systemd.
CLI interface. 12 commands + schedule + targets subcommands. Full feature parity with MCP.
pub struct Project {
pub id: Uuid,
pub name: String,
pub path: String,
pub runtime: Runtime,
pub entrypoint: Option<String>,
pub status: ProjectStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
pub enum ProjectStatus {
Idle, // Not running
Running, // Process active
Stopped, // Manually stopped
Failed, // Non-zero exit code
}
impl Project {
pub fn new(name: String, path: String, runtime: Runtime) -> Self
// Generates UUID v4, sets status to Idle, timestamps to now
}
pub enum Runtime {
Python, Node, Go, Rust, Shell, Unknown,
}
pub struct RuntimeInfo {
pub runtime: Runtime,
pub version_file: Option<String>,
pub entrypoint: Option<String>,
pub confidence: f32, // 0.0 - 1.0
pub dependencies: Vec<String>,
pub scripts: HashMap<String, String>,
}
pub fn detect_runtime(path: &Path) -> RuntimeInfo
| Marker File | Runtime | Entrypoints Checked |
|---|---|---|
requirements.txt, pyproject.toml, setup.py | Python | main.py, app.py, run.py, __main__.py |
package.json | Node | index.js, index.ts, main.js, app.js |
go.mod | Go | main.go, cmd/main.go |
Cargo.toml | Rust | src/main.rs |
*.sh, *.bash | Shell | run.sh, start.sh, main.sh |
pub struct LogLine {
pub stream: LogStream, // Stdout or Stderr
pub text: String,
pub timestamp: DateTime<Utc>,
}
pub enum LogStream { Stdout, Stderr }
pub async fn spawn_and_stream(
runtime: Runtime,
entrypoint: &str,
working_dir: &str,
) -> anyhow::Result<(Child, mpsc::Receiver<LogLine>)>
| Runtime | Command |
|---|---|
| Python | python3 -u <entrypoint> |
| Node | node <entrypoint> |
| Go | go run <entrypoint> |
| Rust | cargo run |
| Shell / Unknown | sh <entrypoint> |
All commands run with kill_on_drop(true), piped stdout/stderr, and two background tokio tasks reading into a 256-capacity mpsc channel.
pub struct ProjectStore { conn: Connection }
impl ProjectStore {
pub fn open(path: &str) -> Result<Self>
pub fn open_in_memory() -> Result<Self>
pub fn list(&self) -> Result<Vec<Project>>
pub fn insert(&self, project: &Project) -> Result<()>
pub fn get(&self, id: Uuid) -> Result<Option<Project>>
pub fn update_status(&self, id: Uuid, status: ProjectStatus) -> Result<()>
pub fn record_run_start(&self, id: Uuid) -> Result<()>
pub fn record_run_end(&self, id: Uuid, exit_code: Option<i32>) -> Result<()>
pub fn delete(&self, id: Uuid) -> Result<()>
// Schedule CRUD
pub fn insert_schedule(&self, schedule: &Schedule) -> Result<()>
pub fn list_schedules(&self) -> Result<Vec<Schedule>>
pub fn get_schedules_for_project(&self, project_id: Uuid) -> Result<Vec<Schedule>>
pub fn update_schedule_after_run(&self, ...) -> Result<()>
pub fn set_schedule_enabled(&self, id: Uuid, enabled: bool) -> Result<()>
pub fn delete_schedule(&self, id: Uuid) -> Result<()>
}
~/Library/Application Support/com.berth.app/berth.db~/.berth/agent.db (remote agent only)| Column | Format | Example |
|---|---|---|
id | UUID v4 string | 8bda6c73-3ead-47f5-a64d-8d8b48f428d3 |
runtime | lowercase enum | python, node, go, rust, shell, unknown |
status | lowercase enum | idle, running, stopped, failed |
created_at | RFC 3339 | 2026-03-06T08:30:00+00:00 |
updated_at | RFC 3339 | 2026-03-06T09:15:42+00:00 |
| Command | Params | Returns | Async |
|---|---|---|---|
list_projects | — | { projects: Project[] } | No |
create_project | name: String, path: String | Project | No |
detect_runtime | path: String | RuntimeInfo | No |
update_project | id: String, ... | Project | No |
delete_project | id: String | () | No |
save_paste_code | name: String, code: String | String (path) | No |
run_project | id: String, target: Option<String> | () | Yes |
stop_project | id: String, target: Option<String> | () | Yes |
list_targets | — | Vec<Target> | No |
add_target | name, host, port | Target | No |
remove_target | id: String | () | No |
ping_target | id: String | HealthResponse | Yes |
get_agent_stats | id: String | AgentStats | Yes |
set_project_target | id: String, targetId: Option<String> | () | No |
set_project_notify | id: String, enabled: bool | () | No |
read_project_file | id: String | String | No |
write_project_file | id: String, content: String | () | No |
import_file | filePath: String | Project | No |
get_settings | — | Record<String, String> | No |
update_setting | key: String, value: String | () | No |
list_schedules | projectId: String | Vec<Schedule> | No |
add_schedule | projectId, cronExpr | Schedule | No |
remove_schedule | id: String | () | No |
toggle_schedule | id: String, enabled: bool | () | No |
list_execution_logs | projectId, limit | Vec<ExecutionLog> | No |
publish_project | id, port, provider?, target? | PublishResult | No |
unpublish_project | id, target? | UnpublishResult | No |
// Embedded local agent (in-process, UDS transport)
// AgentClient connects via ~/.berth/agent.sock
// Lockfile coordination via ~/.berth/agent.lock
// No ProcessRegistry — agent owns all process lifecycle
pub struct AppState {
pub store: ProjectStore,
// Agent started on app init, shared across commands
}
| Event Name | Payload | Emitted By |
|---|---|---|
project-log | LogEvent | Background log task in run_project |
project-status-change | StatusEvent | run_project, stop_project, log task cleanup |
// LogEvent (Rust -> Frontend)
{
"project_id": "8bda6c73-...",
"stream": "stdout", // "stdout" | "stderr"
"text": "[1/10] AAPL: $215.13 (+4.64%)",
"timestamp": "2026-03-06T09:15:42.123+00:00"
}
// StatusEvent (Rust -> Frontend)
{
"project_id": "8bda6c73-...",
"status": "running" // "idle" | "running" | "stopped" | "failed"
}
interface Project {
id: string;
name: string;
path: string;
runtime: string;
entrypoint: string | null;
status: "idle" | "running" | "stopped" | "failed";
created_at: string;
updated_at: string;
}
interface RuntimeInfo {
runtime: string;
version_file: string | null;
entrypoint: string | null;
confidence: number; // 0.0 - 1.0
}
interface LogEvent {
project_id: string;
stream: "stdout" | "stderr";
text: string;
timestamp: string;
}
interface StatusEvent {
project_id: string;
status: "idle" | "running" | "stopped" | "failed";
}
listProjects(): Promise<Project[]>
createProject(name: string, path: string): Promise<Project>
detectRuntime(path: string): Promise<RuntimeInfo>
deleteProject(id: string): Promise<void>
runProject(id: string): Promise<void>
stopProject(id: string): Promise<void>
| Token | Value | Usage |
|---|---|---|
--berth-bg | #1c1c1e | Page background |
--berth-surface | #2c2c2e | Cards, inputs, log viewer |
--berth-border | #3a3a3c | Borders, dividers |
--berth-text | #f5f5f7 | Primary text |
--berth-muted | #8e8e93 | Secondary text, labels |
--berth-accent | #0a84ff | Buttons, links, active states |
--berth-success | #30d158 | Running status |
--berth-error | #ff453a | Failed status, stderr |
| Token | Value |
|---|---|
--berth-bg | #f2f2f7 |
--berth-surface | #ffffff |
--berth-border | #d1d1d6 |
--berth-text | #1c1c1e |
--berth-accent | #007aff |
--berth-success | #34c759 |
--berth-error | #ff3b30 |
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text",
"Helvetica Neue", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
user-select: none;
syntax = "proto3";
package berth;
service AgentService {
// Core RPCs (Phase 1-3)
rpc Execute(ExecuteRequest) returns (stream ExecuteResponse);
rpc Status(StatusRequest) returns (StatusResponse);
rpc StreamLogs(LogStreamRequest) returns (stream LogStreamResponse);
rpc Stop(StopRequest) returns (StopResponse);
rpc Health(HealthRequest) returns (HealthResponse);
rpc Deploy(DeployRequest) returns (DeployResponse);
// Persistent Agent RPCs (Phase 3.5)
rpc GetExecutions(GetExecutionsRequest) returns (GetExecutionsResponse);
rpc GetExecutionLogs(GetExecutionLogsRequest) returns (stream LogStreamResponse);
rpc GetEvents(GetEventsRequest) returns (GetEventsResponse);
rpc AckEvents(AckEventsRequest) returns (AckEventsResponse);
rpc AddSchedule(AddScheduleRequest) returns (AddScheduleResponse);
rpc RemoveSchedule(RemoveScheduleRequest) returns (RemoveScheduleResponse);
rpc ListSchedules(ListSchedulesRequest) returns (ListSchedulesResponse);
rpc Upgrade(stream UpgradeChunk) returns (UpgradeResponse);
}
| Message | Key Fields | Notes |
|---|---|---|
ExecuteRequest | project_id, runtime, entrypoint, code (bytes), working_dir | code field for paste-and-deploy |
ExecuteResponse | stream, text, timestamp, exit_code, is_final | Server-streaming, final message has exit code |
HealthResponse | agent_version, status, uptime_seconds, os, arch | os/arch from std::env::consts |
StatusResponse | agent_id, status, cpu_usage, memory_bytes, projects[] | |
DeployRequest | project_id, runtime, entrypoint, tarball (bytes) | Multi-file deploy via tarball |
GetExecutionsRequest | project_id, limit | Query persistent execution history |
ExecutionInfo | id, project_id, started_at, finished_at, exit_code, trigger, status | Nested in GetExecutionsResponse |
GetEventsRequest | since_id, limit | Poll for store-and-forward events |
AgentEvent | id, event_type, project_id, execution_id, data, created_at | Nested in GetEventsResponse |
AgentScheduleInfo | id, project_id, cron_expr, enabled, last_triggered_at, next_run_at | Agent-side schedule |
UpgradeChunk | data (bytes), chunk_index, total_chunks | Client-streaming binary upload |
UpgradeResponse | success, new_version, message | Sent before systemd restart |
26 tools implemented via JSON-RPC 2.0 stdio transport (MCP protocol version 2024-11-05). E2E tested: deploy inline code, run, capture output, delete.
| Tool | Purpose | Status |
|---|---|---|
berth_list_projects | List all projects with status, runtime, run history | Done |
berth_project_status | Detailed status of one project (by UUID or name) | Done |
berth_deploy | Create project + run (inline code or path) | Done |
berth_run | Run existing project, capture output with timeout | Done |
berth_stop | Stop a running project | Done |
berth_logs | Fetch logs (pending log store) | Partial |
berth_import_code | Import code from path or inline, create project | Done |
berth_detect_runtime | Auto-detect language, entrypoint, deps, scripts | Done |
berth_delete | Delete a project | Done |
berth_health | System health (version, projects, schedules, platform) | Done |
berth_schedule_add | Add a cron-like schedule to a project | Done |
berth_schedule_list | List all schedules | Done |
berth_schedule_remove | Remove a schedule by UUID | Done |
berth_publish | Publish a running project to a public URL via tunnel (cloudflared) | Done |
berth_unpublish | Stop the public URL tunnel for a project | Done |
berth_list_targets | List deploy targets | Done |
berth_add_target | Add a deploy target | Done |
berth_remove_target | Remove a deploy target | Done |
berth_list_agents | List connected agents | Done |
berth_env_set | Set environment variable for a project | Done |
berth_env_get | Get all environment variables for a project | Done |
berth_env_delete | Delete environment variable from a project | Done |
berth_env_import | Import environment variables from .env format | Done |
berth_store_list | List templates from Berth Template Store | Done |
berth_store_search | Search Berth Template Store by keyword | Done |
berth_store_install | Install template from Berth Template Store | Done |
Transport: stdio (primary, for Claude Code). HTTP via axum planned for Phase 3.
Claude Code integration: .mcp.json config included in repo root.
crates/berth-mcp/
src/
main.rs -- Entry point, tokio::main, calls server::run_stdio()
lib.rs -- Module exports (protocol, server, tools)
protocol.rs -- JSON-RPC 2.0 types, MCP types (InitializeResult, Tool, CallToolResult)
server.rs -- Stdio loop: read line -> parse JSON-RPC -> dispatch -> write response
tools.rs -- Tool definitions (list_tools) + handlers (call_tool)
Berth can publish any running project to a public URL via pluggable tunnel providers. Zero Berth-side infrastructure — tunnels run on the user's machine.
| File | Role |
|---|---|
crates/berth-core/src/tunnel.rs | TunnelManager, TunnelProvider enum, cloudflared spawn + URL parsing, available_providers() |
crates/berth-core/src/agent_service.rs | Local agent publish/unpublish gRPC handlers |
crates/berth-agent/src/persistent_service.rs | Remote agent do_publish/do_unpublish + tunnel lifecycle |
crates/berth-core/src/agent_transport.rs | publish()/unpublish() trait methods |
crates/berth-core/src/nats_relay.rs | Publish/Unpublish NatsCommandKind + NatsResponseBody variants |
crates/berth-agent/src/nats_cmd_handler.rs | NATS Publish/Unpublish command dispatch |
proto/berth.proto | Publish/Unpublish RPCs + messages, tunnel_providers in HealthResponse |
src-tauri/src/commands.rs | publish_project/unpublish_project Tauri commands |
src/pages/ProjectDetail.tsx | PublishPanel component (port input, publish/unpublish, URL display) |
crates/berth-mcp/src/tools.rs | berth_publish, berth_unpublish MCP tools |
crates/berth-cli/src/main.rs | berth publish/unpublish CLI commands |
Adding a new provider (e.g. ngrok) requires changes to one file only: tunnel.rs.
TunnelProvider enumTunnelManager::start()start_ngrok() with provider-specific URL parsingavailable_providers()No proto changes, no transport changes, no new Tauri commands needed.
rpc Publish(PublishRequest) returns (PublishResponse);
rpc Unpublish(UnpublishRequest) returns (UnpublishResponse);
message PublishRequest {
string project_id = 1;
uint32 port = 2;
string provider = 3; // "cloudflared" (default)
string provider_config = 4; // reserved for future provider options
}
message PublishResponse {
bool success = 1;
string url = 2; // e.g. "https://random-words.trycloudflare.com"
string message = 3;
string provider = 4;
}
// HealthResponse includes: repeated string tunnel_providers = 9;
-- Added columns to projects table:
ALTER TABLE projects ADD COLUMN tunnel_url TEXT;
ALTER TABLE projects ADD COLUMN tunnel_provider TEXT;
-- Methods: store.set_tunnel_url(), store.clear_tunnel_url()
berth list # List all projects (table format)
berth deploy <path> [--name N] [--target T] # Create project + run
berth run <project> # Run by name or UUID
berth stop <project> # Stop a project
berth logs <project> [-f|--follow] # Run and capture output
berth status <project> # Detailed project info
berth import <path> [--name N] # Import code as project
berth detect <path> # Detect runtime + deps + scripts
berth delete <project> # Delete a project
berth health # System health check
berth targets list # List deploy targets
berth targets add <name> --host <host> # Add target (Phase 3)
berth schedule add <project> --cron <expr> # Add schedule (@every 5m, @hourly, etc.)
berth schedule list # List all schedules
berth schedule remove <id> # Remove a schedule
berth schedule enable <id> # Enable a schedule
berth schedule disable <id> # Disable a schedule
berth schedule tick # Run one scheduler tick
berth publish <project> --port 8080 # Publish via cloudflared tunnel
berth unpublish <project> # Stop the public URL tunnel
| Item | Status |
|---|---|
| Tauri app scaffold (project list, detail, code import) | Done |
| berth-core: project model, runtime detection | Done |
| Run/Stop execution (agent-based via UDS, log streaming via events) | Done |
| Paste & Deploy end-to-end (save to disk, smart quote normalization) | Done |
| xterm.js log viewer (ANSI colors, auto-fit, 10k scrollback) | Done |
| Basic monitoring (run count, last run, exit codes, live uptime) | Done |
| Local agent gRPC (tonic on localhost:50051) | Done |
| Menu bar tray icon (TrayIconBuilder + managed state) | Done |
| UI polish (toasts, skeletons, runtime badges, live status) | Done |
| Item | Status |
|---|---|
| MCP server (stdio, 26 tools, JSON-RPC 2.0) | Done |
| CLI tool (15 commands + schedule/target/env/store subcommands) | Done |
| .mcp.json config for Claude Code integration | Done |
| E2E MCP test (deploy inline code, run, capture output, delete) | Done |
| Scheduling (@every, @hourly, @daily, @weekly, M H * * *) | Done |
| Auto-detect: parse requirements.txt, package.json, go.mod, Cargo.toml | Done |
Persistent remote agent with SQLite store, 14 gRPC RPCs, agent-side scheduler, store-and-forward events, remote upgrade, dependency install during deployment. Agent install script, gRPC communication, remote deploy, agent health monitoring, mDNS LAN discovery, agent-only execution architecture (UDS for local, TCP gRPC for remote). NATS relay for NAT traversal, mTLS, berth-proto shared contract crate, message authentication, environment variable management, template store, opt-in telemetry, onboarding flow. Deployed and verified on remote Linux server.
AWS Lambda, Cloudflare Workers, deployment history, code signing, Homebrew cask.
Pro tier, team features, web dashboard, Windows/Linux builds.
| Layer | Technology | Version |
|---|---|---|
| Desktop Framework | Tauri | 2.10 |
| Frontend | React + TypeScript | 19 / 5.6 |
| Build Tool | Vite | 6.0 |
| Styling | Tailwind CSS | 3.4 |
| Rust Runtime | tokio | 1.x (full) |
| Database | SQLite via rusqlite | 0.32 (bundled) |
| Serialization | serde + serde_json | 1.x |
| gRPC (Phase 3) | tonic + prost | 0.12 / 0.13 |
| CLI Parsing | clap | 4.x |
| Logging | tracing + tracing-subscriber | 0.1 / 0.3 |
Berth v0.5.1 — Documentation generated March 2026
github.com/berth-app/berth