# veld

> Open-source local development environment orchestrator. One command starts every service with real HTTPS URLs — no port numbers, no manual wiring. Works with monorepos, multi-repo setups, or any project directory.

veld is a Rust-based CLI tool that replaces the ritual of opening multiple terminals, remembering ports, and manually wiring services together. You declare your stack in a single `veld.json`, and veld resolves the dependency graph, allocates ports, starts processes, runs health checks, configures Caddy for HTTPS, and gives you clean URLs like `https://frontend.my-feature.myproject.localhost`.

- Source: https://github.com/prosperity-solutions/veld
- Website: https://veld.oss.life.li
  - https://veld.oss.life.li/ (homepage, cinematic experience)
  - https://veld.oss.life.li/agents (structured, agent-optimized)
  - https://veld.oss.life.li/humans (developer documentation)
- License: MIT
- Platforms: macOS (arm64/x64), Linux (x64/arm64)

---

## Install

```sh
curl -fsSL https://veld.oss.life.li/get | bash
```

This detects your OS and architecture, downloads the latest release, and installs:
- `veld` to `~/.local/bin/`
- `veld-helper` and `veld-daemon` to `~/.local/lib/veld/`

No sudo required. Ensure `~/.local/bin` is on your `PATH`.

Setup is optional — commands auto-bootstrap on first use with HTTPS on port 18443.
For the full experience with clean URLs (no port numbers), run the one-time privileged setup:

```sh
veld setup privileged
```

This registers system services and binds ports 80/443, so your URLs are just
`https://frontend.my-feature.myproject.localhost` — no `:18443` suffix. Requires
sudo once; you won't be asked again.

Alternatively, `veld setup unprivileged` does a no-sudo setup with HTTPS on port 18443.
Both modes support the full feature set with one difference: unprivileged mode uses port 18443 in URLs and only supports `.localhost` domains (RFC 6761). Custom apex domains (e.g. `{service}.mycompany.dev`) require `veld setup privileged` since they need `/etc/hosts` or dnsmasq management.

To install a specific version: `VELD_VERSION=1.0.0 curl -fsSL https://veld.oss.life.li/get | bash`

### Build from source

```sh
git clone https://github.com/prosperity-solutions/veld.git
cd veld
cargo build --release
# Binaries: target/release/veld, target/release/veld-helper, target/release/veld-daemon
```

---

## Quick Start

1. Create a `veld.json` in your project root:

```json
{
  "$schema": "https://veld.oss.life.li/schema/v2/veld.schema.json",
  "schemaVersion": "2",
  "name": "myproject",
  "url_template": "{service}.{run}.{project}.localhost",
  "nodes": {
    "backend": {
      "default_variant": "local",
      "variants": {
        "local": {
          "type": "start_server",
          "command": "npm run dev -- --port ${veld.port}",
          "health_check": { "type": "http", "path": "/health", "timeout_seconds": 30 }
        }
      }
    },
    "frontend": {
      "default_variant": "local",
      "variants": {
        "local": {
          "type": "start_server",
          "command": "npm run dev -- --port ${veld.port}",
          "health_check": { "type": "http", "path": "/", "timeout_seconds": 30 },
          "depends_on": { "backend": "local" }
        }
      }
    }
  }
}
```

2. Start the environment:

```sh
veld start frontend:local --name dev
```

Veld resolves the dependency graph (backend first, then frontend), allocates ports, starts processes, runs health checks, configures Caddy routes, and gives you HTTPS URLs.

3. Check status:

```sh
veld status --name dev
veld urls --name dev
```

4. Stop:

```sh
veld stop --name dev
```

---

## CLI Reference

| Command | Description |
|---------|-------------|
| `veld start [NODE:VARIANT...] --name <n> [-d]` | Start an environment (`-d` for detached) |
| `veld stop [--name <n>] [--all]` | Stop a running environment |
| `veld restart [--name <n>]` | Restart an environment |
| `veld status [--name <n>] [--json]` | Show run status |
| `veld urls [--name <n>] [--json]` | Show URLs for a run |
| `veld logs [--name <n>] [--node <n>] [--lines <n>] [-f] [--since <d>] [--source <s>] [-s <term>] [-C <n>]` | View logs (`-f` follow, `-s` search, `-C` context lines) |
| `veld graph [NODE:VARIANT...]` | Print dependency graph |
| `veld nodes` | List all nodes and variants |
| `veld presets` | List presets |
| `veld runs` | List all runs |
| `veld feedback [--name <n>]` | Open feedback UI for a run |
| `veld feedback --wait [--name <n>]` | Block until feedback is submitted (for agent tool-use loops) |
| `veld gc` | Clean up stale state and logs |
| `veld setup [unprivileged\|privileged]` | One-time system setup |
| `veld init` | Create a new veld.json |

All commands support `--json` for structured output, making veld scriptable and CI/agent-friendly.

---

## Architecture

Three binaries work together:

- **`veld`** — CLI. Parses commands, orchestrates environments, displays output.
- **`veld-helper`** — manages DNS entries and Caddy routes via a minimal Unix socket API. Runs as either a system daemon (privileged, for clean URLs on ports 80/443) or a user process (unprivileged, on port 18443).
- **`veld-daemon`** — user-space daemon. Monitors health, runs garbage collection, broadcasts state updates.

Caddy handles HTTPS termination and reverse proxying. Its internal CA is trusted in the system keychain during setup so browsers accept certificates without warnings.

---

## Feedback System

veld includes a built-in feedback overlay for human-AI collaboration. When an AI agent is iterating on UI work, humans can give structured feedback through an in-browser overlay injected into veld-served pages.

### How it works

1. When a `start_server` service is running, veld can inject a feedback overlay into the served pages
2. Humans open the feedback overlay in their browser and leave comments
3. AI agents receive the feedback via `veld feedback --wait`, which blocks until new feedback arrives
4. The agent reads the structured feedback and iterates

### CLI Commands

| Command | Description |
|---------|-------------|
| `veld feedback [--name <n>]` | Open feedback UI for a run |
| `veld feedback --wait [--name <n>]` | Block until feedback is submitted (designed for agent tool-use loops) |

### Feedback API

The feedback server runs per-run and exposes:

- `POST /feedback` — Submit feedback `{ "comment": "text", "selector": "css-selector" }`
- `GET /feedback` — Get all feedback for current run
- `WS /feedback/ws` — Real-time feedback stream

The feedback overlay automatically captures React and Vue component stack traces when available, providing agents with component-level context (not just CSS selectors) for precise modifications.

This makes veld uniquely suited for agentic workflows where AI agents build UI and humans provide iterative feedback without context-switching.

---

## Client-Side Log Collection

Veld automatically captures browser console output from `start_server` nodes. A small script is injected into proxied HTML responses that hooks `console.log`, `console.warn`, `console.error`, `console.info`, and `console.debug`, plus `window.onerror` and `onunhandledrejection`.

Captured logs appear alongside server logs in `veld logs` and the management UI. Filter by source:

```sh
veld logs --source client          # Browser logs only
veld logs --source server          # Server logs only
veld logs --source internal        # Liveness probe & recovery logs
veld logs --source all             # All sources (default)
```

### Configuration

Control which levels to capture with `client_log_levels` at the project, node, or variant level (most specific wins):

```json
"client_log_levels": ["log", "warn", "error", "info", "debug"]
```

Valid levels: `"log"`, `"warn"`, `"error"`, `"info"`, `"debug"`. Default: `["log", "warn", "error"]`. Unhandled exceptions and promise rejections are always captured regardless of this setting.

The management UI also provides source filter controls (All / Server / Client) in the Logs tab.

### Feature Toggles

Control which Veld capabilities are injected into `start_server` nodes' HTML responses with `features` at the project, node, or variant level (most specific wins):

```json
"features": { "feedback_overlay": false, "client_logs": true }
```

Available features: `feedback_overlay` (toolbar/comments UI), `client_logs` (browser log collector), `inject` (auto-inject bootstrap scripts). All default to `true`.

---

## Configuration Reference

### Top-Level Structure

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `$schema` | string | No | JSON Schema URL for editor autocompletion |
| `schemaVersion` | string | Yes | `"1"` or `"2"` (use `"2"` for new projects) |
| `name` | string | Yes | Project name (used in URLs) |
| `url_template` | string | No | URL template for services |
| `presets` | object | No | Named shortcuts for node:variant selections |
| `client_log_levels` | array | No | Browser log levels to capture (default: `["log", "warn", "error"]`) |
| `features` | object | No | Feature toggles: `feedback_overlay`, `client_logs`, `inject` (all default `true`, cascade: variant > node > project) |
| `env` | object | No | Global environment variables inherited by all nodes (cascade: variant > node > project, per-key merge) |
| `setup` | array | No | Lifecycle steps before graph execution (see Setup & Teardown) |
| `teardown` | array | No | Lifecycle steps after all nodes stop (see Setup & Teardown) |
| `nodes` | object | Yes | The dependency graph nodes |

### Setup & Teardown

Project-level lifecycle steps that run outside the dependency graph. Not nodes — no variants, no health checks, no outputs.

**Setup** runs sequentially before any node. Non-zero exit aborts startup. **Teardown** runs after all nodes stop (after per-node `on_stop` hooks). Best-effort.

```json
"setup": [
  { "name": "docker", "command": "docker info", "failureMessage": "Docker must be running" },
  { "name": "network", "command": "docker network create ${veld.name}-net 2>/dev/null || true" }
],
"teardown": [
  { "name": "network", "command": "docker network rm ${veld.name}-net 2>/dev/null || true" }
]
```

Step fields: `name` (required), `command` (required), `failureMessage` (optional — shown on failure).

Variables: `${veld.name}`, `${veld.project}`, `${veld.root}`, `${veld.run}`, plus shell env vars. No node-scoped vars.

### Step Types

#### `start_server`

Starts and manages a long-lived process. Veld allocates a port, injects it as `${veld.port}`, configures DNS and Caddy routing, and monitors health.

- Must specify `command` (required)
- The process must bind to `${veld.port}`
- Built-in outputs: `url` (full HTTPS URL) and `port` (allocated port number)
- Supports synthetic `outputs` as an object (key → template string, interpolated after port allocation)
- Requires `health_check`

```json
{
  "type": "start_server",
  "command": "npm run dev -- --port ${veld.port}",
  "health_check": { "type": "http", "path": "/health" }
}
```

#### `command`

Runs a shell command or script to completion. Used for setup tasks.

- Must specify either `command` or `script` (mutually exclusive)
- Can emit outputs by writing `key=value` lines to `$VELD_OUTPUT_FILE` (preferred) or via `VELD_OUTPUT key=value` on stdout (legacy, discouraged)
- Declares outputs as an array of strings
- Built-in output: `exit_code`
- Supports `skip_if` for idempotency (alias: `verify`)
- Does NOT get a port allocated — `${veld.port}` is not available

```json
{
  "type": "command",
  "script": "./scripts/clone-db.sh",
  "outputs": ["DATABASE_URL"]
}
```

### Health Checks

Two-phase: (1) TCP port check, (2) HTTP endpoint check. Three strategies:

```json
{ "type": "http", "path": "/health", "expect_status": 200, "timeout_seconds": 30 }
{ "type": "port", "timeout_seconds": 10 }
{ "type": "command", "command": "curl -sf http://localhost:${veld.port}/ready" }
```

### Dependencies

Dependencies are explicit `node:variant` pairs. Resolved before start. Started in topological order, parallelized where possible. Reverse-order teardown.

```json
"depends_on": {
  "database": "docker",
  "backend": "local"
}
```

### Environment Variables

Declare `env` at the project, node, or variant level. Variables cascade: variant > node > project (per-key merge, most specific wins). Values support variable interpolation including upstream node outputs:

```json
{
  "env": { "SHARED_FLAG": "1" },
  "nodes": {
    "api": {
      "env": { "LOG_LEVEL": "debug" },
      "variants": {
        "local": {
          "env": {
            "DATABASE_URL": "${nodes.database.DATABASE_URL}",
            "PORT": "${veld.port}",
            "NEXT_PUBLIC_API_URL": "${nodes.backend.url}"
          }
        }
      }
    }
  }
}
```

### Outputs

For `start_server` variants — object (synthetic templates):
```json
"outputs": {
  "DATABASE_URL": "postgresql://postgres:veld@localhost:${veld.port}/app"
}
```

For `command` variants — array (captured from `$VELD_OUTPUT_FILE` or legacy `VELD_OUTPUT` stdout):
```json
"outputs": ["DATABASE_URL", "DB_NAME"]
```

### Variable Substitution

#### Built-in Variables (`${veld.*}`)

| Variable | Value |
|----------|-------|
| `${veld.port}` | Allocated port (start_server only) |
| `${veld.url}` | Full HTTPS URL (start_server only) |
| `${veld.url.hostname}` | DNS name only (e.g. `app.my-run.proj.localhost`) |
| `${veld.url.host}` | hostname:port (omits port if 443) |
| `${veld.url.origin}` | scheme + host (same as `${veld.url}`) |
| `${veld.url.scheme}` | Protocol scheme (`https`) |
| `${veld.url.port}` | HTTPS port (`${veld.port}` is the backend bind port) |
| `${veld.run}` | Run name |
| `${veld.run_id}` | Stable run UUID |
| `${veld.root}` | Path to directory containing veld.json |
| `${veld.project}` | Project name |
| `${veld.worktree}` | Slugified worktree directory name |
| `${veld.branch}` | Current git branch (slugified) |
| `${veld.username}` | OS username |

#### Node Output References (`${nodes.*}`)

```
${nodes.database.DATABASE_URL}   # custom output
${nodes.backend.url}             # start_server built-in: HTTPS URL
${nodes.backend.url.hostname}    # start_server built-in: DNS name only
${nodes.backend.url.host}        # start_server built-in: hostname:port
${nodes.backend.url.origin}      # start_server built-in: scheme + host
${nodes.backend.url.scheme}      # start_server built-in: protocol scheme
${nodes.backend.url.port}        # start_server built-in: HTTPS port
${nodes.backend.port}            # start_server built-in: backend bind port
${nodes.backend:local.url}       # qualified form (when multiple variants active)
```

### URL Templates

URL templates use `{variable}` syntax (single braces). Available variables:

| Variable | Description |
|----------|-------------|
| `{service}` | Node name |
| `{variant}` | Variant name |
| `{run}` | Run name (always non-empty) |
| `{project}` | Project name |
| `{branch}` | Git branch (slugified) |
| `{worktree}` | Worktree directory name (slugified) |
| `{username}` | OS username |
| `{hostname}` | Machine hostname |

Fallback operator: `{branch ?? run}` uses first non-empty value.

Default: `{service}.{run}.{project}.localhost`

`.localhost` resolves to 127.0.0.1 automatically (RFC 6761). Custom domains are supported in privileged mode only — veld manages exact DNS entries via veld-helper (requires `/etc/hosts` or dnsmasq access). In unprivileged/auto mode, `veld start` will refuse to start if any URL template resolves to a non-`.localhost` domain.

### URL Template Cascade

1. Variant-level (highest priority)
2. Node-level
3. Project-level (default)

### Presets

Named shortcuts for node:variant selections:

```json
"presets": {
  "fullstack": ["database:docker", "backend:local", "frontend:local"],
  "frontend-only": ["frontend:local"]
}
```

Usage: `veld start --preset fullstack --name my-feature`

### Additional Variant Fields

| Field | Type | Applies To | Description |
|-------|------|------------|-------------|
| `on_stop` | string | All | Teardown command on `veld stop` |
| `skip_if` | string | `command` only | Idempotency check (exit 0 = skip). Alias: `verify` |
| `probes` | object | All | `{readiness?: HealthCheck, liveness?: LivenessProbe}` — supersedes `health_check` |
| `sensitive_outputs` | array | All | Output keys to mask and encrypt |
| `client_log_levels` | array | `start_server` | Browser log levels override (variant > node > project) |
| `features` | object | `start_server` | Feature toggles override (variant > node > project) |
| `cwd` | string | All | Working directory (relative paths resolve from project root). Cascade: variant > node. Supports `${...}` substitution. |
| `hidden` (node-level) | boolean | All | Hide from `veld nodes` output |

---

## Complete Example

```json
{
  "$schema": "https://veld.oss.life.li/schema/v2/veld.schema.json",
  "schemaVersion": "2",
  "name": "myproject",
  "url_template": "{service}.{run}.myproject.localhost",
  "presets": {
    "fullstack": ["database:docker", "backend:local", "frontend:local"],
    "frontend-only": ["frontend:local"]
  },
  "nodes": {
    "database": {
      "default_variant": "docker",
      "variants": {
        "docker": {
          "type": "start_server",
          "command": "docker run --rm -p ${veld.port}:5432 postgres:16",
          "health_check": { "type": "port", "timeout_seconds": 15 },
          "outputs": {
            "DATABASE_URL": "postgres://dev:dev@localhost:${veld.port}/app"
          }
        }
      }
    },
    "backend": {
      "default_variant": "local",
      "variants": {
        "local": {
          "type": "start_server",
          "command": "cargo run -- --port ${veld.port}",
          "health_check": { "type": "http", "path": "/health" },
          "depends_on": { "database": "docker" },
          "env": {
            "DATABASE_URL": "${nodes.database.DATABASE_URL}"
          }
        }
      }
    },
    "frontend": {
      "default_variant": "local",
      "variants": {
        "local": {
          "type": "start_server",
          "command": "npm run dev -- --port ${veld.port}",
          "health_check": { "type": "http", "path": "/" },
          "depends_on": { "backend": "local" },
          "env": {
            "API_URL": "${nodes.backend.url}"
          }
        }
      }
    }
  }
}
```

---

## Agent Skills

Veld ships skills for AI coding agents. Install them so your agent knows how to configure, use, and collaborate through Veld:

```sh
npx skills add prosperity-solutions/veld
```

This installs three skills:

- **veld-config** — Write and edit `veld.json` configuration files (nodes, health checks, dependencies, URL templates, presets)
- **veld-feedback** — Human-in-the-loop feedback workflow (request reviews, read structured comments with element selectors and component traces, iterate)
- **veld-usage** — CLI reference for agents (start, stop, logs, status, feedback, and all other commands)

Works with Claude Code, Cursor, Codex, Windsurf, Cline, GitHub Copilot, and [40+ more agents](https://github.com/vercel-labs/skills#supported-agents).

---

## Requirements

- macOS (arm64/x64) or Linux (x64/arm64)
- Optional: sudo access for `veld setup privileged` (clean URLs without port numbers, custom apex domains)
